84 lines
4.4 KiB
Python
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
|