268 lines
9.0 KiB
Python
268 lines
9.0 KiB
Python
"""
|
|
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")
|