""" Complaints models - Complaint management with SLA tracking This module implements the complaint management system that: - Tracks complaints with SLA deadlines - Manages complaint workflow (open → in progress → resolved → closed) - Triggers resolution satisfaction surveys - Creates PX actions for negative resolution satisfaction - Maintains complaint timeline and attachments """ from datetime import timedelta from django.conf import settings from django.db import models from django.utils import timezone from apps.core.models import PriorityChoices, SeverityChoices, TimeStampedModel, UUIDModel class ComplaintStatus(models.TextChoices): """Complaint status choices""" OPEN = 'open', 'Open' IN_PROGRESS = 'in_progress', 'In Progress' RESOLVED = 'resolved', 'Resolved' CLOSED = 'closed', 'Closed' CANCELLED = 'cancelled', 'Cancelled' class ComplaintSource(models.TextChoices): """Complaint source choices""" PATIENT = 'patient', 'Patient' FAMILY = 'family', 'Family Member' STAFF = 'staff', 'Staff' SURVEY = 'survey', 'Survey' SOCIAL_MEDIA = 'social_media', 'Social Media' CALL_CENTER = 'call_center', 'Call Center' MOH = 'moh', 'Ministry of Health' CHI = 'chi', 'Council of Health Insurance' OTHER = 'other', 'Other' class Complaint(UUIDModel, TimeStampedModel): """ Complaint model with SLA tracking. Workflow: 1. OPEN - Complaint received 2. IN_PROGRESS - Being investigated 3. RESOLVED - Solution provided 4. CLOSED - Confirmed closed (triggers resolution satisfaction survey) SLA: - Calculated based on severity and hospital configuration - Reminders sent before due date - Escalation triggered when overdue """ # Patient and encounter information patient = models.ForeignKey( 'organizations.Patient', on_delete=models.CASCADE, related_name='complaints' ) encounter_id = models.CharField( max_length=100, blank=True, db_index=True, help_text="Related encounter ID if applicable" ) # Organization hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='complaints' ) department = models.ForeignKey( 'organizations.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='complaints' ) physician = models.ForeignKey( 'organizations.Physician', on_delete=models.SET_NULL, null=True, blank=True, related_name='complaints' ) # Complaint details title = models.CharField(max_length=500) description = models.TextField() # Classification category = models.CharField( max_length=100, choices=[ ('clinical_care', 'Clinical Care'), ('staff_behavior', 'Staff Behavior'), ('facility', 'Facility & Environment'), ('wait_time', 'Wait Time'), ('billing', 'Billing'), ('communication', 'Communication'), ('other', 'Other'), ], db_index=True ) subcategory = models.CharField(max_length=100, blank=True) # Priority and severity priority = models.CharField( max_length=20, choices=PriorityChoices.choices, default=PriorityChoices.MEDIUM, db_index=True ) severity = models.CharField( max_length=20, choices=SeverityChoices.choices, default=SeverityChoices.MEDIUM, db_index=True ) # Source source = models.CharField( max_length=50, choices=ComplaintSource.choices, default=ComplaintSource.PATIENT, db_index=True ) # Status and workflow status = models.CharField( max_length=20, choices=ComplaintStatus.choices, default=ComplaintStatus.OPEN, db_index=True ) # Assignment assigned_to = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_complaints' ) assigned_at = models.DateTimeField(null=True, blank=True) # SLA tracking due_at = models.DateTimeField( db_index=True, help_text="SLA deadline" ) is_overdue = models.BooleanField(default=False, db_index=True) reminder_sent_at = models.DateTimeField(null=True, blank=True) escalated_at = models.DateTimeField(null=True, blank=True) # Resolution resolution = models.TextField(blank=True) resolved_at = models.DateTimeField(null=True, blank=True) resolved_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='resolved_complaints' ) # Closure closed_at = models.DateTimeField(null=True, blank=True) closed_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='closed_complaints' ) # Resolution satisfaction survey resolution_survey = models.ForeignKey( 'surveys.SurveyInstance', on_delete=models.SET_NULL, null=True, blank=True, related_name='complaint_resolution' ) resolution_survey_sent_at = models.DateTimeField(null=True, blank=True) # Metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['status', '-created_at']), models.Index(fields=['hospital', 'status', '-created_at']), models.Index(fields=['is_overdue', 'status']), models.Index(fields=['due_at', 'status']), ] def __str__(self): return f"{self.title} - {self.patient.get_full_name()} ({self.status})" def save(self, *args, **kwargs): """Calculate SLA due date on creation""" if not self.due_at: self.due_at = self.calculate_sla_due_date() super().save(*args, **kwargs) def calculate_sla_due_date(self): """ Calculate SLA due date based on severity and hospital configuration. Uses settings.SLA_DEFAULTS if no hospital-specific config exists. """ sla_hours = settings.SLA_DEFAULTS['complaint'].get( self.severity, settings.SLA_DEFAULTS['complaint']['medium'] ) return timezone.now() + timedelta(hours=sla_hours) def check_overdue(self): """Check if complaint is overdue and update status""" if self.status in [ComplaintStatus.CLOSED, ComplaintStatus.CANCELLED]: return False if timezone.now() > self.due_at: if not self.is_overdue: self.is_overdue = True self.save(update_fields=['is_overdue']) return True return False class ComplaintAttachment(UUIDModel, TimeStampedModel): """Complaint attachment (images, documents, etc.)""" complaint = models.ForeignKey( Complaint, on_delete=models.CASCADE, related_name='attachments' ) file = models.FileField(upload_to='complaints/%Y/%m/%d/') filename = models.CharField(max_length=500) file_type = models.CharField(max_length=100, blank=True) file_size = models.IntegerField(help_text="File size in bytes") uploaded_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, related_name='complaint_attachments' ) description = models.TextField(blank=True) class Meta: ordering = ['-created_at'] def __str__(self): return f"{self.complaint} - {self.filename}" class ComplaintUpdate(UUIDModel, TimeStampedModel): """ Complaint update/timeline entry. Tracks all updates, status changes, and communications. """ complaint = models.ForeignKey( Complaint, on_delete=models.CASCADE, related_name='updates' ) # Update details update_type = models.CharField( max_length=50, choices=[ ('status_change', 'Status Change'), ('assignment', 'Assignment'), ('note', 'Note'), ('resolution', 'Resolution'), ('escalation', 'Escalation'), ('communication', 'Communication'), ], db_index=True ) message = models.TextField() # User who made the update created_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, related_name='complaint_updates' ) # Status change tracking old_status = models.CharField(max_length=20, blank=True) new_status = models.CharField(max_length=20, blank=True) # Metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['complaint', '-created_at']), ] def __str__(self): return f"{self.complaint} - {self.update_type} - {self.created_at.strftime('%Y-%m-%d %H:%M')}" class Inquiry(UUIDModel, TimeStampedModel): """ Inquiry model for general questions/requests. Similar to complaints but for non-complaint inquiries. """ # Patient information patient = models.ForeignKey( 'organizations.Patient', on_delete=models.CASCADE, null=True, blank=True, related_name='inquiries' ) # Contact information (if patient not in system) contact_name = models.CharField(max_length=200, blank=True) contact_phone = models.CharField(max_length=20, blank=True) contact_email = models.EmailField(blank=True) # Organization hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='inquiries' ) department = models.ForeignKey( 'organizations.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='inquiries' ) # Inquiry details subject = models.CharField(max_length=500) message = models.TextField() # Category category = models.CharField( max_length=100, choices=[ ('appointment', 'Appointment'), ('billing', 'Billing'), ('medical_records', 'Medical Records'), ('general', 'General Information'), ('other', 'Other'), ] ) # Status status = models.CharField( max_length=20, choices=[ ('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ], default='open', db_index=True ) # Assignment assigned_to = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_inquiries' ) # Response response = models.TextField(blank=True) responded_at = models.DateTimeField(null=True, blank=True) responded_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='responded_inquiries' ) class Meta: ordering = ['-created_at'] verbose_name_plural = 'Inquiries' indexes = [ models.Index(fields=['status', '-created_at']), models.Index(fields=['hospital', 'status']), ] def __str__(self): return f"{self.subject} ({self.status})"