358 lines
12 KiB
Python
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 |