HH/apps/integrations/api_views.py
2026-07-12 11:18:20 +03:00

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