shipped implementation; Contract Graph status partial (2026-07-15)

Spec: F26 — Approval Requests (multi-step)

Status: shipped implementation; Contract Graph status partial (2026-07-15) Contract: F26 CUE contract Implementation audit: F26 contract audit Test plan: F26 test plan

The Contract Graph owns each generated approval-policy and approval-request operation explicitly, including the destructive policy delete route. This is endpoint ownership, not new behavior or proof: the existing policy, tenant, role, browser, and native evidence boundaries remain in force.

The backend and web implementation are shipped and locally testable. Contract Graph status remains partial until the documented policy contradictions, browser journey, and named native runtime evidence are resolved. Source presence and proof collection do not count as browser, device, live-provider, push-delivery, or production evidence.

1. Problem & CEO Job

Decide fast — for routine, policy-shaped asks. DecisionRequests (F11) capture judgment calls; most “chờ sếp” moments are policy-shaped: a purchase over 50M, discount over 10%, contract signing, or hiring offer. They need the right sign-off in the right order, fast and recorded. Approvals replace Zalo scroll-back with an explicit chain, visible wait, and one-tap sign-off.

Relationship to F11 (the critical PM line): DecisionRequest = “tôi cần anh nghĩ” (ad-hoc, extracted from pulses, judgment). ApprovalRequest = “chính sách bảo cần chữ ký anh” (routed by policy, structured, repeatable). They stay separate models — but converge in ONE queue UX: the Brief and Today render a single “Chờ bạn” section containing both. The user never learns two systems; the org’s policies just make some items pre-routed.

1a. Competitive Positioning — Why Lumi Wins the SME Approval Flow

In a typical Vietnamese SME, approvals live in one of three limbo states:

  • Zalo/Telegram DM chains (“gửi tôi duyệt”): The requester DMs the manager or CEO a screenshot, a PDF, or a text message. The approver reads it on the go, types “ok duyệt” (or never replies). No record survives scroll-back. The requester has no visibility into where the request sits — “anh duyệt chưa?” becomes a daily ping ritual.
  • Email chains: Purchase requests circulate as email threads with scanned PDFs. The chain is threaded but unstructured — no one knows which step is current without reading the whole thread. Approvals buried in inbox triage get missed for days.
  • Paper sign-off: Physical chits (phiếu chi, đề xuất mua hàng) walk from desk to desk. In a hybrid or remote team, this means scanning, printing, re-scanning. The bottleneck is physical presence, not the decision.

How Lumi out-positions each:

Current SME approachLumi’s edge
Zalo/Telegram DMStructured routing with a visible chain — requester sees “sẽ qua: Quản lý kho → CEO” without pinging anyone. Audit log replaces scroll-back.
Email threadsOne-tap approve/reject from the queue. No inbox triage, no thread-reading to find the current step.
Paper sign-offZero async deadlock — the chain advances the moment the current-step approver taps Approve. No waiting for physical handoff.

What we replace is the Zalo “anh duyệt giúp em” status quo, not a workflow tool. The target SME owns no approval software to out-configure — there is no workflow designer, no automation column, no approval database to migrate from. The real incumbent is the same three limbo states above: a screenshot DM’d to the boss, an email thread no one can find the current step in, a paper chit walking desk to desk. Lumi turns that from an organizational-design problem (“what’s our purchase approval flow?”) into a 30-second policy definition, then a single-form submission for every member, with the chain visible from the moment of create. The owner stops being pinged “anh duyệt chưa?” because the requester can already see exactly where the request sits.

External sign-off routes via F71 (Loop 2). F26 is the internal approval leg — staff and managers inside the org. When an approval needs an external party to sign off — a client approving a quote (báo giá) or a subcontractor confirming scope — that is the external leg, handled by F71 inter-company confirmation links (Loop 2): a no-login link the owner sends whose completion updates the brief and puts a future Lumi buyer in contact with the product. Keep the two separate: F26 routes sign-off among people who already have Lumi accounts; F71 reaches people who don’t.

1b. Success Metrics

  • Median time-to-decision ≤ 4 hours for approvals requiring only team-manager step, ≤ 12 hours for CEO-finish step: measured as the median of action.timestamp - request.created_at for each completed (approved or rejected) ApprovalRequest, segmented by chain length.
  • % of approvals resolved within org-defined SLA ≥ 80%: measured as the share of ApprovalRequest records where the total resolution time falls below a configurable threshold per policy category (default: 24 hours for purchasing, 8 hours for discounts, 48 hours for contracts).
  • % of rejected approvals with a recorded reject reason ≥ 90%: measured as the share of ApprovalAction records where action="reject" and comment is non-empty. Lumi’s approve/reject form makes the comment field required on reject (C-r26 frontend: reject_comment_required locale key + canConfirm guard).

2. User Stories & Acceptance Criteria

  • As an admin, I want to define approval policies once, so that routing is automatic forever.
    • Given a policy (name, category, optional money threshold, ordered steps), when a matching request is created, then the chain is resolved and shown to the requester immediately (“sẽ qua: Quản lý kho → CEO”).
    • Step approver = a specific member or a role (“any admin”, “team manager of requester”). Sequential only in v1 — no parallel, no quorum (complexity that kills adoption).
    • Given amount ≥ threshold, the higher-chain policy wins; below threshold with no match, a single-step default applies (requester’s team manager, else any admin) — a request must never be unroutable.
  • As a requester (any member), I want to ask in one form and see where it sits, so that I stop pinging people.
    • Create = category + title + amount? + description. Chain renders with live state per step (waiting / approved ✓ / current).
    • Given approval/rejection at any step, then I’m informed (in-app; Telegram per digest discipline) with the comment.
  • As an approver, I want my queue and one-tap action, so that sign-offs take seconds.
    • Approve/reject with optional comment; reject ends the chain (status rejected); full chain approved → approved.
    • Given I approve, the next step’s approver becomes current — no gap where nobody is “it”.
    • Brief action controls use the same authorization decision as the approval queue: a multi-member requester never receives a forbidden self-action, while the current approver can approve or reject inline; rejection requires a non-empty reason before submission.
    • A populated native queue remains readable through refresh failure, exposes a compact explicit Retry, and preserves unaffected requests when approve or reject succeeds from stale state.
  • As a CEO, I want stuck approvals to surface, so that my org’s bottleneck (often me) is visible.
    • Pending approvals appear in Brief/Today “Chờ bạn” alongside decisions, ranked with them; an approval pending > 3 days is flagged.
  • As a CEO, I want the Business lane to distinguish an unavailable approval summary from an empty queue, so that a failed approvals request never looks like zero pending work.
    • A failed approval-summary request shows the shared unavailable row and retryable partial-data state; a successful empty response remains a valid zero.
    • A total Business summary outage stays on the existing error path so refresh preserves the last successful summary.
  • Every action writes an audit log row (F16). Cancelled requests keep their action history.

3. Scope Boundaries

  • In: policies (CRUD, admin-only), sequential chains, request lifecycle, approver queue, Brief/Today integration, audit, optional links to Deal (F27) / Task (F05).
  • Out (v1): parallel/quorum approval, delegation/out-of-office, escalation timers, editing a policy mid-flight (in-flight requests keep their resolved chain), approval via Telegram reply (later; web first).

4. Data Model (as built — app approvals)

  • ApprovalPolicy — org FK, name, category (purchasing/expense/discount/contract/hiring/other), min_amount nullable, steps_json (ordered: [{kind: "member"|"role", value}]), is_active.
  • ApprovalRequest — org FK, requester (OrganizationMember), category, title, description, money fields, policy FK (nullable for default route), resolved_steps_json (frozen at create), current_step int, status: pending → approved | rejected | cancelled, optional deal FK / task FK.
  • ApprovalAction — request FK, step index, approver, action approve/reject, comment, timestamp.

State machines → backend/DIAGRAMS.md; terms → GLOSSARY. ADR required: role-resolution semantics (“any admin” race: first action wins) and default-route rule.

5. API Contract (as built — PR-1/PR-2)

MethodPathMin roleNotes
CRUD/api/approval-policies/adminPolicy management
POST/api/approvals/memberCreate; response includes resolved chain
GET/api/approvals/memberMine + (approver) my queue via ?queue=me
POST/api/approvals/<id>/approve/ …/reject/current-step approver onlyOne-tap actions; 403 for anyone else incl. admins not in chain
POST/api/approvals/<id>/cancel/requesterOnly while pending

Tenant-scoped everywhere; approver authorization checked server-side per step.

6. UI / UX

  • New route /crm/approvals — two tabs at most: “Chờ bạn” (queue) and “Của bạn” (my requests). No admin-console feel for members.
  • Policy setup lives in /settings (admin) — plain language builder: “Mua hàng trên 50tr → Quản lý → CEO”. If it needs a manual, it’s wrong.
  • The chain visual is the feature: requester anxiety drops when the path is visible. Same component renders in queue, request detail, and (compact) brief items.
  • States: empty queue = positive (“Không có gì chờ bạn duyệt”); pending-with-stale flag; rejected shows the comment prominently (the why travels).
  • i18n: new approvals namespace, en+vi together.

7. Test Plan

#CaseTypeWhere
1Policy match incl. threshold precedence; default route fallbackbackendnew classes in backend/tests/test_api.py
2Chain walk: approve→advance, reject→halt, cancel; frozen chain despite policy editbackendsame
3Authorization: only current-step approver can act; cross-tenant blockedbackendmandatory
4Brief includes pending approvals ranked with decisionsbackendextend TestBriefGeneration
5Request → approve flow; queue renderinge2enew describe("Approvals")

8. Current Behavior

Shipped. The approvals app, routing, chain resolution, and queue/UI flows described in the audit section are live. On Android, successful request creation starts a reload of the organization-scoped approvals list before the create destination returns; the returned route may briefly render its loading/stale state while that request completes. A populated shared queue now keeps committed rows visible behind one compact warning when refresh fails, and explicit Retry requests authoritative state without discarding the stale snapshot. Successful approve or reject from stale state removes only the acted-on request locally. Named-device proof of the created row and iOS recovery runtime remain open. The remaining value of this spec is documenting operational edges and the distinction from decision requests.

9. Error States

#ErrorUser messageHTTPLoggedNotes
ES1Cross-tenant request access — a member of org A tries to access org B’s approval request by UUID.Standard 404 (don’t reveal existence)404NoApprovalRequestViewSet queryset filters by request.tenant.
ES2Non-current approver action — an admin who is not the current-step approver tries to approve or reject.approvals.error_not_current_approver403NoChecked in approve/reject view actions via _is_current_approver() in backend/apps/approvals/views.py (~L95–96, L119–120). act_on_request() itself has no authz check — only a status guard.
ES3Non-requester cancel — an admin tries to cancel someone else’s request.approvals.error_not_requester403NoChecked in the cancel view action.
ES4Cancel already-resolved request — user tries to cancel an approved/rejected request.approvals.error_not_pending400NoChecked in act_on_request() — raises ValueError caught by view as 400.
ES5Approve already-approved request (race) — two admins both act simultaneously on the same step.approvals.error_already_resolved409Yes (AuditLog for the first)act_on_request() locks and re-reads the row; supported-database concurrent proof remains open.
ES6Policy with 0 steps — an admin creates a policy with steps_json=[].Model and serializer validation reject empty steps (minimum 1 step required).400NoCovered by TestApprovalAPI.test_policy_create_rejects_empty_steps.
ES7Unknown category — a request is created with a category that doesn’t match any active policy.Default route applies: team manager → any admin fallback. Request never unroutable (by design — §2).201 (request created with default route)NoDefault route guarantees fallback. The policy stays null in the request.
ES8Self-approval — the requester acts on their own request when _is_authorized_approver() returns True for them.approvals.error_self_approval403No_check_self_approval() rejects with 403 unless the org has ≤1 active member. Fixed in C-r27.
ES9Business approval summary unavailable — GET /api/approvals/ fails while another Business summary request succeeds.Shared partial-data banner with Retry; the approvals row says unavailable instead of zero.n/aYesEndpoint-level failure metadata is kept in the shared Business summary; F26 owns approval-row truth.
ES10Populated native queue refresh fails — the list request fails after approvals have loaded.One-line warning with explicit Retry; committed approval rows stay visible and actionable.n/aYesRepeated failure retains LoadState.Error.last; successful approve/reject filters from current-or-stale state so unaffected rows remain.

10. Diagrams & Docs Touched

backend/DIAGRAMS.md (ApprovalRequest state machine, ER) · frontend/DIAGRAMS.md (route) · GLOSSARY (Approval vs Decision entry — mandatory, the distinction confuses agents otherwise) · new ADR.

2026-08-04 — Preserve Business approval summary failure truth

The shared Business lane now keeps an approval-summary request failure distinct from a valid empty queue, exposing the unavailable row and retry action while leaving total-outage stale-data handling on the existing LoadState.Error.last path. Retry also keeps that stale snapshot visible while loading, without presenting an old partial warning as current request truth. Repository/model/source proof is present; named Android/iOS runtime evidence remains open.

11. Implementation Audit (2026-06-14)

#ClaimStatusEvidence
1Policy CRUD (admin-only); resolve_chain() picks correct policy by category + amount threshold✅ matchTestApprovalRouting (15 tests); TestApprovalAPI (23 tests)
2Chain walk: approve→advance, reject→halt (terminal), cancel while pending; frozen resolved_steps_json✅ matchTestApprovalAPI chain-walk tests; 433 tests 100% coverage
3Authorization: only current-step approver can act; non-current 403✅ match_is_current_approver() in backend/apps/approvals/views.py (~L176–186), called from approve/reject actions (~L95–96, L119–120); act_on_request() in services.py has no authz check (only a status guard) — direct callers must enforce authorization themselves; TestApprovalAPI role tests
4Default route fallback: team manager → any admin; request never unroutable✅ match_default_steps() always returns ≥1 step; TestApprovalRouting fallback tests
5Brief includes pending approvals interleaved with decisions; stale >3 days → critical✅ matchTestBriefApprovalInterleave (6 tests); BriefItem.TYPE_CHOICES += ("approval", ...); migration 0004
6AuditLog row written on every action including create✅ match2026-06-12 perform_create fix; test_create_writes_audit_log
7Self-approval guard (E10/ES8): 403 unless single-member org✅ match_check_self_approval() (C-r27); TestF26SelfApprovalGap (5 tests)
8Deactivated approver cannot act (E1): runtime protection via IsOrgMember layer✅ matchIsOrgMember.has_permission() in backend/apps/organizations/permissions.py (~L20–21) filters status="active" before view code runs; _is_current_approver() in views.py has no status check; _is_authorized_approver() in services.py does check status but is NOT called by the HTTP path (only called by unit tests directly); C-r25 fix; TestF26ApprovalGaps::test_deactivated_approver_cannot_act
9Approve race: row lock and stale-action conflict handling are implemented; concurrent proof remains open⚠️ gapES5; supported-database concurrency evidence
10any_admin step in org with zero admins: chain permanently stuck⚠️ gapE3; no test for zero-admins edge case

Summary: 9/11 match; 2 gaps (concurrent approval proof, zero-admins stuck chain).

Edge Cases Catalog (formerly §11, 2026-06-12; E1–E10): Key items integrated above. Remaining: E4 no deadline field/enforcement (request stays pending indefinitely; stale >3d flagged in Brief as advisory only); E5 circular chain (same member at multiple steps — allowed by design, no guard or test); E6 duplicate approval requests — no deduplication; by design (each is a separate record). Full catalog in git history.

12. Open Questions

See the explicit decisions embedded in the implementation audit and later hardening notes. There are no currently blocking open design questions for the shipped v1 surface.

  • Should DecisionRequest eventually become a one-step approval? Tempting; decided no for now — extraction-born judgment asks and policy routes have different creation paths and different failure modes. Revisit after 6 months of real usage.

13. Bug / Change Log

2026-08-10 — Preserve the native approval queue through refresh failure

The shared model previously derived refresh and post-action state only from LoadState.Success. After one failed refresh, a second failure could therefore discard the retained queue; approving or rejecting from the stale error state could replace every row with an empty list. Current-or-stale state now protects repeated recovery and action reconciliation. The shared surface keeps rows behind a compact one-line warning with explicit Retry, while first-load failure remains blocking. Focused model/source policy proof and guarded Android flow 161 own this contract; iOS, named-device, accessibility-service, store, deployed, and production proof remain open.

Flow 161 passed on the lifecycle-owned Pixel 7 API 35 emulator with identical built/installed prodDebug SHA-256 09dc5bcaf4ffad28de474efefd58fddeb5726ec3521ee7c4d44192735fdc5ef3. Two actionable cards remained visible through exactly two allowlisted approval-list 503s, Retry forwarded recovery, both approval IDs remained pending, cleanup reached 0:0:0, both proof ports closed, and the emulator shut down. This does not prove stale approve/reject runtime, iOS, named-device, accessibility-service, store, deployed, or production behavior.

2026-08-09 — Brief approval controls share queue authorization truth

Type: Authorization feedback / action completeness (feedback streamentry/lumi-fb#36, #38)

The CEO Brief previously derived approval_can_act from chain membership alone even though the approval API and queue also enforce the multi-member self-action boundary. A requester matching an any_admin step therefore saw an Approve button that could only fail with 403. Brief also exposed Approve without the paired Reject action promised by F26. Brief serialization now uses the same actionability predicate as the queue, and actionable cards expose Reject through a confirmation dialog whose reason is required. Backend and web regressions cover self-action denial, action visibility, the reject-reason guard, and the reject request payload.

2026-08-09 — Android create returns through an authoritative list reload

Type: Mobile state reconciliation (feedback streamentry/lumi-fb#58)

After the backend accepted a new approval, Android previously popped the create destination without invalidating the approvals list model. The user could therefore return to an empty or stale list despite a successful server mutation. The Android host now starts loadApprovals() before popping the destination. A focused source regression and F26 Contract Graph node protect that ordering; named-device create→returned-list proof remains open and is not inferred from the source test.

2026-07-25 — Actionable queue no longer offers forbidden self-actions

Type: Authorization feedback / queue truth repair (feedback streamentry/lumi-fb#36, #38)

Summary:

  • ?queue=me now contains only requests the member can actually approve or reject. A requester matching an any_admin step in a multi-member organization remains visible under My Requests but is not presented as waiting for their action.
  • The serializer’s can_act field now includes the existing self-action boundary instead of reporting only chain membership. The active-member count is cached once per request so list serialization does not introduce an N+1 query.
  • Web action buttons follow can_act; cancellation stays requester-only on My Requests. Namespaced backend errors such as approvals.error_self_approval resolve through the active locale rather than appearing as raw keys.
  • The intentional separation-of-duties rule is unchanged: in a multi-member organization, the requester cannot approve or reject their own request.

2026-07-24 — Rejection reason copy matches the required guard

Type: Web UX copy repair (feedback streamentry/lumi-fb#37)

Summary:

  • The approval action form previously reused the optional approval-comment placeholder while rejecting, even though rejection correctly requires a non-empty reason.
  • Reject now exposes a localized Reject reason / Lý do từ chối label and required-action placeholder. Approve retains its explicitly optional comment copy.
  • The existing F26 reject-guard test now verifies the accessible label and placeholder distinction.

2026-07-19 — Focused web action and recovery proof closure

The web approvals suite now executes the shipped create, approve, reject, cancel, and create-failure recovery paths with runner-native Vitest proof IDs (F26.AC005.CREATE, APPROVE, REJECT, CANCEL, and FAILURE). This closes the non-blocking focused web proof gap only; browser execution, named native runtime, concurrent database proof, and zero-admin routing remain explicit gaps.

2026-07-16 — Transactional approval stale-action boundary

Type: Backend correctness and evidence boundary

Summary:

  • act_on_request() now re-reads the approval row under select_for_update() before creating an action, and the HTTP approve/reject actions return 409 when a stale concurrent attempt loses the lock.
  • Added F26.AC003.STALE_RECHECK for the deterministic stale-instance path. A true concurrent supported-database proof remains an explicit gap; source or sequential tests do not replace it.

2026-07-15 — Contract Graph migration boundary

Type: Contract and audit synchronization

Summary:

  • F26 is now the canonical Contract Graph owner for approval policy/request APIs, the /crm/approvals web surface, and the shared native approval boundary.
  • Local backend integration and web unit proof IDs are registered. The dedicated browser target and native source symbols remain collection/source evidence only until their target runtimes execute.
  • The approval-race and zero-admin default-route docs-vs-code conflicts remain explicit blocking gaps. Empty-policy-step validation and the focused web action/failure paths are executable; the browser and native runtime boundaries remain explicit.
  • The registry status is partial rather than shipped-green until those evidence and behavior gaps close.

2026-07-02 — §11/§9 audit drift: gate locations, test counts (docs-only)

Type: Documentation

Summary:

  • §11 row 1: TestApprovalAPI count corrected 14 → 23 (verified grep -c "def test_" in backend/tests/test_approvals.py lines 276–590).
  • §11 row 3: evidence corrected — the 403 gate is _is_current_approver() in backend/apps/approvals/views.py (~L176–186), called from approve/reject actions (~L95–96, L119–120). act_on_request() in services.py has only a status check; it has no authz guard — direct callers of act_on_request() must enforce authorization themselves.
  • §11 row 5: TestBriefApprovalInterleave count corrected 4 → 6 (verified grep -c "def test_" lines 591–711).
  • §11 row 8: evidence corrected — runtime protection for deactivated approvers is IsOrgMember.has_permission() in backend/apps/organizations/permissions.py (~L20–21), which requires status="active" before view code executes. _is_current_approver() (views.py) has no status check. _is_authorized_approver() (services.py) does check status but is not called by the HTTP path.
  • §9 ES2: Notes cell corrected to match §11 row 3 — references _is_current_approver() in view actions, not act_on_request().
  • No code changes.

2026-06-19 — Visual QC: approvals empty-state and hierarchy pass

Type: Polish/UI

Summary:

  • Screenshot-first pass for /crm/approvals (frontend/screenshots/25-crm-approvals/01-02-*.png) showed the page was functionally correct but visually inert on a quiet day: both tabs collapsed into a large blank slab with little explanation of what the surface was for or what the next action should be.
  • ApprovalsPage now adds a real page description, uses a calmer segmented-control treatment for the queue/requester tabs, and turns both empty states into workflow-specific guidance:
    • Waiting for me explains that requests appear here when they reach the current approver step and offers a direct switch to My Requests.
    • My Requests explains the value of starting the first tracked request here and offers a direct Create request action.
  • Query failures are now visible at the page level as an inline destructive alert using the existing approvals.error_load copy instead of silently collapsing into an empty screen.
  • Screenshot coverage: frontend/tests/e2e/screenshot-flow.spec.ts now captures 25-crm-approvals in both queue and requester tabs.
  • Verification: frontend/src/test/ApprovalsPage.test.tsx green (9), pnpm exec tsc --noEmit clean, node scripts/check-design-tokens.mjs clean.

2026-06-15 — Frontend route namespaced

Frontend route namespaced: /approvals -> /crm/approvals (app-wide IA restructure; API paths unchanged).

2026-06-14 — Strategy v2 realignment (positioning)

Type: Documentation Change

Summary:

  • §1a: deleted the “Competitor comparison — Jira, Asana, Trello, Monday, ClickUp, Notion” table and replaced it with a short paragraph framing the real incumbent as the Zalo “anh duyệt giúp em” status quo (DM screenshots, email threads, paper chits), not a workflow tool. The §1a Zalo/email/paper limbo states and the Zalo/email/paper “Lumi’s edge” table are unchanged.
  • §1a: added an “External sign-off routes via F71 (Loop 2)” cross-link — F26 is the internal approval leg (people with Lumi accounts); an approval needing an external client/subcontractor sign-off is the external leg, handled by F71 inter-company confirmation links.

Reason:

  • Realignment to competitive-strategy-v2.md and the registry’s Positioning doctrine (v2): do not position vs US SaaS (Jira/Asana/Trello/monday/ClickUp/Notion); the competitor is Zalo/WhatsApp + Excel + owner memory. The F71 cross-link reflects doctrine point 5 (company→company acquisition = F71 / Loop 2).

Impact:

  • Positioning only. No product behavior, API, data model, test, or status changed. The internal approval flow (§4/§5/§7/§11) is untouched.

Tests:

  • None (documentation).

Notes:

  • F71 is draft/validation-gated (G4); the cross-link describes the intended internal/external split, not a shipped F26↔F71 integration.

2026-06-14 — Restructure to TEMPLATE.md section order; expand §11 audit (SP fleet)

Type: Documentation Change

Summary:

  • Numbered “Current Behavior” as §8; §8 Diagrams → §10; §9 Implementation Audit (PR-1..PR-4 narrative) → §11 (10-row table); “Open Questions” (unnumbered) → §12; §11 Edge Cases absorbed into §11 note; §12 Error States → §9; §13 Bug Log stays §13.
  • §11 audit: 10 rows, 8 ✅, 2 gaps (approve race, zero-admins stuck chain).

2026-06-14 — Notification gap closed: requester and next-approver push on approve/reject (C-r28)

Type: Bug Fix

Summary:

  • _notify_requester() and _notify_after_approve() helpers added to backend/apps/approvals/views.py.
  • approve action now calls _notify_after_approve after act_on_request(). reject action now calls _notify_requester.

Reason:

  • §2 AC “Given approval/rejection at any step, then I’m informed with the comment” was unimplemented — handlers wrote AuditLog but never called send_push_to_member.

Tests:

  • TestApprovalNotifications (3 tests): test_reject_notifies_requester, test_approve_final_step_notifies_requester, test_approve_intermediate_step_notifies_next_approver.

2026-06-13 — E10/ES8 gap fixed: self-approval guard with single-member opt-out (C-r27)

Type: Bug Fix

Summary:

  • _check_self_approval() added; both approve and reject actions check approver.id == request.requester_id. Multi-member org: 403 approvals.error_self_approval. Single-member org: 200, allowed.

Tests:

  • TestF26SelfApprovalGap in backend/tests/test_red_r24_f.py (5 tests).

2026-06-13 — Error states completion: E8/E9/E10 documented (D-r26)

Type: Documentation Change

Summary:

  • Added E8 (zero-step resolved chain — documented as unreachable in practice), E9 (parallel approvals — v1 boundary, not a bug), E10/ES8 (self-approval gap → fixed C-r27).

2026-06-13 — G-r26: Competitive positioning vs Zalo/email/paper and Jira/Asana/Trello/Monday/ClickUp/Notion, success metrics

Type: Documentation Change

Summary:

  • Added §1a Competitive Positioning and §1b Success Metrics.

2026-06-13 — E1 gap fixed: deactivated approver blocked by _is_authorized_approver (C-r25)

Type: Bug Fix

Summary:

  • _is_authorized_approver now returns False immediately when approver.status != "active".
  • TestF26ApprovalGaps::test_deactivated_approver_cannot_act — xfail removed; now green.

2026-06-12 — Added audit log write to create path

Type: Feature

Summary:

  • ApprovalRequestViewSet.perform_create now writes an AuditLog row with action approval.created.

Tests:

  • test_create_writes_audit_log — removed xfail marker; now green.

2026-06-12 — Fixed design token violations on ApprovalsPage (C-r22)

Type: Design fix

Summary:

  • Replaced all indigo-* Tailwind classes with Lumi Blue design tokens. H1, badges, buttons, card radius corrected per DESIGN.md.

2026-06-10 — PR-1..PR-4 shipped

Type: Feature

Summary:

  • PR-1: apps/approvals models + resolve_chain() + act_on_request() services + ADR-006. TestApprovalRouting (15 tests) 100% coverage.
  • PR-2: ApprovalPolicyViewSet + ApprovalRequestViewSet + audit writes + routes. TestApprovalAPI (14 tests).
  • PR-3: BriefItem.TYPE_CHOICES += ("approval", ...) + related_approval_request FK + generate_morning_brief interleaving. 4 tests in TestBriefApprovalInterleave.
  • PR-4: ApprovalsPage frontend at /approvals — two tabs, chain visual, inline actions, approvals locale namespace (en+vi).