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

360 lines
29 KiB
Markdown

# 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` (`<pk>/activate/`) | `ui_views.py:3179` | PX/Hosp-Admin/PX-Mgmt/PX-Employee | Assigns to self; open→in_progress + `activated_at` |
| Assign | `inquiry_assign` (`<pk>/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` (`<pk>/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` (`<pk>/reopen/`) | `ui_views.py:3388` | PX/Hosp-Admin/Dept-Manager | Only from resolved/closed; → in_progress |
| Add note | `inquiry_add_note` (`<pk>/add-note/`) | `ui_views.py:3439` | `@login_required` (no role gate) | Timeline note |
| **Respond (PX)** | `inquiry_respond` (`<pk>/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` (`<pk>/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` (`<pk>/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` (`<pk>/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` (`<pk>/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` (`<pk>/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` (`<pk>/update-contact/`) | `ui_views.py:4227` | PX/Hosp-Admin/Assignee | Updates 3-stage contact timeline + `timeline_sla` |
| Edit | `inquiry_edit` (`<pk>/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` (`<pk>/respond/<token>/`) | `views.py:5141` | **None (token)** | Champion submits dept response via token (§14) |
| Token explanation (staff) | `inquiry_explanation_form` (`<id>/explain/<token>/`) | `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/<id>/explain/<token>/`.
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/<pk>/respond/<token>/``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/<pk>/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.