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