488 lines
17 KiB
Python
488 lines
17 KiB
Python
"""
|
|
Integrations views and viewsets
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from rest_framework import status, views, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
from apps.accounts.permissions import IsPXAdmin
|
|
from apps.core.services import AuditService
|
|
|
|
from .models import EventMapping, HISTestPatient, HISTestVisit, InboundEvent, IntegrationConfig
|
|
from .serializers import (
|
|
EventMappingSerializer,
|
|
HISPatientDataSerializer,
|
|
IntegrationConfigSerializer,
|
|
)
|
|
from .services.his_adapter import HISAdapter
|
|
|
|
logger = logging.getLogger("apps.integrations")
|
|
|
|
|
|
class HISPatientDataView(views.APIView):
|
|
"""
|
|
API View for receiving complete HIS patient data.
|
|
|
|
This replaces the old event-based approach. HIS systems send
|
|
complete patient data including demographics and visit timeline.
|
|
The system determines survey type from PatientType and sends
|
|
appropriate survey via SMS.
|
|
|
|
POST /api/integrations/events/ - Send complete HIS patient data
|
|
|
|
Request Format:
|
|
{
|
|
"FetchPatientDataTimeStampList": [{...patient demographics...}],
|
|
"FetchPatientDataTimeStampVisitDataList": [
|
|
{"Type": "Consultation", "BillDate": "05-Jun-2025 11:06"},
|
|
...
|
|
],
|
|
"Code": 200,
|
|
"Status": "Success"
|
|
}
|
|
|
|
PatientType Codes:
|
|
- "1" → Inpatient Survey
|
|
- "2" or "O" → OPD Survey
|
|
- "3" or "E" → EMS Survey
|
|
- "4" or "D" → Day Case Survey
|
|
|
|
Response Format:
|
|
{
|
|
"success": true,
|
|
"message": "Patient data processed successfully",
|
|
"patient": {
|
|
"id": 123,
|
|
"mrn": "878943",
|
|
"name": "AFAF NASSER ALRAZoooOOQ"
|
|
},
|
|
"patient_type": "1",
|
|
"survey": {
|
|
"id": 456,
|
|
"status": "SENT",
|
|
"survey_url": "https://..."
|
|
},
|
|
"survey_sent": true
|
|
}
|
|
"""
|
|
|
|
permission_classes = [] # Allow public access for HIS integration
|
|
|
|
def post(self, request):
|
|
"""Process HIS patient data and create/send survey"""
|
|
# Validate HIS data format
|
|
serializer = HISPatientDataSerializer(data=request.data)
|
|
|
|
if not serializer.is_valid():
|
|
return Response(
|
|
{"success": False, "error": "Invalid HIS data format", "details": serializer.errors},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
his_data = serializer.validated_data
|
|
|
|
# Log the incoming data
|
|
patient_list = his_data.get("FetchPatientDataTimeStampList", [])
|
|
patient_data = patient_list[0] if patient_list else {}
|
|
patient_id = patient_data.get("PatientID", "Unknown")
|
|
patient_type = patient_data.get("PatientType", "Unknown")
|
|
|
|
logger.info(f"HIS patient data received: PatientID={patient_id}, PatientType={patient_type}")
|
|
|
|
# Process HIS data using HISAdapter
|
|
result = HISAdapter.process_his_data(his_data)
|
|
|
|
# Create audit log
|
|
if result["success"]:
|
|
AuditService.log_from_request(
|
|
event_type="his_integration",
|
|
description=(
|
|
f"HIS patient data processed: PatientID={patient_id}, "
|
|
f"PatientType={patient_type}, "
|
|
f"Survey Sent={result.get('survey_sent', False)}"
|
|
),
|
|
request=request,
|
|
metadata={
|
|
"patient_id": patient_id,
|
|
"patient_type": patient_type,
|
|
"survey_sent": result.get("survey_sent", False),
|
|
"survey_id": result.get("survey").id if result.get("survey") else None,
|
|
},
|
|
)
|
|
|
|
# Prepare response
|
|
response_data = {"success": result["success"], "message": result["message"]}
|
|
|
|
# Add patient info if available
|
|
if result.get("patient"):
|
|
patient = result["patient"]
|
|
response_data["patient"] = {
|
|
"id": patient.id,
|
|
"mrn": patient.mrn,
|
|
"name": f"{patient.first_name} {patient.last_name}".strip(),
|
|
}
|
|
response_data["patient_type"] = result.get("patient_type")
|
|
|
|
# Add survey info if available
|
|
if result.get("survey"):
|
|
survey = result["survey"]
|
|
response_data["survey"] = {"id": survey.id, "status": survey.status, "survey_url": survey.get_survey_url()}
|
|
response_data["survey_sent"] = result.get("survey_sent", False)
|
|
|
|
# Return appropriate status code
|
|
if result["success"]:
|
|
status_code = status.HTTP_200_OK
|
|
else:
|
|
status_code = status.HTTP_400_BAD_REQUEST
|
|
|
|
return Response(response_data, status=status_code)
|
|
|
|
|
|
class InboundEventViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Legacy ViewSet for Inbound Events (DEPRECATED).
|
|
|
|
This viewset is kept for backward compatibility but is no longer
|
|
the primary integration method. Use HISPatientDataView instead.
|
|
"""
|
|
|
|
queryset = InboundEvent.objects.all()
|
|
permission_classes = [IsAuthenticated, IsPXAdmin]
|
|
filterset_fields = ["status", "source_system", "event_code", "encounter_id"]
|
|
search_fields = ["encounter_id", "patient_identifier", "event_code"]
|
|
ordering_fields = ["received_at", "processed_at"]
|
|
ordering = ["-received_at"]
|
|
|
|
def get_serializer_class(self):
|
|
"""Return appropriate serializer based on action"""
|
|
from .serializers import (
|
|
InboundEventSerializer,
|
|
InboundEventListSerializer,
|
|
)
|
|
|
|
if self.action == "list":
|
|
return InboundEventListSerializer
|
|
return InboundEventSerializer
|
|
|
|
@action(detail=False, methods=["post"], permission_classes=[IsPXAdmin])
|
|
def bulk_create(self, request):
|
|
"""
|
|
Bulk create events.
|
|
|
|
DEPRECATED: This endpoint is kept for backward compatibility.
|
|
Use HISPatientDataView for new integrations.
|
|
"""
|
|
from .serializers import InboundEventCreateSerializer
|
|
|
|
events_data = request.data.get("events", [])
|
|
|
|
if not events_data:
|
|
return Response({"error": "No events provided"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
created_events = []
|
|
errors = []
|
|
|
|
for event_data in events_data:
|
|
serializer = InboundEventCreateSerializer(data=event_data)
|
|
if serializer.is_valid():
|
|
event = serializer.save()
|
|
created_events.append(event)
|
|
else:
|
|
errors.append({"data": event_data, "errors": serializer.errors})
|
|
|
|
return Response(
|
|
{
|
|
"created": len(created_events),
|
|
"failed": len(errors),
|
|
"errors": errors,
|
|
"message": "This endpoint is deprecated. Use HISPatientDataView for new integrations.",
|
|
},
|
|
status=status.HTTP_201_CREATED if created_events else status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
|
|
class IntegrationConfigViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
ViewSet for Integration Configurations.
|
|
|
|
Permissions:
|
|
- Only PX Admins can manage integration configurations
|
|
"""
|
|
|
|
queryset = IntegrationConfig.objects.all()
|
|
serializer_class = IntegrationConfigSerializer
|
|
permission_classes = [IsPXAdmin]
|
|
filterset_fields = ["source_system", "is_active"]
|
|
search_fields = ["name", "description"]
|
|
ordering_fields = ["name", "created_at"]
|
|
ordering = ["name"]
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().prefetch_related("event_mappings")
|
|
|
|
|
|
class EventMappingViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
ViewSet for Event Mappings.
|
|
|
|
Permissions:
|
|
- Only PX Admins can manage event mappings
|
|
"""
|
|
|
|
queryset = EventMapping.objects.all()
|
|
serializer_class = EventMappingSerializer
|
|
permission_classes = [IsPXAdmin]
|
|
filterset_fields = ["integration_config", "is_active"]
|
|
search_fields = ["external_event_code", "internal_event_code"]
|
|
ordering_fields = ["external_event_code"]
|
|
ordering = ["integration_config", "external_event_code"]
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().select_related("integration_config")
|
|
|
|
|
|
class TestHISDataView(views.APIView):
|
|
"""
|
|
Test endpoint that mimics the real HIS API.
|
|
|
|
GET /api/integrations/test-his-data/?FromDate=...&ToDate=...&SSN=...&MobileNo=...
|
|
|
|
Reads from HISTestPatient/HISTestVisit tables (loaded via load_his_test_data command).
|
|
Supports the same query parameters as the real HIS API:
|
|
- FromDate / ToDate: Filter by AdmitDate range (format: DD-Mon-YYYY HH:MM:SS)
|
|
- SSN: Filter by SSN ("0" = all)
|
|
- MobileNo: Filter by MobileNo ("0" = all)
|
|
"""
|
|
|
|
permission_classes = []
|
|
|
|
def _parse_his_date(self, date_str):
|
|
if not date_str:
|
|
return None
|
|
try:
|
|
from django.utils import timezone
|
|
|
|
naive = datetime.strptime(date_str, "%d-%b-%Y %H:%M:%S")
|
|
return timezone.make_aware(naive)
|
|
except ValueError:
|
|
pass
|
|
try:
|
|
from django.utils import timezone
|
|
|
|
naive = datetime.strptime(date_str, "%d-%b-%Y %H:%M")
|
|
return timezone.make_aware(naive)
|
|
except ValueError:
|
|
return None
|
|
|
|
def get(self, request):
|
|
from_date_str = request.GET.get("FromDate")
|
|
to_date_str = request.GET.get("ToDate")
|
|
ssn = request.GET.get("SSN", "0")
|
|
mobile_no = request.GET.get("MobileNo", "0")
|
|
|
|
from_date = self._parse_his_date(from_date_str)
|
|
to_date = self._parse_his_date(to_date_str)
|
|
|
|
if from_date and to_date and to_date < from_date:
|
|
return Response(
|
|
{"Code": 400, "Status": "Error", "Message": "ToDate must be after FromDate"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
qs = HISTestPatient.objects.all()
|
|
|
|
if from_date:
|
|
qs = qs.filter(admit_date__gte=from_date)
|
|
|
|
if to_date:
|
|
qs = qs.filter(admit_date__lte=to_date)
|
|
|
|
if ssn and ssn != "0":
|
|
qs = qs.filter(ssn=ssn)
|
|
|
|
if mobile_no and mobile_no != "0":
|
|
qs = qs.filter(mobile_no=mobile_no)
|
|
|
|
patients = list(qs[:1000])
|
|
admission_ids = [p.admission_id for p in patients]
|
|
|
|
patient_list = [p.patient_data for p in patients]
|
|
|
|
visits = HISTestVisit.objects.filter(admission_id__in=admission_ids)
|
|
ed_visits = []
|
|
ip_visits = []
|
|
op_visits = []
|
|
for v in visits:
|
|
if v.visit_category == "ED":
|
|
ed_visits.append(v.visit_data)
|
|
elif v.visit_category == "IP":
|
|
ip_visits.append(v.visit_data)
|
|
elif v.visit_category == "OP":
|
|
op_visits.append(v.visit_data)
|
|
|
|
return Response(
|
|
{
|
|
"FetchPatientDataTimeStampList": patient_list,
|
|
"FetchPatientDataTimeStampVisitEDDataList": ed_visits,
|
|
"FetchPatientDataTimeStampVisitIPDataList": ip_visits,
|
|
"FetchPatientDataTimeStampVisitOPDataList": op_visits,
|
|
"Code": 200,
|
|
"Status": "Success",
|
|
"MobileNo": "",
|
|
"Message": "",
|
|
"Message2L": "",
|
|
}
|
|
)
|
|
|
|
|
|
class SimulateHISPayloadView(views.APIView):
|
|
"""
|
|
POST endpoint to simulate receiving HIS data for testing.
|
|
|
|
Accepts the same JSON format as the real HIS API response and:
|
|
1. Stores patients/visits into HISTestPatient/HISTestVisit (persistent)
|
|
2. Calls HISAdapter.process_his_response() to process everything
|
|
3. Returns full processing results (surveys created, errors, etc.)
|
|
|
|
POST /api/integrations/simulate-his-payload/
|
|
Body: same as HIS response (visit_data.json format)
|
|
|
|
After posting, the data will also be picked up by the regular
|
|
fetch-his-surveys cron if the "HIS Test" IntegrationConfig is active.
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated, IsPXAdmin]
|
|
|
|
def _parse_date(self, date_str):
|
|
if not date_str:
|
|
return None
|
|
try:
|
|
naive = datetime.strptime(date_str, "%d-%b-%Y %H:%M")
|
|
return timezone.make_aware(naive)
|
|
except ValueError:
|
|
pass
|
|
try:
|
|
naive = datetime.strptime(date_str, "%d-%b-%Y %H:%M:%S")
|
|
return timezone.make_aware(naive)
|
|
except ValueError:
|
|
return None
|
|
|
|
def post(self, request):
|
|
payload = request.data
|
|
if not isinstance(payload, dict):
|
|
return Response({"detail": "Request body must be a JSON object"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
patient_list = payload.get("FetchPatientDataTimeStampList", [])
|
|
if not patient_list:
|
|
return Response({"detail": "FetchPatientDataTimeStampList is empty"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
ed_visits = payload.get("FetchPatientDataTimeStampVisitEDDataList", [])
|
|
ip_visits = payload.get("FetchPatientDataTimeStampVisitIPDataList", [])
|
|
op_visits = payload.get("FetchPatientDataTimeStampVisitOPDataList", [])
|
|
|
|
admission_ids = [p.get("AdmissionID") for p in patient_list]
|
|
admission_id_set = {aid for aid in admission_ids if aid}
|
|
|
|
patients_stored = 0
|
|
visits_stored = 0
|
|
skipped_patients = 0
|
|
|
|
existing_patients = set(
|
|
HISTestPatient.objects.filter(admission_id__in=admission_id_set).values_list("admission_id", flat=True)
|
|
)
|
|
existing_visits = set(
|
|
HISTestVisit.objects.filter(admission_id__in=admission_id_set).values_list(
|
|
"admission_id", "event_type", "bill_date"
|
|
)
|
|
)
|
|
|
|
patient_batch = []
|
|
for p in patient_list:
|
|
aid = p.get("AdmissionID", "")
|
|
if aid in existing_patients:
|
|
skipped_patients += 1
|
|
continue
|
|
|
|
patient_batch.append(
|
|
HISTestPatient(
|
|
admission_id=aid,
|
|
patient_id=str(p.get("PatientID", "")),
|
|
patient_type=p.get("PatientType", ""),
|
|
reg_code=p.get("RegCode", ""),
|
|
ssn=p.get("SSN", ""),
|
|
mobile_no=p.get("MobileNo", ""),
|
|
admit_date=self._parse_date(p.get("AdmitDate")),
|
|
discharge_date=self._parse_date(p.get("DischargeDate")),
|
|
patient_data=p,
|
|
hospital_id=str(p.get("HospitalID", "")),
|
|
hospital_name=p.get("HospitalName", ""),
|
|
patient_name=p.get("PatientName", ""),
|
|
)
|
|
)
|
|
|
|
if patient_batch:
|
|
HISTestPatient.objects.bulk_create(patient_batch, batch_size=1000)
|
|
patients_stored = len(patient_batch)
|
|
|
|
visit_batch = []
|
|
for v in ed_visits + ip_visits + op_visits:
|
|
aid = v.get("AdmissionID", "")
|
|
event_type = v.get("Type", "")
|
|
bill_date = self._parse_date(v.get("BillDate"))
|
|
visit_key = (aid, event_type, bill_date)
|
|
if visit_key in existing_visits:
|
|
continue
|
|
|
|
visit_category = "ED"
|
|
pt = v.get("PatientType", "")
|
|
if pt == "IP":
|
|
visit_category = "IP"
|
|
elif pt in ("OP", "OPD"):
|
|
visit_category = "OP"
|
|
|
|
visit_batch.append(
|
|
HISTestVisit(
|
|
admission_id=aid,
|
|
patient_id=str(v.get("PatientID", "")),
|
|
visit_category=visit_category,
|
|
event_type=event_type,
|
|
bill_date=bill_date,
|
|
reg_code=v.get("RegCode", ""),
|
|
ssn=v.get("SSN", ""),
|
|
mobile_no=v.get("MobileNo", ""),
|
|
visit_data=v,
|
|
)
|
|
)
|
|
|
|
if visit_batch:
|
|
HISTestVisit.objects.bulk_create(visit_batch, batch_size=2000)
|
|
visits_stored = len(visit_batch)
|
|
|
|
his_data = {
|
|
"FetchPatientDataTimeStampList": patient_list,
|
|
"FetchPatientDataTimeStampVisitEDDataList": ed_visits,
|
|
"FetchPatientDataTimeStampVisitIPDataList": ip_visits,
|
|
"FetchPatientDataTimeStampVisitOPDataList": op_visits,
|
|
"Code": payload.get("Code", 200),
|
|
"Status": payload.get("Status", "Success"),
|
|
"Message": payload.get("Message", ""),
|
|
}
|
|
|
|
process_result = HISAdapter.process_his_response(his_data)
|
|
|
|
return Response(
|
|
{
|
|
"storage": {
|
|
"patients_stored": patients_stored,
|
|
"patients_skipped": skipped_patients,
|
|
"visits_stored": visits_stored,
|
|
},
|
|
"processing": process_result,
|
|
}
|
|
)
|