""" 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 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}"