1409 lines
46 KiB
Python
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)
|