# Appreciations — Workflow & Lifecycle > Source of truth: `apps/appreciation/`. This is an **outbound recognition/recognition** module (recognition sent *to* staff/physicians), fundamentally different from inbound case-management. This document describes the **current implementation only**. Every claim is cited as `file:line`. > **⚠ Read first — two important findings:** > 1. `VALID_APPRECIATION_TRANSITIONS` (`models.py:28-34`) is **defined but never referenced** anywhere (dead code). The state machine is enforced **only** by runtime `ValueError` guards inside the model transition methods. > 2. The REST API `create` and complaint→appreciation conversion call `appreciation.send()` on a freshly-created DRAFT, but `send()` requires ACTIVATED/AI_ANALYZED — **both would raise `ValueError`** (likely bugs). The **working path is the UI activation-gate flow**. --- ## 1. Purpose **Appreciations = BOTH inbound patient feedback AND outbound staff recognition, but the dominant designed workflow is OUTBOUND recognition FROM PX/hospital staff TO staff/physicians.** Evidence: - `apps/appreciation/models.py:2`, `__init__.py:2`, `README.md:3`, `IMPLEMENTATION_SUMMARY.md:4`: *"Send and track appreciation to users and physicians."* - The **outbound** character: `appreciation_create` is gated to PX/hospital staff (`ui_views.py:155`); the email subject is *"Staff Appreciation"* (`signals.py:72`); the body *"A staff member in your department has received an appreciation. Please acknowledge their good work."* (`signals.py:179`); `tasks.py:18` *"Send email + SMS to champion, manager, and appreciated staff."* - The **inbound patient-feedback** channel: `public_appreciation_submit` (`ui_views.py:1196`) — patients/public submit a message, created as DRAFT, then PX staff triage and activate/send. Plus complaint→appreciation conversion (`complaints/views.py:2076`). So: **a recognition engine whose originating actors can be (a) PX/hospital staff sending peer/leadership recognition, (b) public/patient submissions awaiting staff review, or (c) appreciation-type complaints being formally converted.** The terminal recipient is always a staff member or physician. --- ## 2. How a Case Starts ### 2.1 Who can create | Channel | View/Serializer | Permission | File:line | |---|---|---|---| | Internal UI form | `appreciation_create` + `AppreciationForm` | `@login_required`; PX/Hosp-Admin/PX-Mgmt/PX-Employee | `forms.py:8`; `ui_views.py:153` | | Public portal | `public_appreciation_submit` | **No auth** (`@csrf_exempt`); rate-limited (5/5min/IP) | `ui_views.py:1196` | | External API (HIS integration) | `integrations/api_views.py` create | API-key scoped to a hospital | `integrations/api_views.py:606` | | REST API create | `AppreciationCreateSerializer` | `IsAuthenticated` | `views.py:55`, `serializers.py:132` | | Complaint → Appreciation conversion | `convert_to_appreciation` | Edit permission on the complaint | `complaints/views.py:2075` | ### 2.2 Creation channels in detail **(a) Internal UI form** (`forms.py:8`): ModelForm exposing only `["hospital", "department", "message_en"]` (`forms.py:13`); `department` restricted to active departments (`forms.py:34`). On POST: `sender = request.user`, `status = DRAFT` (`ui_views.py:173`). **Category/recipient/visibility are NOT set here** — decided later during activate/send. **(b) Public portal** (`ui_views.py:1196`): Inputs `contact_name`, `contact_phone`, `message`, `hospital`, optional `staff_name`/`department`/`section`. Required: name, phone, message, hospital (`ui_views.py:1224`). Creates with `status=DRAFT`, `visibility=PUBLIC`, `is_anonymous=False`, `category=None`, metadata `{source:"public_form", ...}` (`ui_views.py:1239`). Fires `notify_staff_new_item.delay("appreciation", ...)` (`ui_views.py:1258`). Returns `reference_number`. **(c) External API** (`integrations/api_views.py:606`): Creates DRAFT, visibility PUBLIC, metadata `{source:"external_api", ...}`. Builds its own `APR-YYYYMMDD-` (`api_views.py:602`) — informational, since `save()` also generates the canonical `reference_number`. Fires `notify_staff_new_item`. **(d) REST API create** (`views.py:143`, serializer `serializers.py:132`): Required: `recipient_type` (`['user','staff']`), `recipient_id` (UUID), `message_en`, `hospital_id`. Optional: `category_id`, `message_ar`, `visibility` (default PRIVATE), `is_anonymous`, `department_id`. Validation: recipient must belong to hospital; category hospital-compatible. `AppreciationViewSet.create` resolves a `User`/`Staff` via `ContentType`, creates the record (status DRAFT), then **immediately calls `appreciation.send()`** (`views.py:198`). **(e) Complaint → Appreciation conversion** (`complaints/views.py:2076`): Guards — complaint active (`is_active_status`, `complaints/views.py:2087`), `complaint_type == "appreciation"` (`:2096`), not already converted (`:2103`). Defaults: `message_en` ← `complaint.description`; `message_ar` ← `complaint.short_description_ar`; `visibility="private"`; `is_anonymous=True` (`:2113`). Creates DRAFT (`:2171`), then `appreciation.send()` (`:2183`). Links back via `metadata.appreciation_id` (`:2186`); optionally closes complaint (`:2196`). Default category "Patient Feedback Appreciation" (code `patient_feedback`, `create_patient_feedback_category.py`). ### 2.3 What happens immediately after submission - **Reference number** generated in `Appreciation.save()` via `generate_reference("APR", hospital)` (`models.py:243`) → `APR-YYYYMM-NNNN` (global monthly sequence). Stored on `reference_number` (`models.py:198`), comment *"internal-only, not publicly trackable"*. - **Default status = DRAFT** (`models.py:193`). - **`post_save` signal `handle_appreciation_sent`** (`signals.py:27`) only acts when `status == SENT and not notification_sent` — does **nothing** at creation (DRAFT). - For public/external: **PX-admin notification task** `notify_staff_new_item("appreciation", id)` fired (`ui_views.py:1259`, `api_views.py:625`) → `emails/new_appreciation_notification.html`. ### 2.4 Why DRAFT + activation gate? Mirrors the complaints activation gate. A DRAFT appreciation is **untriaged**: no recipient set (internal form), no AI analysis, no decision to publish at chosen visibility. **Activation** (`ui_views.py:195`) is the PX-staff gate that confirms content, triggers AI analysis, stamps `activated_at`/`activated_by`, emits audit. Only after activation may it be sent (`models.py:335`). The draft count is surfaced to reviewers via `core/context_processors.py:74` (`draft_appreciation_count`) for PX/Hosp-Admin/PX-Mgmt. --- ## 3. Complete Lifecycle ### 3.1 The declared state machine — `VALID_APPRECIATION_TRANSITIONS` (`models.py:28-34`) — verified ``` DRAFT → {ACTIVATED} ACTIVATED → {AI_ANALYZED, SENT} AI_ANALYZED → {SENT} SENT → {ACKNOWLEDGED} ACKNOWLEDGED → set() (terminal) ``` ### 3.2 Where enforced — NOWHERE via the constant `VALID_APPRECIATION_TRANSITIONS` is **never imported or referenced** anywhere else (grep confirmed). The **model transition methods** each re-check the current status and raise `ValueError`: - `activate()` (`models.py:309`) — requires DRAFT. - `mark_ai_analyzed(analysis_data)` (`models.py:320`) — requires ACTIVATED. - `send()` (`models.py:331`) — requires ACTIVATED or AI_ANALYZED. - `acknowledge()` (`models.py:344`) — requires SENT. ### 3.3 `clean()` and DB CheckConstraints - **No `clean()` method** on `Appreciation`. - **No `CheckConstraint`s** in any migration. Only `unique_together` on category (`models.py:84`) and `AppreciationStats` (`models.py:517`), `unique=True` on `reference_number`/badge `code`. **Conclusion:** status integrity enforced **only at runtime in Python methods**, not at DB layer. --- ## 4. Status Definitions | Status | Constant | When entered | Who advances | Next | Key code | |---|---|---|---|---|---| | **DRAFT** | `AppreciationStatus.DRAFT` (`models.py:21`) | On creation (model default) | PX/hospital staff via `appreciation_activate` | ACTIVATED | created; ref number generated; PX admins notified (public/external only) | | **ACTIVATED** | `ACTIVATED` (`models.py:22`) | Staff approves/triages draft; runs AI | AI step (auto) or staff `appreciation_send`/`send_to` | AI_ANALYZED, SENT | `activated_at`/`activated_by` stamped (`models.py:316`, `ui_views.py:211`) | | **AI_ANALYZED** | `AI_ANALYZED` (`models.py:23`) | `mark_ai_analyzed()` called by activate view after a successful AI call | Staff via `send`/`send_to` | SENT | `ai_analyzed_at` + `ai_analysis` JSON (`models.py:326`, `ui_views.py:254`) | | **SENT** | `SENT` (`models.py:24`) | `send()` invoked | The recipient (User/Staff) via `acknowledge` | ACKNOWLEDGED | `sent_at` stamped (`models.py:339`); triggers `post_save` cascade (notifications, stats, badges) | | **ACKNOWLEDGED** | `ACKNOWLEDGED` (`models.py:25`) | Recipient acknowledges | (terminal) | none | `acknowledged_at` (`models.py:352`) | ### 4.1 The AI_ANALYZED status — what AI analysis happens Performed **synchronously inside `appreciation_activate`** (`ui_views.py:230-258`), immediately after DRAFT→ACTIVATED. Uses `apps.core.ai_service.AIService.chat_completion` (NOT `analyze_complaint`): - Prompt asks for JSON: `summary_en`, `summary_ar`, `themes` (list), `tone` (`warm|formal|casual`), `suggested_response_en`, `suggested_response_ar` (`ui_views.py:234-246`). - Parsed and passed to `appreciation.mark_ai_analyzed(analysis_data)` (`ui_views.py:251`). - On failure: logged + swallowed; appreciation stays ACTIVATED (does NOT auto-advance) — still sendable (`ui_views.py:255`). - `AIService.chat_completion` (`ai_service.py:685`) → OpenRouter `/chat/completions`, model `google/gemini-2.5-flash-lite` default (`ai_service.py:41`). **So AI_ANALYZED is a "content enrichment" step (summary/themes/tone/draft reply), not a sentiment/severity gate.** The appreciation can still be SENT directly from ACTIVATED without ever becoming AI_ANALYZED (`models.py:30`). --- ## 5. Workflow Actions ### 5.1 UI actions (`ui_views.py`) | Action | Function | URL name | Status gate | Permission | |---|---|---|---|---| | List | `appreciation_list` `:37` | `appreciation_list` | — | `@login_required`; hospital-filtered | | Detail | `appreciation_detail` `:92` | `appreciation_detail` | — | login; PX/Hosp-Admin or same-hospital | | Create (internal) | `appreciation_create` `:153` | `appreciation_create` | creates DRAFT | PX/Hosp-Admin/PX-Mgmt/PX-Employee | | **Activate** (DRAFT→ACTIVATED + AI) | `appreciation_activate` `:193` | `appreciation_activate` | must be DRAFT (`:198`) | PX/Hosp-Admin/PX-Mgmt/PX-Employee | | Send (form) | `appreciation_send` `:264` | `appreciation_send` | ACTIVATED or AI_ANALYZED (`:269`) | PX/Hosp-Admin/PX-Mgmt/PX-Employee | | **Send-to** (AJAX, person/department) | `appreciation_send_to` `:320` | `appreciation_send_to` | ACTIVATED or AI_ANALYZED (`:344`) | PX/Hosp-Admin/Dept-Manager/PX-Mgmt/PX-Employee | | **Acknowledge** (SENT→ACKNOWLEDGED) | `appreciation_acknowledge` `:485` | `appreciation_acknowledge` | must be SENT (`:500`) | **recipient only** (User ContentType match, `:492`) | | Public submit | `public_appreciation_submit` `:1196` | `public_appreciation_submit` | creates DRAFT | **none** | | Restore deleted | `appreciation_restore` `:1036` | `admin_appreciation_restore` | — | PX/Hosp-Admin | | PDF | `appreciation_pdf` `:1276` | `appreciation_pdf` | — | `@login_required` | | Leaderboard | `leaderboard_view` `:515` | `leaderboard_view` | — | login | | My Badges | `my_badges_view` `:580` | `my_badges_view` | — | login | | Category CRUD | `:666-843` | `category_*` | — | PX/Hosp-Admin only | | Badge CRUD | `:850-1033` | `badge_*` | — | PX/Hosp-Admin only | | AJAX: users/staff/physicians/departments | `:1057-1147` | `ajax/*` | — | login | Detail view exposes `can_activate` and `can_send` flags (`ui_views.py:142`). ### 5.2 REST API (`views.py`) `AppreciationCategoryViewSet` (`:33`), `AppreciationViewSet` (`:55`, actions: `acknowledge` `:210`, `my_appreciations` `:232`, `sent_by_me` `:263`, `summary` `:277`), `AppreciationStatsViewSet` (`:363`), `AppreciationBadgeViewSet` (`:394`, PX/Hosp-Admin), `UserBadgeViewSet` (`:416`), `LeaderboardView` (`:445`). `acknowledge` verifies the requester is the recipient via ContentType+id (`views.py:216`), else HTTP 403. > **⚠ Note:** there is **no `activate` or `ai_analyze` REST action** — those are UI-only. > **⚠ Gap — REST `create` vs status:** `AppreciationViewSet.create` (`views.py:143`) creates the record and immediately calls `appreciation.send()` (`views.py:198`). Because `send()` requires ACTIVATED/AI_ANALYZED (`models.py:335`), this raises `ValueError` on a DRAFT. --- ## 6. Decision Points 1. **Activation gate (DRAFT→ACTIVATED)** (`ui_views.py:195`). Mirrors the complaints activation gate. 2. **AI analysis path vs direct-send** — AI attempted at `appreciation_activate` (`ui_views.py:230`). If it succeeds → AI_ANALYZED; if it throws → stays ACTIVATED and is still sendable. From ACTIVATED the send step is allowed (`models.py:30`), so **AI is optional** to progressing. 3. **Visibility/audience decision** — `AppreciationVisibility` (`models.py:37`): PRIVATE/DEPARTMENT/HOSPITAL/PUBLIC. Default PRIVATE (`models.py:190`). Public portal forces PUBLIC (`ui_views.py:1248`); external API forces PUBLIC (`api_views.py:613`); complaint conversion defaults `"private"` (`complaints/views.py:2115`). Read-access filtered in `AppreciationViewSet.get_queryset` (`views.py:78`). 4. **Recipient/target selection** — done at send time, not creation, in `appreciation_send_to`: `recipient_type ∈ {person, department}` (`ui_views.py:350`). If department: auto-targets champion+manager via `get_champion_and_manager`. Optional `staff_id` sets recipient to a `Staff`. 5. **Badge awarding decision** — `signals.check_and_award_badges` (`signals.py:306`) runs whenever an appreciation reaches SENT. --- ## 7. Assignment Flow — RECIPIENTS There is **no "case assignment to an agent"**. The analogous concept is the **recipient** (who the recognition is for) plus the **visibility audience**. ### 7.1 Recipient model - Recipient is a **GenericForeignKey** (`recipient_content_type` + `recipient_object_id` + `recipient`) (`models.py:132`). Eligible concrete types: `accounts.User` and `organizations.Staff` (incl. physicians as `staff_type='physician'`, `ui_views.py:1110`). - Sender: `models.ForeignKey("accounts.User")` (`models.py:127`). - `AppreciationForm` (internal) does **not** set a recipient — only `hospital`, `department`, `message_en`. Recipient bound at send step (`ui_views.py:427`) for the department path, or via `recipient_type`/`recipient_id` in REST/conversion. - The "owner" (for routing) is computed by `get_owner()` (`models.py:250`) — a cascade over section/department role-holders. **⚠ Gap:** `get_owner()` is defined but not invoked by any code in the appreciation app (verify external callers). ### 7.2 Visibility `AppreciationVisibility` (`models.py:37`): PRIVATE, DEPARTMENT, HOSPITAL, PUBLIC. Read-side enforcement in `AppreciationViewSet.get_queryset` (`views.py:78`): a user sees their sent items, those received by their User/Staff profile, DEPARTMENT-vis in their dept, HOSPITAL-vis in their hospital, and all PUBLIC. ### 7.3 Organization context `hospital` (required, `models.py:139`), `department` (nullable, `models.py:173`), `section` (nullable, `models.py:165`), plus three **DEPRECATED** legacy fields (`legacy_location`, `legacy_main_section`, `legacy_subsection`, `models.py:140-163`). --- ## 8. Investigation Process — N/A Appreciations have **no investigation phase**. No investigator field, no investigation status, no evidence model. The closest analog is the **AI_ANALYZED** step, which is automated content enrichment (summary/themes/tone/suggested reply), not a human investigation. **Why N/A:** appreciations are positive recognition, not problems requiring root-cause analysis. --- ## 9. Communication Flow — notifications ### 9.1 On reaching SENT — `post_save` cascade `signals.handle_appreciation_sent` (`signals.py:27`) fires when `status == SENT and not notification_sent`. It calls: - **`send_appreciation_notification`** (`signals.py:49`): builds HTML email ("Staff Appreciation"); resolves recipient email/phone via `get_recipient_email`/`get_recipient_phone` (`models.py:287`); sends email + SMS; sender display "Anonymous" if `is_anonymous`; calls `_send_department_head_notification` (`signals.py:140`) emailing `department.manager` + `Staff(department=dept, is_head=True)`; calls `_send_cc_notifications` (`signals.py:203`); sets `notification_sent=True`/`notification_sent_at`. - **`update_appreciation_stats`** (`signals.py:227`) — §12. - **`check_and_award_badges`** (`signals.py:306`) — §Gamification. ### 9.2 The `send_appreciation_notifications` Celery task `tasks.py:9` (`@shared_task`) — a **separate, richer** notification path used by the UI `appreciation_send_to` department branch (`ui_views.py:442`). Requires a `department`; `get_champion_and_manager(department)`; per target sends email + SMS; if `staff_id` provided, emails/SMS the Staff *"You've Been Appreciated!"*. > **⚠ Two notification codepaths** — `signals.py` (sync, fires on every SENT) and `tasks.py` (async, fires only from UI `send_to` department branch). Both can run for the same appreciation if sent via `send_to`, potentially double-notifying. ### 9.3 On creation of a public/external DRAFT `notify_staff_new_item("appreciation", id)` emails PX admins/on-call roster (`complaints/tasks.py:2489`), template `emails/new_appreciation_notification.html`. ### 9.4 On badge award `signals.check_and_award_badges` emails the recipient user *"You earned a badge: !"* (`signals.py:357`). --- ## 10. Escalation Flow — N/A **No SLA timers, no escalation levels, no overdue fields** in `Appreciation` (`models.py:110-237`). No celery beat task references appreciation SLA. **Why N/A:** appreciation is non-urgent recognition; urgency/SLA semantics belong to complaints. --- ## 11. Resolution Process — "acknowledgment" The analog of resolution is the **acknowledge** transition (SENT→ACKNOWLEDGED). - Model method: `acknowledge()` (`models.py:344`) — sets `status=ACKNOWLEDGED`, `acknowledged_at=now()`. - UI: `appreciation_acknowledge` (`ui_views.py:485`) — POST; **only the recipient** (matched by User ContentType + id, `ui_views.py:492`); rejects if not SENT (`ui_views.py:500`). - REST: `AppreciationViewSet.acknowledge` (`views.py:210`) — same recipient check, HTTP 403 otherwise. "Acknowledgment" = the recipient has confirmed/thanked — it is the **recipient's terminal action**. There is **no sender-side resolution**. --- ## 12. Closure Process ### 12.1 Terminal state & reopen ACKNOWLEDGED is terminal: `VALID_APPRECIATION_TRANSITIONS[ACKNOWLEDGED] = set()` (`models.py:33`), and `acknowledge()` leaves no forward method. There is **no reopen** action/view anywhere. Once ACKNOWLEDGED, permanently complete. ### 12.2 Soft delete / restore `Appreciation` extends `SoftDeleteModel` (`models.py:110`); soft-deletable + restorable via `appreciation_restore` (`ui_views.py:1036`, uses `Appreciation.all_objects` + `restore()`). Restore does not change status. ### 12.3 Statistics aggregation `AppreciationStats` (`models.py:476`) — **monthly per recipient**. Unique key `(recipient_content_type, recipient_object_id, year, month)` (`models.py:517`). Fields: `received_count`, `sent_count`, `acknowledged_count`, `hospital_rank`, `department_rank`, `category_breakdown` (JSON). Aggregated in `signals.update_appreciation_stats` (`signals.py:227`) when an appreciation reaches SENT: `get_or_create` the month row; atomically increment counts via `F('...') + 1`; update category JSON; `recalculate_rankings` (`signals.py:269`) re-numerates ranks by `-received_count`. ### 12.4 Leaderboard `LeaderboardView` (`views.py:445`): reads `year`/`month` (defaults current month); filters `AppreciationStats` by hospital; enumerates rank; attaches earned badges. UI equivalent `leaderboard_view` (`ui_views.py:515`). ### 12.5 Summary `AppreciationViewSet.summary` (`views.py:277`) and `appreciation_summary_ajax` (`ui_views.py:1150`) compute total/this-month received & sent, badges earned, top category, hospital rank. --- ## 13. Exception Flows ### 13.1 Complaint → Appreciation conversion `complaints/views.py:2076`. Guards: `is_active_status`, `complaint_type == "appreciation"`, `metadata.appreciation_id` absent. Default category "Patient Feedback Appreciation" (code `patient_feedback`). ### 13.2 ⚠ INCONSISTENCY (likely bug): conversion and REST create call `send()` on DRAFT - Conversion: creates `status=DRAFT` (`complaints/views.py:2171`) then `appreciation.send()` (`:2183`). - REST create: creates DRAFT then `appreciation.send()` (`views.py:198`). - `send()` (`models.py:335`) requires ACTIVATED/AI_ANALYZED, else `ValueError`. **Both paths would raise `ValueError` at runtime.** Neither wraps `send()` in try/except. This strongly indicates the **primary, exercised path is the UI activation-gate flow**, and these two callers are out of sync with the gate model. ### 13.3 Draft abandonment A DRAFT has no expiry task and no cleanup. Remains queryable as DRAFT (counted in `core/context_processors.py:79`, `ui_views.py:71`). No auto-close/activation. ### 13.4 Visibility changes `visibility` is a plain CharField (`models.py:190`) with no transition guard — editable at any status via admin or direct mutation. Public/external forced PUBLIC at creation, not otherwise change-protected. ### 13.5 Badge revocation **No revoke/un-award path.** `UserBadge` (`models.py:432`) has only create + read. `badge_delete` (`ui_views.py:1004`) refuses to delete a badge with any `UserBadge` (`ui_views.py:1018`) — earned badges are protected. A badge, once awarded, persists. ### 13.6 Duplicate appreciation **No deduplication** on creation. The only dedup is per-badge-award (`signals.py:326`) and per-stats-month (`unique_together` `models.py:517`, guarded by `get_or_create`). ### 13.7 Restore from soft-delete `appreciation_restore` (`ui_views.py:1036`) — PX/Hosp-Admin only; redirects to `config:deleted_items`. ### 13.8 AI-failure handling In `appreciation_activate`, an AI exception is logged but swallowed (`ui_views.py:255`) — appreciation stays ACTIVATED and is still sendable. At infra level, `AIService._notify_ai_failure` (`ai_service.py:172`) emails PX admins (debounced hourly) on HTTP 401/402. --- ## 14. Department-Response Sub-Flow — NOT used (confirmed) Appreciations **do not** use the inbound send-to-department → department-responds → resolve cycle. There is no `responded`/`department_action` status, no department-side response model, no SLA on department reply. The `send_to` step (`ui_views.py:320`) is **outbound only** — it delivers the appreciation to a department's champion+manager+staff and marks the appreciation SENT; the department does not "respond back" through the system (their only return action is the recipient's `acknowledge`). **Why:** this module is outbound recognition. The "send to department" verb here means *deliver recognition to that department*, not *open a case for the department to handle*. --- ## 15. End-to-End Example ``` Patient submits appreciation via public form (mentions Nurse A) ↓ (ui_views.py:1196) → status=DRAFT, ref APR-202607-0001, visibility=PUBLIC, PX admins notified PX staff reviews + activates ↓ (ui_views.py:193) → status=ACTIVATED, activated_at/by AI analysis runs synchronously [DECISION: AI succeeds → AI_ANALYZED, or stays ACTIVATED] ↓ (ui_views.py:230) → ai_analysis JSON stored (summary/themes/tone/suggested reply) PX sends to Department X (target Nurse A / champion / manager) ↓ (ui_views.py:320) → recipient=Staff(Nurse A), status=SENT, sent_at [DECISION: visibility PRIVATE/DEPARTMENT/HOSPITAL/PUBLIC] post_save cascade fires: - email+SMS to recipient ("Staff Appreciation") - email department manager + dept heads - cc notifications - update_appreciation_stats (received/sent count, rank) - check_and_award_badges (e.g. "First Appreciation") → badge email to recipient Nurse A (the recipient) acknowledges ↓ (ui_views.py:485) → status=ACKNOWLEDGED, acknowledged_at [TERMINAL — no reopen, no further actions] ``` --- ## Appendix A — Gamification (Badges) ### A.1 `AppreciationBadge` (`models.py:360`) — definition Fields: `hospital` (null = system-wide), `code` (unique), bilingual `name_en/ar`/`description_en/ar`, `icon`, `color`, `order`. **`criteria_type`** (`models.py:389`): `received_count`, `received_month`, `streak_weeks`, `diverse_senders`. `criteria_value` (int threshold). `is_active`. ### A.2 Seed data (`seed_appreciation_data.py`) **Categories:** `excellent_care`, `team_player`, `innovation`, `leadership`, `mentorship`, `going_extra_mile`, `reliability`, `positive_attitude`. **Badges** (seed file is authoritative — `README.md` differs): | code | name_en | criteria_type | value | |---|---|---|---| | first_appreciation | First Appreciation | received_count | 1 | | appreciated_5 | Rising Star | received_count | 5 | | appreciated_10 | Shining Star | received_count | 10 | | appreciated_25 | Super Star | received_count | 25 | | appreciated_50 | Legendary | received_count | 50 | | monthly_champion | Monthly Champion | received_month | 10 | | streak_4_weeks | Consistent | streak_weeks | 4 | | diverse_appreciation | Well-Loved | diverse_senders | 10 | ### A.3 `UserBadge` (`models.py:432`) — award record GenericFK recipient, FK `badge`, `earned_at` (`auto_now_add`), `appreciation_count` (count-at-award), `metadata` JSON. No unique constraint — awarder dedups manually (`signals.py:326`). ### A.4 Awarding logic — `signals.check_and_award_badges` (`signals.py:306`) Triggered from `handle_appreciation_sent` when SENT. Iterates active badges for the hospital/system-wide; skip if already earned; `check_badge_criteria` (`signals.py:383`): `received_count` (total SENT to recipient), `received_month` (this month), `streak_weeks` (`check_appreciation_streak` `signals.py:498` — Mon-start weeks; gap breaks streak), `diverse_senders` (distinct senders). If qualifies: `UserBadge.objects.create`, email recipient. ### A.5 Badge progress UI — `my_badges_view` (`ui_views.py:580`) Computes per-badge progress % toward threshold. Template `appreciation/my_badges.html`. --- ## Appendix B — Flagged gaps / ambiguities 1. **`VALID_APPRECIATION_TRANSITIONS` is dead code** — defined but never enforced via lookup; transitions rely solely on per-method `ValueError`. Constant and methods agree today but nothing keeps them in sync. 2. **REST `create` and complaint conversion call `send()` on DRAFT** — would raise `ValueError` (`views.py:198`, `complaints/views.py:2183` vs guard `models.py:335`). Likely bug/stale code; UI activation flow is the working path. 3. **`AppreciationForm` does not capture recipient/category/visibility** — decided later (at send-time for UI path). An internally-created DRAFT has no recipient until `send_to` runs. 4. **Two notification codepaths** — `signals.py` (sync, every SENT) and `tasks.py` (async, UI `send_to` dept branch). Potential double-notification. 5. **No badge revocation, no reopen, no draft expiry** — by design. 6. **README badge list vs seed file badge list differ** — seed file is authoritative. 7. **`get_owner()` (`models.py:250`) defined but not invoked** in the appreciation app — verify external callers.