""" Communications app models. """ import uuid from datetime import timedelta from django.db import models from django.utils import timezone from django.core.validators import MinValueValidator, MaxValueValidator from decimal import Decimal from django.conf import settings class Message(models.Model): """ Model for internal messaging system. """ class MessageType(models.TextChoices): INTERNAL = 'INTERNAL', 'Internal Message' EMAIL = 'EMAIL', 'Email' SMS = 'SMS', 'SMS' PUSH = 'PUSH', 'Push Notification' SLACK = 'SLACK', 'Slack Message' TEAMS = 'TEAMS', 'Microsoft Teams' WEBHOOK = 'WEBHOOK', 'Webhook' SYSTEM = 'SYSTEM', 'System Message' ALERT = 'ALERT', 'Alert Message' class PriorityLevel(models.TextChoices): LOW = 'LOW', 'Low' NORMAL = 'NORMAL', 'Normal' HIGH = 'HIGH', 'High' URGENT = 'URGENT', 'Urgent' CRITICAL = 'CRITICAL', 'Critical' class MessageStatus(models.TextChoices): DRAFT = 'DRAFT', 'Draft' PENDING = 'PENDING', 'Pending' SENDING = 'SENDING', 'Sending' SENT = 'SENT', 'Sent' DELIVERED = 'DELIVERED', 'Delivered' READ = 'READ', 'Read' FAILED = 'FAILED', 'Failed' CANCELLED = 'CANCELLED', 'Cancelled' # Primary identification message_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, help_text="Unique identifier for the message" ) tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, help_text="Tenant organization" ) # Message details subject = models.CharField( max_length=255, help_text="Message subject line" ) content = models.TextField( help_text="Message content/body" ) message_type = models.CharField( max_length=20, choices=MessageType.choices, default=MessageType.INTERNAL, help_text="Type of message" ) priority = models.CharField( max_length=20, choices=PriorityLevel.choices, default=PriorityLevel.NORMAL, help_text="Message priority level" ) # Sender and recipients sender = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sent_messages', help_text="Message sender" ) # Message status status = models.CharField( max_length=20, choices=MessageStatus.choices, default=MessageStatus.DRAFT, help_text="Message status" ) # Timing created_at = models.DateTimeField( auto_now_add=True, help_text="Message creation timestamp" ) scheduled_at = models.DateTimeField( null=True, blank=True, help_text="Scheduled send time" ) sent_at = models.DateTimeField( null=True, blank=True, help_text="Actual send timestamp" ) expires_at = models.DateTimeField( null=True, blank=True, help_text="Message expiration time" ) # Message configuration is_urgent = models.BooleanField( default=False, help_text="Urgent message flag" ) requires_acknowledgment = models.BooleanField( default=False, help_text="Requires recipient acknowledgment" ) is_confidential = models.BooleanField( default=False, help_text="Confidential message flag" ) # Delivery tracking delivery_attempts = models.PositiveIntegerField( default=0, help_text="Number of delivery attempts" ) max_delivery_attempts = models.PositiveIntegerField( default=3, help_text="Maximum delivery attempts" ) # Message metadata message_thread_id = models.UUIDField( null=True, blank=True, help_text="Thread ID for message grouping" ) reply_to_message = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, help_text="Original message if this is a reply" ) # External references external_message_id = models.CharField( max_length=255, null=True, blank=True, help_text="External system message ID" ) # Attachments and formatting has_attachments = models.BooleanField( default=False, help_text="Message has attachments" ) content_type = models.CharField( max_length=50, default='text/plain', help_text="Content MIME type" ) class Meta: db_table = 'communications_message' indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['sender', 'created_at']), models.Index(fields=['message_type', 'priority']), models.Index(fields=['scheduled_at']), models.Index(fields=['message_thread_id']), ] ordering = ['-created_at'] def __str__(self): return f"{self.subject} ({self.message_type})" @property def is_overdue(self): """Check if message is overdue for delivery.""" if self.scheduled_at and self.status in ['PENDING', 'SENDING']: return timezone.now() > self.scheduled_at return False @property def is_expired(self): """Check if message has expired.""" if self.expires_at: return timezone.now() > self.expires_at return False class MessageRecipient(models.Model): class RecipientType(models.TextChoices): USER = 'USER', 'User' EMAIL = 'EMAIL', 'Email Address' PHONE = 'PHONE', 'Phone Number' ROLE = 'ROLE', 'User Role' DEPARTMENT = 'DEPARTMENT', 'Department' GROUP = 'GROUP', 'User Group' class RecipientStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' SENT = 'SENT', 'Sent' DELIVERED = 'DELIVERED', 'Delivered' READ = 'READ', 'Read' ACKNOWLEDGED = 'ACKNOWLEDGED', 'Acknowledged' FAILED = 'FAILED', 'Failed' BOUNCED = 'BOUNCED', 'Bounced' UNSUBSCRIBED = 'UNSUBSCRIBED', 'Unsubscribed' # Primary identification recipient_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, help_text="Unique identifier for the recipient" ) message = models.ForeignKey( Message, on_delete=models.CASCADE, related_name='recipients', help_text="Associated message" ) # Recipient details recipient_type = models.CharField( max_length=20, choices=RecipientType.choices, help_text="Type of recipient" ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True, help_text="User recipient" ) email_address = models.EmailField( null=True, blank=True, help_text="Email address recipient" ) phone_number = models.CharField( max_length=20, null=True, blank=True, help_text="Phone number recipient" ) role_name = models.CharField( max_length=100, null=True, blank=True, help_text="Role name for role-based recipients" ) # Delivery status status = models.CharField( max_length=20, choices=RecipientStatus.choices, default=RecipientStatus.PENDING, help_text="Delivery status" ) # Timing sent_at = models.DateTimeField( null=True, blank=True, help_text="Sent timestamp" ) delivered_at = models.DateTimeField( null=True, blank=True, help_text="Delivered timestamp" ) read_at = models.DateTimeField( null=True, blank=True, help_text="Read timestamp" ) acknowledged_at = models.DateTimeField( null=True, blank=True, help_text="Acknowledged timestamp" ) # Delivery tracking delivery_attempts = models.PositiveIntegerField( default=0, help_text="Number of delivery attempts" ) last_attempt_at = models.DateTimeField( null=True, blank=True, help_text="Last delivery attempt timestamp" ) error_message = models.TextField( null=True, blank=True, help_text="Last delivery error message" ) # External tracking external_delivery_id = models.CharField( max_length=255, null=True, blank=True, help_text="External delivery tracking ID" ) class Meta: db_table = 'communications_message_recipient' indexes = [ models.Index(fields=['message', 'status']), models.Index(fields=['user', 'status']), models.Index(fields=['recipient_type']), models.Index(fields=['sent_at']), ] unique_together = [ ['message', 'user'], ['message', 'email_address'], ['message', 'phone_number'], ] def __str__(self): if self.user: return f"{self.user.get_full_name()} - {self.message.subject}" elif self.email_address: return f"{self.email_address} - {self.message.subject}" elif self.phone_number: return f"{self.phone_number} - {self.message.subject}" return f"{self.recipient_type} - {self.message.subject}" class NotificationTemplate(models.Model): """ Model for notification templates. """ class TemplateType(models.TextChoices): EMAIL = 'EMAIL', 'Email Template' SMS = 'SMS', 'SMS Template' PUSH = 'PUSH', 'Push Notification Template' SLACK = 'SLACK', 'Slack Template' TEAMS = 'TEAMS', 'Teams Template' WEBHOOK = 'WEBHOOK', 'Webhook Template' SYSTEM = 'SYSTEM', 'System Notification Template' class TemplateCategory(models.TextChoices): APPOINTMENT = 'APPOINTMENT', 'Appointment Notifications' MEDICATION = 'MEDICATION', 'Medication Reminders' LAB_RESULTS = 'LAB_RESULTS', 'Lab Results' BILLING = 'BILLING', 'Billing Notifications' EMERGENCY = 'EMERGENCY', 'Emergency Alerts' SYSTEM = 'SYSTEM', 'System Notifications' MARKETING = 'MARKETING', 'Marketing Communications' CLINICAL = 'CLINICAL', 'Clinical Notifications' ADMINISTRATIVE = 'ADMINISTRATIVE', 'Administrative Messages' QUALITY = 'QUALITY', 'Quality Alerts' # Primary identification template_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, help_text="Unique identifier for the template" ) tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, help_text="Tenant organization" ) # Template details name = models.CharField( max_length=255, help_text="Template name" ) description = models.TextField( null=True, blank=True, help_text="Template description" ) template_type = models.CharField( max_length=20, choices=TemplateType.choices, help_text="Type of template" ) category = models.CharField( max_length=30, choices=TemplateCategory.choices, help_text="Template category" ) # Template content subject_template = models.CharField( max_length=255, null=True, blank=True, help_text="Subject line template" ) content_template = models.TextField( help_text="Message content template" ) # Template configuration variables = models.JSONField( default=dict, help_text="Available template variables" ) default_values = models.JSONField( default=dict, help_text="Default variable values" ) formatting_rules = models.JSONField( default=dict, help_text="Content formatting rules" ) # Template settings is_active = models.BooleanField( default=True, help_text="Template is active" ) is_system_template = models.BooleanField( default=False, help_text="System-defined template" ) requires_approval = models.BooleanField( default=False, help_text="Requires approval before use" ) # Usage tracking usage_count = models.PositiveIntegerField( default=0, help_text="Number of times template has been used" ) last_used_at = models.DateTimeField( null=True, blank=True, help_text="Last usage timestamp" ) # Metadata created_at = models.DateTimeField( auto_now_add=True, help_text="Template creation timestamp" ) updated_at = models.DateTimeField( auto_now=True, help_text="Last update timestamp" ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, help_text="Template creator" ) class Meta: db_table = 'communications_notification_template' indexes = [ models.Index(fields=['tenant', 'template_type']), models.Index(fields=['category', 'is_active']), models.Index(fields=['is_system_template']), models.Index(fields=['usage_count']), ] unique_together = [['tenant', 'name', 'template_type']] def __str__(self): return f"{self.name} ({self.template_type})" class AlertRule(models.Model): """ Model for automated alert rules. """ class TriggerType(models.TextChoices): THRESHOLD = 'THRESHOLD', 'Threshold Alert' PATTERN = 'PATTERN', 'Pattern Alert' SCHEDULE = 'SCHEDULE', 'Scheduled Alert' EVENT = 'EVENT', 'Event-based Alert' ANOMALY = 'ANOMALY', 'Anomaly Detection' SYSTEM = 'SYSTEM', 'System Alert' CLINICAL = 'CLINICAL', 'Clinical Alert' OPERATIONAL = 'OPERATIONAL', 'Operational Alert' class SeverityLevel(models.TextChoices): INFO = 'INFO', 'Information' WARNING = 'WARNING', 'Warning' ERROR = 'ERROR', 'Error' CRITICAL = 'CRITICAL', 'Critical' EMERGENCY = 'EMERGENCY', 'Emergency' # Primary identification rule_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, help_text="Unique identifier for the alert rule" ) tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, help_text="Tenant organization" ) # Rule details name = models.CharField( max_length=255, help_text="Alert rule name" ) description = models.TextField( null=True, blank=True, help_text="Alert rule description" ) trigger_type = models.CharField( max_length=20, choices=TriggerType.choices, help_text="Type of alert trigger" ) severity = models.CharField( max_length=20, choices=SeverityLevel.choices, default=SeverityLevel.WARNING, help_text="Alert severity level" ) # Rule configuration trigger_conditions = models.JSONField( default=dict, help_text="Conditions that trigger the alert" ) evaluation_frequency = models.PositiveIntegerField( default=300, # 5 minutes help_text="Evaluation frequency in seconds" ) cooldown_period = models.PositiveIntegerField( default=3600, # 1 hour help_text="Cooldown period between alerts in seconds" ) # Notification configuration notification_template = models.ForeignKey( NotificationTemplate, on_delete=models.SET_NULL, null=True, blank=True, help_text="Notification template to use" ) notification_channels = models.JSONField( default=list, help_text="Notification channels to use" ) escalation_rules = models.JSONField( default=dict, help_text="Escalation configuration" ) # Recipients default_recipients = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, help_text="Default alert recipients" ) recipient_roles = models.JSONField( default=list, help_text="Recipient roles" ) # Rule status is_active = models.BooleanField( default=True, help_text="Alert rule is active" ) is_system_rule = models.BooleanField( default=False, help_text="System-defined rule" ) # Tracking trigger_count = models.PositiveIntegerField( default=0, help_text="Number of times rule has triggered" ) last_triggered_at = models.DateTimeField( null=True, blank=True, help_text="Last trigger timestamp" ) last_evaluated_at = models.DateTimeField( null=True, blank=True, help_text="Last evaluation timestamp" ) # Metadata created_at = models.DateTimeField( auto_now_add=True, help_text="Rule creation timestamp" ) updated_at = models.DateTimeField( auto_now=True, help_text="Last update timestamp" ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_alert_rules', help_text="Rule creator" ) class Meta: db_table = 'communications_alert_rule' indexes = [ models.Index(fields=['tenant', 'is_active']), models.Index(fields=['trigger_type', 'severity']), models.Index(fields=['last_evaluated_at']), models.Index(fields=['is_system_rule']), ] unique_together = [['tenant', 'name']] def __str__(self): return f"{self.name} ({self.severity})" @property def is_in_cooldown(self): """Check if rule is in cooldown period.""" if self.last_triggered_at: cooldown_end = self.last_triggered_at + timedelta( seconds=self.cooldown_period ) return timezone.now() < cooldown_end return False class AlertInstance(models.Model): """ Model for alert instances. """ class AlertStatus(models.TextChoices): ACTIVE = 'ACTIVE', 'Active' ACKNOWLEDGED = 'ACKNOWLEDGED', 'Acknowledged' RESOLVED = 'RESOLVED', 'Resolved' SUPPRESSED = 'SUPPRESSED', 'Suppressed' ESCALATED = 'ESCALATED', 'Escalated' EXPIRED = 'EXPIRED', 'Expired' # Primary identification alert_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, help_text="Unique identifier for the alert instance" ) alert_rule = models.ForeignKey( AlertRule, on_delete=models.CASCADE, related_name='instances', help_text="Associated alert rule" ) # Alert details title = models.CharField( max_length=255, help_text="Alert title" ) description = models.TextField( help_text="Alert description" ) severity = models.CharField( max_length=20, choices=AlertRule.SeverityLevel.choices, help_text="Alert severity level" ) # Alert data trigger_data = models.JSONField( default=dict, help_text="Data that triggered the alert" ) context_data = models.JSONField( default=dict, help_text="Additional context data" ) # Alert status status = models.CharField( max_length=20, choices=AlertStatus.choices, default=AlertStatus.ACTIVE, help_text="Alert status" ) # Timing triggered_at = models.DateTimeField( auto_now_add=True, help_text="Alert trigger timestamp" ) acknowledged_at = models.DateTimeField( null=True, blank=True, help_text="Acknowledgment timestamp" ) resolved_at = models.DateTimeField( null=True, blank=True, help_text="Resolution timestamp" ) expires_at = models.DateTimeField( null=True, blank=True, help_text="Alert expiration time" ) # Response tracking acknowledged_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='acknowledged_alerts', help_text="User who acknowledged the alert" ) resolved_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='resolved_alerts', help_text="User who resolved the alert" ) resolution_notes = models.TextField( null=True, blank=True, help_text="Resolution notes" ) # Escalation tracking escalation_level = models.PositiveIntegerField( default=0, help_text="Current escalation level" ) escalated_at = models.DateTimeField( null=True, blank=True, help_text="Last escalation timestamp" ) # Notification tracking notifications_sent = models.PositiveIntegerField( default=0, help_text="Number of notifications sent" ) last_notification_at = models.DateTimeField( null=True, blank=True, help_text="Last notification timestamp" ) class Meta: db_table = 'communications_alert_instance' indexes = [ models.Index(fields=['alert_rule', 'status']), models.Index(fields=['severity', 'triggered_at']), models.Index(fields=['status', 'triggered_at']), models.Index(fields=['expires_at']), ] ordering = ['-triggered_at'] def __str__(self): return f"{self.title} ({self.severity})" @property def is_expired(self): """Check if alert has expired.""" if self.expires_at: return timezone.now() > self.expires_at return False @property def duration(self): """Get alert duration.""" end_time = self.resolved_at or timezone.now() return end_time - self.triggered_at class CommunicationChannel(models.Model): """ Model for communication channels. """ class ChannelType(models.TextChoices): EMAIL = 'EMAIL', 'Email' SMS = 'SMS', 'SMS' PUSH = 'PUSH', 'Push Notification' SLACK = 'SLACK', 'Slack' TEAMS = 'TEAMS', 'Microsoft Teams' WEBHOOK = 'WEBHOOK', 'Webhook' PHONE = 'PHONE', 'Phone Call' FAX = 'FAX', 'Fax' PAGER = 'PAGER', 'Pager' class ProviderType(models.TextChoices): SMTP = 'SMTP', 'SMTP Email' SENDGRID = 'SENDGRID', 'SendGrid' MAILGUN = 'MAILGUN', 'Mailgun' TWILIO = 'TWILIO', 'Twilio SMS' AWS_SNS = 'AWS_SNS', 'AWS SNS' FIREBASE = 'FIREBASE', 'Firebase' SLACK_API = 'SLACK_API', 'Slack API' TEAMS_API = 'TEAMS_API', 'Teams API' WEBHOOK = 'WEBHOOK', 'Webhook' CUSTOM = 'CUSTOM', 'Custom Provider' # Primary identification channel_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, help_text="Unique identifier for the channel" ) tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, help_text="Tenant organization" ) # Channel details name = models.CharField( max_length=255, help_text="Channel name" ) description = models.TextField( null=True, blank=True, help_text="Channel description" ) channel_type = models.CharField( max_length=20, choices=ChannelType.choices, help_text="Type of communication channel" ) provider_type = models.CharField( max_length=20, choices=ProviderType.choices, help_text="Provider type" ) # Configuration configuration = models.JSONField( default=dict, help_text="Channel configuration settings" ) authentication_config = models.JSONField( default=dict, help_text="Authentication configuration" ) rate_limits = models.JSONField( default=dict, help_text="Rate limiting configuration" ) # Channel status is_active = models.BooleanField( default=True, help_text="Channel is active" ) is_healthy = models.BooleanField( default=True, help_text="Channel health status" ) last_health_check = models.DateTimeField( null=True, blank=True, help_text="Last health check timestamp" ) health_check_interval = models.PositiveIntegerField( default=300, # 5 minutes help_text="Health check interval in seconds" ) # Usage tracking message_count = models.PositiveIntegerField( default=0, help_text="Total messages sent through channel" ) success_count = models.PositiveIntegerField( default=0, help_text="Successful message deliveries" ) failure_count = models.PositiveIntegerField( default=0, help_text="Failed message deliveries" ) last_used_at = models.DateTimeField( null=True, blank=True, help_text="Last usage timestamp" ) # Metadata created_at = models.DateTimeField( auto_now_add=True, help_text="Channel creation timestamp" ) updated_at = models.DateTimeField( auto_now=True, help_text="Last update timestamp" ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, help_text="Channel creator" ) class Meta: db_table = 'communications_communication_channel' indexes = [ models.Index(fields=['tenant', 'channel_type']), models.Index(fields=['is_active', 'is_healthy']), models.Index(fields=['last_health_check']), models.Index(fields=['provider_type']), ] unique_together = [['tenant', 'name']] def __str__(self): return f"{self.name} ({self.channel_type})" @property def success_rate(self): """Calculate success rate.""" if self.message_count > 0: return (self.success_count / self.message_count) * 100 return 0 @property def needs_health_check(self): """Check if health check is needed.""" if not self.last_health_check: return True next_check = self.last_health_check + timedelta( seconds=self.health_check_interval ) return timezone.now() >= next_check class DeliveryLog(models.Model): """ Model for delivery logging. """ class DeliveryStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' PROCESSING = 'PROCESSING', 'Processing' SENT = 'SENT', 'Sent' DELIVERED = 'DELIVERED', 'Delivered' FAILED = 'FAILED', 'Failed' BOUNCED = 'BOUNCED', 'Bounced' REJECTED = 'REJECTED', 'Rejected' TIMEOUT = 'TIMEOUT', 'Timeout' # Primary identification log_id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, help_text="Unique identifier for the delivery log" ) message = models.ForeignKey( Message, on_delete=models.CASCADE, related_name='delivery_logs', help_text="Associated message" ) recipient = models.ForeignKey( MessageRecipient, on_delete=models.CASCADE, related_name='delivery_logs', help_text="Associated recipient" ) channel = models.ForeignKey( CommunicationChannel, on_delete=models.CASCADE, related_name='delivery_logs', help_text="Communication channel used" ) # Delivery details status = models.CharField( max_length=20, choices=DeliveryStatus.choices, default=DeliveryStatus.PENDING, help_text="Delivery status" ) attempt_number = models.PositiveIntegerField( default=1, help_text="Delivery attempt number" ) # Timing started_at = models.DateTimeField( auto_now_add=True, help_text="Delivery start timestamp" ) completed_at = models.DateTimeField( null=True, blank=True, help_text="Delivery completion timestamp" ) # Response tracking external_id = models.CharField( max_length=255, null=True, blank=True, help_text="External delivery ID" ) response_code = models.CharField( max_length=50, null=True, blank=True, help_text="Response code from provider" ) response_message = models.TextField( null=True, blank=True, help_text="Response message from provider" ) error_details = models.JSONField( default=dict, help_text="Detailed error information" ) # Performance metrics processing_time_ms = models.PositiveIntegerField( null=True, blank=True, help_text="Processing time in milliseconds" ) payload_size_bytes = models.PositiveIntegerField( null=True, blank=True, help_text="Payload size in bytes" ) # Metadata metadata = models.JSONField( default=dict, help_text="Additional delivery metadata" ) class Meta: db_table = 'communications_delivery_log' indexes = [ models.Index(fields=['message', 'status']), models.Index(fields=['recipient', 'status']), models.Index(fields=['channel', 'started_at']), models.Index(fields=['status', 'started_at']), models.Index(fields=['external_id']), ] ordering = ['-started_at'] def __str__(self): return f"{self.message.subject} - {self.recipient} ({self.status})" @property def duration(self): """Get delivery duration.""" if self.completed_at: return self.completed_at - self.started_at return timezone.now() - self.started_at