# 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//visit//` → `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//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 `GovernmentTicket`s 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_id` → `national_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-activated** — `activated_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:291` → `ComplaintService.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"`** | §14 | | Confirm AI dept suggestion | `confirm_ai_department_suggestion` (`urls.py:172`) | `ui_views.confirm_ai_department_suggestion` (`ui_views.py:5500`) | `can_manage_complaint` | Creates a PRIMARY `ComplaintInvolvedDepartment` | | 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` | role ACCUSED/WITNESS/RESPONSIBLE/INVESTIGATOR/SUPPORT/PX_MANAGEMENT | | 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 classification** — `analyze_complaint_with_ai` (`tasks.py:668`) sets severity, priority, taxonomy, emotion, `complaint_type` (complaint vs appreciation), staff matches. - **Taxonomy review gate** — `taxonomy_reviewed_at` must be set via `confirm_taxonomy` before Send-to-Department is unblocked (`ui_views.py:2157`). - **Manager approval tier** — `ComplaintInvolvedDepartment.manager_review_status` (`models.py:2687-2707`); set only in `apps/organizations/ui_views.py:4200/4255`. - **PX acceptance tier** — `acceptance_status` (`models.py:2653-2662`); set in `involved_department_review_response` (`ui_views.py:5886`). - **Resolution category** — `ResolutionCategory` (`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 outcome** — `ResolutionOutcome` (`models.py:64-69`): PATIENT / HOSPITAL / OTHER. - **Delay reasons** — `DelayReasonChoices` (`models.py:46-51`): DEPARTMENT_NO_RESPONSE, ESCALATED, PATIENT_NOT_SATISFIED. Only settable if overdue or >72h (`ui_views.py:1877-1880`). - **Duplicate detection** — `services/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 assignment** — `assigned_to` (case manager) and `staff` (subject) are distinct and independently assignable (`docs/COMPLAINT_DUAL_ASSIGNMENT_FEATURE.md`). **Owner cascade** — `Complaint.get_owner()` (`models.py:711-732`): section(champion→supervisor→deputy_supervisor) → department(champion→deputy_manager→supervisor→deputy_supervisor→manager_2nd→manager_3rd). **Reassignment / transfer** — `ComplaintService.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** `InvestigationQuestion`s (`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//investigate/respond//`. - `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:439` → `tasks.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_to` → `staff.department.manager` → `complaint.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 detection** — `ComplaintDuplicateDetector` (threshold 0.75, likely ≥0.85). **⚠ Gap:** no call site in create views — advisory only. - **Withdrawn by patient** — `ResolutionCategory.PATIENT_WITHDRAWN` (`models.py:61`); chosen at resolve time. - **Invalid submission / wrong department** — `involved_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 response** — `PatientContactStatus.CONTACTED_NO_RESPONSE` (`models.py:43`) + `DelayReasonChoices.PATIENT_NOT_SATISFIED`/`DEPARTMENT_NO_RESPONSE`. - **Complaint → Appreciation conversion** — `ComplaintViewSet.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 conversion** — `convert_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_create`s 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-in** — `involved_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 `ManagerReviewQuestion`s (`models.py:3623`); creates `DepartmentManagerReview` (`models.py:3671`) + `ManagerReviewAnswer`s (`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.