243 lines
7.3 KiB
Python
243 lines
7.3 KiB
Python
"""
|
|
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}"
|