1094 lines
36 KiB
Python
1094 lines
36 KiB
Python
"""
|
|
External API views for complaints, inquiries, observations, appreciations, and suggestions.
|
|
|
|
All endpoints require X-API-Key header authentication.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from django.utils import timezone
|
|
from rest_framework import generics, status
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from apps.complaints.models import Complaint, ComplaintSourceType, ComplaintUpdate, Inquiry
|
|
from apps.core.services import AuditService
|
|
from apps.observations.models import Observation
|
|
from apps.appreciation.models import Appreciation, AppreciationStatus, AppreciationVisibility
|
|
from apps.feedback.models import Feedback, FeedbackType, FeedbackStatus, FeedbackCategory
|
|
|
|
from .api_serializers import (
|
|
ExternalAppreciationCreateSerializer,
|
|
ExternalAppreciationRetrieveSerializer,
|
|
ExternalComplaintCreateSerializer,
|
|
ExternalComplaintRetrieveSerializer,
|
|
ExternalComplaintSatisfactionSerializer,
|
|
ExternalDoctorRatingCreateSerializer,
|
|
ExternalInquiryCreateSerializer,
|
|
ExternalInquiryRetrieveSerializer,
|
|
ExternalObservationCreateSerializer,
|
|
ExternalObservationRetrieveSerializer,
|
|
ExternalSuggestionCreateSerializer,
|
|
ExternalSuggestionRetrieveSerializer,
|
|
)
|
|
from .authentication import ExternalAPIKeyAuthentication
|
|
|
|
|
|
logger = logging.getLogger("apps.integrations")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Base mixin
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalAPIBase:
|
|
"""Shared behaviour for all external API views."""
|
|
|
|
authentication_classes = [ExternalAPIKeyAuthentication]
|
|
permission_classes = []
|
|
entity_name = None # Override in subclass
|
|
|
|
def check_entity_access(self):
|
|
"""Ensure the API key is allowed to access this entity."""
|
|
api_key = getattr(self.request, "api_key", None)
|
|
if api_key and not api_key.can_access(self.entity_name):
|
|
return Response(
|
|
{"detail": f"This API key does not have access to {self.entity_name}."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
return None
|
|
|
|
def filter_queryset_by_hospital(self, qs):
|
|
"""Scope query to the API key's hospital (if set)."""
|
|
api_key = getattr(self.request, "api_key", None)
|
|
if api_key and api_key.hospital:
|
|
hospital_field = self._get_hospital_field()
|
|
if hospital_field:
|
|
qs = qs.filter(**{hospital_field: api_key.hospital})
|
|
return qs
|
|
|
|
def _get_hospital_field(self):
|
|
"""Return the ORM field name for hospital on the model."""
|
|
return "hospital"
|
|
|
|
def apply_list_filters(self, qs, request):
|
|
"""Apply date range and status filters from query params."""
|
|
|
|
created_from = request.query_params.get("created_from", "").strip()
|
|
created_to = request.query_params.get("created_to", "").strip()
|
|
updated_from = request.query_params.get("updated_from", "").strip()
|
|
updated_to = request.query_params.get("updated_to", "").strip()
|
|
status_val = request.query_params.get("status", "").strip()
|
|
|
|
if created_from:
|
|
try:
|
|
qs = qs.filter(created_at__gte=self._parse_date(created_from, start=True))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if created_to:
|
|
try:
|
|
qs = qs.filter(created_at__lte=self._parse_date(created_to, start=False))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if updated_from:
|
|
try:
|
|
qs = qs.filter(updated_at__gte=self._parse_date(updated_from, start=True))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if updated_to:
|
|
try:
|
|
qs = qs.filter(updated_at__lte=self._parse_date(updated_to, start=False))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if status_val:
|
|
qs = qs.filter(status=status_val)
|
|
|
|
return qs
|
|
|
|
def _parse_date(self, value, start=True):
|
|
"""Parse a date string. If date-only, start=True → 00:00, start=False → 23:59:59."""
|
|
from datetime import datetime
|
|
from django.utils import timezone as tz
|
|
|
|
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M", "%Y-%m-%d"):
|
|
try:
|
|
dt = datetime.strptime(value, fmt)
|
|
if fmt == "%Y-%m-%d" and not start:
|
|
dt = dt.replace(hour=23, minute=59, second=59)
|
|
return tz.make_aware(dt) if tz.is_naive(dt) else dt
|
|
except ValueError:
|
|
continue
|
|
raise ValueError(f"Invalid date format: {value}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Complaints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalComplaintCreateView(ExternalAPIBase, APIView):
|
|
"""
|
|
POST /api/v1/external/complaints/
|
|
|
|
Create a complaint via external API.
|
|
"""
|
|
|
|
entity_name = "complaints"
|
|
|
|
def post(self, request):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
ser = ExternalComplaintCreateSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
data = ser.validated_data
|
|
|
|
# Enforce hospital scoping
|
|
api_key = getattr(request, "api_key", None)
|
|
if api_key and api_key.hospital and data["hospital"] != api_key.hospital:
|
|
return Response(
|
|
{"detail": "Hospital does not match API key scope."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
# Reference number generated by Complaint.save() (unified CMP-YYYYMM-NNNN)
|
|
|
|
complaint = Complaint.objects.create(
|
|
patient=None,
|
|
hospital=data["hospital"],
|
|
department=data.get("department"),
|
|
section=data.get("section"),
|
|
area=data.get("area"),
|
|
location_type=data.get("location_type", ""),
|
|
title=data["title"],
|
|
description=data["description"],
|
|
severity="medium",
|
|
priority="medium",
|
|
status="open",
|
|
complaint_source_type=ComplaintSourceType.EXTERNAL,
|
|
contact_name=data["contact_name"],
|
|
contact_phone=data["contact_phone"],
|
|
contact_email=data.get("contact_email", ""),
|
|
relation_to_patient=data.get("relation_to_patient", ""),
|
|
patient_name=data.get("patient_name", ""),
|
|
national_id=data.get("national_id", ""),
|
|
incident_date=data.get("incident_date"),
|
|
expected_result=data.get("expected_result", ""),
|
|
metadata={"source": "external_api"},
|
|
)
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message="Complaint submitted via external API.",
|
|
)
|
|
|
|
reference_number = complaint.reference_number
|
|
|
|
# Trigger background tasks
|
|
try:
|
|
from apps.complaints.tasks import analyze_complaint_with_ai, notify_staff_new_item
|
|
|
|
analyze_complaint_with_ai.delay(str(complaint.id))
|
|
notify_staff_new_item.delay("complaint", str(complaint.id))
|
|
except Exception:
|
|
pass
|
|
|
|
AuditService.log_event(
|
|
event_type="external_complaint_created",
|
|
description=f"Complaint created via external API: {reference_number}",
|
|
content_object=complaint,
|
|
metadata={"reference": reference_number, "source": "external_api"},
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"reference_number": reference_number,
|
|
"status": complaint.status,
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
class ExternalComplaintRetrieveView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/complaints/
|
|
GET /api/v1/external/complaints/<reference_number>/
|
|
|
|
List complaints (scoped to hospital) or retrieve a single complaint by reference number.
|
|
"""
|
|
|
|
entity_name = "complaints"
|
|
|
|
def get(self, request, reference_number=None):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
if reference_number:
|
|
return self._retrieve(request, reference_number)
|
|
return self._list(request)
|
|
|
|
def _retrieve(self, request, reference_number):
|
|
qs = Complaint.all_objects.filter(reference_number=reference_number)
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
complaint = qs.first()
|
|
if not complaint:
|
|
return Response({"detail": "Complaint not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
ser = ExternalComplaintRetrieveSerializer(complaint)
|
|
return Response(ser.data)
|
|
|
|
def _list(self, request):
|
|
qs = Complaint.all_objects.all()
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
qs = self.apply_list_filters(qs, request)
|
|
|
|
page_size = min(int(request.query_params.get("page_size", 20)), 100)
|
|
page = int(request.query_params.get("page", 1))
|
|
offset = (page - 1) * page_size
|
|
total = qs.count()
|
|
|
|
items = qs[offset : offset + page_size]
|
|
ser = ExternalComplaintRetrieveSerializer(items, many=True)
|
|
|
|
return Response(
|
|
{
|
|
"count": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"results": ser.data,
|
|
}
|
|
)
|
|
|
|
|
|
class ExternalComplaintSatisfactionView(ExternalAPIBase, APIView):
|
|
"""
|
|
PATCH /api/v1/external/complaints/<reference_number>/satisfaction/
|
|
|
|
Set patient satisfaction for a complaint.
|
|
"""
|
|
|
|
entity_name = "complaints"
|
|
|
|
def patch(self, request, reference_number):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
qs = Complaint.all_objects.filter(reference_number=reference_number)
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
complaint = qs.first()
|
|
if not complaint:
|
|
return Response({"detail": "Complaint not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
if not complaint.resolution:
|
|
return Response(
|
|
{"detail": "Cannot set satisfaction before a resolution is recorded."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
ser = ExternalComplaintSatisfactionSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
|
|
complaint.satisfaction = ser.validated_data["satisfaction"]
|
|
complaint.satisfaction_set_at = timezone.now()
|
|
complaint.save(update_fields=["satisfaction", "satisfaction_set_at", "updated_at"])
|
|
|
|
AuditService.log_event(
|
|
event_type="external_complaint_satisfaction_set",
|
|
description=f"Satisfaction set to '{complaint.satisfaction}' via external API for {reference_number}",
|
|
content_object=complaint,
|
|
metadata={"reference": reference_number, "satisfaction": complaint.satisfaction, "source": "external_api"},
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"reference_number": complaint.reference_number,
|
|
"satisfaction": complaint.satisfaction,
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Inquiries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalInquiryCreateView(ExternalAPIBase, APIView):
|
|
"""
|
|
POST /api/v1/external/inquiries/
|
|
|
|
Create an inquiry via external API.
|
|
"""
|
|
|
|
entity_name = "inquiries"
|
|
|
|
def post(self, request):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
ser = ExternalInquiryCreateSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
data = ser.validated_data
|
|
|
|
api_key = getattr(request, "api_key", None)
|
|
if api_key and api_key.hospital and data["hospital"] != api_key.hospital:
|
|
return Response(
|
|
{"detail": "Hospital does not match API key scope."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
today = datetime.now().strftime("%Y%m%d")
|
|
random_suffix = str(uuid.uuid4().int)[:6]
|
|
reference_number = f"INQ-{today}-{random_suffix}"
|
|
|
|
inquiry = Inquiry.objects.create(
|
|
patient=None,
|
|
hospital=data["hospital"],
|
|
department=data.get("department"),
|
|
section=data.get("section"),
|
|
area=data.get("area"),
|
|
location_type=data.get("location_type", ""),
|
|
subject=data["subject"],
|
|
message=data["message"],
|
|
category=data.get("category", "general"),
|
|
contact_name=data["contact_name"],
|
|
contact_phone=data["contact_phone"],
|
|
contact_email=data.get("contact_email", ""),
|
|
status="open",
|
|
reference_number=reference_number,
|
|
is_outgoing=False,
|
|
metadata={"source": "external_api"},
|
|
)
|
|
|
|
try:
|
|
from apps.complaints.tasks import analyze_inquiry_with_ai, notify_staff_new_item
|
|
|
|
analyze_inquiry_with_ai.delay(str(inquiry.id))
|
|
notify_staff_new_item.delay("inquiry", str(inquiry.id))
|
|
except Exception:
|
|
pass
|
|
|
|
AuditService.log_event(
|
|
event_type="external_inquiry_created",
|
|
description=f"Inquiry created via external API: {reference_number}",
|
|
content_object=inquiry,
|
|
metadata={"reference": reference_number, "source": "external_api"},
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"reference_number": reference_number,
|
|
"status": inquiry.status,
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
class ExternalInquiryRetrieveView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/inquiries/
|
|
GET /api/v1/external/inquiries/<reference_number>/
|
|
"""
|
|
|
|
entity_name = "inquiries"
|
|
|
|
def get(self, request, reference_number=None):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
if reference_number:
|
|
return self._retrieve(request, reference_number)
|
|
return self._list(request)
|
|
|
|
def _retrieve(self, request, reference_number):
|
|
qs = Inquiry.all_objects.filter(reference_number=reference_number)
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
inquiry = qs.first()
|
|
if not inquiry:
|
|
return Response({"detail": "Inquiry not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
ser = ExternalInquiryRetrieveSerializer(inquiry)
|
|
return Response(ser.data)
|
|
|
|
def _list(self, request):
|
|
qs = Inquiry.all_objects.all()
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
qs = self.apply_list_filters(qs, request)
|
|
|
|
page_size = min(int(request.query_params.get("page_size", 20)), 100)
|
|
page = int(request.query_params.get("page", 1))
|
|
offset = (page - 1) * page_size
|
|
total = qs.count()
|
|
|
|
items = qs[offset : offset + page_size]
|
|
ser = ExternalInquiryRetrieveSerializer(items, many=True)
|
|
|
|
return Response(
|
|
{
|
|
"count": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"results": ser.data,
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Observations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalObservationCreateView(ExternalAPIBase, APIView):
|
|
"""
|
|
POST /api/v1/external/observations/
|
|
|
|
Create an observation via external API.
|
|
"""
|
|
|
|
entity_name = "observations"
|
|
|
|
def post(self, request):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
ser = ExternalObservationCreateSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
data = ser.validated_data
|
|
|
|
api_key = getattr(request, "api_key", None)
|
|
if api_key and api_key.hospital and data["hospital"] != api_key.hospital:
|
|
return Response(
|
|
{"detail": "Hospital does not match API key scope."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
from django.utils import timezone as tz
|
|
|
|
observation = Observation.objects.create(
|
|
hospital=data["hospital"],
|
|
category=data.get("category"),
|
|
title=data.get("title", ""),
|
|
description=data["description"],
|
|
severity=data.get("severity", "medium"),
|
|
location_text=data.get("location_text", ""),
|
|
incident_datetime=data.get("incident_datetime") or tz.now(),
|
|
reporter_staff_id=data.get("reporter_staff_id", ""),
|
|
reporter_name=data.get("contact_name", ""),
|
|
reporter_phone=data.get("contact_phone", ""),
|
|
reporter_email=data.get("contact_email", ""),
|
|
patient_file_number=data.get("patient_file_number", ""),
|
|
source="other",
|
|
status="new",
|
|
metadata={"source": "external_api"},
|
|
)
|
|
|
|
try:
|
|
from apps.complaints.tasks import notify_staff_new_item
|
|
|
|
notify_staff_new_item.delay("observation", str(observation.id))
|
|
except Exception:
|
|
pass
|
|
|
|
AuditService.log_event(
|
|
event_type="external_observation_created",
|
|
description=f"Observation created via external API: {observation.tracking_code}",
|
|
content_object=observation,
|
|
metadata={"reference_number": observation.tracking_code, "source": "external_api"},
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"reference_number": observation.tracking_code,
|
|
"status": observation.status,
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
class ExternalObservationRetrieveView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/observations/
|
|
GET /api/v1/external/observations/<tracking_code>/
|
|
"""
|
|
|
|
entity_name = "observations"
|
|
|
|
def get(self, request, tracking_code=None):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
if tracking_code:
|
|
return self._retrieve(request, tracking_code)
|
|
return self._list(request)
|
|
|
|
def _retrieve(self, request, tracking_code):
|
|
qs = Observation.all_objects.filter(tracking_code=tracking_code)
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
observation = qs.first()
|
|
if not observation:
|
|
return Response({"detail": "Observation not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
ser = ExternalObservationRetrieveSerializer(observation)
|
|
return Response(ser.data)
|
|
|
|
def _list(self, request):
|
|
qs = Observation.all_objects.all()
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
qs = self.apply_list_filters(qs, request)
|
|
|
|
page_size = min(int(request.query_params.get("page_size", 20)), 100)
|
|
page = int(request.query_params.get("page", 1))
|
|
offset = (page - 1) * page_size
|
|
total = qs.count()
|
|
|
|
items = qs[offset : offset + page_size]
|
|
ser = ExternalObservationRetrieveSerializer(items, many=True)
|
|
|
|
return Response(
|
|
{
|
|
"count": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"results": ser.data,
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Appreciations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalAppreciationCreateView(ExternalAPIBase, APIView):
|
|
"""
|
|
POST /api/v1/external/appreciations/
|
|
|
|
Create an appreciation via external API.
|
|
"""
|
|
|
|
entity_name = "appreciations"
|
|
|
|
def post(self, request):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
ser = ExternalAppreciationCreateSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
data = ser.validated_data
|
|
|
|
api_key = getattr(request, "api_key", None)
|
|
if api_key and api_key.hospital and data["hospital"] != api_key.hospital:
|
|
return Response(
|
|
{"detail": "Hospital does not match API key scope."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
appreciation = Appreciation.objects.create(
|
|
hospital=data["hospital"],
|
|
message_en=data["message"],
|
|
message_ar="",
|
|
category=None,
|
|
is_anonymous=False,
|
|
status=AppreciationStatus.DRAFT,
|
|
visibility=AppreciationVisibility.PUBLIC,
|
|
metadata={
|
|
"source": "external_api",
|
|
"submitted_by_name": data["contact_name"],
|
|
"submitted_by_phone": data["contact_phone"],
|
|
},
|
|
)
|
|
|
|
try:
|
|
from apps.complaints.tasks import notify_staff_new_item
|
|
|
|
notify_staff_new_item.delay("appreciation", str(appreciation.id))
|
|
except Exception:
|
|
pass
|
|
|
|
AuditService.log_event(
|
|
event_type="external_appreciation_created",
|
|
description=f"Appreciation created via external API",
|
|
content_object=appreciation,
|
|
metadata={"source": "external_api"},
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"reference_number": appreciation.reference_number,
|
|
"status": appreciation.status,
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
class ExternalAppreciationRetrieveView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/appreciations/
|
|
GET /api/v1/external/appreciations/<reference_number>/
|
|
"""
|
|
|
|
entity_name = "appreciations"
|
|
|
|
def get(self, request, reference_number=None):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
if reference_number:
|
|
return self._retrieve(request, reference_number)
|
|
return self._list(request)
|
|
|
|
def _retrieve(self, request, reference_number):
|
|
qs = Appreciation.all_objects.filter(reference_number=reference_number)
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
appreciation = qs.first()
|
|
if not appreciation:
|
|
return Response({"detail": "Appreciation not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
ser = ExternalAppreciationRetrieveSerializer(appreciation)
|
|
return Response(ser.data)
|
|
|
|
def _list(self, request):
|
|
qs = Appreciation.all_objects.all()
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
qs = self.apply_list_filters(qs, request)
|
|
|
|
page_size = min(int(request.query_params.get("page_size", 20)), 100)
|
|
page = int(request.query_params.get("page", 1))
|
|
offset = (page - 1) * page_size
|
|
total = qs.count()
|
|
|
|
items = qs[offset : offset + page_size]
|
|
ser = ExternalAppreciationRetrieveSerializer(items, many=True)
|
|
|
|
return Response(
|
|
{
|
|
"count": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"results": ser.data,
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Suggestions (Feedback)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalSuggestionCreateView(ExternalAPIBase, APIView):
|
|
"""
|
|
POST /api/v1/external/suggestions/
|
|
|
|
Create a suggestion via external API.
|
|
"""
|
|
|
|
entity_name = "suggestions"
|
|
|
|
def post(self, request):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
ser = ExternalSuggestionCreateSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
data = ser.validated_data
|
|
|
|
api_key = getattr(request, "api_key", None)
|
|
if api_key and api_key.hospital and data["hospital"] != api_key.hospital:
|
|
return Response(
|
|
{"detail": "Hospital does not match API key scope."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
category_map = {
|
|
"general": FeedbackCategory.OTHER,
|
|
"clinical_care": FeedbackCategory.CLINICAL_CARE,
|
|
"facility": FeedbackCategory.FACILITY,
|
|
"staff_service": FeedbackCategory.STAFF_SERVICE,
|
|
"communication": FeedbackCategory.COMMUNICATION,
|
|
"technology": FeedbackCategory.TECHNOLOGY,
|
|
"food_service": FeedbackCategory.FOOD_SERVICE,
|
|
"appointment": FeedbackCategory.APPOINTMENT,
|
|
"other": FeedbackCategory.OTHER,
|
|
}
|
|
|
|
title = data.get("title") or data["message"][:100]
|
|
|
|
feedback = Feedback.objects.create(
|
|
hospital=data["hospital"],
|
|
feedback_type=FeedbackType.SUGGESTION,
|
|
title=title,
|
|
message=data["message"],
|
|
category=category_map.get(data.get("category", "general"), FeedbackCategory.OTHER),
|
|
contact_name=data["contact_name"],
|
|
contact_phone=data["contact_phone"],
|
|
rating=data.get("rating"),
|
|
is_anonymous=False,
|
|
status=FeedbackStatus.SUBMITTED,
|
|
metadata={
|
|
"source": "external_api",
|
|
"suggestion_area": data.get("category", "general"),
|
|
},
|
|
)
|
|
|
|
try:
|
|
from apps.feedback.tasks import analyze_suggestion_with_ai
|
|
from apps.complaints.tasks import notify_staff_new_item
|
|
|
|
analyze_suggestion_with_ai.delay(str(feedback.id))
|
|
notify_staff_new_item.delay("suggestion", str(feedback.id))
|
|
except Exception:
|
|
pass
|
|
|
|
AuditService.log_event(
|
|
event_type="external_suggestion_created",
|
|
description=f"Suggestion created via external API",
|
|
content_object=feedback,
|
|
metadata={"source": "external_api"},
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"reference_number": feedback.reference_number,
|
|
"status": feedback.status,
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
class ExternalSuggestionRetrieveView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/suggestions/
|
|
GET /api/v1/external/suggestions/<reference_number>/
|
|
"""
|
|
|
|
entity_name = "suggestions"
|
|
|
|
def get(self, request, reference_number=None):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
if reference_number:
|
|
return self._retrieve(request, reference_number)
|
|
return self._list(request)
|
|
|
|
def _retrieve(self, request, reference_number):
|
|
qs = Feedback.all_objects.filter(reference_number=reference_number)
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
feedback = qs.first()
|
|
if not feedback:
|
|
return Response({"detail": "Suggestion not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
ser = ExternalSuggestionRetrieveSerializer(feedback)
|
|
return Response(ser.data)
|
|
|
|
def _list(self, request):
|
|
qs = Feedback.all_objects.all()
|
|
qs = self.filter_queryset_by_hospital(qs)
|
|
qs = self.apply_list_filters(qs, request)
|
|
|
|
page_size = min(int(request.query_params.get("page_size", 20)), 100)
|
|
page = int(request.query_params.get("page", 1))
|
|
offset = (page - 1) * page_size
|
|
total = qs.count()
|
|
|
|
items = qs[offset : offset + page_size]
|
|
ser = ExternalSuggestionRetrieveSerializer(items, many=True)
|
|
|
|
return Response(
|
|
{
|
|
"count": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"results": ser.data,
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lookup / Dropdown endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalHospitalsListView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/hospitals/
|
|
|
|
List all active hospitals. Used by external integrations to build dropdowns.
|
|
"""
|
|
|
|
entity_name = "lookup"
|
|
|
|
def get(self, request):
|
|
from apps.organizations.models import Hospital
|
|
|
|
qs = Hospital.objects.filter(status="active").order_by("name")
|
|
results = [
|
|
{
|
|
"id": str(h.id),
|
|
"name": h.name,
|
|
"code": h.code,
|
|
}
|
|
for h in qs
|
|
]
|
|
return Response({"count": len(results), "results": results})
|
|
|
|
|
|
class ExternalLocationTypesView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/location-types/
|
|
|
|
Static list of location types.
|
|
"""
|
|
|
|
entity_name = "lookup"
|
|
|
|
def get(self, request):
|
|
results = [
|
|
{"value": "OP", "label": "Outpatient", "label_ar": "خارجي"},
|
|
{"value": "IP", "label": "Inpatient", "label_ar": "تنويم"},
|
|
{"value": "ER", "label": "Emergency", "label_ar": "طوارئ"},
|
|
{"value": "GENERAL", "label": "General", "label_ar": "عام"},
|
|
]
|
|
return Response({"count": len(results), "results": results})
|
|
|
|
|
|
class ExternalAreasListView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/areas/?hospital=<name>&location_type=<type>
|
|
|
|
List areas for a hospital, optionally filtered by location_type.
|
|
"""
|
|
|
|
entity_name = "lookup"
|
|
|
|
def get(self, request):
|
|
from apps.organizations.models import Area, Hospital
|
|
|
|
hospital_name = request.query_params.get("hospital", "").strip()
|
|
location_type = request.query_params.get("location_type", "").strip()
|
|
|
|
if not hospital_name:
|
|
return Response(
|
|
{"detail": "hospital query parameter is required."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
try:
|
|
hospital = Hospital.objects.get(name__iexact=hospital_name, status="active")
|
|
except Hospital.DoesNotExist:
|
|
return Response({"detail": "Hospital not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
qs = Area.objects.filter(hospital=hospital, status="active")
|
|
if location_type:
|
|
qs = qs.filter(location_type=location_type)
|
|
qs = qs.order_by("name_en")
|
|
|
|
results = [
|
|
{
|
|
"id": str(a.id),
|
|
"name": a.name_en,
|
|
"name_ar": a.name_ar,
|
|
"code": a.code,
|
|
}
|
|
for a in qs
|
|
]
|
|
return Response({"count": len(results), "results": results})
|
|
|
|
|
|
class ExternalDepartmentsListView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/departments/?hospital=<name>
|
|
|
|
List departments for a hospital.
|
|
"""
|
|
|
|
entity_name = "lookup"
|
|
|
|
def get(self, request):
|
|
from apps.organizations.models import Department, Hospital
|
|
|
|
hospital_name = request.query_params.get("hospital", "").strip()
|
|
|
|
if not hospital_name:
|
|
return Response(
|
|
{"detail": "hospital query parameter is required."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
try:
|
|
hospital = Hospital.objects.get(name__iexact=hospital_name, status="active")
|
|
except Hospital.DoesNotExist:
|
|
return Response({"detail": "Hospital not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
qs = Department.objects.filter(hospital=hospital, status="active").order_by("name")
|
|
|
|
results = [
|
|
{
|
|
"id": str(d.id),
|
|
"name": d.name,
|
|
"name_en": d.name_en,
|
|
"name_ar": d.name_ar,
|
|
"code": d.code,
|
|
}
|
|
for d in qs
|
|
]
|
|
return Response({"count": len(results), "results": results})
|
|
|
|
|
|
class ExternalSectionsListView(ExternalAPIBase, APIView):
|
|
"""
|
|
GET /api/v1/external/sections/?hospital=<name>&department=<name>
|
|
|
|
List sections for a department.
|
|
"""
|
|
|
|
entity_name = "lookup"
|
|
|
|
def get(self, request):
|
|
from apps.organizations.models import Department, Hospital, Section
|
|
|
|
hospital_name = request.query_params.get("hospital", "").strip()
|
|
department_name = request.query_params.get("department", "").strip()
|
|
|
|
if not hospital_name or not department_name:
|
|
return Response(
|
|
{"detail": "hospital and department query parameters are required."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
try:
|
|
hospital = Hospital.objects.get(name__iexact=hospital_name, status="active")
|
|
except Hospital.DoesNotExist:
|
|
return Response({"detail": "Hospital not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
try:
|
|
department = Department.objects.get(
|
|
name__iexact=department_name, hospital=hospital, status="active"
|
|
)
|
|
except Department.DoesNotExist:
|
|
return Response({"detail": "Department not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
qs = Section.objects.filter(department=department, status="active").order_by("name_en")
|
|
|
|
results = [
|
|
{
|
|
"id": str(s.id),
|
|
"name": s.name_en,
|
|
"name_ar": s.name_ar,
|
|
"code": s.code,
|
|
}
|
|
for s in qs
|
|
]
|
|
return Response({"count": len(results), "results": results})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Doctor Ratings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalDoctorRatingCreateView(ExternalAPIBase, APIView):
|
|
"""
|
|
POST /api/v1/external/doctor-ratings/
|
|
|
|
Submit a patient rating for a doctor via external API.
|
|
Requires doctor_id (employee_id) that exists in the Staff table for the given hospital.
|
|
"""
|
|
|
|
entity_name = "doctor_ratings"
|
|
|
|
def post(self, request):
|
|
access_err = self.check_entity_access()
|
|
if access_err:
|
|
return access_err
|
|
|
|
ser = ExternalDoctorRatingCreateSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
data = ser.validated_data
|
|
|
|
hospital = data["hospital_id"]
|
|
staff = data.pop("_staff")
|
|
|
|
api_key = getattr(request, "api_key", None)
|
|
if api_key and api_key.hospital and hospital != api_key.hospital:
|
|
return Response(
|
|
{"detail": "Hospital does not match API key scope."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
from django.utils import timezone as tz
|
|
from apps.physicians.models import PhysicianIndividualRating
|
|
|
|
rating_date = data.get("rating_date")
|
|
if rating_date:
|
|
from datetime import datetime as _dt
|
|
rating_dt = tz.make_aware(_dt.combine(rating_date, _dt.min.time()))
|
|
else:
|
|
rating_dt = tz.now()
|
|
|
|
doctor_name_raw = data.get("doctor_name", "") or staff.get_full_name()
|
|
|
|
rating = PhysicianIndividualRating.objects.create(
|
|
staff=staff,
|
|
hospital=hospital,
|
|
source="his_api",
|
|
doctor_name_raw=doctor_name_raw,
|
|
doctor_id=data["doctor_id"],
|
|
doctor_name=data.get("doctor_name", ""),
|
|
rating=data["rating"],
|
|
feedback=data.get("feedback", ""),
|
|
rating_date=rating_dt,
|
|
patient_uhid=data.get("patient_uhid", "") or None,
|
|
patient_name=data.get("patient_name", "") or None,
|
|
patient_type=data.get("patient_type", "") or None,
|
|
department_name=data.get("department_name", ""),
|
|
admit_date=data.get("admit_date"),
|
|
discharge_date=data.get("discharge_date"),
|
|
metadata={"source": "external_api"},
|
|
)
|
|
|
|
if not staff.physician:
|
|
staff.physician = True
|
|
staff.save(update_fields=["physician"])
|
|
|
|
AuditService.log_event(
|
|
event_type="external_doctor_rating_created",
|
|
description=f"Doctor rating created via external API: {staff.get_full_name()} - {data['rating']}/5",
|
|
content_object=rating,
|
|
metadata={
|
|
"staff_id": str(staff.id),
|
|
"doctor_id": data["doctor_id"],
|
|
"rating": data["rating"],
|
|
"hospital_id": str(hospital.id),
|
|
"source": "external_api",
|
|
},
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"rating_id": str(rating.id),
|
|
"staff_id": str(staff.id),
|
|
"doctor_name": staff.get_full_name(),
|
|
"rating": data["rating"],
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|