from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField from django_choices_field import TextChoicesField from core.models.base import BaseModel from core.models.enums import ( EventTypeChoices, NotificationChannelChoices, NotificationStatusChoices, DeliveryStatusChoices, RoleChoices ) class Event(BaseModel): """ Event model to track system events that may trigger notifications. Provides audit trail and basis for notification system. """ event_type = TextChoicesField( choices_enum=EventTypeChoices, help_text="Type of event that occurred" ) entity_type = models.CharField( max_length=100, help_text="Type of entity (e.g., 'Project', 'Report', 'Invoice')" ) entity_id = models.UUIDField( help_text="UUID of the entity that triggered this event" ) metadata = models.JSONField( default=dict, blank=True, help_text="Additional event metadata (e.g., old_status, new_status, changed_fields)" ) # Generic foreign key to support both TeamProfile and CustomerProfile triggered_by_content_type = models.ForeignKey( ContentType, on_delete=models.SET_NULL, null=True, blank=True, related_name='triggered_events' ) triggered_by_object_id = models.UUIDField( null=True, blank=True ) triggered_by = GenericForeignKey('triggered_by_content_type', 'triggered_by_object_id') class Meta: db_table = 'events' ordering = ['-created_at'] indexes = [ models.Index(fields=['event_type', 'created_at']), models.Index(fields=['entity_type', 'entity_id']), models.Index(fields=['created_at']), ] def __str__(self): return f"{self.event_type} - {self.entity_type}:{self.entity_id} at {self.created_at}" class NotificationRule(BaseModel): """ Admin-defined rules for generating notifications based on events. """ name = models.CharField( max_length=200, help_text="Descriptive name for this notification rule" ) description = models.TextField( blank=True, help_text="Description of when and how this rule applies" ) event_types = ArrayField( TextChoicesField(choices_enum=EventTypeChoices), help_text="List of event types that trigger this rule" ) channels = ArrayField( TextChoicesField(choices_enum=NotificationChannelChoices), help_text="Delivery channels for notifications (IN_APP, EMAIL, SMS)" ) target_roles = ArrayField( TextChoicesField(choices_enum=RoleChoices), blank=True, default=list, help_text="Roles that should receive notifications (empty = all authenticated users)" ) target_team_profiles = models.ManyToManyField( 'TeamProfile', blank=True, related_name='notification_rules', help_text="Specific team profiles to notify" ) target_customer_profiles = models.ManyToManyField( 'CustomerProfile', blank=True, related_name='notification_rules', help_text="Specific customer profiles to notify" ) is_active = models.BooleanField( default=True, help_text="Whether this rule is currently active" ) template_subject = models.CharField( max_length=500, blank=True, help_text="Template for notification subject (supports variables)" ) template_body = models.TextField( blank=True, help_text="Template for notification body (supports variables)" ) conditions = models.JSONField( default=dict, blank=True, help_text="Additional conditions for when this rule applies (e.g., {'status': 'COMPLETED'})" ) class Meta: db_table = 'notification_rules' ordering = ['name'] def __str__(self): return f"{self.name} ({', '.join(self.event_types)})" class Notification(BaseModel): """ Individual notification instance sent to a specific recipient. """ event = models.ForeignKey( Event, on_delete=models.CASCADE, related_name='notifications', help_text="Event that triggered this notification" ) rule = models.ForeignKey( NotificationRule, on_delete=models.SET_NULL, null=True, blank=True, related_name='notifications', help_text="Rule that generated this notification" ) # Generic foreign key to support both TeamProfile and CustomerProfile recipient_content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, related_name='notifications' ) recipient_object_id = models.UUIDField() recipient = GenericForeignKey('recipient_content_type', 'recipient_object_id') status = TextChoicesField( choices_enum=NotificationStatusChoices, default=NotificationStatusChoices.PENDING, help_text="Current status of the notification" ) subject = models.CharField( max_length=500, help_text="Notification subject line" ) body = models.TextField( help_text="Notification body content" ) action_url = models.URLField( blank=True, max_length=500, help_text="Optional URL for action button (e.g., link to project detail)" ) read_at = models.DateTimeField( null=True, blank=True, help_text="Timestamp when notification was marked as read" ) metadata = models.JSONField( default=dict, blank=True, help_text="Additional notification metadata" ) class Meta: db_table = 'notifications' ordering = ['-created_at'] indexes = [ models.Index(fields=['recipient_content_type', 'recipient_object_id', 'status', 'created_at']), models.Index(fields=['recipient_content_type', 'recipient_object_id', 'read_at']), models.Index(fields=['event']), ] def __str__(self): return f"Notification for {self.recipient} - {self.subject}" def mark_as_read(self): """Mark notification as read""" if not self.read_at: from django.utils import timezone self.read_at = timezone.now() self.status = NotificationStatusChoices.READ self.save(update_fields=['read_at', 'status', 'updated_at']) class NotificationDelivery(BaseModel): """ Track delivery attempts for a notification via specific channels. """ notification = models.ForeignKey( Notification, on_delete=models.CASCADE, related_name='deliveries', help_text="Notification being delivered" ) channel = TextChoicesField( choices_enum=NotificationChannelChoices, help_text="Delivery channel (IN_APP, EMAIL, SMS)" ) status = TextChoicesField( choices_enum=DeliveryStatusChoices, default=DeliveryStatusChoices.PENDING, help_text="Current delivery status" ) attempts = models.PositiveIntegerField( default=0, help_text="Number of delivery attempts" ) last_attempt_at = models.DateTimeField( null=True, blank=True, help_text="Timestamp of last delivery attempt" ) sent_at = models.DateTimeField( null=True, blank=True, help_text="Timestamp when successfully sent" ) delivered_at = models.DateTimeField( null=True, blank=True, help_text="Timestamp when delivery was confirmed (if supported by channel)" ) error_message = models.TextField( blank=True, help_text="Error message from failed delivery attempts" ) external_id = models.CharField( max_length=200, blank=True, help_text="External service ID (e.g., Twilio message SID, email message ID)" ) metadata = models.JSONField( default=dict, blank=True, help_text="Additional delivery metadata" ) class Meta: db_table = 'notification_deliveries' ordering = ['-created_at'] indexes = [ models.Index(fields=['notification', 'channel']), models.Index(fields=['status', 'last_attempt_at']), models.Index(fields=['channel', 'status']), ] unique_together = [['notification', 'channel']] def __str__(self): return f"{self.channel} delivery for notification {self.notification_id} - {self.status}"