fix sms
This commit is contained in:
1 parent
1dea3509a7
commit
e3a6d1f9d4
42 files changed
+33086
-15779
No files matched your search
@@ -30,6 +30,10 @@ NOTICES = {
|
||||
"approved": ("success", "پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
|
||||
"not_pending": ("warning", "این پیامک در وضعیت «در انتظار تأیید» نیست."),
|
||||
"deleted": ("success", "پیامک از صف حذف شد."),
|
||||
"delete_done_blocked": (
|
||||
"warning",
|
||||
"این پیامک ارسال شده است و از صف حذف نمیشود؛ سابقهٔ آن در «گزارش ارسال» میماند.",
|
||||
),
|
||||
"bulk_approved_all": ("success", "{count} پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
|
||||
"bulk_approved": ("success", "{count} پیامک تأیید شد."),
|
||||
"bulk_deleted": ("success", "{count} پیامک از صف حذف شد."),
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""محاسبهٔ «تعداد پیامکِ محاسبهشده» (parts) و کدگذاری متن.
|
||||
|
||||
هر پیامک روی شبکهٔ موبایل بر اساس **کدگذاری** و **طول** به یک یا چند «بخش» (part)
|
||||
شکسته میشود و اپراتور بر همان تعداد بخش هزینه میگیرد. این ماژول همان قاعدهٔ
|
||||
استاندارد (3GPP/GSM 03.38) را پیاده میکند تا بتوان هزینهٔ ارسال را تحلیل کرد:
|
||||
|
||||
* **GSM-7** — برای متنهای انگلیسی/عددی. هر بخش ۱۶۰ کاراکتر؛ در حالت چندبخشی
|
||||
(multipart) هر بخش ۱۵۳ کاراکتر (۷ کاراکتر برای هدر الحاق).
|
||||
برخی کاراکترها (مثل `{}[]~\\|^€`) در جدول «الحاقی» هستند و **۲ کاراکتر** حساب
|
||||
میشوند.
|
||||
* **UCS-2** — اگر متن حتی **یک** کاراکتر خارج از جدول GSM-7 داشته باشد (فارسی،
|
||||
عربی، ایموجی…)، **کل پیامک** با UCS-2 کدگذاری میشود: هر بخش ۷۰ کاراکتر؛ در
|
||||
حالت چندبخشی هر بخش ۶۷ کاراکتر. ایموجیها (خارج از BMP) دو واحد کدِ UTF-16
|
||||
حساب میشوند.
|
||||
|
||||
بنابراین یک متن فارسی حتی اگر کوتاه باشد، در «۷۰ کاراکتر» جا میشود و ممکن است
|
||||
چند پیامک حساب شود؛ و یک متن ترکیبی فارسی+انگلیسی همیشه UCS-2 است.
|
||||
"""
|
||||
from math import ceil
|
||||
|
||||
ENCODING_GSM7 = "gsm7"
|
||||
ENCODING_UCS2 = "ucs2"
|
||||
|
||||
# جدول کامل GSM 03.38 (بخش پایه). کاراکتر 0x1B عمداً وارد نشده چون نقش «پیشوند
|
||||
# الحاق» دارد و کاراکتر مستقل قانونی نیست.
|
||||
GSM7_BASIC = set(
|
||||
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ"
|
||||
" !\"#¤%&'()*+,-./0123456789:;<=>?"
|
||||
"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ"
|
||||
"¿abcdefghijklmnopqrstuvwxyzäöñüà"
|
||||
)
|
||||
|
||||
# کاراکترهای «جدول الحاقی»: در GSM-7 دو واحد حساب میشوند.
|
||||
GSM7_EXTENDED = set("^{}\\[~]|€")
|
||||
|
||||
# (سقف تکبخشی، سقف هر بخش چندبخشی) برای هر کدگذاری.
|
||||
LIMITS = {
|
||||
ENCODING_GSM7: (160, 153),
|
||||
ENCODING_UCS2: (70, 67),
|
||||
}
|
||||
|
||||
ENCODING_LABELS = {
|
||||
ENCODING_GSM7: "GSM-7 (انگلیسی/عدد)",
|
||||
ENCODING_UCS2: "UCS-2 (فارسی/یونیکد)",
|
||||
}
|
||||
|
||||
|
||||
def is_gsm7(text):
|
||||
"""آیا کل متن با جدول GSM-7 قابل نمایش است؟ (شرط کافی و لازم برای GSM-7)."""
|
||||
return all(ch in GSM7_BASIC or ch in GSM7_EXTENDED for ch in text or "")
|
||||
|
||||
|
||||
def detect_encoding(text):
|
||||
"""کدگذاری متن: ``gsm7`` یا ``ucs2``. متن خالی = ``gsm7``."""
|
||||
return ENCODING_GSM7 if is_gsm7(text) else ENCODING_UCS2
|
||||
|
||||
|
||||
def encoded_length(text):
|
||||
"""طول متن بر حسب «واحدهایی که اپراتور میشمارد».
|
||||
|
||||
* GSM-7: کاراکترهای الحاقی ۲ واحد.
|
||||
* UCS-2: واحدهای کد UTF-16 (ایموجی = ۲).
|
||||
"""
|
||||
text = text or ""
|
||||
if is_gsm7(text):
|
||||
return sum(2 if ch in GSM7_EXTENDED else 1 for ch in text)
|
||||
return len(text.encode("utf-16-le")) // 2
|
||||
|
||||
|
||||
def parts_count(text):
|
||||
"""تعداد بخشهایی که اپراتور برای این متن حساب میکند (۱ برای متن خالی = ۰)."""
|
||||
text = text or ""
|
||||
if not text:
|
||||
return 0
|
||||
single, multipart = LIMITS[detect_encoding(text)]
|
||||
length = encoded_length(text)
|
||||
if length <= single:
|
||||
return 1
|
||||
return ceil(length / multipart)
|
||||
|
||||
|
||||
def describe(text):
|
||||
"""خلاصهٔ محاسبه برای نمایش در گزارش (کدگذاری، طول، تعداد بخش)."""
|
||||
encoding = detect_encoding(text)
|
||||
return {
|
||||
"encoding": encoding,
|
||||
"encoding_label": ENCODING_LABELS[encoding],
|
||||
"length": encoded_length(text),
|
||||
"parts": parts_count(text),
|
||||
}
|
||||
@@ -4,7 +4,7 @@ from ..models import CustomerSMSSetting, SMSType
|
||||
|
||||
def is_sms_enabled(customer, sms_type):
|
||||
"""اگر رکوردی با is_enabled=False وجود داشته باشد غیرفعال است؛ در غیر اینصورت فعال."""
|
||||
if customer is None or sms_type == SMSType.CUSTOM:
|
||||
if customer is None:
|
||||
return True
|
||||
setting = CustomerSMSSetting.objects.filter(customer=customer, sms_type=sms_type).first()
|
||||
if setting is None:
|
||||
@@ -21,5 +21,4 @@ def customer_toggle_map(customer):
|
||||
return [
|
||||
{"code": code, "label": label, "enabled": overrides.get(code, True)}
|
||||
for code, label in SMSType.choices
|
||||
if code != SMSType.CUSTOM
|
||||
]
|
||||
+27
-137
@@ -9,31 +9,29 @@
|
||||
در هر اجرا حداکثر ``backlog_max`` مورد قدیمی هم صف میشود.
|
||||
* **سقف هر اجرا (``max_per_run``)**: سقف کل صفکردن هر اسکنر در یک اجرا.
|
||||
|
||||
اسکنرها فقط انواع «دورهای» را پوشش میدهند (تأمین موجودی چک، سررسید نسیه، بازدید
|
||||
دورهای). انواع «رویدادی» (خوشآمدگویی، نظرسنجی، ثبت بازدید، تشکر از پرداخت) هنگام
|
||||
وقوع رخداد توسط ویوها و از طریق ``admin_panel/sms/triggers.py`` صف میشوند.
|
||||
|
||||
هر اسکنر مستقل و مقاوم به خطاست؛ یک خطا بقیه را متوقف نمیکند. ``dedup_key`` از
|
||||
ارسال تکراری جلوگیری میکند و تضمین میکند اجرای دوبارهٔ اسکنر رکورد جدیدی نسازد.
|
||||
|
||||
یادآوریهای **دورهای** (بازدید و ثبت چک) کلیدشان شامل شمارهٔ دوره است. یعنی اگر
|
||||
مدیر یک مورد را از صف حذف کند، فقط همان دوره کنسل میشود و در دورهٔ بعد دوباره
|
||||
ساخته میشود؛ برای همیشه خاموش نمیماند.
|
||||
یادآوریهای **دورهای** (بازدید) کلیدشان شامل شمارهٔ دوره است. یعنی اگر مدیر یک مورد
|
||||
را از صف حذف کند، فقط همان دوره کنسل میشود و در دورهٔ بعد دوباره ساخته میشود؛ برای
|
||||
همیشه خاموش نمیماند.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
from django.db.models import Q, Sum
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import (Article, Customer, Factor, Sarresid, SMSProviderConfig,
|
||||
SMSTemplate, SMSType)
|
||||
from ..models import Customer, Sarresid, SMSProviderConfig, SMSTemplate, SMSType
|
||||
from .enqueue import enqueue_sms
|
||||
from .timing import DEFAULT_VISIT_MONTHS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# پیشفرض فاصلهٔ تکرار «یادآوری ثبت چک» (روز) وقتی قالبی تنظیم نشده باشد.
|
||||
CHEQUE_REG_INTERVAL_DEFAULT = 3
|
||||
|
||||
|
||||
|
||||
def _template(sms_type):
|
||||
return SMSTemplate.objects.filter(sms_type=sms_type, is_enabled=True).first()
|
||||
@@ -82,33 +80,39 @@ class _Budget:
|
||||
self.new_count += 1
|
||||
|
||||
|
||||
def scan_cheque_due(today):
|
||||
template = _template(SMSType.CHEQUE_DUE)
|
||||
def scan_cheque_funding(today):
|
||||
"""یادآوری «تأمین موجودی چک» چند روز پس از سررسید چکِ پرداختنشده.
|
||||
|
||||
چند روزِ پس از سررسید از قالب همین نوع (قابل تنظیم از پنل) خوانده میشود و مبلغ
|
||||
چک در متن قرار میگیرد. اگر چک پرداخت شود (``payStatus=true``) دیگر یادآوری
|
||||
نمیگیرد؛ ``dedup_key`` نیز مانع ارسال تکراری در اجراهای بعدی میشود. اگر اسکنر
|
||||
چند روز از دست برود، چکهای «معوقتر» هم واجد شرط میشوند تا یادآوری از دست نرود.
|
||||
"""
|
||||
template = _template(SMSType.CHEQUE_REGISTRATION)
|
||||
if template is None:
|
||||
return 0
|
||||
budget = _Budget(SMSProviderConfig.load())
|
||||
offset = template.offset_days or 0
|
||||
target = today - timedelta(days=offset)
|
||||
qs = Sarresid.objects.filter(
|
||||
type="check", payStatus="false",
|
||||
sarresidDT__date__gte=today,
|
||||
sarresidDT__date__lte=today + timedelta(days=offset),
|
||||
type="check", payStatus="false", sarresidDT__date__lte=target,
|
||||
).select_related("customer").order_by("sarresidDT")
|
||||
count = 0
|
||||
for s in qs:
|
||||
if not s.customer or not s.sarresidDT:
|
||||
continue
|
||||
# پنجرهٔ یادآوری از «سررسید منهای offset» باز میشود.
|
||||
trigger = _aware(s.sarresidDT) - timedelta(days=offset)
|
||||
# لحظهٔ باز شدن پنجرهٔ یادآوری = سررسید + offset روز.
|
||||
trigger = _aware(s.sarresidDT) + timedelta(days=offset)
|
||||
if not budget.can_send(trigger):
|
||||
continue
|
||||
days = (s.sarresidDT.date() - today).days
|
||||
days = (today - s.sarresidDT.date()).days
|
||||
made = enqueue_sms(
|
||||
customer=s.customer, sms_type=SMSType.CHEQUE_DUE,
|
||||
customer=s.customer, sms_type=SMSType.CHEQUE_REGISTRATION,
|
||||
context={
|
||||
"name": s.customer.name_family, "amount": s.price,
|
||||
"date": s.sarresidDT, "days": days, "check_row": s.check_row_number,
|
||||
},
|
||||
dedup_key=f"cheque_due:{s.id}",
|
||||
dedup_key=f"cheque_fund:{s.id}",
|
||||
)
|
||||
if made:
|
||||
budget.record(trigger)
|
||||
@@ -149,119 +153,6 @@ def scan_nesiye_due(today):
|
||||
return count
|
||||
|
||||
|
||||
def scan_cheque_registration(today):
|
||||
"""یادآوری «ثبت اطلاعات چک» برای چکهای پرداختنشدهٔ ثبتنشده.
|
||||
|
||||
هر ``interval`` روز یکبار تکرار میشود و بهمحض اینکه شمارهٔ صیادی چک ثبت شود
|
||||
(یا چک پرداخت شود) متوقف میگردد. دورهٔ تکرار از پنل قابل تنظیم است.
|
||||
"""
|
||||
template = _template(SMSType.CHEQUE_REGISTRATION)
|
||||
if template is None:
|
||||
return 0
|
||||
budget = _Budget(SMSProviderConfig.load())
|
||||
interval = template.repeat_interval_days or CHEQUE_REG_INTERVAL_DEFAULT
|
||||
qs = Sarresid.objects.filter(
|
||||
type="check", payStatus="false", sarresidDT__date__gte=today,
|
||||
).filter(
|
||||
Q(check_sayyad_id="") | Q(check_sayyad_id__isnull=True),
|
||||
).select_related("customer")
|
||||
count = 0
|
||||
for s in qs:
|
||||
if not s.customer:
|
||||
continue
|
||||
anchor = s.createdDT or s.sarresidDT
|
||||
if not anchor:
|
||||
continue
|
||||
days = (today - anchor.date()).days
|
||||
if days <= 0 or days % interval != 0:
|
||||
continue
|
||||
cycle = days // interval
|
||||
# لحظهٔ باز شدن همین دورهٔ تکرار (نه خودِ چک) ملاک خط شروع است.
|
||||
trigger = _aware(anchor) + timedelta(days=days)
|
||||
if not budget.can_send(trigger):
|
||||
continue
|
||||
made = enqueue_sms(
|
||||
customer=s.customer, sms_type=SMSType.CHEQUE_REGISTRATION,
|
||||
context={
|
||||
"name": s.customer.name_family, "amount": s.price,
|
||||
"date": s.sarresidDT, "days": (s.sarresidDT.date() - today).days
|
||||
if s.sarresidDT else "",
|
||||
"check_row": s.check_row_number,
|
||||
},
|
||||
dedup_key=f"cheque_reg:{s.id}:{cycle}",
|
||||
)
|
||||
if made:
|
||||
budget.record(trigger)
|
||||
count += made
|
||||
return count
|
||||
|
||||
|
||||
def scan_debt_due(today):
|
||||
"""یادآوری بدهی برای ماندهدارها روی سررسید وصول مطالبات."""
|
||||
template = _template(SMSType.DEBT_DUE)
|
||||
if template is None:
|
||||
return 0
|
||||
budget = _Budget(SMSProviderConfig.load())
|
||||
offset = template.offset_days or 0
|
||||
target = today - timedelta(days=offset)
|
||||
qs = Sarresid.objects.filter(
|
||||
payStatus="false", sarresidDT__date=target,
|
||||
).select_related("customer").order_by("sarresidDT")
|
||||
count = 0
|
||||
seen = set()
|
||||
for s in qs:
|
||||
if not s.customer or s.customer_id in seen or not s.sarresidDT:
|
||||
continue
|
||||
trigger = _aware(s.sarresidDT)
|
||||
if not budget.can_send(trigger):
|
||||
continue
|
||||
totals = Article.objects.filter(customer=s.customer).aggregate(
|
||||
d=Sum("debit"), c=Sum("credit"),
|
||||
)
|
||||
net = (totals["d"] or 0) - (totals["c"] or 0)
|
||||
if net <= 0:
|
||||
continue
|
||||
made = enqueue_sms(
|
||||
customer=s.customer, sms_type=SMSType.DEBT_DUE,
|
||||
context={"name": s.customer.name_family, "amount": net},
|
||||
dedup_key=f"debt_due:{s.id}",
|
||||
)
|
||||
if made:
|
||||
seen.add(s.customer_id)
|
||||
budget.record(trigger)
|
||||
count += made
|
||||
return count
|
||||
|
||||
|
||||
def scan_survey(today):
|
||||
"""نظرسنجی چند روز پس از ایجاد فاکتور."""
|
||||
template = _template(SMSType.SURVEY)
|
||||
if template is None:
|
||||
return 0
|
||||
budget = _Budget(SMSProviderConfig.load())
|
||||
offset = template.offset_days or 3
|
||||
target = today - timedelta(days=offset)
|
||||
qs = Factor.objects.filter(
|
||||
is_active=True, create_date__date=target,
|
||||
).exclude(pay_status="cancell").select_related("customer").order_by("create_date")
|
||||
count = 0
|
||||
for f in qs:
|
||||
if not f.customer or not f.create_date:
|
||||
continue
|
||||
trigger = _aware(f.create_date) + timedelta(days=offset)
|
||||
if not budget.can_send(trigger):
|
||||
continue
|
||||
made = enqueue_sms(
|
||||
customer=f.customer, sms_type=SMSType.SURVEY,
|
||||
context={"name": f.customer.name_family, "amount": f.amount},
|
||||
dedup_key=f"survey:{f.id}",
|
||||
)
|
||||
if made:
|
||||
budget.record(trigger)
|
||||
count += made
|
||||
return count
|
||||
|
||||
|
||||
def scan_visit(today):
|
||||
"""یادآوری بازدید برای مشتریان عقبافتاده از بازدید (منطق صفحهٔ «انتظار»).
|
||||
|
||||
@@ -313,12 +204,11 @@ def scan_visit(today):
|
||||
return count
|
||||
|
||||
|
||||
# فقط همان ۷ نوع مجاز؛ انواع رویدادی (خوشآمدگویی، نظرسنجی، ثبت بازدید، تشکر از
|
||||
# پرداخت) در ``admin_panel/sms/triggers.py`` توسط ویوها صف میشوند، نه اینجا.
|
||||
SCANNERS = [
|
||||
("cheque_due", scan_cheque_due),
|
||||
("cheque_registration", scan_cheque_funding),
|
||||
("nesiye_due", scan_nesiye_due),
|
||||
("cheque_registration", scan_cheque_registration),
|
||||
("debt_due", scan_debt_due),
|
||||
("survey", scan_survey),
|
||||
("visit", scan_visit),
|
||||
]
|
||||
|
||||
|
||||
+24
-34
@@ -9,8 +9,7 @@ from ..models import SMSType
|
||||
|
||||
NONE = "none"
|
||||
BEFORE_DUE = "before_due"
|
||||
AFTER_CREATION = "after_creation"
|
||||
RECURRING = "recurring"
|
||||
AFTER_DUE = "after_due"
|
||||
PERIOD_MONTHS = "period_months"
|
||||
|
||||
# پیشفرض دورهٔ بازدید (ماه) وقتی مدیر مقداری در قالب نگذاشته باشد.
|
||||
@@ -22,39 +21,18 @@ TIMING = {
|
||||
"field": None,
|
||||
"note": "بهمحض ثبت مشتری و بدون تأخیر ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||
},
|
||||
SMSType.CHEQUE_DUE: {
|
||||
"kind": BEFORE_DUE,
|
||||
"field": "offset_days",
|
||||
"label": "چند روز قبل از سررسید چک",
|
||||
"default": 2,
|
||||
"note": "پنجرهٔ یادآوری از «سررسید − این تعداد روز» باز میشود و تا سررسید ادامه دارد.",
|
||||
},
|
||||
SMSType.NESIYE_DUE: {
|
||||
"kind": BEFORE_DUE,
|
||||
"field": "offset_days",
|
||||
"label": "چند روز قبل از سررسید نسیه",
|
||||
"default": 2,
|
||||
"note": "پنجرهٔ یادآوری از «سررسید − این تعداد روز» باز میشود و تا سررسید ادامه دارد.",
|
||||
},
|
||||
SMSType.CHEQUE_REGISTRATION: {
|
||||
"kind": RECURRING,
|
||||
"field": "repeat_interval_days",
|
||||
"label": "هر چند روز یکبار",
|
||||
"default": 3,
|
||||
"note": "تا زمانی که اطلاعات ثبت چک (شمارهٔ صیادی) وارد نشده و چک پرداخت نشده، "
|
||||
"با این فاصله تکرار میشود.",
|
||||
},
|
||||
SMSType.SURVEY: {
|
||||
"kind": AFTER_CREATION,
|
||||
"field": "offset_days",
|
||||
"label": "چند روز بعد از صدور فاکتور",
|
||||
"default": 3,
|
||||
"note": "این تعداد روز پس از ثبت فاکتور ارسال میشود.",
|
||||
},
|
||||
SMSType.DEBT_DUE: {
|
||||
"kind": NONE,
|
||||
"field": None,
|
||||
"note": "روی سررسیدِ وصول مطالبات و فقط برای مشتریانی که مانده بدهکارند ارسال میشود.",
|
||||
"note": "بیدرنگ پس از ثبت فاکتور ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||
},
|
||||
SMSType.CHEQUE_REGISTRATION: {
|
||||
"kind": AFTER_DUE,
|
||||
"field": "offset_days",
|
||||
"label": "چند روز پس از سررسید چک",
|
||||
"default": 1,
|
||||
"note": "این تعداد روز پس از سررسید چکِ پرداختنشده، پیامک یادآوری "
|
||||
"تأمین موجودی با مبلغ چک ارسال میشود.",
|
||||
},
|
||||
SMSType.VISIT: {
|
||||
"kind": PERIOD_MONTHS,
|
||||
@@ -63,10 +41,22 @@ TIMING = {
|
||||
"default": DEFAULT_VISIT_MONTHS,
|
||||
"note": "برای مشتریانی که از این مدت بازدید نشدهاند ارسال میشود.",
|
||||
},
|
||||
SMSType.CUSTOM: {
|
||||
SMSType.VISIT_CREATED: {
|
||||
"kind": NONE,
|
||||
"field": None,
|
||||
"note": "فقط بهصورت دستی از صفحهٔ مشتری ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||
"note": "بهمحض ثبت «اقدام از نوع بازدید» ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||
},
|
||||
SMSType.PAYMENT_THANKS: {
|
||||
"kind": NONE,
|
||||
"field": None,
|
||||
"note": "بیدرنگ پس از ثبت پرداخت (سند وصول/بستانکار) ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||
},
|
||||
SMSType.NESIYE_DUE: {
|
||||
"kind": BEFORE_DUE,
|
||||
"field": "offset_days",
|
||||
"label": "چند روز قبل از سررسید نسیه",
|
||||
"default": 0,
|
||||
"note": "پنجرهٔ یادآوری از «سررسید − این تعداد روز» باز میشود و تا سررسید ادامه دارد.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""تریگرهای رویدادی پیامک (لایهٔ سرویس، بدون signal).
|
||||
|
||||
چهار نوع پیامک «رویدادی» هستند و لحظهٔ وقوعشان یک اکشن پنل است، نه یک سررسید
|
||||
تاریخی؛ پس بهجای اسکنر دورهای، ویوها بعد از انجام کارِ اصلی این توابع را صدا
|
||||
میزنند:
|
||||
|
||||
* :func:`notify_welcome` — ایجاد مشتری.
|
||||
* :func:`notify_factor_survey` — ثبت فاکتور (نظرسنجی).
|
||||
* :func:`notify_visit_scheduled` — ثبت «اقدام از نوع بازدید».
|
||||
* :func:`notify_payment_thanks` — ثبت پرداخت (سند بستانکار / وصول).
|
||||
|
||||
قرارداد پروژه: استفاده از signal ممنوع است؛ همهچیز صریح از دلِ ویو صدا زده میشود.
|
||||
این توابع هیچ استثنایی به فراخوان پرتاب نمیکنند (``enqueue_sms`` خودش مقاوم است) تا
|
||||
مسیر اصلی هرگز بهخاطر پیامک مختل نشود.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ..models import SMSType
|
||||
from .enqueue import enqueue_sms
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# روشهای «غیرپرداخت»: سند بستانکاری که بهخاطر تخفیف صادر شده، پرداختِ واقعی نیست
|
||||
# و نباید پیامک «تشکر از پرداخت» بگیرد.
|
||||
NON_PAYMENT_METHODS = {"تخفیف"}
|
||||
|
||||
|
||||
def notify_welcome(customer, *, created_by=None):
|
||||
"""پیامک خوشآمدگویی در لحظهٔ ساخت مشتری."""
|
||||
if customer is None or customer.pk is None:
|
||||
return 0
|
||||
return enqueue_sms(
|
||||
customer=customer, sms_type=SMSType.WELCOME,
|
||||
context={"name": customer.name_family},
|
||||
dedup_key=f"welcome:{customer.pk}",
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
|
||||
def notify_factor_survey(customer, factor, *, created_by=None):
|
||||
"""پیامک نظرسنجی، بیدرنگ پس از ثبت فاکتور."""
|
||||
if customer is None or factor is None or factor.pk is None:
|
||||
return 0
|
||||
return enqueue_sms(
|
||||
customer=customer, sms_type=SMSType.SURVEY,
|
||||
context={"name": customer.name_family, "amount": factor.amount},
|
||||
dedup_key=f"survey:{factor.pk}",
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
|
||||
def notify_visit_scheduled(customer, action, *, created_by=None):
|
||||
"""پیامک «بازدید شما برای تاریخ … ثبت شد» پس از ایجاد اقدام بازدید."""
|
||||
if customer is None or action is None or action.pk is None:
|
||||
return 0
|
||||
return enqueue_sms(
|
||||
customer=customer, sms_type=SMSType.VISIT_CREATED,
|
||||
context={"name": customer.name_family, "date": action.actionDateTime},
|
||||
dedup_key=f"visit_created:{action.pk}",
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
|
||||
def notify_payment_thanks(customer, amount, *, article_id=None, method=None,
|
||||
created_by=None):
|
||||
"""پیامک تشکر از پرداخت؛ پس از ثبت سند پرداخت (بستانکار/وصول).
|
||||
|
||||
``article_id`` برای یکتاسازی استفاده میشود تا یک پرداخت هرگز دو پیامک نگیرد.
|
||||
``method`` نحوهٔ پرداخت است؛ اگر «تخفیف» باشد (که پرداختِ واقعی نیست) هیچ پیامکی
|
||||
ساخته نمیشود.
|
||||
"""
|
||||
if customer is None:
|
||||
return 0
|
||||
if method and str(method).strip() in NON_PAYMENT_METHODS:
|
||||
return 0
|
||||
dedup_key = f"payment_thanks:{article_id}" if article_id is not None else None
|
||||
return enqueue_sms(
|
||||
customer=customer, sms_type=SMSType.PAYMENT_THANKS,
|
||||
context={"name": customer.name_family, "amount": amount},
|
||||
dedup_key=dedup_key,
|
||||
created_by=created_by,
|
||||
)
|
||||
Reference in new issue
Block a user