Files
customerTicket/apps/notifications/models.py
T
2026-09-14 15:48:13 +03:30

67 lines
5.2 KiB
Python

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from apps.core.models import PersianTextNormalizationMixin
def sms_template_tags():
return ["magic_link", "first_name", "last_name", "recipient_name", "unit_name", "company_name", "ticket_number", "subject", "province", "city", "expert_name"]
class SMSTemplate(PersianTextNormalizationMixin, models.Model):
persian_text_fields={"title":False,"body":True}
class Event(models.TextChoices):
CUSTOMER_TICKET_CREATED="CUSTOMER_TICKET_CREATED","ثبت تیکت توسط مشتری"
STAFF_TICKET_CREATED="STAFF_TICKET_CREATED","ثبت تیکت توسط کارشناس"
STAFF_REPLY="STAFF_REPLY","پاسخ کارشناس"
unit=models.ForeignKey("staff.OrganizationalUnit",on_delete=models.CASCADE,null=True,blank=True,related_name="sms_templates",help_text="خالی یعنی الگوی عمومی و fallback.")
event_type=models.CharField(max_length=50,choices=Event.choices)
title=models.CharField(max_length=120)
body=models.TextField(help_text="تگ‌ها را مانند {magic_link} در متن قرار دهید.")
allowed_tags=models.JSONField(default=sms_template_tags,blank=True)
is_active=models.BooleanField(default=True)
updated_at=models.DateTimeField(auto_now=True)
class Meta:
constraints=[models.UniqueConstraint(fields=["unit","event_type"],name="unique_sms_template_per_unit"),models.UniqueConstraint(fields=["event_type"],condition=models.Q(unit__isnull=True),name="unique_global_sms_template_event")]
def __str__(self): return self.get_event_type_display()
def clean(self):
from string import Formatter
tags={field for _,field,_,_ in Formatter().parse(self.body) if field}
invalid=tags-set(self.allowed_tags)
if invalid: raise ValidationError({"body":f"تگ‌های غیرمجاز: {', '.join(sorted(invalid))}"})
class NotificationPolicy(models.Model):
class Key(models.TextChoices):
STAFF_REPLY_SMS="STAFF_REPLY_SMS","پیامک پاسخ کارشناس"
CUSTOMER_REPLY_BALE="CUSTOMER_REPLY_BALE","اعلان پاسخ مشتری در بله"
key=models.CharField(max_length=50,choices=Key.choices,unique=True)
quiet_period_seconds=models.PositiveIntegerField(default=300,help_text="پس از آخرین فعالیت تیکت، چند ثانیه برای ادامه گفت‌وگو صبر شود.")
is_active=models.BooleanField(default=True)
updated_at=models.DateTimeField(auto_now=True)
class Meta:
verbose_name="سیاست اعلان"
verbose_name_plural="سیاست‌های اعلان"
def __str__(self): return self.get_key_display()
class NotificationJob(models.Model):
class Channel(models.TextChoices): SMS="SMS","پیامک"; BALE="BALE","بله"
class Status(models.TextChoices): PENDING="PENDING","در صف"; PROCESSING="PROCESSING","در حال ارسال"; RETRYING="RETRYING","در انتظار تلاش مجدد"; SENT="SENT","ارسال‌شده"; FAILED="FAILED","ناموفق"; CANCELLED="CANCELLED","لغوشده"
class DeliveryStatus(models.TextChoices):
PENDING="PENDING","در انتظار استعلام"
DELIVERED="DELIVERED","رسیده به گوشی"
UNDELIVERED="UNDELIVERED","نرسیده به گوشی"
EXPIRED="EXPIRED","منقضی‌شده"
BLOCKED="BLOCKED","مسدود توسط مخاطب"
UNKNOWN="UNKNOWN","نامشخص"
ERROR="ERROR","خطای دلیوری"
CHECK_FAILED="CHECK_FAILED","خطای موقت استعلام"
channel=models.CharField(max_length=10,choices=Channel.choices); event_type=models.CharField(max_length=50); customer=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,null=True,blank=True,related_name="customer_notifications"); staff_user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,null=True,blank=True,related_name="staff_notifications"); ticket=models.ForeignKey("tickets.Ticket",on_delete=models.CASCADE,null=True,blank=True); payload=models.JSONField(default=dict); status=models.CharField(max_length=15,choices=Status.choices,default=Status.PENDING,db_index=True); attempts=models.PositiveSmallIntegerField(default=0); next_attempt_at=models.DateTimeField(db_index=True); last_error=models.TextField(blank=True); provider_message_id=models.CharField(max_length=500,blank=True); dedupe_key=models.CharField(max_length=200,unique=True); created_at=models.DateTimeField(auto_now_add=True); sent_at=models.DateTimeField(null=True,blank=True)
provider_status_code=models.CharField(max_length=50,blank=True)
delivery_status=models.CharField(max_length=20,choices=DeliveryStatus.choices,blank=True,db_index=True)
delivery_status_code=models.CharField(max_length=50,blank=True)
delivery_description=models.CharField(max_length=500,blank=True)
delivery_checked_at=models.DateTimeField(null=True,blank=True)
delivery_check_attempts=models.PositiveSmallIntegerField(default=0)
delivery_next_check_at=models.DateTimeField(null=True,blank=True,db_index=True)
class Meta:
indexes=[models.Index(fields=["status","next_attempt_at"],name="notif_due_idx"),models.Index(fields=["ticket","channel","status"],name="notif_ticket_chan_idx")]