""" Notifications models - Multi-channel notification delivery This module implements the notification system that: - Sends SMS, WhatsApp, and Email notifications - Logs all notification attempts - Tracks delivery status - Supports retry logic """ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from apps.core.models import BaseChoices, TimeStampedModel, UUIDModel class NotificationChannel(BaseChoices): """Notification channel choices""" SMS = "sms", "SMS" WHATSAPP = "whatsapp", "WhatsApp" EMAIL = "email", "Email" PUSH = "push", "Push Notification" class NotificationStatus(BaseChoices): """Notification delivery status""" PENDING = "pending", "Pending" SENDING = "sending", "Sending" SENT = "sent", "Sent" DELIVERED = "delivered", "Delivered" FAILED = "failed", "Failed" BOUNCED = "bounced", "Bounced" class NotificationLog(UUIDModel, TimeStampedModel): """ Notification log - tracks all notification attempts. Logs every SMS, WhatsApp, and Email sent by the system. Used for: - Delivery tracking - Debugging - Compliance - Analytics """ # Channel and recipient channel = models.CharField(max_length=20, choices=NotificationChannel.choices, db_index=True) recipient = models.CharField(max_length=200, help_text="Phone number or email address") # Message content subject = models.CharField(max_length=500, blank=True) message = models.TextField() # Related object (generic foreign key) content_type = models.ForeignKey(ContentType, on_delete=models.SET_NULL, null=True, blank=True) object_id = models.UUIDField(null=True, blank=True) content_object = GenericForeignKey("content_type", "object_id") # Delivery status status = models.CharField( max_length=20, choices=NotificationStatus.choices, default=NotificationStatus.PENDING, db_index=True ) # Timestamps sent_at = models.DateTimeField(null=True, blank=True) delivered_at = models.DateTimeField(null=True, blank=True) # Provider response provider = models.CharField(max_length=50, blank=True, help_text="SMS/Email provider used") provider_message_id = models.CharField(max_length=200, blank=True, help_text="Message ID from provider") provider_response = models.JSONField(default=dict, blank=True, help_text="Full response from provider") # Error tracking error = models.TextField(blank=True) retry_count = models.IntegerField(default=0) # Metadata metadata = models.JSONField(default=dict, blank=True, help_text="Additional metadata (campaign, template, etc.)") class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["channel", "status", "-created_at"]), models.Index(fields=["recipient", "-created_at"]), models.Index(fields=["content_type", "object_id"]), ] def __str__(self): return f"{self.channel} to {self.recipient} ({self.status})" def mark_sent(self, provider_message_id=None): """Mark notification as sent""" from django.utils import timezone self.status = NotificationStatus.SENT self.sent_at = timezone.now() if provider_message_id: self.provider_message_id = provider_message_id self.save(update_fields=["status", "sent_at", "provider_message_id"]) def mark_delivered(self): """Mark notification as delivered""" from django.utils import timezone self.status = NotificationStatus.DELIVERED self.delivered_at = timezone.now() self.save(update_fields=["status", "delivered_at"]) def mark_failed(self, error_message): """Mark notification as failed""" self.status = NotificationStatus.FAILED self.error = error_message self.retry_count += 1 self.save(update_fields=["status", "error", "retry_count"]) class NotificationTemplate(UUIDModel, TimeStampedModel): """ Notification template for consistent messaging. Supports: - Bilingual templates (AR/EN) - Variable substitution - Multiple channels """ name = models.CharField(max_length=200, unique=True) description = models.TextField(blank=True) # Template type template_type = models.CharField( max_length=50, choices=[ ("survey_invitation", "Survey Invitation"), ("survey_reminder", "Survey Reminder"), ("complaint_acknowledgment", "Complaint Acknowledgment"), ("complaint_update", "Complaint Update"), ("action_assignment", "Action Assignment"), ("sla_reminder", "SLA Reminder"), ("sla_breach", "SLA Breach"), ("onboarding_invitation", "Onboarding Invitation"), ("onboarding_reminder", "Onboarding Reminder"), ("onboarding_completion", "Onboarding Completion"), ], db_index=True, ) # Channel-specific templates sms_template = models.TextField(blank=True, help_text="SMS template with {{variables}}") sms_template_ar = models.TextField(blank=True) whatsapp_template = models.TextField(blank=True, help_text="WhatsApp template with {{variables}}") whatsapp_template_ar = models.TextField(blank=True) email_subject = models.CharField(max_length=500, blank=True) email_subject_ar = models.CharField(max_length=500, blank=True) email_template = models.TextField(blank=True, help_text="Email HTML template with {{variables}}") email_template_ar = models.TextField(blank=True) # Configuration is_active = models.BooleanField(default=True) class Meta: ordering = ["name"] def __str__(self): return self.name class UserNotification(UUIDModel, TimeStampedModel): """ In-app notification for users - created for every email sent. Auto-deleted after 30 days via management command. """ user = models.ForeignKey("accounts.User", on_delete=models.CASCADE, related_name="notifications") # Content (bilingual) title = models.CharField(max_length=200) title_ar = models.CharField(max_length=200, blank=True) message = models.TextField() message_ar = models.TextField(blank=True) # Type matching email/notification types notification_type = models.CharField(max_length=50) # Related object (generic foreign key) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) object_id = models.UUIDField(null=True, blank=True) content_object = GenericForeignKey("content_type", "object_id") # Navigation action_url = models.CharField(max_length=500, blank=True) # Status is_read = models.BooleanField(default=False) read_at = models.DateTimeField(null=True, blank=True) is_dismissed = models.BooleanField(default=False) dismissed_at = models.DateTimeField(null=True, blank=True) # Link to email log email_log = models.ForeignKey( NotificationLog, on_delete=models.SET_NULL, null=True, blank=True, related_name="user_notification" ) class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["user", "is_dismissed", "-created_at"]), models.Index(fields=["user", "is_read", "-created_at"]), models.Index(fields=["created_at"]), ] def __str__(self): return f"{self.notification_type} for {self.user.email}" def mark_as_read(self): """Mark notification as read""" from django.utils import timezone self.is_read = True self.read_at = timezone.now() self.save(update_fields=["is_read", "read_at"]) def mark_as_dismissed(self): """Mark notification as dismissed""" from django.utils import timezone self.is_dismissed = True self.dismissed_at = timezone.now() self.save(update_fields=["is_dismissed", "dismissed_at"]) def get_title(self): """Get title based on user language preference""" from django.utils.translation import get_language if get_language() == "ar" and self.title_ar: return self.title_ar return self.title def get_message(self): """Get message based on user language preference""" from django.utils.translation import get_language if get_language() == "ar" and self.message_ar: return self.message_ar return self.message def get_icon(self): """Get icon based on notification type""" icons = { "complaint_assigned": "user-check", "complaint_update": "file-text", "sla_reminder": "clock", "sla_breach": "alert-triangle", "action_required": "alert-circle", "mention": "at-sign", "survey_invitation": "clipboard", "survey_reminder": "bell", "onboarding_invitation": "user-plus", "system": "bell", } return icons.get(self.notification_type, "bell")