HH/apps/notifications/models.py
2026-03-28 14:03:56 +03:00

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")