HH/apps/integrations/models.py
2025-12-24 12:42:31 +03:00

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