This commit is contained in:
javad committed 2026-10-11 19:04:20 +03:30
1 parent 1dea3509a7
commit e3a6d1f9d4
42 files changed
+33086 -15779

No files matched your search

+27 -137
View File
@@ -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),
]