""" Simulator views for testing external notification APIs. This module provides API endpoints that: - Simulate external email and SMS services - Receive and process HIS journey events - Create journeys, send surveys, and trigger notifications - Generate fake HIS visit data for testing """ import logging import random from datetime import datetime, timedelta from django.conf import settings from django.core.mail import send_mail from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import status import json from .serializers import HISJourneyEventSerializer, HISJourneyEventListSerializer from .models import HISRequestLog from apps.journeys.models import ( JourneyType, PatientJourneyTemplate, PatientJourneyInstance, PatientJourneyStageInstance, StageStatus, ) from apps.organizations.models import Hospital, Department, Patient from apps.surveys.models import SurveyTemplate, SurveyInstance from apps.notifications.services import NotificationService import time def make_json_serializable(data): """ Convert datetime objects to ISO strings for JSON serialization. Recursively processes dictionaries and lists to ensure all datetime objects are converted to ISO format strings for storage in JSON fields. Args: data: Data structure that may contain datetime objects Returns: Data structure with datetime objects converted to ISO strings """ if isinstance(data, dict): return {k: make_json_serializable(v) for k, v in data.items()} elif isinstance(data, list): return [make_json_serializable(item) for item in data] elif isinstance(data, datetime): return data.isoformat() else: return data logger = logging.getLogger(__name__) # Request counter for tracking request_counter = {"email": 0, "sms": 0} # Request history (last 10 requests) request_history = [] def log_simulator_request( channel, payload, status, response_data=None, error_message=None, processing_time_ms=None, request=None ): """ Log simulator request to history, file, and database. Args: channel: Channel type (email, sms, his_event) payload: Request payload data status: Request status (success, failed, partial, sent) response_data: Response data from simulator error_message: Error message if failed processing_time_ms: Processing time in milliseconds request: HTTP request object for IP/user agent """ # Log to in-memory history (keeps last 10) request_id = len(request_history) + 1 entry = { "id": request_id, "channel": channel, "timestamp": datetime.now().isoformat(), "status": status, "payload": payload, } request_history.append(entry) if len(request_history) > 10: request_history.pop(0) # Log to file logger.info(f"[Simulator] {channel.upper()} Request #{request_id}: {status}") # Log to database try: # Convert datetime objects to ISO strings for JSON serialization serializable_response_data = make_json_serializable(response_data) if response_data else None log_entry = HISRequestLog( channel=channel, payload=payload, status=status, response_data=serializable_response_data, processing_time_ms=processing_time_ms, error_message=error_message, ip_address=request.META.get("REMOTE_ADDR") if request else None, user_agent=request.META.get("HTTP_USER_AGENT") if request else None, ) # Extract channel-specific fields if channel == "email": log_entry.recipient = payload.get("to") log_entry.subject = payload.get("subject") message = payload.get("message", "") log_entry.message_preview = message[:500] if message else None elif channel == "sms": log_entry.recipient = payload.get("to") message = payload.get("message", "") log_entry.message_preview = message[:500] if message else None elif channel == "his_event": # Extract from first event if it's a list events = payload.get("events", []) if events and isinstance(events, list) and len(events) > 0: first_event = events[0] log_entry.patient_id = first_event.get("mrn") log_entry.journey_id = first_event.get("encounter_id") log_entry.hospital_code = first_event.get("hospital_code") log_entry.event_type = first_event.get("event_type") log_entry.visit_type = first_event.get("visit_type") log_entry.department = first_event.get("department") log_entry.save() logger.info(f"[Simulator] Logged to database with ID: {log_entry.request_id}") except Exception as e: logger.error(f"[Simulator] Failed to log to database: {str(e)}") @csrf_exempt @require_http_methods(["POST"]) def email_simulator(request): """ Simulate external email API endpoint. Accepts POST request with JSON payload: { "to": "recipient@example.com", "subject": "Email subject", "message": "Plain text message", "html_message": "Optional HTML content" } Sends real email via Django SMTP and returns 200 OK. """ request_counter["email"] += 1 start_time = time.time() try: # Parse request body data = json.loads(request.body) # Validate required fields required_fields = ["to", "subject", "message"] missing_fields = [field for field in required_fields if field not in data] if missing_fields: processing_time = int((time.time() - start_time) * 1000) response = {"success": False, "error": f"Missing required fields: {', '.join(missing_fields)}"} log_simulator_request( "email", data, "failed", response_data=response, error_message=response["error"], processing_time_ms=processing_time, request=request, ) return JsonResponse(response, status=400) # Extract fields to_email = data["to"] subject = data["subject"] message = data["message"] html_message = data.get("html_message", None) # Log the request logger.info(f"[Email Simulator] Sending email to {to_email}: {subject}") # Print formatted email to terminal print(f"\n{'╔' + '═' * 68 + '╗'}") print(f"║{' ' * 15}📧 EMAIL SIMULATOR{' ' * 34}║") print(f"╠{'═' * 68}╣") print(f"║ Request #: {request_counter['email']:<52} ║") print(f"╠{'═' * 68}╣") print(f"║ To: {to_email:<52} ║") print(f"║ Subject: {subject[:52]:<52} ║") print(f"╠{'═' * 68}╣") print(f"║ Message:{' ' * 55} ║") # Word wrap message words = message.split() line = "║ " for word in words: if len(line) + len(word) + 1 > 68: print(f"{line:<68} ║") line = "║ " + word else: if line == "║ ": line += word else: line += " " + word # Print last line if not empty if line != "║ ": print(f"{line:<68} ║") # Print HTML section if present if html_message: print(f"╠{'═' * 68}╣") print(f"║ HTML Message:{' ' * 48} ║") html_preview = html_message[:200].replace("\n", " ") print(f"║ {html_preview:<66} ║") if len(html_message) > 200: print(f"║ ... ({len(html_message)} total characters){' ' * 30} ║") print(f"╚{'═' * 68}╝\n") # Send real email via Django SMTP try: send_mail( subject=subject, message=message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[to_email], html_message=html_message, fail_silently=False, ) logger.info(f"[Email Simulator] Email sent via SMTP to {to_email}") email_status = "sent" except Exception as email_error: logger.error(f"[Email Simulator] SMTP Error: {str(email_error)}") # Log as 'partial' since we displayed it but failed to send email_status = "partial" processing_time = int((time.time() - start_time) * 1000) # Return error response response = { "success": False, "error": f"Email displayed but failed to send via SMTP: {str(email_error)}", "data": { "to": to_email, "subject": subject, "message_length": len(message), "has_html": html_message is not None, "displayed": True, "sent": False, }, } log_simulator_request( "email", data, email_status, response_data=response, error_message=response["error"], processing_time_ms=processing_time, request=request, ) return JsonResponse(response, status=500) processing_time = int((time.time() - start_time) * 1000) # Log success logger.info(f"[Email Simulator] Email sent successfully to {to_email}") response = { "success": True, "message": "Email sent successfully", "data": { "to": to_email, "subject": subject, "message_length": len(message), "has_html": html_message is not None, }, } log_simulator_request( "email", data, email_status, response_data=response, processing_time_ms=processing_time, request=request ) return JsonResponse(response, status=200) except json.JSONDecodeError: processing_time = int((time.time() - start_time) * 1000) response = {"success": False, "error": "Invalid JSON format"} log_simulator_request( "email", {}, "failed", response_data=response, error_message=response["error"], processing_time_ms=processing_time, request=request, ) return JsonResponse(response, status=400) except Exception as e: processing_time = int((time.time() - start_time) * 1000) logger.error(f"[Email Simulator] Error: {str(e)}") response = {"success": False, "error": str(e)} log_simulator_request( "email", data if "data" in locals() else {}, "failed", response_data=response, error_message=str(e), processing_time_ms=processing_time, request=request, ) return JsonResponse(response, status=500) @csrf_exempt @require_http_methods(["POST"]) def sms_simulator(request): """ Simulate external SMS API endpoint. Accepts POST request with JSON payload: { "to": "+966501234567", "message": "SMS message text" } Prints SMS to terminal with formatted output and returns 200 OK. """ request_counter["sms"] += 1 start_time = time.time() try: # Parse request body data = json.loads(request.body) # Validate required fields required_fields = ["to", "message"] missing_fields = [field for field in required_fields if field not in data] if missing_fields: processing_time = int((time.time() - start_time) * 1000) response = {"success": False, "error": f"Missing required fields: {', '.join(missing_fields)}"} log_simulator_request( "sms", data, "failed", response_data=response, error_message=response["error"], processing_time_ms=processing_time, request=request, ) return JsonResponse(response, status=400) # Extract fields to_phone = data["to"] message = data["message"] timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Log the request logger.info(f"[SMS Simulator] Sending SMS to {to_phone}") # Print formatted SMS to terminal print(f"\n{'╔' + '═' * 68 + '╗'}") print(f"║{' ' * 15}📱 SMS SIMULATOR{' ' * 35}║") print(f"╠{'═' * 68}╣") print(f"║ Request #: {request_counter['sms']:<52} ║") print(f"╠{'═' * 68}╣") print(f"║ To: {to_phone:<52} ║") print(f"║ Time: {timestamp:<52} ║") print(f"╠{'═' * 68}╣") print(f"║ Message:{' ' * 55} ║") # Word wrap message words = message.split() line = "║ " for word in words: if len(line) + len(word) + 1 > 68: print(f"{line:<68} ║") line = "║ " + word else: if line == "║ ": line += word else: line += " " + word # Print last line if not empty if line != "║ ": print(f"{line:<68} ║") print(f"╚{'═' * 68}╝\n") processing_time = int((time.time() - start_time) * 1000) # Log success logger.info(f"[SMS Simulator] SMS sent to {to_phone}: {message[:50]}...") response = { "success": True, "message": "SMS sent successfully", "data": {"to": to_phone, "message_length": len(message)}, } log_simulator_request( "sms", data, "sent", response_data=response, processing_time_ms=processing_time, request=request ) return JsonResponse(response, status=200) except json.JSONDecodeError: processing_time = int((time.time() - start_time) * 1000) response = {"success": False, "error": "Invalid JSON format"} log_simulator_request( "sms", {}, "failed", response_data=response, error_message=response["error"], processing_time_ms=processing_time, request=request, ) return JsonResponse(response, status=400) except Exception as e: processing_time = int((time.time() - start_time) * 1000) logger.error(f"[SMS Simulator] Error: {str(e)}") response = {"success": False, "error": str(e)} log_simulator_request( "sms", data if "data" in locals() else {}, "failed", response_data=response, error_message=str(e), processing_time_ms=processing_time, request=request, ) return JsonResponse(response, status=500) @csrf_exempt @require_http_methods(["GET"]) def health_check(request): """ Health check endpoint for simulator. Returns simulator status and statistics. """ return JsonResponse( { "status": "healthy", "timestamp": datetime.now().isoformat(), "statistics": { "total_requests": request_counter["email"] + request_counter["sms"], "email_requests": request_counter["email"], "sms_requests": request_counter["sms"], }, "recent_requests": request_history[-5:], # Last 5 requests }, status=200, ) @csrf_exempt @require_http_methods(["GET"]) def reset_simulator(request): """ Reset simulator statistics and history. Clears request counter and history. """ global request_counter, request_history request_counter = {"email": 0, "sms": 0} request_history = [] logger.info("[Simulator] Reset statistics and history") return JsonResponse({"success": True, "message": "Simulator reset successfully"}, status=200) from rest_framework.permissions import AllowAny from rest_framework.decorators import permission_classes # Import HIS adapter and serializers from apps.integrations.services.his_adapter import HISAdapter from apps.integrations.serializers import HISPatientDataSerializer @api_view(["POST"]) @permission_classes([AllowAny]) def his_patient_data_handler(request): """ HIS Patient Data API Endpoint - Real HIS Format Receives patient data from HIS simulator in real HIS format: - Creates or updates patient records - Creates journey instances with stage completions - Sends post-discharge surveys when patient is discharged Expected payload (real HIS format): { "FetchPatientDataTimeStampList": [{ "Type": "Patient Demographic details", "PatientID": "123456", "AdmissionID": "789012", "HospitalID": "1", "HospitalName": "ALH-main", "AdmitDate": "05-Jun-2025 11:06", "DischargeDate": "05-Jun-2025 15:30", "SSN": "1234567890", "PatientName": "Ahmed Mohammed", "MobileNo": "0501234567", ... }], "FetchPatientDataTimeStampVisitDataList": [ {"Type": "Consultation", "BillDate": "05-Jun-2025 11:06"}, {"Type": "Doctor Visited", "BillDate": "05-Jun-2025 11:20"}, ... ], "Code": 200, "Status": "Success" } """ start_time = time.time() try: # Validate request data using HIS serializer # print("---------------------------------------------") serializer = HISPatientDataSerializer(data=request.data) if not serializer.is_valid(): processing_time = int((time.time() - start_time) * 1000) return Response( {"error": "Invalid HIS data format", "details": serializer.errors}, status=status.HTTP_400_BAD_REQUEST ) # Process HIS data using adapter result = HISAdapter.process_his_data(serializer.validated_data) processing_time = int((time.time() - start_time) * 1000) if not result["success"]: # Log the failed request log_simulator_request( "his_event", request.data, "failed", response_data={"error": result["message"]}, error_message=result["message"], processing_time_ms=processing_time, request=request, ) return Response({"error": result["message"]}, status=status.HTTP_400_BAD_REQUEST) # Prepare success response - only survey-related data (no journey data) response_data = { "success": True, "message": result["message"], "patient_id": str(result["patient"].id) if result["patient"] else None, "patient_mrn": result["patient"].mrn if result["patient"] else None, "patient_name": result["patient"].get_full_name() if result["patient"] else None, "patient_type": result.get("patient_type"), "survey_id": str(result["survey"].id) if result["survey"] else None, "survey_sent": result["survey_sent"], "survey_url": result["survey_url"], } # Log the successful request log_simulator_request( "his_event", request.data, "success", response_data=response_data, processing_time_ms=processing_time, request=request, ) return Response(response_data, status=status.HTTP_200_OK) except Exception as e: processing_time = int((time.time() - start_time) * 1000) logger.error(f"[HIS Patient Data Handler] Error: {str(e)}", exc_info=True) # Log the failed request log_simulator_request( "his_event", request.data, "failed", response_data={"error": str(e)}, error_message=str(e), processing_time_ms=processing_time, request=request, ) return Response( {"error": "Internal server error", "details": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @api_view(["POST"]) @permission_classes([AllowAny]) def his_events_handler(request): """ HIS Events API Endpoint - Legacy Format Receives patient journey events from HIS simulator: - Creates or updates patient records - Creates journey instances - Processes stage completions - Sends post-discharge surveys when journey is complete Expected payload: { "events": [ { "encounter_id": "ENC-2024-001", "mrn": "MRN-12345", "national_id": "1234567890", "first_name": "Ahmed", "last_name": "Mohammed", "phone": "+966501234567", "email": "patient@example.com", "event_type": "OPD_STAGE_1_REGISTRATION", "timestamp": "2024-01-20T10:30:00Z", "visit_type": "opd", "department": "Cardiology", "hospital_code": "ALH-main" } ] } NOTE: This endpoint is kept for backward compatibility. New integrations should use /api/simulator/his-patient-data/ """ start_time = time.time() try: # Validate request data serializer = HISJourneyEventListSerializer(data=request.data) if not serializer.is_valid(): return Response({"error": "Invalid data", "details": serializer.errors}, status=status.HTTP_400_BAD_REQUEST) events_data = serializer.validated_data["events"] # Process each event results = [] survey_invitations_sent = [] for event_data in events_data: result = process_his_event(event_data) results.append(result) # Track if survey was sent if result.get("survey_sent"): survey_invitations_sent.append( { "encounter_id": event_data["encounter_id"], "survey_id": result["survey_id"], "survey_url": result["survey_url"], } ) processing_time = int((time.time() - start_time) * 1000) response_data = { "success": True, "message": f"Processed {len(events_data)} events successfully", "results": results, "surveys_sent": len(survey_invitations_sent), "survey_details": survey_invitations_sent, } # Log the successful request log_simulator_request( "his_event", request.data, "success", response_data=response_data, processing_time_ms=processing_time, request=request, ) return Response(response_data, status=status.HTTP_200_OK) except Exception as e: processing_time = int((time.time() - start_time) * 1000) logger.error(f"[HIS Events Handler] Error: {str(e)}", exc_info=True) # Log the failed request log_simulator_request( "his_event", request.data, "failed", response_data={"error": str(e)}, error_message=str(e), processing_time_ms=processing_time, request=request, ) return Response( {"error": "Internal server error", "details": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) def process_his_event(event_data): """ Process a single HIS journey event Steps: 1. Get or create patient 2. Get or create journey instance 3. Find and update stage instance 4. Check if journey is complete and send survey if needed """ try: # 1. Get or create patient patient = get_or_create_patient(event_data) # 2. Get or create journey instance journey_instance = get_or_create_journey_instance(event_data, patient) # 3. Find and update stage instance stage_instance = update_stage_instance(journey_instance, event_data) result = { "encounter_id": event_data["encounter_id"], "patient_id": str(patient.id) if patient else None, "journey_id": str(journey_instance.id), "stage_id": str(stage_instance.id) if stage_instance else None, "stage_status": stage_instance.status if stage_instance else "not_found", "survey_sent": False, } # 4. Check if journey is complete and send survey if journey_instance.is_complete(): survey_result = send_post_discharge_survey(journey_instance, patient) if survey_result: result.update(survey_result) return result except Exception as e: logger.error( f"[Process HIS Event] Error for encounter {event_data.get('encounter_id')}: {str(e)}", exc_info=True ) return {"encounter_id": event_data.get("encounter_id"), "error": str(e), "success": False} def get_or_create_patient(event_data): """Get or create patient from event data""" try: patient, created = Patient.objects.get_or_create( mrn=event_data["mrn"], defaults={ "first_name": event_data["first_name"], "last_name": event_data["last_name"], "phone": event_data["phone"], "email": event_data["email"], "national_id": event_data.get("national_id", ""), }, ) logger.info(f"{'Created' if created else 'Found'} patient {patient.mrn}: {patient.get_full_name()}") return patient except Exception as e: logger.error(f"Error creating patient: {str(e)}") raise def get_or_create_journey_instance(event_data, patient): """Get or create journey instance for this encounter""" try: # Get hospital from event data or default to ALH-main hospital_code = event_data.get("hospital_code", "ALH-main") hospital = Hospital.objects.filter(code=hospital_code).first() if not hospital: raise ValueError( f"Hospital with code '{hospital_code}' not found. Please run seed_journey_surveys command first." ) # Map visit_type to JourneyType journey_type_map = {"ems": JourneyType.EMS, "inpatient": JourneyType.INPATIENT, "opd": JourneyType.OPD} journey_type = journey_type_map.get(event_data["visit_type"]) if not journey_type: raise ValueError(f"Invalid visit_type: {event_data['visit_type']}") # Get journey template journey_template = PatientJourneyTemplate.objects.filter( hospital=hospital, journey_type=journey_type, is_active=True ).first() if not journey_template: raise ValueError(f"No active journey template found for {journey_type}") # Get or create journey instance journey_instance, created = PatientJourneyInstance.objects.get_or_create( encounter_id=event_data["encounter_id"], defaults={ "journey_template": journey_template, "patient": patient, "hospital": hospital, "status": "active", }, ) # Create stage instances if this is a new journey if created: for stage_template in journey_template.stages.filter(is_active=True): PatientJourneyStageInstance.objects.create( journey_instance=journey_instance, stage_template=stage_template, status=StageStatus.PENDING ) logger.info( f"Created new journey instance {journey_instance.id} with {journey_template.stages.count()} stages" ) return journey_instance except Exception as e: logger.error(f"Error creating journey instance: {str(e)}") raise def update_stage_instance(journey_instance, event_data): """Find and update stage instance based on event_type""" try: # Find stage template by trigger_event_code stage_template = journey_instance.journey_template.stages.filter( trigger_event_code=event_data["event_type"], is_active=True ).first() if not stage_template: logger.warning(f"No stage template found for event_type: {event_data['event_type']}") return None # Get or create stage instance stage_instance, created = PatientJourneyStageInstance.objects.get_or_create( journey_instance=journey_instance, stage_template=stage_template, defaults={"status": StageStatus.PENDING} ) # Complete the stage if stage_instance.status != StageStatus.COMPLETED: from django.utils import timezone stage_instance.status = StageStatus.COMPLETED stage_instance.completed_at = timezone.now() stage_instance.save() logger.info(f"Completed stage {stage_template.name} for journey {journey_instance.encounter_id}") else: logger.info(f"Stage {stage_template.name} already completed for journey {journey_instance.encounter_id}") return stage_instance except Exception as e: logger.error(f"Error updating stage instance: {str(e)}") raise def send_post_discharge_survey(journey_instance, patient): """ Send post-discharge survey to patient Creates a survey instance and sends invitation via email and SMS """ try: # Check if journey template has post-discharge survey enabled journey_template = journey_instance.journey_template if not journey_template.send_post_discharge_survey: return None # Check if survey already sent for this journey existing_survey = SurveyInstance.objects.filter(journey_instance=journey_instance).first() if existing_survey: logger.info(f"Survey already sent for journey {journey_instance.encounter_id}") return None # Get survey template from journey template # Use first stage's survey template as the comprehensive survey first_stage = journey_template.stages.filter(is_active=True).order_by("order").first() if not first_stage or not first_stage.survey_template: logger.warning(f"No survey template found for journey {journey_instance.encounter_id}") return None survey_template = first_stage.survey_template # Create survey instance survey_instance = SurveyInstance.objects.create( survey_template=survey_template, patient=patient, journey_instance=journey_instance, hospital=journey_instance.hospital, delivery_channel="email", # Primary channel is email status="pending", recipient_email=patient.email, recipient_phone=patient.phone, ) logger.info(f"Created survey instance {survey_instance.id} for journey {journey_instance.encounter_id}") # Send survey invitation via email try: email_log = NotificationService.send_survey_invitation(survey_instance, language="en") logger.info(f"Survey invitation sent via email to {patient.email}") except Exception as e: logger.error(f"Error sending survey email: {str(e)}") # Also send via SMS (as backup) try: sms_log = NotificationService.send_sms( phone=patient.phone, message=f"Your experience survey is ready: {survey_instance.get_survey_url()}", related_object=survey_instance, metadata={"survey_id": str(survey_instance.id)}, ) logger.info(f"Survey invitation sent via SMS to {patient.phone}") except Exception as e: logger.error(f"Error sending survey SMS: {str(e)}") # Return survey details return { "survey_sent": True, "survey_id": str(survey_instance.id), "survey_url": survey_instance.get_survey_url(), "delivery_channel": "email_and_sms", } except Exception as e: logger.error(f"Error sending post-discharge survey: {str(e)}", exc_info=True) return {"survey_sent": False, "error": str(e)} HIS_HOSPITALS = { "2": {"name": "SUWAIDI", "id": "2"}, "3": {"name": "NUZHA", "id": "3"}, } ARABIC_FIRST_NAMES_MALE = [ "Mohammed", "Ahmed", "Abdullah", "Omar", "Ali", "Saud", "Fahad", "Turki", "Khalid", "Youssef", "Abdulrahman", "Abdulaziz", "Nasser", "Majid", "Ibrahim", "Hassan", "Saleh", "Faisal", "Saeed", "Walid", "Naif", "Mishaal", "Abdullah", "Bader", "Sultan", ] ARABIC_FIRST_NAMES_FEMALE = [ "Fatima", "Aisha", "Sarah", "Nora", "Layla", "Hessa", "Reem", "Mona", "Dalal", "Jawaher", "Maryam", "Ghaida", "Shaima", "Suha", "Fahdah", "Amwaj", "Nagwa", "Fadiya", "Afnan", "Hailah", "Meznah", "Manar", "Ghala", "Sharifah", "Inayah", "Aljuri", ] ARABIC_LAST_NAMES = [ "Alharbi", "Alotaibi", "Alshammari", "Aldosari", "Almutairi", "Alqahtani", "Alzahrani", "Alghamdi", "Alsubaie", "Alanazi", "Almalki", "Alahmari", "Aldkhini", "Albakhet", "Alangari", "Almas", "Bu Subayt", "Alkathiri", "Al Dimshaq", "Alhumaid", "Alshaddadi", "Altambakti", "Abunemeh", "Iqbal", "Khan", "Mustafa", "Munir", "Abdelghafar", "Ba Saad", "Abounada", "Jamur", "Alasmri", "Badawoud", "Allaboun", "Alsuobie", ] NATIONALITIES = [ ("SAUDI", 55), ("PAKISTAN", 10), ("JORDAN", 8), ("YEMEN", 7), ("EGYPT", 5), ("SYRIA", 3), ("SUDAN", 3), ("NIGERIA", 2), ("PALESTINIAN EGYPTIAN", 2), ("PALESTINIAN", 2), ("INDIAN", 2), ("FILIPINO", 1), ] INSURANCE_COMPANIES = [ "The Cooperative Company for Cooperative Insurance", "Bupa Arabia for Cooperative Insurance", "The Mediterranean and Gulf Cooperative Insurance and Reinsurance Company (MedGulf)", "Al-Rajhi Company for Cooperative Insurance", "Saudi United Cooperative Insurance Company (Wala)", "Center for National Health Insurance", "Arabian Shield Cooperative Insurance Company", "Tawuniya Cooperative Insurance Company", ] COMPANIES_GRADES = [ ("Rasan Al Arabiya Information Technology // TAWUNIYA", "GL/A"), ("Walaa CoOperative Insurance Co // Walaa - GlobeMed", "A"), ("NATIONAL WATER COMPANY / TAWUNIYA", "VIP"), ("BUREAU OF EXPERTS AT THE C OF MINISTERS // TAWUNIYA", "VVIP"), ("M/S SFDA // MEDGULF", "VIP"), ("IHP MEDTRONIC // BUPA", "VIP"), ("SAVETO LTD // MEDGULF", "A"), ("IHP - WSP MIDDLE EAST AND PARTNER SAUDI COMPANY // BUPA", "VIP"), ( "Al Hammadi for Mgmt / Pol # 1054568804, 805, 806, 807, 808, 809, 810, 811, 829, 837, 1054572893 - Al Rajhi Takaful", "A", ), ("Wons Beverage Establishment // AL Rajhi", "GOLD 5.5"), ("Watani Iron Steel Company // AL RAJHI", "A"), ("MAJID AL FUTTAIM FASHION SPC // NCCI", "B"), ("IHP - WSP MIDDLE EAST AND PARTNER SAUDI COMPANY // BUPA", "VIP"), ("SAUDI PORTS AUTHORITY // TAWUNIYA", "VIP"), ("SAUDI ARAMCO // BUPA", "GENERAL CLASS"), ("CNHI (Center for National Health Insurance)", "GENERAL CLASS"), ("OPERATIONS ALLIANCE OPS O AND M COMPANY // BUPA", "GOLD"), ("ELITE LAW FIRM AND L //TAWUNIYA", "GL/B"), ("Dynamic Solutions For General Contracting Est // Al Rajhi", "A"), ("NATIONAL SECURITY SERVICES COMPANY SAFE // BUPA", "VIP"), ("Dhuruma O M Company Limited // Bupa", "A"), ("Al Ayuni Investment and Contracting Branch Co. (2) // BUPA", "B"), ("AlFuttaim First Commercial Company // BUPA", "SILVER"), ("International Human Resources / Tawuniya", "A"), ("HULOOL ALSAHABA FOR IT AND COMMUNICATION// Tawuniya", "A"), ("Hisham Nabil Ali for contracting Co // BUPA", "PREMIUM 2.1 S"), ("YOUSSEF MARROUN CONT CO // NCCI", "B"), ("BALSAM 10", "GENERAL - MAX. SR. 50"), ("BANK ALBILAD // BUPA", "VIP"), ("Al Hammadi for Mgmt / Pol # 1054568804 - Al Rajhi Takaful", "A"), ] DOCTORS = [ ("12524", "MAI ABOELNASR ELBORI"), ("16468", "HEBA ELSHABOURY ABDELATTY"), ("18816", "ABDELMONEIM MOHAMED GAMIL IBRAHIM"), ("8663", "RAZETHA MOHAMMED IBRAHIM"), ("13383", "ABDELRHMAN SAAD ELAMIN"), ("15091", "MOHAMAD AMAR MOHAMAD YASER MSALAM"), ("15289", "HASSAN ASHRAF AHMED"), ("12055", "SAMEH SAEED ABDELFATTAH"), ("8902", "SHADI FAWAZ AL MAGRABI"), ("14761", "DOAA AWAD MASSOUD"), ("6092", "ADEL ABDELTAWAB ELSHENAWY"), ("8606", "AHMED NABIL YAKOUT"), ("18829", "ATHEEL SALEH H ALDBASSI"), ("13031", "ABDULAZIZ ALANAZI"), ("13524", "BAHA TOHAMY DIAB"), ("16764", "WALID MOHAMED ABDELSATTAR"), ("2957", "AHMED ALBORAEY KABEEL"), ("18189", "SAMER MOUHAMED HAMADEH"), ("18176", "ELAWAD KHALID OMER NAFIE"), ("15453", "EMAN HAMDY MAHMOUD ABDELRAHIM"), ("18884", "EMAN ABDELMAGEED BASIONY ELHAMRAWY"), ("13454", "HAGIR ASIM HUSSEIN"), ("4904", "ABDULRAHMAN ABBAS ALMUTAWAKEL"), ("11259", "WALID MOHAMED ABDELSATTAR"), ("6824", "MOHAMMED MAISARA ABDULHAMID"), ("4873", "ORWA MOHAMED HAMICH"), ("12461", "ELAWAD KHALID OMER NAFIE"), ("9922", "HASSAN ASHRAF AHMED"), ("8484", "ABDELRHMAN SAAD ELAMIN"), ("13010", "ABDELMONEIM MOHAMED GAMIL IBRAHIM"), ] HIS_VISIT_EVENT_TYPES = [ "Consultation", "Doctor Visited", "Clinical Condtion", "ChiefComplaint", "Prescribed Drugs", ] def _weighted_choice(choices_with_weights): total = sum(w for _, w in choices_with_weights) r = random.uniform(0, total) cumulative = 0 for choice, weight in choices_with_weights: cumulative += weight if r <= cumulative: return choice return choices_with_weights[0][0] def _format_his_date(dt): months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] return f"{dt.day:02d}-{months[dt.month - 1]}-{dt.year} {dt.hour:02d}:{dt.minute:02d}" def _generate_name(gender): if gender == "Male": first = random.choice(ARABIC_FIRST_NAMES_MALE) else: first = random.choice(ARABIC_FIRST_NAMES_FEMALE) middle_options = [ "Mohammed", "Ahmed", "Ibrahim", "Abdullah", "Saleh", "Husam", "Saad", "Fahad", "Mutaz", "Othman", "Abdullatif", "Yousef", "", ] middle = random.choice(middle_options) last = random.choice(ARABIC_LAST_NAMES) if middle: full_name = f"{first} {middle} {last}" else: full_name = f"{first} {last}" return full_name def _generate_age(dob): today = datetime.now() years = today.year - dob.year months = today.month - dob.month if months < 0: years -= 1 months += 12 if years >= 1: if years == 1: return f"1 Year(s)" return f"{years} Year(s)" else: return f"{months} Month(s)" def _generate_patient_data(hospital_id, hospital_name, patient_type_code): patient_id = str(random.randint(2000000, 2999999)) admission_id = str(random.randint(811900, 899999)) reg_code = f"ALHH.{random.randint(100000000, 999999999)}" is_male = random.random() < 0.45 gender = "Male" if is_male else "Female" gender_id = "1" if is_male else "2" patient_name = _generate_name(gender) dob = datetime.now() - timedelta(days=random.randint(60, 95 * 365)) full_age = _generate_age(dob) nationality = _weighted_choice(NATIONALITIES) ssn = "".join([str(random.randint(0, 9)) for _ in range(10)]) if nationality not in ["SAUDI", "JORDAN", "YEMEN", "EGYPT", "SYRIA", "SUDAN"]: if random.random() < 0.3: ssn = f"P{random.randint(10000000, 99999999)}" phone = f"05{random.randint(0, 9)}{random.randint(10000000, 99999999)}" doctor_id, doctor_name = random.choice(DOCTORS) consultant_id = str(random.randint(100, 9999)) admit_date = datetime.now() - timedelta( days=random.randint(0, 6), hours=random.randint(0, 23), minutes=random.randint(0, 59), ) has_insurance = random.random() < 0.65 if has_insurance: company_name, grade_name = random.choice(COMPANIES_GRADES) company_id = str(random.randint(10000, 99999)) grade_id = str(random.randint(1, 9999)) insurance_company = random.choice(INSURANCE_COMPANIES) bill_type = "CR" else: company_name = "" grade_name = "" company_id = "" grade_id = "0" insurance_company = "" bill_type = "CS" discharge_date = None if patient_type_code == "ED": if random.random() < 0.80: hours = random.randint(0, 6) discharge_date = admit_date + timedelta(hours=max(1, hours), minutes=random.randint(0, 59)) elif patient_type_code == "IP": if random.random() < 0.50: days = random.randint(1, 5) discharge_date = admit_date + timedelta(days=days, hours=random.randint(0, 14)) elif patient_type_code == "OP": if random.random() < 0.60: hours = random.randint(1, 3) discharge_date = admit_date + timedelta(hours=hours, minutes=random.randint(0, 59)) return ( { "Type": "Patient Demographic details", "PatientID": patient_id, "AdmissionID": admission_id, "HospitalID": hospital_id, "HospitalName": hospital_name, "PatientTypeID": patient_type_code, "PatientType": patient_type_code, "AdmitDate": _format_his_date(admit_date), "DischargeDate": _format_his_date(discharge_date) if discharge_date else None, "RegCode": reg_code, "SSN": ssn, "PatientName": patient_name, "GenderID": gender_id, "Gender": gender, "FullAge": full_age, "PatientNationality": nationality, "MobileNo": phone, "DOB": _format_his_date(dob), "ConsultantID": consultant_id, "PrimaryDoctor": f"{doctor_id}-{doctor_name}", "CompanyID": company_id, "GradeID": grade_id, "CompanyName": company_name, "GradeName": grade_name, "InsuranceCompanyName": insurance_company, "BillType": bill_type, "IsVIP": "0", }, admit_date, discharge_date, ) def _generate_visit_timeline( admit_date, discharge_date, patient_type_code, patient_id, admission_id, reg_code, ssn, mobile_no ): visit_data = [] is_discharged = discharge_date is not None if patient_type_code == "ED": if is_discharged: num_events = random.randint(3, 5) else: num_events = random.randint(1, 3) elif patient_type_code == "IP": if is_discharged: num_events = random.randint(3, 5) else: num_events = random.randint(1, 3) else: num_events = random.randint(1, 2) selected_types = random.sample(HIS_VISIT_EVENT_TYPES, min(num_events, len(HIS_VISIT_EVENT_TYPES))) base_time = admit_date + timedelta(minutes=random.randint(15, 45)) for i, event_type in enumerate(selected_types): event_time = base_time + timedelta( minutes=i * random.randint(10, 30), seconds=random.randint(0, 59), ) visit_data.append( { "Type": event_type, "BillDate": _format_his_date(event_time), "PatientID": patient_id, "PatientType": patient_type_code, "AdmissionID": admission_id, "RegCode": reg_code, "SSN": ssn, "MobileNo": mobile_no, } ) return visit_data @api_view(["GET"]) @permission_classes([AllowAny]) def generate_visit_data(request): """ Generate fake HIS visit data matching the real HIS format (visit_data.json). GET /api/simulator/generate-visit/?patient_type=ED&hospital_id=2 Query Parameters: patient_type: "ED", "IP", or "OP" (default: random with realistic distribution) hospital_id: "2" (SUWAIDI) or "3" (NUZHA) (default: random) Returns a single fake patient record in the exact HIS response format. """ patient_type_param = request.GET.get("patient_type", "").upper() hospital_id_param = request.GET.get("hospital_id", "") if patient_type_param in ("ED", "IP", "OP"): patient_type_code = patient_type_param else: r = random.random() if r < 0.55: patient_type_code = "ED" elif r < 0.80: patient_type_code = "IP" else: patient_type_code = "OP" if hospital_id_param in HIS_HOSPITALS: hospital = HIS_HOSPITALS[hospital_id_param] else: hospital = random.choice(list(HIS_HOSPITALS.values())) hospital_id = hospital["id"] hospital_name = hospital["name"] patient_data, admit_date, discharge_date = _generate_patient_data(hospital_id, hospital_name, patient_type_code) visit_data = _generate_visit_timeline( admit_date, discharge_date, patient_type_code, patient_data["PatientID"], patient_data["AdmissionID"], patient_data["RegCode"], patient_data["SSN"], patient_data["MobileNo"], ) response = { "FetchPatientDataTimeStampList": [patient_data], "FetchPatientDataTimeStampVisitDataList": visit_data, "FetchPatientDataTimeStampVisitEDDataList": visit_data if patient_type_code == "ED" else [], "FetchPatientDataTimeStampVisitIPDataList": visit_data if patient_type_code == "IP" else [], "FetchPatientDataTimeStampVisitOPDataList": visit_data if patient_type_code == "OP" else [], "Code": 200, "Status": "Success", "Message": "", "Message2L": "", "MobileNo": "", "ValidateMessage": "", } return Response(response)