HH/apps/integrations/services/his_adapter.py

358 lines
12 KiB
Python

"""
HIS Adapter Service - Transforms real HIS data format to internal format
This service handles the transformation of HIS patient data into PX360's
internal format for sending surveys based on PatientType.
Simplified Flow:
1. Parse HIS patient data
2. Determine survey type from PatientType
3. Create survey instance
4. Send survey via SMS
"""
from datetime import datetime
from typing import Dict, Optional, Tuple
from django.utils import timezone
from apps.organizations.models import Hospital, Patient
from apps.surveys.models import SurveyTemplate, SurveyInstance, SurveyStatus
from apps.integrations.models import InboundEvent
class HISAdapter:
"""
Adapter for transforming HIS patient data format to internal format.
HIS Data Structure:
{
"FetchPatientDataTimeStampList": [{...patient demographics...}],
"FetchPatientDataTimeStampVisitDataList": [
{"Type": "Consultation", "BillDate": "05-Jun-2025 11:06"},
...
],
"Code": 200,
"Status": "Success"
}
PatientType Codes:
- "1" → Inpatient
- "2" or "O" → OPD (Outpatient)
- "3" or "E" → EMS (Emergency)
"""
@staticmethod
def parse_date(date_str: Optional[str]) -> Optional[datetime]:
"""Parse HIS date format 'DD-Mon-YYYY HH:MM' to timezone-aware datetime"""
if not date_str:
return None
try:
# HIS format: "05-Jun-2025 11:06"
naive_dt = datetime.strptime(date_str, "%d-%b-%Y %H:%M")
# Make timezone-aware using Django's timezone
from django.utils import timezone
return timezone.make_aware(naive_dt)
except ValueError:
return None
@staticmethod
def map_patient_type_to_survey_type(patient_type: str) -> str:
"""
Map HIS PatientType code to survey type name.
Returns survey type name for template lookup.
"""
if patient_type == "1":
return "INPATIENT"
elif patient_type in ["2", "O"]:
return "OPD"
elif patient_type in ["3", "E"]:
return "EMS"
elif patient_type == "4":
return "DAYCASE"
else:
# Default to OPD if unknown
return "OPD"
@staticmethod
def split_patient_name(full_name: str) -> Tuple[str, str]:
"""Split patient name into first and last name"""
# Handle names like "AFAF NASSER ALRAZoooOOQ"
parts = full_name.strip().split()
if len(parts) == 1:
return parts[0], ""
elif len(parts) == 2:
return parts[0], parts[1]
else:
# Multiple parts - first is first name, rest is last name
return parts[0], " ".join(parts[1:])
@staticmethod
def get_or_create_hospital(hospital_data: Dict) -> Optional[Hospital]:
"""Get or create hospital from HIS data"""
hospital_name = hospital_data.get("HospitalName")
hospital_id = hospital_data.get("HospitalID")
if not hospital_name:
return None
# Try to find existing hospital by name
hospital = Hospital.objects.filter(name__icontains=hospital_name).first()
if hospital:
return hospital
# If not found, create new hospital (optional - can be disabled in production)
hospital_code = hospital_id if hospital_id else f"HOSP-{hospital_name[:3].upper()}"
hospital, created = Hospital.objects.get_or_create(
code=hospital_code,
defaults={
'name': hospital_name,
'status': 'active'
}
)
return hospital
@staticmethod
def get_or_create_patient(patient_data: Dict, hospital: Hospital) -> Patient:
"""Get or create patient from HIS demographic data"""
patient_id = patient_data.get("PatientID")
mrn = patient_id # PatientID serves as MRN
national_id = patient_data.get("SSN")
phone = patient_data.get("MobileNo")
email = patient_data.get("Email")
full_name = patient_data.get("PatientName")
# Split name
first_name, last_name = HISAdapter.split_patient_name(full_name)
# Parse date of birth
dob_str = patient_data.get("DOB")
date_of_birth = HISAdapter.parse_date(dob_str) if dob_str else None
# Extract additional info
gender = patient_data.get("Gender", "").lower()
# Try to find existing patient by MRN
patient = Patient.objects.filter(mrn=mrn, primary_hospital=hospital).first()
if patient:
# Update patient information if changed
patient.first_name = first_name
patient.last_name = last_name
patient.national_id = national_id
patient.phone = phone
# Only update email if it's not None (to avoid NOT NULL constraint)
if email is not None:
patient.email = email
patient.date_of_birth = date_of_birth
patient.gender = gender
patient.save()
return patient
# Create new patient
patient = Patient.objects.create(
mrn=mrn,
primary_hospital=hospital,
first_name=first_name,
last_name=last_name,
national_id=national_id,
phone=phone,
email=email if email else '', # Use empty string if email is None
date_of_birth=date_of_birth,
gender=gender
)
return patient
@staticmethod
def get_survey_template(patient_type: str, hospital: Hospital) -> Optional[SurveyTemplate]:
"""
Get appropriate survey template based on PatientType using explicit mapping.
Uses SurveyTemplateMapping to determine which template to send.
Args:
patient_type: HIS PatientType code (1, 2, 3, 4, O, E, APPOINTMENT)
hospital: Hospital instance
Returns:
SurveyTemplate or None if not found
"""
from apps.integrations.models import SurveyTemplateMapping
# Use explicit mapping to get template
survey_template = SurveyTemplateMapping.get_template_for_patient_type(
patient_type, hospital
)
return survey_template
@staticmethod
def create_and_send_survey(
patient: Patient,
hospital: Hospital,
patient_data: Dict,
survey_template: SurveyTemplate
) -> Optional[SurveyInstance]:
"""
Create survey instance and send via SMS.
Args:
patient: Patient instance
hospital: Hospital instance
patient_data: HIS patient data
survey_template: SurveyTemplate instance
Returns:
SurveyInstance or None if failed
"""
admission_id = patient_data.get("AdmissionID")
discharge_date_str = patient_data.get("DischargeDate")
discharge_date = HISAdapter.parse_date(discharge_date_str) if discharge_date_str else None
# Check if survey already sent for this admission
existing_survey = SurveyInstance.objects.filter(
patient=patient,
hospital=hospital,
metadata__admission_id=admission_id
).first()
if existing_survey:
return existing_survey
# Create survey instance
survey = SurveyInstance.objects.create(
survey_template=survey_template,
patient=patient,
hospital=hospital,
status=SurveyStatus.SENT, # Set to SENT as it will be sent immediately
delivery_channel="SMS", # Send via SMS
recipient_phone=patient.phone,
recipient_email=patient.email,
metadata={
'admission_id': admission_id,
'patient_type': patient_data.get("PatientType"),
'hospital_id': patient_data.get("HospitalID"),
'insurance_company': patient_data.get("InsuranceCompanyName"),
'is_vip': patient_data.get("IsVIP") == "1"
}
)
# Send survey via SMS
try:
from apps.surveys.services import SurveyDeliveryService
delivery_success = SurveyDeliveryService.deliver_survey(survey)
if delivery_success:
return survey
else:
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Survey created but SMS delivery failed for survey {survey.id}")
return survey
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error sending survey SMS: {str(e)}", exc_info=True)
return survey
@staticmethod
def process_his_data(his_data: Dict) -> Dict:
"""
Main method to process HIS patient data and send surveys.
Simplified Flow:
1. Extract patient data
2. Get or create patient and hospital
3. Determine survey type from PatientType
4. Create and send survey via SMS
Args:
his_data: HIS data in real format
Returns:
Dict with processing results
"""
result = {
'success': False,
'message': '',
'patient': None,
'survey': None,
'survey_sent': False
}
try:
# Extract patient data
patient_list = his_data.get("FetchPatientDataTimeStampList", [])
if not patient_list:
result['message'] = "No patient data found"
return result
patient_data = patient_list[0]
# Validate status
if his_data.get("Code") != 200 or his_data.get("Status") != "Success":
result['message'] = f"HIS Error: {his_data.get('Message', 'Unknown error')}"
return result
# Check if patient is discharged (required for ALL patient types)
patient_type = patient_data.get("PatientType")
discharge_date_str = patient_data.get("DischargeDate")
# All patient types require discharge date
if not discharge_date_str:
result['message'] = f'Patient type {patient_type} not discharged - no survey sent'
result['success'] = True # Not an error, just no action needed
return result
# Get or create hospital
hospital = HISAdapter.get_or_create_hospital(patient_data)
if not hospital:
result['message'] = "Could not determine hospital"
return result
# Get or create patient
patient = HISAdapter.get_or_create_patient(patient_data, hospital)
# Get survey template based on PatientType
patient_type = patient_data.get("PatientType")
survey_template = HISAdapter.get_survey_template(patient_type, hospital)
if not survey_template:
result['message'] = f"No survey template found for patient type '{patient_type}'"
return result
# Create and send survey
survey = HISAdapter.create_and_send_survey(
patient, hospital, patient_data, survey_template
)
if survey:
from apps.surveys.models import SurveyStatus
survey_sent = survey.status == SurveyStatus.SENT
else:
survey_sent = False
result.update({
'success': True,
'message': 'Patient data processed successfully',
'patient': patient,
'patient_type': patient_type,
'survey': survey,
'survey_sent': survey_sent,
'survey_url': survey.get_survey_url() if survey else None
})
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error processing HIS data: {str(e)}", exc_info=True)
result['message'] = f"Error processing HIS data: {str(e)}"
result['success'] = False
return result