Files
khatibi/admin_panel/sms/enqueue.py
T
2026-10-16 17:54:07 +03:30

84 lines
4.4 KiB
Python

"""قرار دادن پیامک در صف ارسال (insert-only، بی‌خطر برای مسیر request)."""
import logging
from uuid import uuid4
from django.db import IntegrityError
from ..models import SMSDedup, SMSQueue, SMSTemplate, SMSType
from .balance import customer_balance
from .recipients import resolve_recipients
from .render import render_template
from .rules import is_sms_enabled
logger = logging.getLogger(__name__)
def enqueue_sms(*, customer=None, sms_type, context=None, dedup_key=None,
message=None, recipients=None, created_by=None, approved=False):
"""پیامک را در صف قرار می‌دهد و تعداد رکوردهای ساخته‌شده را برمی‌گرداند.
به‌صورت پیش‌فرض پیامک در وضعیت «در انتظار تأیید» ثبت می‌شود و تا وقتی مدیر
آن را از پنل تأیید نکند، worker آن را ارسال نمی‌کند. دو راه برای پرش از این
بازبینی هست: ``approved=True`` برای ارسال‌های دستیِ صریح مدیر (مثل پیامک سفارشی
یا آزمایشی)، و ``SMSTemplate.auto_send`` که مدیر در صفحهٔ قالب‌ها برای هر نوع
پیامک روشن می‌کند تا پیامک‌های خودکارِ اسکنر بدون صف تأیید ارسال شوند.
متغیرهای مشترکِ همهٔ قالب‌ها (مثل ``{balance}`` = مانده حساب مشتری) همین‌جا و
یک‌بار به ``context`` افزوده می‌شوند تا هر قالبی بتواند از آن‌ها استفاده کند؛
فراخوان‌ها لازم نیست هر بار آن‌ها را بسازند. اگر فراخوان خودش مقداری داده باشد
(``context``) همان اولویت دارد.
هیچ استثنایی به فراخوان پرتاب نمی‌شود؛ خطاها فقط لاگ می‌شوند تا مسیر اصلی
(مثلاً ایجاد مشتری یا اسکنر) هرگز به‌خاطر پیامک مختل نشود.
"""
try:
if message is None:
template = SMSTemplate.objects.filter(sms_type=sms_type, is_enabled=True).first()
if template is None:
return 0
ctx = dict(context or {})
# متغیرهای سراسری قالب‌ها؛ هرگز مقدار فراخوان را بازنویسی نمی‌کنند.
ctx.setdefault("balance", customer_balance(customer))
message = render_template(template.text, ctx)
# «ارسال خودکار» هر نوع پیامک در صفحهٔ قالب‌ها تعیین می‌شود؛ خاموش = بازبینی مدیر.
approved = approved or template.auto_send
if not is_sms_enabled(customer, sms_type):
return 0
numbers = recipients if recipients is not None else resolve_recipients(customer)
if not numbers:
return 0
if dedup_key is None:
dedup_key = f"{sms_type}:{uuid4().hex}"
created = 0
for number in numbers:
key = dedup_key if len(numbers) == 1 else f"{dedup_key}:{number}"
try:
# دفتر کلیدها مستقل از صف است: کلیدِ یک‌بار خورده هرگز دوباره
# ایجاد نمی‌شود، حتی اگر رکورد صف را مدیر حذف کرده باشد.
if not SMSDedup.objects.get_or_create(key=key)[1]:
continue
_, made = SMSQueue.objects.get_or_create(
dedup_key=key,
defaults={
"customer": customer,
"sms_type": sms_type,
"phone_number": number,
"message": message,
"created_by": created_by,
"status": (SMSQueue.Status.APPROVED if approved
else SMSQueue.Status.PENDING),
},
)
created += 1 if made else 0
except IntegrityError:
# رقابت هم‌زمان روی dedup_key؛ نادیده گرفته می‌شود.
continue
return created
except Exception:
logger.exception("enqueue_sms برای نوع=%s شکست خورد", sms_type)
return 0