HH/apps/simulator/views.py
2026-04-08 17:13:35 +03:00

1409 lines
46 KiB
Python

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