import uuid from django.conf import settings from django.db import models class NotificationEventType(models.TextChoices): COURSE_ASSIGNED = "course_assigned", "Course Assigned" COURSE_DUE = "course_due", "Course Due Soon" ATTEMPT_LIMIT_REACHED = "attempt_limit_reached", "Attempt Limit Reached" CERTIFICATE_ISSUED = "certificate_issued", "Certificate Issued" CERTIFICATE_EXPIRING = "certificate_expiring", "Certificate Expiring" class DeliveryChannel(models.TextChoices): IN_APP = "in_app", "In-App" EMAIL = "email", "Email" class DeliveryStatus(models.TextChoices): PENDING = "pending", "Pending" SENT = "sent", "Sent" FAILED = "failed", "Failed" DELIVERED = "delivered", "Delivered" class Notification(models.Model): """ In-app notification record. One row per (recipient, event, object). idempotency_key prevents duplicate delivery for the same logical event. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) recipient = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notifications", ) event_type = models.CharField(max_length=40, choices=NotificationEventType.choices, db_index=True) title = models.CharField(max_length=255) body = models.TextField(blank=True) object_type = models.CharField(max_length=64, blank=True) object_id = models.CharField(max_length=128, blank=True) # Caller-supplied dedup key; format: ":::" idempotency_key = models.CharField(max_length=255, unique=True, db_index=True) created_at = models.DateTimeField(auto_now_add=True) read_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "notifications_notification" ordering = ["-created_at"] def __str__(self): return f"{self.event_type} → {self.recipient_id}" @property def is_read(self): return self.read_at is not None class NotificationDelivery(models.Model): """Audit record for each delivery attempt per channel.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) notification = models.ForeignKey( Notification, on_delete=models.CASCADE, related_name="deliveries" ) channel = models.CharField(max_length=20, choices=DeliveryChannel.choices) status = models.CharField( max_length=20, choices=DeliveryStatus.choices, default=DeliveryStatus.PENDING ) sent_at = models.DateTimeField(null=True, blank=True) error_detail = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "notifications_delivery" ordering = ["-created_at"] def __str__(self): return f"{self.channel}/{self.status} for notification {self.notification_id}"