2025-08-12 13:33:25 +03:00

1105 lines
29 KiB
Python

"""
Communications app models.
"""
import uuid
from django.db import models
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
from decimal import Decimal
User = get_user_model()
class Message(models.Model):
"""
Model for internal messaging system.
"""
MESSAGE_TYPES = [
('INTERNAL', 'Internal Message'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PUSH', 'Push Notification'),
('SLACK', 'Slack Message'),
('TEAMS', 'Microsoft Teams'),
('WEBHOOK', 'Webhook'),
('SYSTEM', 'System Message'),
('ALERT', 'Alert Message'),
]
PRIORITY_LEVELS = [
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
('CRITICAL', 'Critical'),
]
STATUS_CHOICES = [
('DRAFT', 'Draft'),
('PENDING', 'Pending'),
('SENDING', 'Sending'),
('SENT', 'Sent'),
('DELIVERED', 'Delivered'),
('READ', 'Read'),
('FAILED', 'Failed'),
('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=MESSAGE_TYPES,
default='INTERNAL',
help_text="Type of message"
)
priority = models.CharField(
max_length=20,
choices=PRIORITY_LEVELS,
default='NORMAL',
help_text="Message priority level"
)
# Sender and recipients
sender = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='sent_messages',
help_text="Message sender"
)
# Message status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='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):
"""
Model for message recipients.
"""
RECIPIENT_TYPES = [
('USER', 'User'),
('EMAIL', 'Email Address'),
('PHONE', 'Phone Number'),
('ROLE', 'User Role'),
('DEPARTMENT', 'Department'),
('GROUP', 'User Group'),
]
STATUS_CHOICES = [
('PENDING', 'Pending'),
('SENT', 'Sent'),
('DELIVERED', 'Delivered'),
('READ', 'Read'),
('ACKNOWLEDGED', 'Acknowledged'),
('FAILED', 'Failed'),
('BOUNCED', 'Bounced'),
('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=RECIPIENT_TYPES,
help_text="Type of recipient"
)
user = models.ForeignKey(
User,
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=STATUS_CHOICES,
default='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.
"""
TEMPLATE_TYPES = [
('EMAIL', 'Email Template'),
('SMS', 'SMS Template'),
('PUSH', 'Push Notification Template'),
('SLACK', 'Slack Template'),
('TEAMS', 'Teams Template'),
('WEBHOOK', 'Webhook Template'),
('SYSTEM', 'System Notification Template'),
]
TEMPLATE_CATEGORIES = [
('APPOINTMENT', 'Appointment Notifications'),
('MEDICATION', 'Medication Reminders'),
('LAB_RESULTS', 'Lab Results'),
('BILLING', 'Billing Notifications'),
('EMERGENCY', 'Emergency Alerts'),
('SYSTEM', 'System Notifications'),
('MARKETING', 'Marketing Communications'),
('CLINICAL', 'Clinical Notifications'),
('ADMINISTRATIVE', 'Administrative Messages'),
('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=TEMPLATE_TYPES,
help_text="Type of template"
)
category = models.CharField(
max_length=30,
choices=TEMPLATE_CATEGORIES,
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(
User,
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.
"""
TRIGGER_TYPES = [
('THRESHOLD', 'Threshold Alert'),
('PATTERN', 'Pattern Alert'),
('SCHEDULE', 'Scheduled Alert'),
('EVENT', 'Event-based Alert'),
('ANOMALY', 'Anomaly Detection'),
('SYSTEM', 'System Alert'),
('CLINICAL', 'Clinical Alert'),
('OPERATIONAL', 'Operational Alert'),
]
SEVERITY_LEVELS = [
('INFO', 'Information'),
('WARNING', 'Warning'),
('ERROR', 'Error'),
('CRITICAL', 'Critical'),
('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=TRIGGER_TYPES,
help_text="Type of alert trigger"
)
severity = models.CharField(
max_length=20,
choices=SEVERITY_LEVELS,
default='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(
User,
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(
User,
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 + timezone.timedelta(
seconds=self.cooldown_period
)
return timezone.now() < cooldown_end
return False
class AlertInstance(models.Model):
"""
Model for alert instances.
"""
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('ACKNOWLEDGED', 'Acknowledged'),
('RESOLVED', 'Resolved'),
('SUPPRESSED', 'Suppressed'),
('ESCALATED', 'Escalated'),
('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.SEVERITY_LEVELS,
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=STATUS_CHOICES,
default='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(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='acknowledged_alerts',
help_text="User who acknowledged the alert"
)
resolved_by = models.ForeignKey(
User,
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.
"""
CHANNEL_TYPES = [
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PUSH', 'Push Notification'),
('SLACK', 'Slack'),
('TEAMS', 'Microsoft Teams'),
('WEBHOOK', 'Webhook'),
('PHONE', 'Phone Call'),
('FAX', 'Fax'),
('PAGER', 'Pager'),
]
PROVIDER_TYPES = [
('SMTP', 'SMTP Email'),
('SENDGRID', 'SendGrid'),
('MAILGUN', 'Mailgun'),
('TWILIO', 'Twilio SMS'),
('AWS_SNS', 'AWS SNS'),
('FIREBASE', 'Firebase'),
('SLACK_API', 'Slack API'),
('TEAMS_API', 'Teams API'),
('WEBHOOK', 'Webhook'),
('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=CHANNEL_TYPES,
help_text="Type of communication channel"
)
provider_type = models.CharField(
max_length=20,
choices=PROVIDER_TYPES,
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(
User,
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 + timezone.timedelta(
seconds=self.health_check_interval
)
return timezone.now() >= next_check
class DeliveryLog(models.Model):
"""
Model for delivery logging.
"""
STATUS_CHOICES = [
('PENDING', 'Pending'),
('PROCESSING', 'Processing'),
('SENT', 'Sent'),
('DELIVERED', 'Delivered'),
('FAILED', 'Failed'),
('BOUNCED', 'Bounced'),
('REJECTED', 'Rejected'),
('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=STATUS_CHOICES,
default='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