1095 lines
30 KiB
Python
1095 lines
30 KiB
Python
"""
|
|
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
|
|
|