""" Integrations models - Inbound events from external systems This module handles integration events from: - HIS (Hospital Information System) - Lab systems - Radiology systems - Pharmacy systems - MOH (Ministry of Health) - CHI (Council of Health Insurance) - Other external systems """ from django.db import models from apps.core.models import BaseChoices, TimeStampedModel, UUIDModel class EventStatus(BaseChoices): """Event processing status""" PENDING = 'pending', 'Pending' PROCESSING = 'processing', 'Processing' PROCESSED = 'processed', 'Processed' FAILED = 'failed', 'Failed' IGNORED = 'ignored', 'Ignored' class SourceSystem(BaseChoices): """Source system choices""" HIS = 'his', 'Hospital Information System' LAB = 'lab', 'Laboratory System' RADIOLOGY = 'radiology', 'Radiology System' PHARMACY = 'pharmacy', 'Pharmacy System' MOH = 'moh', 'Ministry of Health' CHI = 'chi', 'Council of Health Insurance' PXCONNECT = 'pxconnect', 'PX Connect' OTHER = 'other', 'Other' class InboundEvent(UUIDModel, TimeStampedModel): """ Inbound integration event from external systems. Events trigger journey stage completions. For example: - Event code: "OPD_VISIT_COMPLETED" → completes "MD Consultation" stage - Event code: "LAB_ORDER_COMPLETED" → completes "Lab" stage - Event code: "RADIOLOGY_REPORT_FINALIZED" → completes "Radiology" stage - Event code: "PHARMACY_DISPENSED" → completes "Pharmacy" stage Processing flow: 1. Event received via API (POST /api/integrations/events/) 2. Stored with status=PENDING 3. Celery task processes event: a. Find journey instance by encounter_id b. Find matching stage by trigger_event_code c. Complete the stage d. Create survey instance if configured e. Update event status to PROCESSED """ # Source information source_system = models.CharField( max_length=50, choices=SourceSystem.choices, db_index=True, help_text="System that sent this event" ) event_code = models.CharField( max_length=100, db_index=True, help_text="Event type code (e.g., OPD_VISIT_COMPLETED, LAB_ORDER_COMPLETED)" ) # Identifiers encounter_id = models.CharField( max_length=100, db_index=True, help_text="Encounter ID from HIS system" ) patient_identifier = models.CharField( max_length=100, blank=True, db_index=True, help_text="Patient MRN or other identifier" ) # Event data payload_json = models.JSONField( help_text="Full event payload from source system" ) # Processing status status = models.CharField( max_length=20, choices=EventStatus.choices, default=EventStatus.PENDING, db_index=True ) # Timestamps received_at = models.DateTimeField(auto_now_add=True, db_index=True) processed_at = models.DateTimeField(null=True, blank=True) # Processing results error = models.TextField( blank=True, help_text="Error message if processing failed" ) processing_attempts = models.IntegerField( default=0, help_text="Number of processing attempts" ) # Extracted context (from payload) physician_license = models.CharField( max_length=100, blank=True, help_text="Physician license number from event" ) department_code = models.CharField( max_length=50, blank=True, help_text="Department code from event" ) # Metadata metadata = models.JSONField( default=dict, blank=True, help_text="Additional processing metadata" ) class Meta: ordering = ['-received_at'] indexes = [ models.Index(fields=['status', '-received_at']), models.Index(fields=['encounter_id', 'event_code']), models.Index(fields=['source_system', '-received_at']), ] def __str__(self): return f"{self.source_system} - {self.event_code} - {self.encounter_id} ({self.status})" def mark_processing(self): """Mark event as being processed""" self.status = EventStatus.PROCESSING self.processing_attempts += 1 self.save(update_fields=['status', 'processing_attempts']) def mark_processed(self): """Mark event as successfully processed""" from django.utils import timezone self.status = EventStatus.PROCESSED self.processed_at = timezone.now() self.save(update_fields=['status', 'processed_at']) def mark_failed(self, error_message): """Mark event as failed with error message""" self.status = EventStatus.FAILED self.error = error_message self.save(update_fields=['status', 'error']) def mark_ignored(self, reason): """Mark event as ignored (e.g., no matching journey)""" self.status = EventStatus.IGNORED self.error = reason self.save(update_fields=['status', 'error']) class IntegrationConfig(UUIDModel, TimeStampedModel): """ Configuration for external system integrations. Stores API endpoints, credentials, and mapping rules. """ name = models.CharField(max_length=200, unique=True) source_system = models.CharField( max_length=50, choices=SourceSystem.choices, unique=True ) # Connection details api_url = models.URLField(blank=True, help_text="API endpoint URL") api_key = models.CharField(max_length=500, blank=True, help_text="API key (encrypted)") # Configuration is_active = models.BooleanField(default=True) config_json = models.JSONField( default=dict, blank=True, help_text="Additional configuration (event mappings, field mappings, etc.)" ) # Metadata description = models.TextField(blank=True) last_sync_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ['name'] def __str__(self): return f"{self.name} ({self.source_system})" class PatientType(BaseChoices): """HIS Patient Type codes""" INPATIENT = '1', 'Inpatient (Type 1)' OPD = '2', 'Outpatient (Type 2)' EMS = '3', 'Emergency (Type 3)' DAYCASE = '4', 'Day Case (Type 4)' APPOINTMENT = 'APPOINTMENT', 'Appointment' class SurveyTemplateMapping(UUIDModel, TimeStampedModel): """ Maps patient types to survey templates for automatic survey delivery. This replaces the search-based template selection with explicit mappings. Allows administrators to control which survey template is sent for each patient type and hospital. Example: - PatientType: "1" (Inpatient) → Inpatient Satisfaction Survey - PatientType: "2" (OPD) → Outpatient Satisfaction Survey - PatientType: "APPOINTMENT" → Appointment Satisfaction Survey """ # Mapping key patient_type = models.CharField( max_length=20, choices=PatientType.choices, db_index=True, help_text="Patient type from HIS system" ) # Target survey survey_template = models.ForeignKey( 'surveys.SurveyTemplate', on_delete=models.CASCADE, related_name='patient_type_mappings', help_text="Survey template to send for this patient type" ) # Hospital specificity (null = global mapping) hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='survey_template_mappings', null=True, blank=True, help_text="Hospital for this mapping (null = applies to all hospitals)" ) # Activation is_active = models.BooleanField( default=True, db_index=True, help_text="Whether this mapping is active" ) class Meta: ordering = ['hospital', 'patient_type'] indexes = [ models.Index(fields=['patient_type', 'hospital', 'is_active']), ] # Ensure only one active mapping per patient type per hospital constraints = [ models.UniqueConstraint( fields=['patient_type', 'hospital'], condition=models.Q(is_active=True), name='unique_active_mapping_per_type_hospital' ) ] def __str__(self): hospital_name = self.hospital.name if self.hospital else 'All Hospitals' status = 'Active' if self.is_active else 'Inactive' return f"{self.get_patient_type_display()} → {self.survey_template.name} ({hospital_name}) [{status}]" @staticmethod def get_template_for_patient_type(patient_type: str, hospital): """ Get the active survey template for a patient type and hospital. Search order: 1. Hospital-specific active mapping 2. Global active mapping (hospital is null) Args: patient_type: HIS PatientType code (e.g., "1", "2", "APPOINTMENT") hospital: Hospital instance Returns: SurveyTemplate or None if no active mapping found """ # Try hospital-specific mapping first mapping = SurveyTemplateMapping.objects.filter( patient_type=patient_type, hospital=hospital, is_active=True ).first() if mapping: return mapping.survey_template # Fall back to global mapping mapping = SurveyTemplateMapping.objects.filter( patient_type=patient_type, hospital__isnull=True, is_active=True ).first() return mapping.survey_template if mapping else None class EventMapping(UUIDModel, TimeStampedModel): """ Maps external event codes to internal trigger codes. Example: - External: "VISIT_COMPLETE" → Internal: "OPD_VISIT_COMPLETED" - External: "LAB_RESULT_READY" → Internal: "LAB_ORDER_COMPLETED" """ integration_config = models.ForeignKey( IntegrationConfig, on_delete=models.CASCADE, related_name='event_mappings' ) external_event_code = models.CharField( max_length=100, help_text="Event code from external system" ) internal_event_code = models.CharField( max_length=100, help_text="Internal event code used in journey stages" ) # Field mappings field_mappings = models.JSONField( default=dict, blank=True, help_text="Maps external field names to internal field names" ) is_active = models.BooleanField(default=True) class Meta: unique_together = [['integration_config', 'external_event_code']] ordering = ['integration_config', 'external_event_code'] def __str__(self): return f"{self.external_event_code} → {self.internal_event_code}"