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

29 KiB

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 signalsapps/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_statusblocks 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 gateresponse 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-activationinquiry_activate sets assigned_to = current_user, assigned_at, (if open) status=in_progress + activated_at (ui_views.py:3202).
  • Directed assignmentinquiry_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 cascadeInquiry.get_owner() (models.py:2037): section(champion→supervisor→deputy_supervisor) → department(champion→deputy_manager→supervisor→deputy_supervisor→manager_2nd→manager_3rd).
  • Escalationinquiry_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 existsInquirySLAConfig 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

Manualinquiry_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-inquiriescheck_overdue_inquiries (tasks.py:2886) every 15 min: sets is_overdue/breached_at.
  • send-inquiry-sla-reminderssend_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 continues (tasks.py:3178), even though dept_response_auto_escalate_enabled defaults True (models.py:1487). So escalation is manual only.


11. Resolution Process

VERIFIEDdocs/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).
  • Reopeninquiry_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 infoinquiry_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.
  • Transferredtransfer_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_respondstatus="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.