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