HH/docs/workflows/suggestions.md
2026-07-12 11:18:20 +03:00

24 KiB

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. Returns the canonical reference_number generated by save() (SGT-YYYYMM-NNNN); the same value is used to retrieve the record via GET /api/v1/external/suggestions/<reference_number>/. 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-sentimentprocess_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 (external API resolved)save() generates canonical SGT-YYYYMM-NNNN; the external API now returns and retrieves by this canonical reference. Public portal still diverges: the public submit endpoint returns a different SG-{uuid[:8]} (views.py:1026), so the public cannot track by the real reference via the web portal.
  3. FeedbackForm unused for creationfeedback_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.