HH/docs/workflows/complaints.md
2026-07-11 19:24:28 +03:00

40 KiB

Complaints — Workflow & Lifecycle

Source of truth: current code in apps/complaints/ (+ apps/organizations/ui_views.py for the manager-review tier). This document describes the current implementation only. Every claim is cited as file:line. It is not a redesign. Where code disagrees with docs/workflows.md, the code wins and the gap is flagged as ⚠ Gap.


1. Purpose

The Complaints module is the case-management engine for patient complaints. Per the model docstring (apps/complaints/models.py:1-10, :197-207) it:

  • Tracks complaints with SLA deadlines (models.py:501 due_at).
  • Manages the workflow open → in progress → resolved → closed (plus extra states).
  • Triggers a resolution-satisfaction survey on closure (models.py:570-573; task tasks.py:386).
  • Auto-creates PX Actions from negative resolution satisfaction (tasks.py:477).
  • Maintains a complaint timeline (ComplaintUpdate, models.py:1177) and attachments (ComplaintAttachment, models.py:1131).

The same Complaint model also handles appreciations via ComplaintType.APPRECIATION (models.py:72-76) and the sibling Inquiry model (models.py:1528) shares the app — both documented in their own files.


2. How a Case Starts

2.1 Creation channels

Channel Entry point View function Source recorded
Public portal (no login) POST /complaints/public/submit/complaints:public_complaint_submit (urls.py:123) ui_views.public_complaint_submit (ui_views.py:4369) complaint_source_type=INTERNAL (hardcoded ui_views.py:4463); source=PXSource("Public Form")
Authenticated staff/internal form GET/POST /complaints/new/complaints:complaint_create (urls.py:39) ui_views.complaint_create (ui_views.py:897) complaint_source_type default INTERNAL (forms.py:374-380); source from form/user, else PXSource("staff") (ui_views.py:930-946)
Patient SMS portal (token-auth session) POST /complaints/patient/<token>/visit/<visit_id>/complaints:patient_complaint_visit_form (urls.py:154) ui_views.patient_complaint_visit_form (ui_views.py:6686) via PatientComplaintSession (models.py:3449) complaint_source_type="internal"; metadata submitted_via:"patient_link"
DRF API (any authenticated user) POST /complaints/api/complaints/ComplaintViewSet (views.py:117) ComplaintViewSet.perform_create (views.py:229) via serializer
Government ticket conversion (MOH/CHI/CCHI) /complaints/government-tickets/<pk>/convert/complaints:convert_to_complaint (urls.py:214) ui_views.convert_to_complaint (ui_views.py:6887) complaint_source_type=external for MOH/CCHI (ui_views.py:6905); moh_reference/chi_reference (ui_views.py:6912-6913)
Call center / Survey / Social media / MOH / CHI No dedicated endpoint — these are ComplaintSource taxonomy values (models.py:86-97) selected via the source FK (models.py:443) on whichever channel is used. MOH/CHI typically arrive as GovernmentTickets then convert.

2.2 Who can create

  • Public form & patient SMS portal: no auth.
  • Internal staff form (complaint_create): @login_required only (ui_views.py:895).
  • Government ticket create/import/convert: is_px_admin() or is_px_management() (ui_views.py:6839, 6865, 6892, 6925).
  • DRF API: IsAuthenticated (views.py:127).

2.3 Information required before submission

Public form (PublicComplaintForm, forms.py:57, Meta.fields forms.py:228-247). Required: complainant_name, relation_to_patient, mobile_number, patient_name, national_id, incident_date, hospital, location_type, complaint_details. Optional: email, department, area, section, staff_name, expected_result, attachments (≤5 files, ≤10 MB, jpg/png/gif/pdf/doc/docx — forms.py:319-339).

Validators: Saudi mobile 05xxxxxxxx (forms.py:282-293); national_id 10 digits (forms.py:295-306); no future incident date (forms.py:308-317).

⚠ Gap: The view re-validates inline and additionally requires department + location_type + hospital even though the form marks department optional — the view is stricter than the form (ui_views.py:4401-4422, department required at ui_views.py:4411).

Internal form (ComplaintForm, forms.py:351, Meta.fields forms.py:483-501). Required: relation_to_patient, patient_name, national_id, incident_date, hospital, location_type, department, description. Optional: complaint_type, complaint_source_type, source, area, section, staff, expected_result.

2.4 What happens immediately after submission

Complaint.save() (models.py:737-772) runs on every save and:

  1. Records previous status into _status_was for the signal (models.py:740-745).
  2. Generates the reference number if absent: generate_reference("CMP", hospital) (models.py:748-751) → format CMP-{YYYYMM}-{SEQ:04d} (global monthly sequence, apps/core/reference.py:32-43).
  3. Computes the SLA deadline due_at via calculate_sla_due_date() (models.py:753-754, 784-830).
  4. Hashes national_idnational_id_hash (models.py:765-768).
  5. Syncs department send/forward timestamps (models.py:770, 774-782).
  6. Default status = ComplaintStatus.OPEN (models.py:463-465). Cases are created OPEN, not auto-activatedactivated_at is null until the activation action (§6).

Then ComplaintService.post_create_hooks (complaint_service.py:1049-1086):

  • Writes a ComplaintUpdate "Complaint created. AI analysis running in background."
  • Dispatches analyze_complaint_with_ai (tasks.py:668) and notify_admins_new_complaint (tasks.py:2253).
  • Logs audit event complaint_created.

Public form additionally dispatches link_complaint_patient and notify_staff_new_item (ui_views.py:4482-4486).

Signals (signals.py):

  • pre_save sync_department_from_staff (signals.py:21-39): auto-sets complaint.department from staff.department.
  • post_save send_complaint_creation_sms (signals.py:42-56): dispatches creation SMS only if contact_phone/contact_email present.
  • post_save send_complaint_status_change_sms (signals.py:59-89): SMS when status becomes resolved/closed.
  • post_save on ComplaintInvolvedDepartment notify_champion_on_department_assignment (signals.py:104-126): notifies the champion when a sent dept row is created.

3. Complete Lifecycle

3.1 The 8 statuses (ComplaintStatus, models.py:25-35)

OPEN, IN_PROGRESS, PARTIALLY_RESOLVED, RESOLVED, CLOSED, CANCELLED, PENDING_EXTERNAL, OVR_PENDING.

3.2 The transition map

ComplaintService.VALID_STATUS_TRANSITIONS (complaint_service.py:292-301):

open               -> [in_progress, cancelled]
in_progress        -> [partially_resolved, resolved, cancelled, pending_external, ovr_pending]
partially_resolved -> [resolved, in_progress, cancelled, pending_external]
resolved           -> [closed, in_progress]
closed             -> [in_progress]
cancelled          -> [open, in_progress]
pending_external   -> [resolved, in_progress, cancelled, closed]
ovr_pending        -> [in_progress, resolved, cancelled]

3.3 Enforcement layers

⚠ Gap vs docs/workflows.md:12: that doc claims invalid statuses are "rejected at the model (clean()) and DB (CheckConstraint) level." This is not true for Complaints. There is no clean() method on Complaint and no DB CheckConstraint in any migration. The only enforcement is ComplaintService.change_status (complaint_service.py:386-498), which:

  • requires activated_at before any change except → CLOSED (the activation gate, complaint_service.py:406-407);
  • looks up valid_next for old_status; raises ComplaintServiceError if new_status not allowed unless the user is_px_admin() (complaint_service.py:412-417) — PX Admin can force any transition;
  • permission: is_px_admin() or is_hospital_admin() or is_px_management() or is_px_employee() (complaint_service.py:398-400).

⚠ Gap: Several writers bypass change_status and set status directly: toggle_escalated_ovr (ui_views.py:1560), approve_ovr_escalation/reject_ovr_escalation (ui_views.py:1650, 1680, 1762), and ComplaintService.assign (complaint_service.py:228-237) which reopens resolved/closed/cancelled by flipping back to IN_PROGRESS.

3.4 All paths

                 ┌─────────────────────────── cancelled ────────────┐ (reopen→open/in_progress)
                 │                                                         │
[create]→ OPEN ──┼─(activate)→ IN_PROGRESS ──┬→ partially_resolved ──┬→ resolved ──┬→ closed ──(reopen)→ IN_PROGRESS
                 │                           │                        │             │
                 │                           ├→ pending_external ────┘             └→ IN_PROGRESS (reopen)
                 │                           ├→ ovr_pending ──(approve/reject)→ IN_PROGRESS
                 │                           └→ cancelled
                 └─(cancel)→ cancelled

is_active_status property (models.py:880-892): active = OPEN, IN_PROGRESS, PARTIALLY_RESOLVED, PENDING_EXTERNAL. Note OVR_PENDING is not active.


4. Status Definitions

Status Purpose When entered Who moves it forward Next statuses
OPEN "Received" (public label, progress 15%, amber, models.py:938-950). Logged + AI-classified but not being worked; activated_at null. On creation (default, models.py:464) Activation (ComplaintService.activate complaint_service.py:106) IN_PROGRESS, CANCELLED
IN_PROGRESS "In Progress" (50%, blue). Owned/being worked. From OPEN (activate), PARTIALLY_RESOLVED, RESOLVED, CLOSED, CANCELLED, OVR_PENDING, PENDING_EXTERNAL PX/hospital/management/employee roles (complaint_service.py:398) — department managers cannot change status PARTIALLY_RESOLVED, RESOLVED, CANCELLED, PENDING_EXTERNAL, OVR_PENDING
PARTIALLY_RESOLVED "In Progress" (75%, blue). Some aspects resolved. Stamps partially_resolved_at/by (complaint_service.py:451). From IN_PROGRESS PX/hospital/management/employee RESOLVED, IN_PROGRESS, CANCELLED, PENDING_EXTERNAL
RESOLVED "Resolved" (100%, emerald). Solution provided. Stamps resolved_at/by + optional resolution/resolution_category/resolution_outcome (complaint_service.py:420-434). From IN_PROGRESS, PARTIALLY_RESOLVED, PENDING_EXTERNAL, OVR_PENDING Same roles CLOSED, IN_PROGRESS (reopen)
CLOSED "Closed" (100%, slate). Stamps closed_at/by; dispatches resolution-satisfaction survey (complaint_service.py:436-441). From RESOLVED Same roles IN_PROGRESS (reopen)
CANCELLED "Cancelled" (0%, rose). Withdrawn/invalid. Stamps cancelled_at/by. From OPEN, IN_PROGRESS, PARTIALLY_RESOLVED, PENDING_EXTERNAL, OVR_PENDING Same roles OPEN, IN_PROGRESS
PENDING_EXTERNAL (no public label). Awaiting outside party (MOH/CHI/insurance). Sets pending_external_set_at + was_pending_external=True (complaint_service.py:443-445). From IN_PROGRESS, PARTIALLY_RESOLVED Same roles RESOLVED, IN_PROGRESS, CANCELLED, CLOSED
OVR_PENDING "OVR Pending Approval". Escalation/oversight awaiting PX-Admin/Hospital-Admin approval. toggle_escalated_ovr (ui_views.py:1560) approve_ovr_escalation/reject_ovr_escalation (PX/Hospital admin only, ui_views.py:1650, 1680) IN_PROGRESS, RESOLVED, CANCELLED

"OVR" is never expanded anywhere in code/docs. From fields is_escalated_ovr, escalated_ovr_by (models.py:676-685) it functions as an admin-approved oversight/escalation tier.


5. Workflow Actions

Decorators are consistently @login_required + @require_http_methods(["POST"]) for mutations. Permissions are enforced inside the function (no @permission_required decorators). Shared helper: can_manage_complaint (ui_views.py:291ComplaintService.can_manage complaint_service.py:78-94).

5.1 Lifecycle / status actions

Action URL name View (file:line) Permission / gate Effect
Activate complaint_activate (urls.py:63) ui_views.complaint_activate (ui_views.py:2141) can_activate (complaint_service.py:96-104) Assigns to current user; OPEN→IN_PROGRESS; sets activated_at; audit complaint_activated
Change status (resolve/close/cancel) complaint_change_status (urls.py:42) ui_views.complaint_change_status (ui_views.py:1376) PX/Hosp-Admin/PX-Mgmt/PX-Employee; activation gate (except→closed); transition map (PX-Admin bypasses) §3.2
Reopen complaint_reopen (urls.py:48) ui_views.complaint_reopen (ui_views.py:1788) PX/Hosp-Admin/assignee/dept-manager; only from resolved/closed (view) Creates a NEW OPEN complaint linked via reopened_from; original keeps terminal status
Request/cancel OVR toggle_escalated_ovr (urls.py:45) ui_views.toggle_escalated_ovr (ui_views.py:1560) can_manage_complaint Toggles status ↔ OVR_PENDING; emails PX admins (ui_views.py:1591)
Approve OVR approve_ovr_escalation (urls.py:46) ui_views.approve_ovr_escalation (ui_views.py:1650) PX-Admin/Hosp-Admin; must be OVR_PENDING → IN_PROGRESS + is_escalated_ovr=True
Reject OVR reject_ovr_escalation (urls.py:47) ui_views.reject_ovr_escalation (ui_views.py:1680) + duplicate at ui_views.py:1762 PX-Admin/Hosp-Admin; must be OVR_PENDING → IN_PROGRESS, not escalated
Escalate (to a person) complaint_escalate (urls.py:62) ui_views.complaint_escalate (ui_views.py:2002) PX/Hosp-Admin/PX-Mgmt/PX-Employee; active status; must be activated (ui_views.py:2015) Selects a Staff to email; sets escalated_at; does NOT reassign (ui_views.py:2052-2054)
Update patient-contact status update_patient_contact_status (urls.py:44) ui_views.update_patient_contact_status (ui_views.py:1520) can_manage_complaint Sets patient_contact_status (not_contacted/contacted/contacted_no_response)
Update satisfaction update_satisfaction (urls.py:43) ui_views.update_satisfaction (ui_views.py:1418) can_manage_complaint; resolved/closed only; blocked if patient-locked or >5 days Sets satisfaction; max 3 changes (models.py:626-627)
Update closure delay reason update_delay_reason_closure (urls.py:50) ui_views.update_delay_reason_closure (ui_views.py:1864) can_manage_complaint; only if is_overdue or >72h; not on closed/resolved Sets delay_reason_closure (72h rule)
Add note complaint_add_note (urls.py:61) ui_views.complaint_add_note (ui_views.py:1913) active status only Writes a "note" ComplaintUpdate
Change department complaint_change_department (urls.py:59) ui_views.complaint_change_department (ui_views.py:1930) active status; PX/Hosp-Admin; same hospital Changes complaint.department
Update location complaint_update_location (urls.py:60) ui_views.complaint_update_location (ui_views.py:1955) active status Updates location_type/area/department/section/zone/floor
Confirm taxonomy gate confirm_taxonomy (urls.py:64) ui_views.confirm_taxonomy (ui_views.py:2157) can_manage_complaint Sets taxonomy_reviewed_at/by to unblock Send-to-Department
Soft delete / restore / trash complaint_soft_delete/restore/trash_list (urls.py:94,95,93) ui_views.py:7199,7213,7240 @login_required Soft-delete

5.2 Assignment actions

Action URL name View Permission
Assign case manager (UI) complaint_assign (urls.py:41) ui_views.complaint_assign (ui_views.py:862) assigner PX/Hosp-Admin/assignee; target in groups PX Employee/PX Admin/PX Management (complaint_service.py:213-219); reopens resolved/closed/cancelled when reassigned
Assign case manager (API) complaint-api:assign ComplaintViewSet.assign (views.py:257) same
Assign staff (the subject) complaint-api:assign_staff ComplaintViewSet.assign_staff (views.py:485) PX Admin only; active status; sets complaint.staff; auto-syncs department; clears needs_staff_review
Send to person OR dept (AJAX) complaint_send_to (urls.py:182) ui_views.complaint_send_to (ui_views.py:1092) can_manage_complaint; rejects status=="open"
Confirm AI dept suggestion confirm_ai_department_suggestion (urls.py:172) ui_views.confirm_ai_department_suggestion (ui_views.py:5500) can_manage_complaint
Add/edit/remove involved department involved_department_add/edit/remove (urls.py:170,176,177) ui_views.py:5562,5645,5701 can_manage_complaint
Add/edit/remove involved staff involved_staff_add/edit/remove (urls.py:186-188) ui_views.py:6022,6101,6148 can_manage_complaint
Bulk assign/status/escalate complaint_bulk_* (urls.py:89-91) ui_views.py:2498,2526,2555 @require_http_methods only (no @login_required) — possible auth gap

5.3 Department-response & investigation (token, no-login)

Action URL name View
Champion/manager submits response involved_department_response (urls.py:178) ui_views.involved_department_response (ui_views.py:5749)
PX accepts/rejects response involved_department_review_response (urls.py:179) ui_views.involved_department_review_response (ui_views.py:5853)
Champion rejects routing (wrong dept) involved_department_reject_routing (urls.py:180) ui_views.involved_department_reject_routing (ui_views.py:5955)
Manager review (approve/reject) organizations:department_manager_review apps/organizations/ui_views.py:4101
Staff explanation form (token) complaint_explanation_form (urls.py:140) views.complaint_explanation_form (views.py:3554)
Champion starts investigation champion_start_investigation (urls.py:143) views.champion_start_investigation (views.py:4009)
Staff investigation response (token) staff_investigation_form (urls.py:145) views.staff_investigation_form (views.py:4338)
Champion reviews answers champion_review_answers (urls.py:146) views.champion_review_answers (views.py:4489)

5.4 Other actions

Convert to appreciation (ComplaintViewSet.convert_to_appreciation views.py:2076); generate AI resolution (views.py:1323); create PX action from AI (views.py:602); adverse action CRUD (adverse_action_* ui_views.py:6245-6622); government ticket create/import/convert (ui_views.py:6836,6922,6887); PDF (views.py:4951); public tracking (ui_views.public_complaint_track ui_views.py:4555).


6. Decision Points

  • Activation gate — complaint cannot be worked/sent/escalated until activated. Enforced at: ComplaintService.activate (complaint_service.py:108); complaint_send_to rejects status=="open" (ui_views.py:1111); complaint_escalate requires activated_at (ui_views.py:2015); change_status requires activated_at except →closed (complaint_service.py:406).
  • AI classificationanalyze_complaint_with_ai (tasks.py:668) sets severity, priority, taxonomy, emotion, complaint_type (complaint vs appreciation), staff matches.
  • Taxonomy review gatetaxonomy_reviewed_at must be set via confirm_taxonomy before Send-to-Department is unblocked (ui_views.py:2157).
  • Manager approval tierComplaintInvolvedDepartment.manager_review_status (models.py:2687-2707); set only in apps/organizations/ui_views.py:4200/4255.
  • PX acceptance tieracceptance_status (models.py:2653-2662); set in involved_department_review_response (ui_views.py:5886).
  • Resolution categoryResolutionCategory (models.py:54-61): FULL_ACTION_TAKEN, PARTIAL_ACTION_TAKEN, NO_ACTION_NEEDED, CANNOT_RESOLVE, PATIENT_WITHDRAWN. Set at resolve (complaint_service.py:429).
  • Resolution outcomeResolutionOutcome (models.py:64-69): PATIENT / HOSPITAL / OTHER.
  • Delay reasonsDelayReasonChoices (models.py:46-51): DEPARTMENT_NO_RESPONSE, ESCALATED, PATIENT_NOT_SATISFIED. Only settable if overdue or >72h (ui_views.py:1877-1880).
  • Duplicate detectionservices/duplicate_detection.py (weights: patient 0.30, date 0.20, description 0.35, category 0.15; threshold 0.75, "likely duplicate" ≥0.85; date window ±3 days). ⚠ Gap: no call site in apps/complaints create views — advisory library only, not wired to block creation.
  • Patient confirmation — satisfaction, set/locked via public tracker (ui_views.py:4626-4638) or PX update_satisfaction.

7. Assignment Flow

On Complaint:

  • assigned_to (User, models.py:487) — the case manager. Set by activate (activator becomes assignee) or assign.
  • staff (Staff, models.py:270) — the subject of the complaint. PX-Admin-only via assign_staff (views.py:485). Syncs department via pre_save signal (signals.py:21-39).
  • department (models.py:267), section (models.py:387).

ComplaintInvolvedDepartment (multi-department join, models.py:2593): department FK; role (PRIMARY/SECONDARY/COORDINATION/INVESTIGATING, models.py:2601-2605); is_primary (only one per complaint — enforced in save() models.py:2757-2764); per-department assigned_to (models.py:2629); response, acceptance, manager-review, routing, reminder/delay fields; unique_together=[complaint, department] (models.py:2744).

ComplaintInvolvedStaff (multi-staff join, models.py:2792): staff FK; role (ACCUSED/WITNESS/RESPONSIBLE/INVESTIGATOR/SUPPORT/PX_MANAGEMENT); per-staff explanation tracking. Auto-created for the primary staff via ComplaintService.ensure_involved_records (complaint_service.py:1020-1047) unless primary_staff_involved_removed.

Dual assignmentassigned_to (case manager) and staff (subject) are distinct and independently assignable (docs/COMPLAINT_DUAL_ASSIGNMENT_FEATURE.md).

Owner cascadeComplaint.get_owner() (models.py:711-732): section(champion→supervisor→deputy_supervisor) → department(champion→deputy_manager→supervisor→deputy_supervisor→manager_2nd→manager_3rd).

Reassignment / transferComplaintService.assign (complaint_service.py:194) reassigns the case manager; change_department (complaint_service.py:546) transfers; complaint_escalate notifies a person without reassigning. Final owner is assigned_to until resolved/closed.


8. Investigation Process

Two parallel mechanisms:

8.1 Legacy ComplaintExplanation direct-response

ComplaintExplanation (models.py:2251): one per (complaint, staff); token (models.py:2273), is_used, SLA tracking (sla_due_at, is_overdue), and ExplanationAttachment (models.py:2359). Token link emailed by ComplaintService.send_to_department (complaint_service.py:796) or complaint_send_to (ui_views.py:1252). Champion opens complaint_explanation_form (views.py:3554) and either submits a direct OTP-verified reply (views.py:3620-3889) or follows the "investigate" link.

8.2 ChampionInvestigation per-staff question flow (main investigation)

  • champion_start_investigation (views.py:4009): champion selects involved staff, writes per-staff InvestigationQuestions (models.py:3793); creates a ChampionInvestigation (models.py:3740, status QUESTIONS_SENT) + an InvestigationResponse per staff (models.py:3824) with its own token; emails/SMSes each staff a no-login link /complaints/<id>/investigate/respond/<token>/.
  • staff_investigation_form (views.py:4338): staff answer; InvestigationAnswer (models.py:3849) + InvestigationResponseAttachment (models.py:3902).
  • champion_review_answers (views.py:4489): champion reviews, writes a final_reply, sets assessment flags (negligence_finding, policy_issue_finding, requires_improvement_project), OTP-verifies (6-digit, 10-min expiry, views.py:4613), sets InvestigationStatus.REPLY_SUBMITTED, marks ComplaintExplanation.is_used=True, writes the ComplaintInvolvedDepartment response fields (first-responder-wins within the same department, views.py:4743-4750).

8.3 Other investigation models

  • ComplaintAdverseAction (models.py:3047): corrective/adverse-action. ActionType (models.py:3063), SeverityLevel (models.py:3077), VerificationStatus (models.py:3085: reported→under_investigation→verified/unfounded/resolved).
  • ComplaintPRInteraction (models.py:2480): PR/Patient-Relations contact log.
  • ComplaintMeeting (models.py:2543): meeting record (management_intervention/pr_follow_up/department_review).

When investigation starts: implicitly when sent to a department. Who investigates: the department champion/manager (token-authenticated, no login). Review tiers: (1) champion composes → (2) department manager approves/rejects → (3) PX accepts/rejects.


9. Communication Flow

ComplaintCommunication (models.py:3375) with ComplaintCommunicationType (models.py:3364: PHONE_CALL/EMAIL/SMS/MEETING/LETTER/OTHER), direction (inbound/outbound). Exposed via DRF only.

ComplaintUpdate (models.py:1177) is the unified timeline; update_type: status_change/assignment/note/resolution/escalation/communication (models.py:1187-1198).

Patient touchpoint When Where
Acknowledgement/creation On create send_complaint_creation_sms_task (tasks.py:3331) via signal (signals.py:42); notify_admins_new_complaint (tasks.py:2253)
Need more info Manual update_patient_contact_status (ui_views.py:1520) — records status, no auto-SMS
Progress update Manual notes/communications
Resolution sent On resolve resolution_sent_at set (complaint_service.py:426); SMS/email via send_complaint_status_change_task (tasks.py:3418, signal signals.py:59)
Department responded Champion response SMS+email to complainant (ui_views.py:5816-5835)
Closure On close resolution survey dispatched (complaint_service.py:439tasks.py:386)
Routing rejected Wrong dept emails handler + PX admins (complaint_service.py:1181-1221)
OVR requested/decided OVR flow emails PX admins/managers (ui_views.py:1591, 1704)
Champion notified Dept assigned notify_champion_on_dept_assignment_task (tasks.py:3509)
Satisfaction lock Public tracker patient sets + locks satisfaction for 5 days (ui_views.py:4626; models.py:894-908)

Delivery via apps.notifications.services.NotificationService, often offloaded to Celery.


10. Escalation Flow

10.1 SLA configuration

  • ComplaintSLAConfig (models.py:1224): per (hospital, source, severity, priority) sla_hours; reminder timings. unique_together=[hospital, source, severity, priority] (models.py:1295).
  • ComplaintThreshold (models.py:1309): threshold breaches (resolution_survey_score, response_time, resolution_time) with action_type (create_px_action/send_notification/escalate). check_threshold() at models.py:1367.
  • Defaults (config/settings/base.py:355-368): low=72h, medium=48h, high=24h, critical=12h.
  • calculate_sla_due_date (models.py:784-830) precedence: source-based config → severity/priority config → severity-only config → settings defaults.

10.2 Automatic tasks (tasks.py)

  • check_overdue_complaints (tasks.py:359, every 15 min) → Complaint.check_overdue() (models.py:867-878). ⚠ Gap: only checks OPEN, IN_PROGRESS, RESOLVED — not partially_resolved/pending_external/ovr_pending.
  • send_sla_reminders (tasks.py:1781, hourly) — first + second reminders; emails assigned user or dept manager; uses on-call schedule.
  • send_explanation_reminders (tasks.py:1616) + check_overdue_explanation_requests (tasks.py:1593) — for ComplaintExplanation SLA.
  • check_resolution_survey_threshold (tasks.py:478) — auto-creates a PXAction if a closed complaint's survey breaches ComplaintThreshold.

10.3 Manual escalation

  • complaint_escalate (ui_views.py:2002): pick a Staff; sets escalated_at; emails them; does not reassign.
  • OVR escalation (toggle_escalated_ovr ui_views.py:1560): two-step approval flow (request → admin approve/reject). When approved, is_escalated_ovr=True.

10.4 Escalation hierarchy

ComplaintService.get_escalation_target (complaint_service.py:36-76): for a staff explanation → staff.report_tostaff.department.managercomplaint.department.manager → hospital admins & PX staff.


11. Resolution Process

  • Who can mark resolved: any PX/Hosp-Admin/PX-Mgmt/PX-Employee via change_status (complaint_service.py:398).
  • Approval required? No separate approval to resolve (PX Admin can force). But the department response must pass manager-review + PX-acceptance tiers before a complaint is typically resolved (§14).
  • Patient confirmation required? Not to resolve; satisfaction is captured afterwards (PX sets it or patient submits/locks on the tracker).
  • Fields set on resolve (complaint_service.py:420-434): resolved_at/by; resolution + resolution_sent_at; resolution_category; resolution_outcome + resolution_outcome_other. Special: if was_pending_external, resolved_at is back-dated to pending_external_set_at (complaint_service.py:421-422).
  • Patient-contact status is tracked separately and is not a hard precondition.

12. Closure Process

  • Who closes: same as resolve, via change_status → CLOSED.
  • What happens on close: closed_at/by; dispatches send_complaint_resolution_survey (tasks.py:386 → creates SurveyInstance).
  • 72-hour closure rule: DelayReasonChoices docstring "Delay reason for 72h closure" (models.py:46-47). delay_reason_closure only settable when is_overdue or >72h (ui_views.py:1877-1880); not on closed/resolved. This explains why a complaint wasn't closed within the target window.
  • Reopen conditions: complaint_reopen (ui_views.py:1788) only from resolved/closed (view check ui_views.py:1804). ComplaintService.reopen (complaint_service.py:304) requires resolved/closed/cancelled, and creates a brand-new OPEN complaint linked via reopened_from. The original keeps its terminal status (its status is not changed by reopen).
  • Permanently completed: once closed, terminal unless explicitly reopened. Patient satisfaction window expires after 5 days (models.py:894-908).

13. Exception Flows

  • Duplicate detectionComplaintDuplicateDetector (threshold 0.75, likely ≥0.85). ⚠ Gap: no call site in create views — advisory only.
  • Withdrawn by patientResolutionCategory.PATIENT_WITHDRAWN (models.py:61); chosen at resolve time.
  • Invalid submission / wrong departmentinvolved_department_reject_routing (ui_views.py:5955) + token equivalent (views.py:3892). reject_department_routing (complaint_service.py:1093) sets routing_status=REJECTED, clears complaint.department/section if it was primary, emails handler + PX admins.
  • Missing info / no patient responsePatientContactStatus.CONTACTED_NO_RESPONSE (models.py:43) + DelayReasonChoices.PATIENT_NOT_SATISFIED/DEPARTMENT_NO_RESPONSE.
  • Complaint → Appreciation conversionComplaintViewSet.convert_to_appreciation (views.py:2076); only for complaint_type=="appreciation"; creates an Appreciation, stores metadata.appreciation_id, optionally closes the complaint (docs/COMPLAINT_TO_APPRECIATION_CONVERSION.md).
  • Merged cases⚠ no "merge" action exists in ui_views.py/urls.py. Closest is duplicate-detection (advisory) and reopen-as-new.
  • Reopened cases — creates a new complaint, doesn't mutate the original.
  • Government ticket conversionconvert_to_complaint (ui_views.py:6887) prefills complaint_create; sets references and external source type.

14. Department-Response Sub-Flow

A two-tier review with an embedded investigation loop. State lives on ComplaintInvolvedDepartment (models.py:2593). Shared rules in docs/workflows.md:6-18.

14.1 Activation gate

A complaint cannot be sent to a department until activated. complaint_send_to rejects status == "open" (ui_views.py:1111). The taxonomy gate (taxonomy_reviewed_at, confirm_taxonomy ui_views.py:2157) must also be passed.

14.2 Sending to a department

  • Endpoint complaint_send_to (ui_views.py:1092), URL complaints:complaint_send_to.
  • Primary department (complaint.department): sets complaint.sent_to_department=True, sent_to_department_at, forwarded_to_dept_at (ui_views.py:1197-1201).
  • Other departments: get_or_creates a ComplaintInvolvedDepartment with sent=True, sent_at, forwarded_at (ui_views.py:1203-1213); resets prior rejection (ui_views.py:1219-1229).
  • Resolves recipients via get_champion_and_manager (ui_views.py:1188); creates a ComplaintExplanation token per recipient (ui_views.py:1253-1265); offloads email/SMS to send_department_notification_task (ui_views.py:1342-1359).
  • A parallel implementation ComplaintService.send_to_department (complaint_service.py:737) exists (champion-only, contact-person picker). ⚠ Gap: two overlapping "send to department" implementations.

14.3 Who receives it

The department champion (and manager) — resolved by get_champion_and_manager (ui_views.py:1188). Signal notify_champion_on_department_assignment (signals.py:104) dispatches notify_champion_on_dept_assignment_task (tasks.py:3509) when a sent dept row is created.

14.4 Champion's response submission

Two paths to populate response_notes/response_notes_en/response_notes_ar, response_submitted=True, response_submitted_at (models.py:2641-2650):

  1. Logged-ininvolved_department_response (ui_views.py:5749). Permission: champion OR manager OR involved_dept.assigned_to OR can_manage_complaint. Sets fields + acceptance_status="acceptable" + accepted_at immediately, notifies complainant. ⚠ Note: this path self-accepts, skipping the manager-review tier.
  2. Token (no-login) — direct reply (views.py:3620-3889) or champion_review_answers after investigation (views.py:4680-4778). OTP-verified; apply first-responder-wins within the same department (views.py:3803-3808, 4743-4750); email the complaint assignee.

14.5 Manager review tier (tier 1)

  • View: apps/organizations/ui_views.py:4101 department_manager_review (in the organizations app, not complaints — easy to miss).
  • Permission: PX/Hosp-Admin OR dept manager of that department.
  • Preconditions: involved_dept.response_submitted True and not already approved.
  • Shows configurable ManagerReviewQuestions (models.py:3623); creates DepartmentManagerReview (models.py:3671) + ManagerReviewAnswers (models.py:3707).
  • Approve (organizations/ui_views.py:4199): manager_review_status="approved"; emails assignee.
  • Reject (organizations/ui_views.py:4254): manager_review_status="rejected"; clears the response (response_submitted=False, response_submitted_at=None, response_notes*=""); emails champion+assignee → reject loop back to champion.

14.6 PX acceptance tier (tier 2)

  • View: involved_department_review_response (ui_views.py:5853).
  • Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee.
  • Precondition: manager_review_status == "approved" (ui_views.py:5875) — manager tier must run first.
  • Accept (acceptance_status="acceptable"): sets accepted_by/at/notes.
  • Reject (acceptance_status="not_acceptable"): clears the response (response_submitted=False, response_submitted_at=None, response_notes*="") (ui_views.py:5891-5898); emails champion+manager → reject loop back to champion.

14.7 Reject loops summary

Both manager-reject and PX-reject clear response_submitted, response_submitted_at, and all response_notes* fields, returning the involved department to the champion for a fresh response. ⚠ Gap: PX-reject leaves manager_review_status="approved" (doesn't reset it), so re-approval semantics are unclear after a PX-reject.

14.8 Investigation sub-flow (token questions to involved staff)

The champion, instead of a direct reply, follows "investigate" → champion_start_investigation (views.py:4009): selects ComplaintInvolvedStaff, writes per-staff questions, each staff gets a no-login token emailed/SMSed. Staff answer via staff_investigation_form (views.py:4338). Champion reviews via champion_review_answers (views.py:4489), writes a final_reply, OTP-verifies — that final reply becomes the ComplaintInvolvedDepartment response, feeding back into the manager→PX review tiers.

14.9 Cross-cutting helper properties

  • Complaint.sent_to_any_department (models.py:1109).
  • Complaint.all_departments_responded (models.py:1113).
  • ComplaintExplanation.linked_involved_department (models.py:2343).
  • ComplaintInvolvedDepartment.can_reject_routing (models.py:2780): only if not responded, routing_status SENT, complaint not closed/cancelled.
  • ComplaintInvolvedDepartment.sla_remaining (models.py:2766): hours left (default 48).

15. End-to-End Example

Patient submits via public form
  ↓ (ui_views.py:4369)  → status=OPEN, ref CMP-202607-0001, due_at computed,
                          AI analysis + admin notify dispatched
Complaint auto-classified by AI (severity high, department X)
  ↓ (tasks.py:668)
PX staff activates + confirms taxonomy
  ↓ (ui_views.py:2141, 2157)  → status=IN_PROGRESS, activated_at set,
                                assigned_to = activator, taxonomy_reviewed_at set
PX sends to Department X (primary)
  ↓ (ui_views.py:1092)  → ComplaintInvolvedDepartment(primary, sent=True),
                          ComplaintExplanation token emailed to champion
                          [DECISION: champion investigates vs direct reply]
Champion starts investigation (token questions to involved staff)
  ↓ (views.py:4009)  → InvestigationResponse tokens emailed to staff
Staff answer via no-login token link
  ↓ (views.py:4338)  → InvestigationAnswer stored
Champion reviews answers, writes final_reply, OTP-verifies
  ↓ (views.py:4489)  → ComplaintInvolvedDepartment.response_submitted=True,
                       first-responder-wins; assignee emailed
                       [DECISION POINT: Manager review tier]
Department manager approves
  ↓ (organizations/ui_views.py:4199)  → manager_review_status="approved",
                                        assignee emailed
                       [DECISION POINT: PX acceptance tier]
PX accepts the response
  ↓ (ui_views.py:5853)  → acceptance_status="acceptable"
                          [DECISION POINT: resolution category]
PX resolves (category=FULL_ACTION_TAKEN, outcome=HOSPITAL)
  ↓ (ui_views.py:1376)  → status=RESOLVED, resolved_at/by, resolution_sent_at,
                          patient notified (SMS/email)
  ↓                       patient satisfaction window opens (5 days)
PX closes
  ↓ (ui_views.py:1376)  → status=CLOSED, closed_at/by,
                          resolution-satisfaction survey dispatched
                          [PERMANENT unless reopened → creates NEW complaint]

Appendix — Flagged gaps vs docs/workflows.md

  1. No model-level transition enforcement (clean()/CheckConstraint absent); only ComplaintService.VALID_STATUS_TRANSITIONS, bypassed by PX Admin.
  2. Manager review lives in apps/organizations/ui_views.py:4101, not apps/complaints/.
  3. Duplicate reject_ovr_escalation view definitions (ui_views.py:1680 and 1762) registered to one URL name.
  4. Public form view stricter than PublicComplaintForm (department required).
  5. check_overdue_complaints ignores partially_resolved/pending_external/ovr_pending.
  6. Public submissions hardcoded complaint_source_type=INTERNAL despite an EXTERNAL enum existing.
  7. "Merge cases" action does not exist.
  8. PX-reject leaves manager_review_status="approved".
  9. Bulk actions lack @login_required.
  10. "OVR" acronym never expanded.
  11. Duplicate-detection library not wired into create views.