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

311 lines
24 KiB
Markdown

# Suggestions — Workflow & Lifecycle
> Source of truth: `apps/feedback/`. Suggestions are implemented as `FeedbackType.SUGGESTION` on the shared `Feedback` model (`apps/feedback/models.py:24`). There is **no dedicated model, status set, or workflow of its own** — every lifecycle claim here applies to all `FeedbackType` values (COMPLIMENT, SUGGESTION, GENERAL, INQUIRY, SATISFACTION_CHECK). Suggestion-specific behavior is called out where it exists.
> This document describes the **current implementation only**. Every claim is cited as `file:line`.
---
## 1. Purpose
The Suggestions module captures ideas/recommendations from patients, staff, and external sources and routes them through a lightweight triage-and-acknowledge workflow owned centrally by the Patient Experience (PX) team.
- `FeedbackType.SUGGESTION = "suggestion"` (`models.py:24`).
- The `Feedback` model docstring (`models.py:4-9`): "Tracks patient feedback (compliments, suggestions, general feedback)" and "Manages feedback workflow (submitted → reviewed → acknowledged → closed)".
- The word "Suggestion" is a **naming convention only** throughout the UI layer — the underlying record is a generic `Feedback`. E.g. `templates/feedback/feedback_detail.html:4` ("Suggestion Detail"); every audit event is named `suggestion_*` (`suggestion_created` `views.py:377`, `suggestion_updated` `views.py:437`, `suggestion_deleted` `views.py:494`, `public_suggestion_submitted` `views.py:1020`).
---
## 2. How a Case Starts
**Four distinct creation channels**, all producing a `Feedback` row with `feedback_type=SUGGESTION`, `status=SUBMITTED`, no `assigned_to`/`department`.
### 2.1 Channel A — Public Suggestion Form (unauthenticated)
- View: `public_suggestion_submit` (`views.py:937-1029`); `@csrf_exempt` + POST-only — **deliberately NOT `@login_required`** (`views.py:937`).
- URL: `path("public/suggestion/", ..., name="public_suggestion_submit")` (`urls.py:36`).
- Form: `PublicSuggestionForm` (`forms.py:251`) — a plain `forms.Form`.
- **Rate limiting:** per-IP cap of 5 submissions / 300s → HTTP 429 (`views.py:944`).
- Accepts JSON and form-encoded (`views.py:951`).
- **Required:** `contact_name`, `contact_phone`, `message`, `hospital` (`views.py:966`). Optional: `contact_email`, `category`/Area (defaults to `OTHER`, `views.py:977`), `title` (defaults to `message[:100]`).
Post-save (`views.py:992-1024`): `patient=None` at creation; linked **asynchronously** by `link_feedback_patient.delay(id, phone)` (`views.py:1015`); `analyze_suggestion_with_ai.delay(id)` (`views.py:1013`); `notify_staff_new_item.delay("suggestion", id)` (`views.py:1014`); audit `public_suggestion_submitted` (`views.py:1019`). Returns short pseudo-reference `"SG-{uuid[:8]}"` (`views.py:1026`) — **not** the internal `SGT-...` reference; the public cannot track by reference.
### 2.2 Channel B — Internal Staff Form (authenticated)
- View: `feedback_create` (`views.py:293-405`); `@login_required`. URL `feedback_create` (`urls.py:16`).
- **Does not use `FeedbackForm`** — builds the `Feedback` directly from POST (`views.py:311`), always `feedback_type=SUGGESTION` (`views.py:343`) — i.e. this view is hard-coded to create suggestions only.
- **Required:** `contact_name`, `contact_phone`, `message`, `hospital` (`views.py:318-326`). Optional: `title`.
- Post-save: **synchronous** `find_or_link_patient` (`views.py:336`); `analyze_suggestion_with_ai.delay` (`views.py:365`); `notify_staff_new_item.delay` (`views.py:366`); internal `FeedbackResponse` "Suggestion submitted by {user}" (`views.py:368`); audit `suggestion_created` (`views.py:376`).
### 2.3 Channel C — External API (machine-to-machine)
- View: `ExternalSuggestionCreateView` (`apps/integrations/api_views.py:700`), `POST /api/v1/external/suggestions/`.
- Auth via API key; hospital must match scope. Generates its own ephemeral `SG-{YYYYMMDD}-{random6}` in `metadata` (`api_views.py:740`) — **not** the canonical `reference_number` column (which `save()` still populates). Same post-save tasks (`api_views.py:765`).
### 2.4 Channel D — PX Source-User Portal
- View: `source_user_create_suggestion` (`apps/px_sources/ui_views.py:1387`); `@login_required`; guarded by `source_user.can_create_suggestions`.
- **Uses `PublicSuggestionForm`** (`ui_views.py:1400`). Sets `source=source`, `metadata={"created_via":"source_user_portal"}`. Fires **only** `analyze_suggestion_with_ai` (notably does **NOT** call `notify_staff_new_item`).
### 2.5 What happens immediately after submission (all channels)
1. **`Feedback.save()` override** (`models.py:261`): if `reference_number` blank → `generate_reference("SGT", hospital)``SGT-YYYYMM-NNNN` (global monthly sequence, `apps/core/reference.py:32`). Comment on field (`models.py:168`): *"internal-only, not publicly trackable"*.
2. Default `status = SUBMITTED` (`models.py:196`).
3. **No Django signals** — there is no `apps/feedback/signals.py` and `apps.py` registers no `ready()` handler. All side-effects are triggered explicitly by views/tasks.
> **⚠ Gap:** `FeedbackForm` (`forms.py:13`) exists but is used **only by `feedback_update`** (`views.py:421`), not creation. `feedback_create` builds the object manually, bypassing the form's `clean()` validation.
### 2.6 Are `CommentImport` / `PatientComment` bulk-import channels? **No.**
`CommentImport` (`models.py:479`) and `PatientComment` (`models.py:534`) describe a **separate, parallel workflow** (monthly IT exports of raw comments; "Steps 0-5"). `PatientComment.suggestions` (`models.py:600`) is just free-text extracted from a comment — it does **not** create a `Feedback` row. No code path creates `CommentImport` records (admin-only). See §16.
---
## 3. Complete Lifecycle
### 3.1 `FeedbackStatus` enum (`models.py:30-37`)
`SUBMITTED`, `REVIEWED`, `ACKNOWLEDGED`, `CLOSED`, `REOPENED`.
### 3.2 `VALID_FEEDBACK_TRANSITIONS` (`models.py:40-46`) — verified exact
```
SUBMITTED → [REVIEWED, CLOSED]
REVIEWED → [ACKNOWLEDGED, CLOSED]
ACKNOWLEDGED → [CLOSED]
CLOSED → [REOPENED]
REOPENED → [REVIEWED, ACKNOWLEDGED, CLOSED]
```
### 3.3 Enforcement layers
- **Model:** NOT enforced — no `clean()` on `Feedback`.
- **DB:** NOT enforced — no `CheckConstraint` in `Meta` or migrations.
- **View:** enforced **only** in `feedback_change_status` (`views.py:776-859`): rejects no-op same-status (`views.py:807`); computes `allowed = VALID_FEEDBACK_TRANSITIONS.get(old_status, [])` and rejects `new_status not in allowed` (`views.py:811-817`).
- **Admin:** NOT enforced.
> **Implication:** transitions are **soft**. PX Admin editing via Django admin, or any direct ORM write, can move a suggestion to any status. Only the staff UI enforces the rules.
---
## 4. Status Definitions
| Status | Purpose | When entered | Who moves forward | Next |
|---|---|---|---|---|
| **SUBMITTED** (`models.py:33`) | Initial entry for every suggestion from every channel. UI badge: blue. | At `save()` on creation (`models.py:197`) | Any PX/hospital user with change-status perm (§5) | REVIEWED, CLOSED |
| **REVIEWED** (`models.py:34`) | PX staffer has triaged/read. Sets `reviewed_at`/`reviewed_by` (`views.py:821`). UI badge: orange. | `feedback_change_status` choosing `reviewed` | Same perm group | ACKNOWLEDGED, CLOSED |
| **ACKNOWLEDGED** (`models.py:35`) | PX formally acknowledged/accepted. Sets `acknowledged_at`/`acknowledged_by` (`views.py:824`). UI badge: green. | `feedback_change_status` choosing `acknowledged` | Same | CLOSED |
| **CLOSED** (`models.py:36`) | Terminal. Sets `closed_at`/`closed_by` (`views.py:827`). Reachable from SUBMITTED, REVIEWED, ACKNOWLEDGED, REOPENED. UI badge: slate. When closed, Quick Actions + Edit hidden (`feedback_detail.html:51,299`). | `feedback_change_status` | Same | REOPENED |
| **REOPENED** (`models.py:37`) | Closed suggestion re-opened. **Clears** `closed_at`/`closed_by` (`views.py:830`); does NOT clear `acknowledged_*`/`reviewed_*`. | `feedback_change_status` | Same | REVIEWED, ACKNOWLEDGED, CLOSED |
> **Note on ACKNOWLEDGED:** a `FeedbackResponse` with `response_type="acknowledgment"` is valid (`models.py:372`) and exposed by `FeedbackResponseForm` (`forms.py:121`), but `feedback_change_status` does **not** require a response to exist before allowing ACKNOWLEDGED. No programmatic coupling — acknowledgement is a status change; the optional acknowledgment response is an independent timeline entry.
---
## 5. Workflow Actions
### 5.1 List & Detail (read)
| Action | View | URL name | Permission |
|---|---|---|---|
| List | `feedback_list` (`views.py:37`) | `feedback_list` (`urls.py:13`) | `@login_required`; RBAC filtering (`views.py:59`): PX Admin all/hospital-filtered; Hospital Admin → own; Dept Manager → own dept; else own hospital |
| Detail | `feedback_detail` (`views.py:194`) | `feedback_detail` (`urls.py:14`) | `@login_required`; access checks (`views.py:213`) |
Detail computes a workflow stepper: Submitted → Assigned → Reviewed → Acknowledged → Closed (`views.py:278`) — advisory only, not authoritative.
### 5.2 Status change — `feedback_change_status` (`views.py:776`)
URL `feedback_change_status` (`urls.py:21`); POST-only. Permission (`views.py:783`): `is_px_admin OR is_hospital_admin OR is_px_management OR is_px_employee`. Logic: validate status value; reject no-op; enforce `VALID_FEEDBACK_TRANSITIONS`; apply side-effects (REVIEWED→`reviewed_at/by`, ACKNOWLEDGED→`acknowledged_*`, CLOSED→`closed_*`, REOPENED→clear `closed_*`); create `FeedbackResponse(response_type="status_change")`; audit. Form: `FeedbackStatusChangeForm` (`forms.py:219`) — lists all statuses without filtering (filtering in view).
### 5.3 Assign — `feedback_assign` (`views.py:721`)
URL `feedback_assign` (`urls.py:20`). Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee. Sets `assigned_to`/`assigned_at`; creates `FeedbackResponse("assignment")`; audit. Form: `FeedbackAssignForm` (`forms.py:238`). Candidate set: `get_assignable_users(hospital)` = active users in group **"PX Employee"** at the suggestion's hospital. No status restriction in the view (UI panel hidden when closed).
### 5.4 Add Response / Timeline — `feedback_add_response` (`views.py:862`)
URL `feedback_add_response` (`urls.py:24`). Permission: PX/Hosp-Admin/PX-Mgmt/Department-Manager. Creates `FeedbackResponse` with caller-supplied `response_type` (status_change/assignment/note/response/acknowledgment). No status restriction.
### 5.5 Send to Department — `feedback_send_to_department` (awareness-only)
URL `feedback_send_to_department` (`urls.py:22`). Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee. **Precondition:** `feedback.department` already set (`views.py:1104`). Emails `department.champion`/`manager`/`deputy_manager` with **"This is for your awareness. No response is required."** (`views.py:1126`). Creates `is_internal=False` note. **Does NOT change status, does NOT create a department-response workflow.** See §14.
### 5.6 Send-to (AJAX) — `feedback_send_to`
URL `feedback_send_to` (`urls.py:23`). Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee. Two modes: `recipient_type="person"` (assigns to a User + emails) or `"department"` (links `feedback.department` + emails/SMSs champion+manager via `get_champion_and_manager`; SMS says *"No response is required"*). Rejects dept with no champion/manager.
### 5.7 Toggle flags
| Action | View | Permission | Effect |
|---|---|---|---|
| Toggle featured | `feedback_toggle_featured` (`views.py:895`) | PX/Hosp-Admin | flips `is_featured` (`models.py:225`) |
| Toggle follow-up | `feedback_toggle_follow_up` (`views.py:916`) | PX/Hosp-Admin | flips `requires_follow_up` (`models.py:227`) |
### 5.8 Create PX Action — `feedback_create_action`
URL `feedback_create_action` (`urls.py:28`). Permission: PX/Hosp-Admin. Creates a `PXAction` (generic FK to Feedback), `source_type="suggestion"`, `status="open"`. **One-way spawn** — does not change Feedback status; "promote this suggestion into a tracked improvement action".
### 5.9 Attachments
`FeedbackAttachment` (`models.py:332`): no dedicated view — listed on detail (`views.py:230`) but creatable **only** via Django admin or programmatically.
### 5.10 CRUD
Create (`feedback_create` `views.py:293`); Update (`feedback_update` `views.py:408` — PX/Hosp-Admin only); Delete-soft (`feedback_delete` `views.py:476` — PX/Hosp-Admin only; `feedback.soft_delete` sets `is_deleted`/`deleted_at`/`deleted_by`).
---
## 6. Decision Points
The only **enforced** decision point is the transition gate inside `feedback_change_status` (`views.py:811`): *is `new_status` in `VALID_FEEDBACK_TRANSITIONS[old_status]`?*
Beyond that, **every other "decision" is operator judgement** — the code does not branch:
- "Needs review" — no predicate; SUBMITTED→REVIEWED or SUBMITTED→CLOSED directly.
- "Needs response before acknowledge" — **not enforced**.
- "Can be acknowledged directly" — only from REVIEWED or REOPENED; SUBMITTED→ACKNOWLEDGED forbidden.
- "Close vs reopen" — operator choice.
> **⚠ Gap — reopen unreachable via UI after closure:** the Quick Actions panel (containing the status-change form) is hidden when `status == 'closed'` (`feedback_detail.html:299`). The map allows CLOSED→REOPENED but no staff UI button offers it. Reopen is only possible via Django admin or direct ORM.
---
## 7. Assignment Flow
- Field: `Feedback.assigned_to` (`models.py:201`); `assigned_at` (`models.py:204`).
- **Initial owner:** `None` for every channel.
- **Reassignment:** `feedback_assign` overwrites each call — no history field; history preserved only via `FeedbackResponse("assignment")` entries.
- **Candidate set:** `get_assignable_users(hospital)` = active users in **"PX Employee"** group. Suggestions are assigned **to PX staff only** — never to a department manager/champion via this form.
- **Department routing:** `Feedback.department` (`models.py:148`) is **not auto-assigned** by any creation channel and is **not used for ownership/routing**. Populated only when a PX user manually sends the suggestion to a department — and even then it's for **notification only**, not ownership transfer (§14).
---
## 8. Investigation Process
**No investigation phase.** Compared to complaints/inquiries/observations there is no RCA-required state, no evidence-gathering, no explanation request, no SLA-driven investigation timer.
Closest analogues:
- **AI pre-analysis** (`analyze_suggestion_with_ai`, `tasks.py:184`): async after creation. Calls `AIService.analyze_suggestion(message, title)` (`apps/core/ai_service.py:2124`) and **overwrites** `category`/`priority`, optionally `title`, stores analysis under `metadata["ai_analysis"]` (`tasks.py:219`).
- **Optional RCA linkage:** `feedback_detail` (`views.py:236`) fetches `RootCauseAnalysis` objects; template has a (commented-out) "Initiate RCA" link. An RCA *can* be attached but is **not part of the suggestion workflow**.
### `CommentActionPlan` — a SEPARATE action-tracking sub-flow (NOT on Feedback)
`CommentActionPlan` (`models.py:660`) attaches to `PatientComment` (IT-export pipeline), **not** `Feedback`. It has its own status enum `CommentActionPlanStatus` (PENDING/ON_PROCESS/COMPLETED, `models.py:654`) and `timeframe`/`evidences`/`responsible_department` fields. Unrelated to the live suggestion lifecycle (§16).
---
## 9. Communication Flow
| Event | Channel | Where |
|---|---|---|
| Acknowledgement | none automatic — operator may add `response_type="acknowledgment"` `FeedbackResponse` | `feedback_add_response` |
| Response to patient | `FeedbackResponse("response", is_internal=False)` | `feedback_add_response` |
| Closure | no outbound message — only internal timeline note | `feedback_change_status` |
**Crucially, no email/SMS is ever sent to the suggester (the patient/contact) by the feedback app.** All notifications go **inbound to PX/department staff**:
- On creation: `notify_staff_new_item("suggestion", id)` emails/SMSs **PX Admins** (working-hours: all; after-hours: on-call) — `complaints/tasks.py:2489`; config block `complaints/tasks.py:2548`. Template `emails/new_suggestion_notification.html`.
- On send-to-department: emails champion/manager/deputy (`views.py:1113`) or champion+manager (`views.py:1255`) with explicit "No response is required".
So the suggester receives **only** the immediate `{"reference":"SG-..."}` JSON from the public endpoint (`views.py:1026`) and nothing further unless a PX operator manually contacts them outside the system. **Suggestions are inbound-only communication.**
---
## 10. Escalation Flow
**None.** Confirmed:
- `Feedback` has **no SLA fields** (no `due_date`, `sla_deadline`, `escalated_at`).
- Celery beat (`config/celery.py`) has SLA/escalation tasks for complaints, inquiries, observations — **but nothing for feedback/suggestions**.
- The only feedback-related beat entry is `analyze-feedback-sentiment``process_pending_sentiment_analysis` every 30 min (`config/celery.py:201`) — analytics, not escalation.
- Grep for `feedback.*sla|feedback.*escalat` returned **no matches**.
`AuditEvent.EVENT_TYPES` includes `"escalation"`/`"sla_breach"` (`apps/core/models.py:89`) but these are never emitted by the feedback app.
---
## 11. Resolution Process
Suggestions have no formal "resolution" — the resolution-equivalent statuses are **ACKNOWLEDGED** and **CLOSED**.
- **Who can move to ACKNOWLEDGED/CLOSED:** PX/Hosp-Admin/PX-Mgmt/PX-Employee (`views.py:783`).
- **Approval required:** none.
- **Patient confirmation:** not present — the suggester has no authenticated presence and is never asked to confirm.
- **Evidence:** none on `Feedback` (exists only on `CommentActionPlan.evidences`, `models.py:728`, in the separate IT-export pipeline).
"Resolution" for a suggestion is simply an operator deciding to flip status and optionally recording an internal/external `FeedbackResponse` note.
---
## 12. Closure Process
- **When closeable:** from SUBMITTED, REVIEWED, ACKNOWLEDGED, or REOPENED (`models.py:41-45`) — closable from almost every state.
- **Who closes:** PX/Hosp-Admin/PX-Mgmt/PX-Employee (`views.py:783`).
- **Side-effects:** `closed_at`/`closed_by` (`views.py:827`); `FeedbackResponse("status_change")` created; audit.
- **Reopen conditions:** CLOSED → REOPENED is the only exit; reopening clears `closed_at`/`closed_by`, leaves `acknowledged_*`/`reviewed_*` intact.
- **Permanently completed:** **no "permanent close" flag.** A suggestion can be reopened indefinitely (REOPENED → CLOSED → …).
- **Soft-delete:** independent of status — `feedback_delete` calls `soft_delete`; restorable via `SoftDeleteModel.restore()` (no UI button — admin only).
---
## 13. Exception Flows
The codebase implements **none** of the classic exception statuses as first-class concepts:
| Exception | Implemented? | Evidence |
|---|---|---|
| Duplicate | No | No `duplicate_of` FK, no detection task. Operators close+note. |
| Withdrawn | No | No `WITHDRAWN` status; suggester has no withdraw channel. |
| Invalid | No | No `INVALID`/`REJECTED`. SUBMITTED→CLOSED is closest (`models.py:41`). |
| Spam | No dedicated status | Only the public endpoint's IP rate-limit (5/300s → 429, `views.py:944`). Spam closed+noted manually. |
| Converted-to-complaint | No | No converter. Closest is `feedback_create_action` spawning a `PXAction`, not a Complaint. |
| Merged | No | No merge view, no `merged_into` FK. |
| Reopened | Yes | `models.py:37,44`; `views.py:830`. |
| Missing info | No | No "awaiting info" status. `requires_follow_up` flag (`models.py:227`, toggled `views.py:916`) is the only soft signal — a boolean, not a state. |
---
## 14. Department-Response Sub-Flow — **NOT present for Suggestions**
This is the single most important architectural difference between Suggestions and Complaints/Inquiries/Observations.
**Explicit confirmation — suggestions are NOT routed to departments for response:**
- `feedback_send_to_department` email body: *"This is for your awareness. No response is required."* (`views.py:1126`).
- `feedback_send_to` SMS body: *"PX360: Feedback '...' logged for {dept}. ... No response is required."* (`views.py:1273,1284`).
- `feedback_send_to` docstring: *"Awareness-only: no response required from the department."* (`views.py:1151`).
**What "send to department" actually does:**
1. Optionally sets `feedback.department` (`views.py:1252`; `feedback_send_to_department` requires it set, `views.py:1104`).
2. Looks up champion + manager via `get_champion_and_manager`.
3. Sends email (+ SMS in send_to) to those people.
4. Writes an `is_internal=False` `FeedbackResponse` note recording who was notified.
5. **Does not change status. Does not create any "department response" record. Has no SLA, no reminder, no overdue check.**
Contrast with complaints/inquiries/observations which have dedicated `check-overdue-*-dept-responses` and `send-*-dept-response-reminders` beat tasks — feedback/suggestions have **none**.
**Conclusion:** Suggestions are handled **centrally by PX**. Departments may be informed for awareness but are never obligated to act within the suggestion lifecycle. If PX wants a department to actually *do* something, they must escalate via `feedback_create_action` to spawn a tracked `PXAction` (separate module).
---
## 15. End-to-End Example
```
Patient submits suggestion via public form
↓ (views.py:937) → status=SUBMITTED, ref SGT-202607-0001,
AI analysis dispatched, PX admins notified
[⚠ suggester NOT auto-notified beyond immediate JSON ack]
PX staff assigns to a PX Employee
↓ (views.py:721) → assigned_to set; FeedbackResponse("assignment")
PX reviews
↓ (views.py:776) → status=REVIEWED, reviewed_at/by
[DECISION: acknowledge or close]
PX acknowledges
↓ (views.py:776) → status=ACKNOWLEDGED, acknowledged_at/by
[optional: add FeedbackResponse("acknowledgment") to timeline]
PX closes
↓ (views.py:776) → status=CLOSED, closed_at/by; Quick Actions hidden
[REOPEN only via admin/ORM — not via staff UI]
[NO department involvement; NO outbound patient comms]
```
---
## 16. Parallel Pipelines (NOT part of the Suggestion lifecycle)
These exist in the same app but are **distinct reporting/analytics workflows**, not part of the live suggestion lifecycle. Documented here for completeness.
### 16.1 `CommentActionPlan` (`models.py:660`)
Attaches to `PatientComment`, **not** `Feedback`. Status enum `CommentActionPlanStatus` (COMPLETED/ON_PROCESS/PENDING — default `models.py:715`). Fields: `problem_number`, `comment_text/_en`, `frequency`, `recommendation` (required), `responsible_department`, `timeframe` (e.g. "Q3"), `evidences` (free-text completion evidence), `month`/`year`. No transition map — free-editable via admin. Listed read-only in `action_plan_list` ("Step 5", `views.py:607`).
### 16.2 `PatientComment` (`models.py:534`)
Raw-unit model of the IT-export pipeline. Imported in monthly batches via `CommentImport` (`models.py:479`), which tracks `month`/`year`/`source_file`/`status` (pending/processing/completed/failed) and counts. Each `PatientComment` carries: `source_category` (Appointment/Inpatient/Outpatient), `comment_text`, `classification`/`sub_category`, sentiment keyword fields, **`suggestions` extracted text** (just text, not a link), `sentiment`, `is_classified`, `mentioned_doctor_name`, `frequency`, `month`/`year`. Views: read-only list (`comment_list` `views.py:538`) + Excel exports Step 1/Step 2 (`export_utils.py`). **Creation is admin-only** (no code path creates `CommentImport`).
**Bottom line:** treat Comments & Action Plans as a separate "IT-export reporting" module that happens to live in the same Django app, not part of the Suggestions lifecycle.
---
## Appendix — Flagged gaps / ambiguities
1. **Reopen unreachable via UI** after closure (Quick Actions hidden when `closed`, `feedback_detail.html:299`). CLOSED→REOPENED allowed by map but no staff UI button.
2. **`reference_number` divergence** — `save()` generates canonical `SGT-YYYYMM-NNNN`; public endpoint returns different `SG-{uuid[:8]}` (`views.py:1026`); external API generates `SG-{YYYYMMDD}-{random6}` in metadata only. Public cannot track by real reference.
3. **`FeedbackForm` unused for creation** — `feedback_create` builds manually, bypassing form `clean()`.
4. **Transitions unenforced at DB/model/admin** — only `feedback_change_status` enforces.
5. **No outbound communication to suggester** by design.
6. **`feedback_send_to_department` requires `department` set, `feedback_send_to` sets it** — overlapping confusingly.
7. **AI task overwrites operator-entered category/priority** (`tasks.py:219`) — may clobber a manual categorization made immediately after creation.