"""قرار دادن پیامک در صف ارسال (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