607 lines
23 KiB
Python
607 lines
23 KiB
Python
"""
|
|
External API serializers for public-facing create/retrieve operations.
|
|
"""
|
|
|
|
from rest_framework import serializers
|
|
|
|
from apps.complaints.models import Complaint, Inquiry
|
|
from apps.observations.models import Observation
|
|
from apps.appreciation.models import Appreciation, AppreciationStatus, AppreciationVisibility
|
|
from apps.feedback.models import Feedback, FeedbackType, FeedbackStatus, FeedbackCategory
|
|
from apps.physicians.models import PhysicianIndividualRating
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Complaint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalComplaintCreateSerializer(serializers.Serializer):
|
|
"""Public-facing fields for creating a complaint via external API."""
|
|
|
|
# Contact
|
|
contact_name = serializers.CharField(max_length=200, required=True)
|
|
contact_phone = serializers.CharField(max_length=20, required=True)
|
|
contact_email = serializers.EmailField(required=False, allow_blank=True, default="")
|
|
relation_to_patient = serializers.ChoiceField(
|
|
choices=[("patient", "Patient"), ("relative", "Relative")],
|
|
required=False,
|
|
allow_blank=True,
|
|
default="",
|
|
)
|
|
|
|
# Patient
|
|
patient_name = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
national_id = serializers.CharField(max_length=50, required=False, allow_blank=True, default="")
|
|
incident_date = serializers.DateField(required=False, allow_null=True, default=None)
|
|
|
|
# Hospital context
|
|
hospital = serializers.CharField(max_length=200, required=True)
|
|
|
|
# Location hierarchy
|
|
location_type = serializers.ChoiceField(
|
|
choices=[("OP", "Outpatient"), ("IP", "Inpatient"), ("ER", "Emergency"), ("GENERAL", "General")],
|
|
required=False,
|
|
allow_blank=True,
|
|
default="",
|
|
)
|
|
area = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
department_code = serializers.CharField(max_length=200, required=True)
|
|
section = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
|
|
# Complaint content
|
|
title = serializers.CharField(max_length=500, required=True)
|
|
description = serializers.CharField(required=True)
|
|
expected_result = serializers.CharField(required=False, allow_blank=True, default="")
|
|
|
|
def validate_hospital(self, value):
|
|
from apps.organizations.models import Hospital
|
|
try:
|
|
return Hospital.objects.get(name__iexact=value.strip())
|
|
except Hospital.DoesNotExist:
|
|
raise serializers.ValidationError("Hospital not found.")
|
|
|
|
def validate(self, data):
|
|
hospital = data.get("hospital")
|
|
if hospital:
|
|
area_name = data.get("area")
|
|
if area_name:
|
|
from apps.organizations.models import Area
|
|
try:
|
|
data["area"] = Area.objects.get(
|
|
name_en__iexact=area_name.strip(), hospital=hospital, status="active"
|
|
)
|
|
except Area.DoesNotExist:
|
|
raise serializers.ValidationError({"area": f"Area '{area_name}' not found for this hospital."})
|
|
|
|
department_code = data.get("department_code")
|
|
if department_code:
|
|
from apps.organizations.models import Department
|
|
try:
|
|
data["department"] = Department.objects.get(
|
|
code__iexact=department_code.strip(), hospital=hospital, status="active"
|
|
)
|
|
except Department.DoesNotExist:
|
|
raise serializers.ValidationError(
|
|
{"department_code": f"Department with code '{department_code}' not found for this hospital."}
|
|
)
|
|
else:
|
|
data["department"] = None
|
|
|
|
section_name = data.get("section")
|
|
if section_name:
|
|
department = data.get("department")
|
|
if not department:
|
|
raise serializers.ValidationError({"section": "Department is required to look up section."})
|
|
from apps.organizations.models import Section
|
|
try:
|
|
data["section"] = Section.objects.get(
|
|
name_en__iexact=section_name.strip(), department=department, status="active"
|
|
)
|
|
except Section.DoesNotExist:
|
|
raise serializers.ValidationError(
|
|
{"section": f"Section '{section_name}' not found for this department."}
|
|
)
|
|
|
|
return data
|
|
|
|
|
|
class ExternalComplaintRetrieveSerializer(serializers.ModelSerializer):
|
|
"""Read-only serializer for retrieving a complaint."""
|
|
|
|
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
|
|
|
class Meta:
|
|
model = Complaint
|
|
fields = [
|
|
"id",
|
|
"reference_number",
|
|
"title",
|
|
"description",
|
|
"status",
|
|
"severity",
|
|
"priority",
|
|
"contact_name",
|
|
"contact_phone",
|
|
"contact_email",
|
|
"relation_to_patient",
|
|
"patient_name",
|
|
"incident_date",
|
|
"expected_result",
|
|
"resolution",
|
|
"satisfaction",
|
|
"hospital_name",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Inquiry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalInquiryCreateSerializer(serializers.Serializer):
|
|
"""Public-facing fields for creating an inquiry via external API."""
|
|
|
|
# Contact
|
|
contact_name = serializers.CharField(max_length=200, required=True)
|
|
contact_phone = serializers.CharField(max_length=20, required=True)
|
|
contact_email = serializers.EmailField(required=False, allow_blank=True, default="")
|
|
|
|
# Hospital context
|
|
hospital = serializers.CharField(max_length=200, required=True)
|
|
|
|
# Location hierarchy (all optional)
|
|
location_type = serializers.ChoiceField(
|
|
choices=[("OP", "Outpatient"), ("IP", "Inpatient"), ("ER", "Emergency"), ("GENERAL", "General")],
|
|
required=False,
|
|
allow_blank=True,
|
|
default="",
|
|
)
|
|
area = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
department_code = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
section = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
|
|
# Inquiry content
|
|
subject = serializers.CharField(max_length=500, required=True)
|
|
message = serializers.CharField(required=True)
|
|
category = serializers.ChoiceField(
|
|
choices=[
|
|
("appointment", "Appointment"),
|
|
("billing", "Billing"),
|
|
("medical_records", "Medical Records"),
|
|
("general", "General Information"),
|
|
("other", "Other"),
|
|
],
|
|
required=False,
|
|
default="general",
|
|
)
|
|
|
|
def validate_hospital(self, value):
|
|
from apps.organizations.models import Hospital
|
|
try:
|
|
return Hospital.objects.get(name__iexact=value.strip())
|
|
except Hospital.DoesNotExist:
|
|
raise serializers.ValidationError("Hospital not found.")
|
|
|
|
def validate(self, data):
|
|
hospital = data.get("hospital")
|
|
if hospital:
|
|
area_name = data.get("area")
|
|
if area_name:
|
|
from apps.organizations.models import Area
|
|
try:
|
|
data["area"] = Area.objects.get(
|
|
name_en__iexact=area_name.strip(), hospital=hospital, status="active"
|
|
)
|
|
except Area.DoesNotExist:
|
|
raise serializers.ValidationError({"area": f"Area '{area_name}' not found for this hospital."})
|
|
|
|
department_code = data.get("department_code")
|
|
if department_code:
|
|
from apps.organizations.models import Department
|
|
try:
|
|
data["department"] = Department.objects.get(
|
|
code__iexact=department_code.strip(), hospital=hospital, status="active"
|
|
)
|
|
except Department.DoesNotExist:
|
|
raise serializers.ValidationError(
|
|
{"department_code": f"Department with code '{department_code}' not found for this hospital."}
|
|
)
|
|
else:
|
|
data["department"] = None
|
|
|
|
section_name = data.get("section")
|
|
if section_name:
|
|
department = data.get("department")
|
|
if not department:
|
|
raise serializers.ValidationError({"section": "Department is required to look up section."})
|
|
from apps.organizations.models import Section
|
|
try:
|
|
data["section"] = Section.objects.get(
|
|
name_en__iexact=section_name.strip(), department=department, status="active"
|
|
)
|
|
except Section.DoesNotExist:
|
|
raise serializers.ValidationError(
|
|
{"section": f"Section '{section_name}' not found for this department."}
|
|
)
|
|
|
|
return data
|
|
|
|
|
|
class ExternalInquiryRetrieveSerializer(serializers.ModelSerializer):
|
|
"""Read-only serializer for retrieving an inquiry."""
|
|
|
|
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
|
|
|
class Meta:
|
|
model = Inquiry
|
|
fields = [
|
|
"id",
|
|
"reference_number",
|
|
"subject",
|
|
"message",
|
|
"category",
|
|
"status",
|
|
"contact_name",
|
|
"contact_phone",
|
|
"contact_email",
|
|
"hospital_name",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Observation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalObservationCreateSerializer(serializers.Serializer):
|
|
"""Public-facing fields for creating an observation via external API."""
|
|
|
|
# Hospital context
|
|
hospital = serializers.CharField(max_length=200, required=True)
|
|
|
|
# Classification
|
|
category = serializers.UUIDField(required=False, allow_null=True, default=None)
|
|
|
|
# Content
|
|
title = serializers.CharField(max_length=300, required=False, allow_blank=True, default="")
|
|
description = serializers.CharField(required=True)
|
|
severity = serializers.ChoiceField(
|
|
choices=[("low", "Low"), ("medium", "Medium"), ("high", "High"), ("critical", "Critical")],
|
|
required=False,
|
|
default="medium",
|
|
)
|
|
|
|
# Location and timing
|
|
location_text = serializers.CharField(max_length=500, required=False, allow_blank=True, default="")
|
|
incident_datetime = serializers.DateTimeField(required=False, allow_null=True, default=None)
|
|
|
|
# Reporter info (all optional - anonymous supported)
|
|
contact_name = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
contact_phone = serializers.CharField(max_length=20, required=False, allow_blank=True, default="")
|
|
contact_email = serializers.EmailField(required=False, allow_blank=True, default="")
|
|
reporter_staff_id = serializers.CharField(max_length=50, required=False, allow_blank=True, default="")
|
|
|
|
# Patient info
|
|
patient_file_number = serializers.CharField(max_length=100, required=False, allow_blank=True, default="")
|
|
|
|
def validate_hospital(self, value):
|
|
from apps.organizations.models import Hospital
|
|
try:
|
|
return Hospital.objects.get(name__iexact=value.strip())
|
|
except Hospital.DoesNotExist:
|
|
raise serializers.ValidationError("Hospital not found.")
|
|
|
|
def validate_category(self, value):
|
|
if value is None:
|
|
return None
|
|
from apps.observations.models import ObservationCategory
|
|
try:
|
|
return ObservationCategory.objects.get(id=value)
|
|
except ObservationCategory.DoesNotExist:
|
|
raise serializers.ValidationError("Observation category not found.")
|
|
|
|
|
|
class ExternalObservationRetrieveSerializer(serializers.ModelSerializer):
|
|
"""Read-only serializer for retrieving an observation."""
|
|
|
|
reference_number = serializers.CharField(source="tracking_code", read_only=True)
|
|
hospital_name = serializers.CharField(source="hospital.name", read_only=True, default=None)
|
|
category_name = serializers.CharField(source="category.name", read_only=True, default=None)
|
|
contact_name = serializers.CharField(source="reporter_name", read_only=True)
|
|
contact_phone = serializers.CharField(source="reporter_phone", read_only=True)
|
|
contact_email = serializers.CharField(source="reporter_email", read_only=True)
|
|
|
|
class Meta:
|
|
model = Observation
|
|
fields = [
|
|
"id",
|
|
"reference_number",
|
|
"title",
|
|
"description",
|
|
"severity",
|
|
"status",
|
|
"location_text",
|
|
"incident_datetime",
|
|
"contact_name",
|
|
"contact_phone",
|
|
"contact_email",
|
|
"reporter_staff_id",
|
|
"patient_file_number",
|
|
"hospital_name",
|
|
"category_name",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Appreciation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalAppreciationCreateSerializer(serializers.Serializer):
|
|
"""Public-facing fields for creating an appreciation via external API."""
|
|
|
|
# Contact
|
|
contact_name = serializers.CharField(max_length=200, required=True)
|
|
contact_phone = serializers.CharField(max_length=20, required=True)
|
|
|
|
# Content
|
|
message = serializers.CharField(required=True)
|
|
|
|
# Hospital context
|
|
hospital = serializers.CharField(max_length=200, required=True)
|
|
|
|
def validate_hospital(self, value):
|
|
from apps.organizations.models import Hospital
|
|
try:
|
|
return Hospital.objects.get(name__iexact=value.strip())
|
|
except Hospital.DoesNotExist:
|
|
raise serializers.ValidationError("Hospital not found.")
|
|
|
|
|
|
class ExternalAppreciationRetrieveSerializer(serializers.ModelSerializer):
|
|
"""Read-only serializer for retrieving an appreciation."""
|
|
|
|
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
|
|
|
class Meta:
|
|
model = Appreciation
|
|
fields = [
|
|
"id",
|
|
"reference_number",
|
|
"message_en",
|
|
"status",
|
|
"hospital_name",
|
|
"is_anonymous",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Suggestion (Feedback)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalSuggestionCreateSerializer(serializers.Serializer):
|
|
"""Public-facing fields for creating a suggestion via external API."""
|
|
|
|
# Contact
|
|
contact_name = serializers.CharField(max_length=200, required=True)
|
|
contact_phone = serializers.CharField(max_length=20, required=True)
|
|
|
|
# Content
|
|
title = serializers.CharField(max_length=500, required=False, allow_blank=True, default="")
|
|
message = serializers.CharField(required=True)
|
|
category = serializers.ChoiceField(
|
|
choices=[
|
|
("clinical_care", "Clinical Care"),
|
|
("staff_service", "Staff Service"),
|
|
("facility", "Facility"),
|
|
("communication", "Communication"),
|
|
("appointment", "Appointment"),
|
|
("billing", "Billing"),
|
|
("food_service", "Food Service"),
|
|
("cleanliness", "Cleanliness"),
|
|
("technology", "Technology"),
|
|
("general", "General"),
|
|
("other", "Other"),
|
|
],
|
|
required=False,
|
|
default="general",
|
|
)
|
|
rating = serializers.IntegerField(min_value=1, max_value=5, required=False, allow_null=True, default=None)
|
|
|
|
# Hospital context
|
|
hospital = serializers.CharField(max_length=200, required=True)
|
|
|
|
def validate_hospital(self, value):
|
|
from apps.organizations.models import Hospital
|
|
try:
|
|
return Hospital.objects.get(name__iexact=value.strip())
|
|
except Hospital.DoesNotExist:
|
|
raise serializers.ValidationError("Hospital not found.")
|
|
|
|
|
|
class ExternalSuggestionRetrieveSerializer(serializers.ModelSerializer):
|
|
"""Read-only serializer for retrieving a suggestion."""
|
|
|
|
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
|
|
|
class Meta:
|
|
model = Feedback
|
|
fields = [
|
|
"id",
|
|
"reference_number",
|
|
"title",
|
|
"message",
|
|
"category",
|
|
"rating",
|
|
"status",
|
|
"feedback_type",
|
|
"sentiment",
|
|
"contact_name",
|
|
"contact_phone",
|
|
"hospital_name",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class ExternalComplaintSatisfactionSerializer(serializers.Serializer):
|
|
satisfaction = serializers.ChoiceField(
|
|
choices=[
|
|
("satisfied", "Satisfied"),
|
|
("neutral", "Neutral"),
|
|
("dissatisfied", "Dissatisfied"),
|
|
("no_response", "No Response"),
|
|
],
|
|
required=True,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Doctor Rating
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExternalDoctorRatingCreateSerializer(serializers.Serializer):
|
|
|
|
hospital = serializers.CharField(max_length=200, required=True)
|
|
doctor_id = serializers.CharField(max_length=50, required=True)
|
|
doctor_name = serializers.CharField(max_length=300, required=False, allow_blank=True, default="")
|
|
rating = serializers.IntegerField(min_value=1, max_value=5, required=True)
|
|
feedback = serializers.CharField(required=False, allow_blank=True, default="")
|
|
rating_date = serializers.DateField(required=False, allow_null=True, default=None)
|
|
patient_uhid = serializers.CharField(max_length=100, required=False, allow_blank=True, default="")
|
|
patient_name = serializers.CharField(max_length=300, required=False, allow_blank=True, default="")
|
|
patient_type = serializers.ChoiceField(
|
|
choices=["IP", "OP", "ER", "DC"],
|
|
required=False,
|
|
allow_blank=True,
|
|
default="",
|
|
)
|
|
department_code = serializers.CharField(max_length=200, required=False, allow_blank=True, default="")
|
|
admit_date = serializers.DateField(required=False, allow_null=True, default=None)
|
|
discharge_date = serializers.DateField(required=False, allow_null=True, default=None)
|
|
|
|
def validate_hospital(self, value):
|
|
from apps.organizations.models import Hospital
|
|
|
|
try:
|
|
return Hospital.objects.get(name__iexact=value.strip())
|
|
except Hospital.DoesNotExist:
|
|
raise serializers.ValidationError("Hospital not found.")
|
|
|
|
def validate_doctor_id(self, value):
|
|
return value.strip()
|
|
|
|
def validate(self, data):
|
|
from apps.organizations.models import Department, Staff
|
|
from apps.physicians.adapter import DoctorRatingAdapter
|
|
|
|
hospital = data.get("hospital")
|
|
doctor_id = data.get("doctor_id", "").strip()
|
|
|
|
# Resolve department code → human-readable name (the rating model stores
|
|
# a text department_name; HIS sends a stable department_code instead).
|
|
department_code = data.get("department_code", "").strip()
|
|
data["department_name"] = ""
|
|
dept = None
|
|
if department_code and hospital:
|
|
dept = Department.objects.filter(
|
|
code__iexact=department_code, hospital=hospital, status="active"
|
|
).first()
|
|
if not dept:
|
|
raise serializers.ValidationError(
|
|
{"department_code": f"Department with code '{department_code}' not found for this hospital."}
|
|
)
|
|
data["department_name"] = dept.name_en or dept.name
|
|
|
|
if hospital and doctor_id:
|
|
doctor_name = (data.get("doctor_name") or "").strip()
|
|
|
|
# 1. Canonical multi-strategy lookup (employee_id → license_number → name).
|
|
staff = DoctorRatingAdapter.find_staff_by_doctor_id(
|
|
doctor_id=doctor_id, hospital=hospital, doctor_name=doctor_name
|
|
)
|
|
|
|
# 2. Auto-create the Staff if not found and we have a name to populate
|
|
# the required first_name/last_name fields.
|
|
if not staff:
|
|
if not doctor_name:
|
|
raise serializers.ValidationError(
|
|
{
|
|
"doctor_name": (
|
|
"Staff not found for this doctor_id; provide doctor_name "
|
|
"to auto-create the staff record."
|
|
)
|
|
}
|
|
)
|
|
name_parts = doctor_name.split()
|
|
first_name = name_parts[0]
|
|
last_name = name_parts[-1] if len(name_parts) > 1 else name_parts[0]
|
|
staff, created = Staff.objects.get_or_create(
|
|
employee_id=doctor_id,
|
|
defaults={
|
|
"hospital": hospital,
|
|
"first_name": first_name,
|
|
"last_name": last_name,
|
|
"name": doctor_name,
|
|
"staff_type": Staff.StaffType.PHYSICIAN,
|
|
"job_title": "Physician",
|
|
"physician": True,
|
|
"status": "active",
|
|
"department": dept,
|
|
},
|
|
)
|
|
data["_staff_created"] = created
|
|
|
|
data["_staff"] = staff
|
|
|
|
return data
|
|
|
|
|
|
class ExternalDoctorRatingRetrieveSerializer(serializers.ModelSerializer):
|
|
"""Read-only serializer for retrieving a doctor rating."""
|
|
|
|
hospital_name = serializers.CharField(source="hospital.name", read_only=True)
|
|
staff_id = serializers.CharField(read_only=True, default=None)
|
|
doctor_name_display = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = PhysicianIndividualRating
|
|
fields = [
|
|
"id",
|
|
"staff_id",
|
|
"hospital_name",
|
|
"source",
|
|
"source_reference",
|
|
"doctor_id",
|
|
"doctor_name",
|
|
"doctor_name_raw",
|
|
"doctor_name_display",
|
|
"department_name",
|
|
"patient_uhid",
|
|
"patient_name",
|
|
"patient_type",
|
|
"admit_date",
|
|
"discharge_date",
|
|
"rating",
|
|
"feedback",
|
|
"rating_date",
|
|
"is_aggregated",
|
|
"aggregated_at",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def get_doctor_name_display(self, obj):
|
|
return obj.doctor_name or obj.doctor_name_raw or ""
|