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 approach | Lumi’s edge |
|---|---|
| Zalo/Telegram DM | Structured routing with a visible chain — requester sees “sẽ qua: Quản lý kho → CEO” without pinging anyone. Audit log replaces scroll-back. |
| Email threads | One-tap approve/reject from the queue. No inbox triage, no thread-reading to find the current step. |
| Paper sign-off | Zero 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_atfor 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
ApprovalActionrecords whereaction="reject"andcommentis non-empty. Lumi’s approve/reject form makes the comment field required on reject (C-r26 frontend:reject_comment_requiredlocale key +canConfirmguard).
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.
- Approve/reject with optional comment; reject ends the chain (status
- 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_amountnullable,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_stepint, status:pending → approved | rejected | cancelled, optionaldealFK /taskFK.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)
| Method | Path | Min role | Notes |
|---|---|---|---|
| CRUD | /api/approval-policies/ | admin | Policy management |
| POST | /api/approvals/ | member | Create; response includes resolved chain |
| GET | /api/approvals/ | member | Mine + (approver) my queue via ?queue=me |
| POST | /api/approvals/<id>/approve/ …/reject/ | current-step approver only | One-tap actions; 403 for anyone else incl. admins not in chain |
| POST | /api/approvals/<id>/cancel/ | requester | Only 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
approvalsnamespace, en+vi together.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Policy match incl. threshold precedence; default route fallback | backend | new classes in backend/tests/test_api.py |
| 2 | Chain walk: approve→advance, reject→halt, cancel; frozen chain despite policy edit | backend | same |
| 3 | Authorization: only current-step approver can act; cross-tenant blocked | backend | mandatory |
| 4 | Brief includes pending approvals ranked with decisions | backend | extend TestBriefGeneration |
| 5 | Request → approve flow; queue rendering | e2e | new 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
| # | Error | User message | HTTP | Logged | Notes |
|---|---|---|---|---|---|
| ES1 | Cross-tenant request access — a member of org A tries to access org B’s approval request by UUID. | Standard 404 (don’t reveal existence) | 404 | No | ApprovalRequestViewSet queryset filters by request.tenant. |
| ES2 | Non-current approver action — an admin who is not the current-step approver tries to approve or reject. | approvals.error_not_current_approver | 403 | No | Checked 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. |
| ES3 | Non-requester cancel — an admin tries to cancel someone else’s request. | approvals.error_not_requester | 403 | No | Checked in the cancel view action. |
| ES4 | Cancel already-resolved request — user tries to cancel an approved/rejected request. | approvals.error_not_pending | 400 | No | Checked in act_on_request() — raises ValueError caught by view as 400. |
| ES5 | Approve already-approved request (race) — two admins both act simultaneously on the same step. | approvals.error_already_resolved | 409 | Yes (AuditLog for the first) | act_on_request() locks and re-reads the row; supported-database concurrent proof remains open. |
| ES6 | Policy with 0 steps — an admin creates a policy with steps_json=[]. | Model and serializer validation reject empty steps (minimum 1 step required). | 400 | No | Covered by TestApprovalAPI.test_policy_create_rejects_empty_steps. |
| ES7 | Unknown 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) | No | Default route guarantees fallback. The policy stays null in the request. |
| ES8 | Self-approval — the requester acts on their own request when _is_authorized_approver() returns True for them. | approvals.error_self_approval | 403 | No | _check_self_approval() rejects with 403 unless the org has ≤1 active member. Fixed in C-r27. |
| ES9 | Business 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/a | Yes | Endpoint-level failure metadata is kept in the shared Business summary; F26 owns approval-row truth. |
| ES10 | Populated 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/a | Yes | Repeated 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)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Policy CRUD (admin-only); resolve_chain() picks correct policy by category + amount threshold | ✅ match | TestApprovalRouting (15 tests); TestApprovalAPI (23 tests) |
| 2 | Chain walk: approve→advance, reject→halt (terminal), cancel while pending; frozen resolved_steps_json | ✅ match | TestApprovalAPI chain-walk tests; 433 tests 100% coverage |
| 3 | Authorization: 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 |
| 4 | Default route fallback: team manager → any admin; request never unroutable | ✅ match | _default_steps() always returns ≥1 step; TestApprovalRouting fallback tests |
| 5 | Brief includes pending approvals interleaved with decisions; stale >3 days → critical | ✅ match | TestBriefApprovalInterleave (6 tests); BriefItem.TYPE_CHOICES += ("approval", ...); migration 0004 |
| 6 | AuditLog row written on every action including create | ✅ match | 2026-06-12 perform_create fix; test_create_writes_audit_log |
| 7 | Self-approval guard (E10/ES8): 403 unless single-member org | ✅ match | _check_self_approval() (C-r27); TestF26SelfApprovalGap (5 tests) |
| 8 | Deactivated approver cannot act (E1): runtime protection via IsOrgMember layer | ✅ match | IsOrgMember.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 |
| 9 | Approve race: row lock and stale-action conflict handling are implemented; concurrent proof remains open | ⚠️ gap | ES5; supported-database concurrency evidence |
| 10 | any_admin step in org with zero admins: chain permanently stuck | ⚠️ gap | E3; 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=menow contains only requests the member can actually approve or reject. A requester matching anany_adminstep in a multi-member organization remains visible under My Requests but is not presented as waiting for their action.- The serializer’s
can_actfield 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 asapprovals.error_self_approvalresolve 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ốilabel 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 underselect_for_update()before creating an action, and the HTTP approve/reject actions return409when a stale concurrent attempt loses the lock.- Added
F26.AC003.STALE_RECHECKfor 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/approvalsweb 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:
TestApprovalAPIcount corrected 14 → 23 (verifiedgrep -c "def test_"inbackend/tests/test_approvals.pylines 276–590). - §11 row 3: evidence corrected — the 403 gate is
_is_current_approver()inbackend/apps/approvals/views.py(~L176–186), called fromapprove/rejectactions (~L95–96, L119–120).act_on_request()in services.py has only a status check; it has no authz guard — direct callers ofact_on_request()must enforce authorization themselves. - §11 row 5:
TestBriefApprovalInterleavecount corrected 4 → 6 (verifiedgrep -c "def test_"lines 591–711). - §11 row 8: evidence corrected — runtime protection for deactivated approvers is
IsOrgMember.has_permission()inbackend/apps/organizations/permissions.py(~L20–21), which requiresstatus="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, notact_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. ApprovalsPagenow 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_loadcopy instead of silently collapsing into an empty screen. - Screenshot coverage:
frontend/tests/e2e/screenshot-flow.spec.tsnow captures25-crm-approvalsin both queue and requester tabs. - Verification:
frontend/src/test/ApprovalsPage.test.tsxgreen (9),pnpm exec tsc --noEmitclean,node scripts/check-design-tokens.mjsclean.
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 tobackend/apps/approvals/views.py.approveaction now calls_notify_after_approveafteract_on_request().rejectaction now calls_notify_requester.
Reason:
- §2 AC “Given approval/rejection at any step, then I’m informed with the comment” was unimplemented — handlers wrote
AuditLogbut never calledsend_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; bothapproveandrejectactions checkapprover.id == request.requester_id. Multi-member org: 403approvals.error_self_approval. Single-member org: 200, allowed.
Tests:
TestF26SelfApprovalGapinbackend/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_approvernow returnsFalseimmediately whenapprover.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_createnow writes anAuditLogrow with actionapproval.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/approvalsmodels +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_requestFK +generate_morning_briefinterleaving. 4 tests inTestBriefApprovalInterleave. - PR-4:
ApprovalsPagefrontend at/approvals— two tabs, chain visual, inline actions,approvalslocale namespace (en+vi).