# Inquiries — Workflow & Lifecycle > Source of truth: `apps/complaints/` (the `Inquiry` model lives **inside** the complaints app, not a separate one). URL namespace `inquiries`, mounted at `/inquiries/`. > This document describes the **current implementation only**. Every claim is cited as `file:line`. > **⚠ Read first — `docs/workflows.md` is partly out of date for Inquiries.** The most important change: the `dept_response_acceptance_status` field that backed the "PX accepts/rejects" loop was **removed** (migration `0039`), so that loop no longer exists in code. The model also has **no `clean()`/`CheckConstraint`** enforcing statuses. --- ## 1. Purpose `Inquiry` is "for general questions/requests. Similar to complaints but for non-complaint inquiries." (`apps/complaints/models.py:1528-1533`). Architectural differences vs. Complaint (intended, per `docs/workflows.md:31-39`): - **Single department, flat fields** — Inquiry carries department-response data as *flat columns* on the row (`transferred_to_department`, `outgoing_department`, `department_response_en/ar`, `models.py:1824-1854`). Complaint uses a multi-department join model. - **One-level review** — champion responds → PX accepts/rejects → resolve. **No department-manager tier** (`docs/workflows.md:34-35`). *(See §14 — the accept/reject field was removed.)* - **Resolve requires a PX `response`** — the PX team must write `inquiry.response` before resolving (`ui_views.py:3356-3358`). - **No investigation sub-flow** like Complaint's `InvestigationResponse`. `InquiryExplanation` is a simple token-response mechanism (§8). - **No signals** — `apps/complaints/signals.py` wires receivers only for `Complaint`/`ComplaintUpdate`/`ComplaintInvolvedDepartment`; **zero** `Inquiry` signal receivers. Shared vocabulary with Complaints: `open → in_progress → resolved → closed` (`models.py:1695-1705`). --- ## 2. How a Case Starts ### 2.1 Who can create - **DRF/API:** `CanCreateInquiry` (`apps/complaints/permissions.py:39-67`) — PX Admins, Hospital Admins, Source Users with `can_create_inquiries`, everyone else (incl. patients). `InquiryViewSet` requires `IsAuthenticated` (`views.py:2895`). - **UI form:** `@login_required` (`ui_views.py:2978`). ### 2.2 Creation channels (4 entry points) | Channel | View | URL name | Auth | |---|---|---|---| | Authenticated UI form | `ui_views.inquiry_create` (`ui_views.py:2980`) | `inquiries:inquiry_create` (`urls_inquiries.py:11`) | `@login_required` | | Public portal (complaints app) | `ui_views.public_inquiry_submit` (`ui_views.py:4707`) | `inquiries:public_inquiry_submit` (`urls_inquiries.py:27`) | **None** | | Public portal (core app) | `apps.core.views.public_inquiry_submit` (`apps/core/views.py:170`) | `core:public_inquiry_submit` (`apps/core/urls.py:38`) | **None** | | DRF API | `InquiryViewSet.perform_create` (`views.py:2909`) | — | `IsAuthenticated` + `CanCreateInquiry` | **Incoming vs. Outgoing** — controlled by `is_outgoing` (`models.py:1803-1805`): `False` (default) = **incoming** (from a patient/source into the hospital); `True` = **outgoing** (hospital contacts an external/other department; paired with `outgoing_department`, `models.py:1834`). Set via hidden form field (`forms.py:676-681`), read in create view (`ui_views.py:3001-3011`). Public submissions are always incoming. Reports split on this flag (`inquiry_export_incoming`/`outgoing`, `ui_views.py:4196,4216`) — corresponding to "Reports - Incoming/Outgoing Inquiries.xlsx". ### 2.3 Information required before submission **`InquiryForm` (`forms.py:564`)** — required: `hospital`, `location_type`, `category`, `subject`, `message`. Optional: `patient`, `area`, `department`, `contact_name/phone/email`, `section`, `priority`, `source`, `is_outgoing`, `outgoing_department`. **Public form (`public_inquiry_submit`)** — hand-rolled validation (`ui_views.py:4725-4735`) requires: `name`, `phone`, `hospital`, `subject`, `message`. > **⚠ Gap:** `PublicInquiryForm` (`forms.py:857`) exists and declares `name/phone/hospital/location_type` required, but `public_inquiry_submit` validates manually and does not use it — may be vestigial. ### 2.4 What happens immediately after submission `Inquiry.save()` (`models.py:1980-2027`): 1. Loads previous status into `_status_was` (`models.py:1982`). 2. Stamps `resolved_at`/`closed_at` only on transitions into those statuses, not on creation (`models.py:1993-2000`). 3. **Generates reference number** if missing: `generate_reference("INQ", hospital)` → `INQ-YYYYMM-NNNN` (`models.py:2002-2005`). 4. **Sets SLA deadline** `due_at` = now + `sla_hours` from `InquirySLAConfig` (`models.py:2007-2012`). 5. **Auto-derives `timeline_sla`** bucket (24/48/72/>72h) from `due_at - created_at` (`models.py:2014-2025`). 6. Default `status = OPEN` (`models.py:1703`). Background tasks after creation: - `analyze_inquiry_with_ai.delay(...)` — AI priority/department/taxonomy/emotion (`tasks.py:2991`). - `notify_staff_new_item.delay("inquiry", ...)` — admin notification email (`tasks.py:2490`). - `link_inquiry_patient.delay(...)` — async patient lookup (`tasks.py:3314`). - `InquiryUpdate` "Inquiry created." timeline entry (`ui_views.py:3046`). > **Activation gate:** a new inquiry is `open` and **cannot be sent to a department** until activated to `in_progress`. Send endpoints reject `status == "open"` (`ui_views.py:3594, 3841`). > **⚠ Gap — no creation acknowledgement to inquirer:** unlike Complaint (which has a `send_complaint_creation_sms` post_save signal), Inquiry has **no creation-acknowledgement signal**. The inquirer is not auto-notified on submission; only staff are notified via `notify_staff_new_item`. --- ## 3. Complete Lifecycle ### 3.1 Status "enum" There is **no `InquiryStatus` enum class and no `VALID_INQUIRY_TRANSITIONS` map.** Status is a plain `CharField` with inline choices (`models.py:1695-1705`): `open`, `in_progress`, `resolved`, `closed`. ### 3.2 Transition enforcement — effectively NONE - **No `clean()` method** on `Inquiry`. - **No `constraints`/`CheckConstraint`** in `Meta` or migrations (`models.py:1971-1978`) — only indexes. > **⚠ Gap vs `docs/workflows.md:11-13`:** that doc claims invalid statuses are "rejected at the model (clean()) and DB (CheckConstraint) level." **This is true for Complaint, but NOT for Inquiry.** `inquiry_change_status` accepts *any* string the user POSTs and saves it (the only guard is resolved-requires-response). The `choices` are not DB-enforced. ### 3.3 Where transitions actually happen | From → To | Mechanism | File:line | |---|---|---| | `open → in_progress` | `inquiry_activate` (sets `status` + `activated_at` if open) | `ui_views.py:3207` | | `open → in_progress` | `inquiry_update_contact_stage` stage `under_process` | `ui_views.py:4274` | | resolved/closed → in_progress | `inquiry_assign` auto-reopens on reassignment | `ui_views.py:3282` | | resolved/closed → in_progress | `inquiry_reopen` | `ui_views.py:3409` | | `* → resolved` | `inquiry_respond` (sets `response` + status) — requires non-empty response | `ui_views.py:3486` | | `* → resolved` | API `InquiryViewSet.respond` — requires response | `views.py:2963` | | `* → resolved` | `inquiry_change_status` — **blocks if no `response`** | `ui_views.py:3356` | | `* → closed` | `inquiry_change_status` with `status="closed"` (no dedicated close view) | `ui_views.py:3330` | | `* → anything` | `inquiry_change_status` accepts arbitrary `new_status` | `ui_views.py:3345` | `save()` stamps `resolved_at`/`resolved_by`/`closed_at`/`closed_by` on entry (`models.py:1993-2000`) using `_acting_user` (set by views). --- ## 4. Status Definitions | Status | Purpose | When entered | Who moves forward | Next | |---|---|---|---|---| | **`open`** | Received; not yet owned/worked | On creation (default) | PX/Admin/Manager/Employee via `inquiry_activate` | `in_progress` | | **`in_progress`** | Being actively worked | Activation, reassignment of resolved/closed, reopen, `under_process` stage | Assignee/PX/Champion | `resolved`, `closed` (or reopen) | | **`resolved`** | A PX response has been written and sent | `inquiry_respond` / API `respond` / `change_status` (gated on `response`) | PX/Admin/Hosp-Admin/Manager/Employee/Assignee/Champion | `closed`; reopen → `in_progress` | | **`closed`** | Permanently completed | `inquiry_change_status` to `closed` | PX/Admin/Hosp-Admin/Manager/Employee | reopen → `in_progress` (rare) | `is_active_status` returns True for `open`/`in_progress` (`models.py:2132`). > **⚠ Status-choice anomaly:** the detail view exposes `status_choices` that include `"contacted"` and `"contacted_no_response"` (`ui_views.py:2758-2765`). These are **NOT valid Inquiry statuses** (they're `contact_status` values mixed in). If selected via change-status, they persist as out-of-choices values — treat as a UI bug. --- ## 5. Workflow Actions All UI inquiry URLs are in `urls_inquiries.py` (namespace `inquiries`); views in `ui_views.py`; API on `InquiryViewSet` (`views.py:2890`). | Action | URL name | View (file:line) | Permission | Effect | |---|---|---|---|---| | Activate | `inquiry_activate` (`/activate/`) | `ui_views.py:3179` | PX/Hosp-Admin/PX-Mgmt/PX-Employee | Assigns to self; open→in_progress + `activated_at` | | Assign | `inquiry_assign` (`/assign/`) | `ui_views.py:3252` | PX/Hosp-Admin/Dept-Manager-of-this-dept | Assign to user; **auto-reopens resolved/closed → in_progress** | | Change status | `inquiry_change_status` (`/change-status/`) | `ui_views.py:3330` | PX/Hosp-Admin/PX-Mgmt/PX-Employee | Arbitrary status; **resolved blocked without `response`**; only way to set `closed` | | Reopen | `inquiry_reopen` (`/reopen/`) | `ui_views.py:3388` | PX/Hosp-Admin/Dept-Manager | Only from resolved/closed; → in_progress | | Add note | `inquiry_add_note` (`/add-note/`) | `ui_views.py:3439` | `@login_required` (no role gate) | Timeline note | | **Respond (PX)** | `inquiry_respond` (`/respond/`) | `ui_views.py:3459` | PX/Hosp-Admin/Assignee/Champion-of-(dept/outgoing_dept) | Writes `inquiry.response`, sets `responded_at/_by`, `response_sent_at`, → `resolved`; sends SMS+email to inquirer | | **Send to department** | `inquiry_transfer_to_department` (`/transfer-to-department/`) | `ui_views.py:3578` | PX/Hosp-Admin/Dept-Manager/PX-Mgmt/PX-Employee; **rejects open** | Full send + token (§14) | | **Send-to (AJAX)** | `inquiry_send_to` (`/send-to/`) | `ui_views.py:3818` | PX/Hosp-Admin/Dept-Manager/PX-Mgmt/PX-Employee; **rejects open** | Unified send to person OR department (§14) | | **Dept response (logged-in)** | `inquiry_department_response` (`/department-response/`) | `ui_views.py:4003` | PX/Hosp-Admin/Champion-of-(dept/outgoing_dept) | Champion submits `department_response_en/ar` + AI summary | | Send dept-response reminder | `inquiry_send_dept_response_reminder` | `ui_views.py:4115` | PX/Hosp-Admin/PX-Mgmt/PX-Employee | Manual reminder email to champion | | Send to staff (token explanation) | `inquiry_send_to_staff` (`/send-to-staff/`) | `ui_views.py:2901` | PX/Hosp-Admin/PX-Mgmt/PX-Employee | Token `InquiryExplanation` request to a staff member (§8) | | Escalate | `inquiry_escalate` (`/escalate/`) | `ui_views.py:3728` | PX/Hosp-Admin/PX-Mgmt/PX-Employee; blocks closed/cancelled | Stamps `escalated_at`, emails target | | Update contact stage | `inquiry_update_contact_stage` (`/update-contact/`) | `ui_views.py:4227` | PX/Hosp-Admin/Assignee | Updates 3-stage contact timeline + `timeline_sla` | | Edit | `inquiry_edit` (`/edit/`) | `ui_views.py:3102` | PX/Hosp-Admin/PX-Mgmt/PX-Employee; blocks `closed` | Edit fields | | Export incoming/outgoing | `inquiry_export_*` | `ui_views.py:4186,4206` | `@login_required` (RBAC inside) | Excel reports by `is_outgoing` | | Public submit | `public_inquiry_submit` | `ui_views.py:4707` | **None** | Public creation | | Public track | `public_inquiry_track` | `ui_views.py:4843` | None | Public status lookup by reference | | **Token response (dept)** | `inquiry_respond_with_token` (`/respond//`) | `views.py:5141` | **None (token)** | Champion submits dept response via token (§14) | | Token explanation (staff) | `inquiry_explanation_form` (`/explain//`) | `views.py:5077` | **None (token)** | Staff submits `InquiryExplanation` | | Restore | `inquiry_restore` | `ui_views.py:7227` | PX/Hosp-Admin/PX-Mgmt/PX-Employee | Restores soft-deleted | **API (`InquiryViewSet`):** `respond` (`views.py:2962`), `generate_ai_response` (`views.py:2980` — bilingual AI reply draft), `reanalyze_ai` (`views.py:3103`), plus default ModelViewSet CRUD. --- ## 6. Decision Points - **Activation gate** — every send endpoint rejects `status == "open"` (`ui_views.py:3594, 3841`). Matches `docs/workflows.md:7-9`. - **Resolve gate** — `response` must be non-empty (`ui_views.py:3356, 3482`; API `views.py:2968`). - **Reopen gate** — only from resolved/closed (`ui_views.py:3402`); reassignment also reopens (`ui_views.py:3282`). - **Escalate gate** — blocked if closed/cancelled (`ui_views.py:3744`). - **Department must have champion/manager** to be a transfer target (`ui_views.py:3610, 3916`). - **Need-more-info** — no dedicated "request info" action; analogues are token `InquiryExplanation`, `inquiry_add_note`, and the 3-stage contact tracking (`contacted_no_response`). - **PX acceptance of dept response** — **⚠ NO LONGER EXISTS** (`docs/workflows.md:35` is stale). See §14. - **Patient confirmation** — does not exist. Patient can only *view* status via public track. Reopen is staff-only. --- ## 7. Assignment Flow - **Initial owner** — none. `assigned_to`/`assigned_at` null on creation. `created_by` stamped (`models.py:1728`). - **Self-activation** — `inquiry_activate` sets `assigned_to = current_user`, `assigned_at`, (if open) `status=in_progress` + `activated_at` (`ui_views.py:3202`). - **Directed assignment** — `inquiry_assign` (`ui_views.py:3268`); emails assignee; notifies department. - **Reassignment** — same path; records old assignee; reopens if needed (`ui_views.py:3275`). - **Transfer / outgoing fields** — two parallel concepts: `transferred_to_department` (`models.py:1824`) and `outgoing_department` (`models.py:1834`), both set together by transfer/send-to (`ui_views.py:3638, 3923`). `transferred_at`/`transferred_by`/`transfer_count` track the event. - **Owner cascade** — `Inquiry.get_owner()` (`models.py:2037`): section(champion→supervisor→deputy_supervisor) → department(champion→deputy_manager→supervisor→deputy_supervisor→manager_2nd→manager_3rd). - **Escalation** — `inquiry_escalate`: does NOT reassign; stamps `escalated_at` + emails target. - **Final owner** — whoever performs `inquiry_respond` (sets `responded_by`) / `change_status` to resolved/closed. --- ## 8. Investigation Process `InquiryExplanation` (`models.py:2380`): "Staff/recipient response to an inquiry via token-based link. Mirrors ComplaintExplanation pattern." Fields: `inquiry`, `staff`, `explanation`, `token` (unique), `is_used`, `submitted_via`, SLA fields. `InquiryExplanationAttachment` (`models.py:2460`). **No investigation sub-flow** (no Q&A loop). The mechanism is: 1. PX/Admin triggers `inquiry_send_to_staff` (`ui_views.py:2901`): creates an `InquiryExplanation` with a fresh token; emails the staff a link `/inquiries//explain//`. 2. Staff opens `inquiry_explanation_form` (`views.py:5077`), submits text + attachments; `is_used` flipped, `responded_at` stamped. 3. The explanation is stored but **not** automatically promoted into `department_response_en` — it's an *advisory* collection mechanism, distinct from the primary department-response path (§14). > **⚠ Runtime bug:** `inquiry_send_to_staff` calls `InquirySLAConfig.get_active_config()` (`ui_views.py:2938`) and reads `sla_config.dept_response_sla_hours` (`ui_views.py:2939`). **Neither exists** — `InquirySLAConfig` has no `get_active_config` classmethod (`models.py:1432`) and the field is `dept_response_hours`, not `dept_response_sla_hours`. This code path raises `AttributeError`. --- ## 9. Communication Flow | Event | Where | |---|---| | Acknowledgement/receipt | **⚠ none automatic** (no creation signal); only staff notified via `notify_staff_new_item` | | Progress update | timeline notes (`inquiry_add_note`); contact stages (`inquiry_update_contact_stage`) | | Need-more-info | token explanation request to staff (`inquiry_send_to_staff`); inquirer "no response" = `contacted_no_response` stage | | Department response | `inquiry_department_response` notifies **inquirer** SMS+email (`ui_views.py:4075`); token path `inquiry_respond_with_token` (`views.py:5195`) | | Response sent/closure | `inquiry_respond` sends SMS+email to inquirer (`ui_views.py:3512`); in-app "resolved" notice to admins. No separate closure notification. | | Public tracking | `public_inquiry_track` (`ui_views.py:4843`) — friendly labels/progress + `department_response_en/ar` | Notification helpers (`apps/notifications/settings_service.py`): `send_inquiry_department_assigned` (`:250`), `send_inquiry_assigned` (`:463`), `send_inquiry_resolved` (`:488`), `send_inquiry_reopened` (`:514`). --- ## 10. Escalation Flow **Manual** — `inquiry_escalate` (`ui_views.py:3728`): PX/Hosp-Admin/PX-Mgmt/PX-Employee; blocked if closed/cancelled; requires a valid active `Staff`; stamps `escalated_at`; does NOT change status/assignee; emails target (Hospital Admin/Dept Manager/role-holders, computed in `inquiry_detail` `ui_views.py:2775`). **Automatic (SLA breach)** — `InquirySLAConfig` (`models.py:1432`): `sla_hours`, reminder timings, dept-response SLA fields. Celery beat: - `check-overdue-inquiries` → `check_overdue_inquiries` (`tasks.py:2886`) every 15 min: sets `is_overdue`/`breached_at`. - `send-inquiry-sla-reminders` → `send_inquiry_sla_reminders` (`tasks.py:2915`) every 15 min: emails `assigned_to` first + second reminders. **⚠ Department-response auto-escalation is DISABLED in code:** `check_overdue_inquiry_dept_responses` (`tasks.py:3126`) flags `dept_response_is_overdue` but the auto-escalate branch unconditionally logs "Auto-escalation skipped ... (disabled)" and `continue`s (`tasks.py:3178`), even though `dept_response_auto_escalate_enabled` defaults True (`models.py:1487`). So escalation is **manual only**. --- ## 11. Resolution Process **VERIFIED** — `docs/workflows.md:36-38` is correct: **resolve requires a PX `response`.** - `inquiry_change_status`: `if new_status == "resolved" and not inquiry.response: reject` (`ui_views.py:3356`). - `inquiry_respond`: rejects empty (`ui_views.py:3482`); writes `inquiry.response` + sets `resolved` (`ui_views.py:3486`); stamps `response_sent_at` + notifies inquirer (`ui_views.py:3512`). - API `respond`: same (`views.py:2968`). **Who can mark resolved:** via `inquiry_respond` — PX/Hosp-Admin/assignee/Champion of `department` or `outgoing_department` (`ui_views.py:3466`); via `change_status` — PX/Hosp-Admin/PX-Mgmt/PX-Employee. **Approval required?** No. **Patient confirmation?** Not required/captured. `save()` stamps `resolved_at`/`resolved_by` from `_acting_user`. > **⚠ Gap:** `inquiry_respond` clears `response_en`/`response_ar` and writes only the aggregate `response` field (`ui_views.py:3487`). The bilingual columns exist but this path doesn't populate them. --- ## 12. Closure Process - **No dedicated close view.** Closure is via `inquiry_change_status` with `status="closed"` (`ui_views.py:3330`). - Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee. - `save()` stamps `closed_at`/`closed_by` on entry (`models.py:1997`). - Closed inquiries **cannot be edited** (`inquiry_edit` guard, `ui_views.py:3116`). - **Reopen** — `inquiry_reopen` from resolved/closed → in_progress (`ui_views.py:3388`); `inquiry_assign` auto-reopens resolved/closed on reassignment (`ui_views.py:3282`). - **Permanently completed?** No permanent lock; a closed inquiry can always be reopened. Soft-deletion is separate, restorable. --- ## 13. Exception Flows - **Duplicate** — no duplicate-detection. - **Withdrawn/Invalid** — no withdrawn/invalid/cancelled status (those are Complaint statuses). `cancelled` is referenced defensively (`models.py:2167`, `ui_views.py:3744`) but is never a valid Inquiry status. - **Wrong department** — handled by re-transfer (`transfer_count` increments); no explicit bounce-back. - **Missing info** — `inquiry_add_note` / token `InquiryExplanation`; no "pending info" status. - **No patient response** — 3-stage contact timeline (`contacted_nr_at`, `models.py:1886`) + `contact_status="contacted_no_response"`. No auto-close. - **Transferred** — `transfer_count` tracks; both dept fields set; `sent_to_department`/`sent_to_department_at` cross-module signal set. - **Merged** — no merge feature. - **Reopened** — see §12. --- ## 14. Department-Response Sub-Flow (CRITICAL) ### 14.1 Activation gate `inquiry_transfer_to_department` (`ui_views.py:3594`) and `inquiry_send_to` (`ui_views.py:3841`) both reject `status == "open"` with *"Activate this inquiry before sending it to a department."* So `open → in_progress` (via `inquiry_activate`) is mandatory first. ### 14.2 How an inquiry is sent to a department **(A) `inquiry_transfer_to_department` (`ui_views.py:3578`)** — the richer path: - Validates: permission, not-open, `department_id`, department active with champion/manager, valid contact person. - Sets: `outgoing_department`, `transferred_to_department = department`, `transferred_at`, `transferred_by`, `transfer_count += 1`, **`sent_to_department = True`**, `sent_to_department_at`. - Computes dept-response SLA: `dept_response_sla_due_at = now + dept_response_hours`; resets overdue/reminder/escalation flags. - Generates a one-time `response_token = secrets.token_urlsafe(32)`, sets `response_token` + `response_token_sent_at`. - Builds token link `https://{domain}/inquiries/{pk}/respond/{token}/`; emails the contact person with deadline; calls `send_inquiry_department_assigned`. - Writes `InquiryUpdate` type `transferred_to_department`. **(B) `inquiry_send_to` (`ui_views.py:3818`)** — unified AJAX (person OR department): - Department branch auto-targets champion+manager via `get_champion_and_manager`. - Sets transfer fields + resets dept-response SLA. - **⚠ Inconsistency:** sets `sent_to_department = True` **only if `inquiry.department_id == department.pk`** (`ui_views.py:3929`), unlike path (A) which always sets it. Sending to a *different* department via this endpoint does **not** set the cross-module "sent" signal. - Notifies champion+manager by email+SMS; **does NOT generate a `response_token`** (no token link) — relies on in-app notification only. ### 14.3 Who receives it The department's champion and/or manager (`get_champion_and_manager`). In path (A) the chosen `contact_person` (validated by `department.is_valid_contact_person`). ### 14.4 Champion's response submission (two ways) **(i) Token-response path** — `/inquiries//respond//` → `inquiry_respond_with_token` (`views.py:5141`), **no auth**: - Validates `response_token` and not None; rejects if `response_token_used`. - Accepts `response_en`/`response_ar` (at least one required). - Writes `department_response_en/ar`, `department_responded_at`, clears `dept_response_is_overdue`, `response_token_used = True`. - Generates AI summary into `department_response_summary_en/ar`. - Notifies the **inquirer** SMS+email with public track URL. - **⚠ Dead code:** `inquiry.dept_response_acceptance_status = "pending"` at `views.py:5173` — this field was removed (migration `0039`); silently dropped on save. **(ii) Authenticated champion response** — `/inquiries//department-response/` → `inquiry_department_response` (`ui_views.py:4003`): - Permission: PX/Hosp-Admin/Champion of `department` or `outgoing_department`. - Writes `department_response_en/ar`, `department_responded_at/_by`, clears overdue; AI summary; notifies inquirer. - **Does NOT set any acceptance status** (field doesn't exist). ### 14.5 One-level review — accept/reject **⚠ STALE-DOC — this loop no longer exists in the data model.** `docs/workflows.md:34-35` and the field-name table reference `dept_response_acceptance_status` (pending/acceptable/not_acceptable). Migration `0001_initial.py:440` defined it; **migration `0039` removed** `dept_response_acceptance_status`, `dept_response_acceptance_notes`, `dept_response_accepted_at`, `dept_response_accepted_by`. Consequences: - **No PX "accept"/"reject" view** for Inquiry dept responses (no URL/function). The only `acceptance_status` setters are for **Complaint**. - The Inquiry model has no column to hold an accept/reject decision. - The only surviving reference is the dead assignment at `views.py:5173`. **So today the actual flow is:** champion submits `department_response_en/ar` → it's simply *available* to PX (shown in detail, fed into `generate_ai_response` prompt) → PX writes their own patient-facing `inquiry.response` via `inquiry_respond` → `status="resolved"`. **No explicit accept/reject decision and no reject-loop.** The champion's response is never "cleared" automatically; nothing returns a rejected response to the champion. ### 14.6 Reject loop **⚠ Not implemented for Inquiry** (no backing field, no endpoint). `docs/workflows.md:16-17` describes the loop generally; for Inquiry it's aspirational/dead. --- ## Field-Name Reference (dept-response concept map) | Concept | Field(s) on `Inquiry` | File:line | |---|---|---| | Target dept | `transferred_to_department` / `outgoing_department` | `models.py:1824, 1834` | | Sent flag | `sent_to_department` + `sent_to_department_at` | `models.py:1810, 1813` | | Transfer meta | `transferred_at`, `transferred_by`, `transfer_count` | `models.py:1807, 1816, 1832` | | Response text | `department_response_en/ar` + AI `department_response_summary_en/ar` | `models.py:1843-1846` | | Response at/by | `department_responded_at`, `department_responded_by` | `models.py:1847-1854` | | **Acceptance** | **(REMOVED — migration `0039`)** | was `dept_response_acceptance_status` | | Token | `response_token`, `response_token_used`, `response_token_sent_at` | `models.py:1857-1862` | | Dept SLA | `dept_response_sla_due_at`, `dept_response_is_overdue`, `_reminder_sent_at`, `_escalated_at` | `models.py:1865-1881` | | PX reply | `response`, `response_en/ar`, `response_sent_at`, `responded_at/_by` | `models.py:1784-1791` | --- ## 15. End-to-End Example ``` Patient submits inquiry via public form ↓ (ui_views.py:4707) → status=open, ref INQ-202607-0001, due_at set, AI analysis + admin notify dispatched [⚠ inquirer NOT auto-acknowledged] PX staff activates ↓ (ui_views.py:3179) → status=in_progress, activated_at, assigned_to=self PX transfers to Department X ↓ (ui_views.py:3578) → transferred_to_department=X, sent_to_department=True, response_token generated, token link emailed to champion, dept_response_sla_due_at set [DECISION: champion responds via token or logged-in] Champion submits dept response via token link ↓ (views.py:5141) → department_response_en set, department_responded_at, response_token_used=True, AI summary generated, inquirer notified SMS+email [⚠ NO PX accept/reject — field removed] [DECISION POINT: PX must write patient-facing response] PX writes inquiry.response (generate_ai_response optional) ↓ (ui_views.py:3459) → response set, responded_at/by, response_sent_at, status=resolved, inquirer notified SMS+email, admins notified PX closes ↓ (ui_views.py:3330) → status=closed, closed_at/by [PERMANENT unless reopened → in_progress] ``` --- ## Appendix — Flagged gaps vs `docs/workflows.md` 1. **`dept_response_acceptance_status` removed** (`0039`); the "PX accepts/rejects" loop is no longer backed by code. Dead assignment at `views.py:5173`. 2. **No `clean()`/`CheckConstraint`** on Inquiry; status enforced only ad-hoc in views; arbitrary strings persistable via change-status. 3. **Bug in `inquiry_send_to_staff`** (`ui_views.py:2938`) — calls nonexistent `InquirySLAConfig.get_active_config()` / `dept_response_sla_hours`. 4. **Auto dept-response escalation disabled** (`tasks.py:3178`). 5. **Inconsistent `sent_to_department`** between transfer (always sets) and send-to (only when target == primary dept). 6. **Invalid status choices exposed** in detail view (`contacted`/`contacted_no_response`). 7. **No creation acknowledgement to inquirer.** 8. **`inquiry_respond` clears `response_en/ar`**, fills only aggregate `response`. 9. **`is_straightforward`** field defined/serialized but never used in any decision. 10. **`cancelled`/`partially_resolved`** referenced defensively but never valid Inquiry statuses.