At a Glance
Persona: Approver (Department Head, Budget Controller, Finance Officer / Manager) · Module: purchase-request · Scenarios: ~33
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: maps totests/303-pr-approver-journey.spec.ts,tests/311-pr-returned-flow.spec.ts, andtests/301-pr.spec.ts(hodTest / fcTest / gmTest fixtures) in../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Approver persona directly drives in the purchase-request module. The Approver covers the three intermediate decision-makers in the approval chain — Department Head (Stage 1), Budget Controller (Stage 2), and Finance Officer / Manager (Stage 3) — all sharing the same review-and-decide UI. Scenarios are grouped into the four happy-path actions described in 03-user-flow-approver.md Section 2 (approve, send-back, reject, split-reject), the RBAC boundaries enforced by PR_AUTH_002–PR_AUTH_006 in 02-business-rules.md Section 4, the validation rules that fire on an approval action (especially PR_VAL_013 on approved_qty and PR_VAL_016 on optimistic concurrency), and a small set of boundary / concurrency cases that come from the multi-stage state machine. Cross-persona handoffs that pivot off the Approver (X-PR-02, X-PR-03, X-PR-05, X-PR-06, X-PR-07) live in the parent overview, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| APP-HP-01 | Approve a single-line PR at Stage 1 (Department Head) | PR pr_status = in_progress, workflow_current_stage at the first approve stage; HOD hod@blueledgers.com logged in and present in tb_purchase_request.user_action.execute[]; single line in tb_purchase_request_detail |
1. From the My Approvals queue, open the PR. 2. Review header (requestor, department, pr_date, totals) and the line in the Items tab. 3. Read the Budget Impact panel; do not edit approved_qty. 4. Click Approve from the action bar. 5. Optional comment, confirm in the dialog. |
PR_POST_004 fires: workflow_previous_stage ← Stage 1, workflow_current_stage ← Stage 2, last_action = approved, last_action_by_* = HOD; stages_status for Stage 1 marked complete; pr_status stays in_progress; user_action.execute[] recomputed for Stage 2 (Budget Controller); soft budget commitment unchanged; Stage 2 approver notified. |
| APP-HP-02 | Approve a multi-line PR through all three approval stages | PR in_progress with two lines; three-stage approval chain configured (Department Head → Budget Controller → Finance); test fixtures hod@blueledgers.com, bc@blueledgers.com, fc@blueledgers.com |
1. HOD opens PR from My Approvals → Approve. 2. Budget Controller (Stage 2) receives notification, opens PR, reviews Budget Impact, Approves. 3. Finance (Stage 3) receives notification, opens PR, reviews financial impact, Approves. | After each intermediate approve, PR_POST_004 advances workflow_current_stage; on the final Stage 3 approve, PR_POST_005 flips pr_status from in_progress to approved; Requestor receives "Approved" notification; PR drops into the Purchaser queue; soft budget commitment remains until PO creation. |
| APP-HP-03 | Send-back from Stage 1 with mandatory reason | PR in_progress at Stage 1; HOD logged in; PR has at least one issue worth returning |
1. Open the PR. 2. Click Send Back from the action bar. 3. The dialog prompts for a mandatory reason — type "Please update justification and resubmit". 4. Confirm. |
PR_POST_003 fires: workflow_current_stage rolls back to the create stage; last_action = reviewed; PR effectively returns to draft for the Requestor; soft budget commitment is released; Requestor notified; an immutable type = system comment captures the send-back reason (PR_POST_008). |
| APP-HP-04 | Header-level reject terminates the chain | PR in_progress at any approval stage; current Approver authorised; reason text available |
1. Open the PR. 2. Click Reject from the action bar. 3. Mandatory reason dialog — type "Duplicate of PR-202605-0099". 4. Confirm. |
PR_AUTH_004 + PR_POST_006 apply: pr_status flips from in_progress to voided (terminal); soft budget commitment released; workflow_history appended; type = system comment written with the rejection reason; no subsequent stages run; Requestor receives "Rejected" notification. |
| APP-HP-05 | Split-reject — accept some lines, reject others | PR in_progress at Stage 1 with at least 3 lines (L1, L2, L3); HOD logged in |
1. Open the PR → Items tab → Edit. 2. Mark L1 as accept (default), L2 as reject with reason "Not in scope this quarter", L3 as accept. 3. Save line dispositions. 4. Click Approve at the header. 5. Confirm in the dialog. |
Per PR_AUTH_003: L2 is persisted with current_stage_status = rejected; L1 and L3 advance with the document; workflow_current_stage moves to Stage 2 (PR_POST_004); header roll-up totals recompute excluding L2; rejected line remains visible on the document for audit and never reaches PO conversion. |
| APP-HP-06 | Approve with quantity adjustment (approved_qty < requested_qty) |
PR in_progress at Stage 1; line L1 has requested_qty = 100, requested_unit_id = PCS |
1. Open the PR → Items tab → Edit. 2. On L1, change approved_qty from 100 to 60; approved_unit_id stays PCS, approved_unit_conversion_factor = 1. 3. Save. 4. Click Approve at header. |
PR_VAL_013 passes (approved_qty > 0 and ≤ requested_qty); line persisted with new approved_qty; header recomputes base_sub_total_amount and base_total_amount per PR_CALC_001..PR_CALC_006; soft budget commitment is rebalanced downward; PR_POST_004 advances to Stage 2 with the new totals; the new total feeds the next stage's threshold routing per PR_AUTH_005. |
| APP-HP-07 | Final-stage approval flips pr_status to approved |
PR in_progress at Stage 3 (the last approve stage in the configured chain); Finance Officer logged in |
1. Open the PR from the Finance queue. 2. Review header and lines. 3. Click Approve. 4. Optional comment, confirm. | PR_POST_005 fires: pr_status transitions in_progress → approved; workflow_history records final approve; PR enters the Purchaser queue; Requestor receives "Approved" notification; PR is now eligible for PO conversion (PR_AUTH_008); soft commitment persists until PO creation converts it to a hard commitment. |
| APP-HP-08 | Delegated approval — delegate acts on behalf of the absent Approver | HOD has set up delegation per PR_AUTH_006; delegate user hod-delegate@blueledgers.com is active in the delegation window; PR in_progress at Stage 1 |
1. Delegate logs in and opens My Approvals — the PR appears in the delegate's queue. 2. Open the PR → review → Approve. 3. Confirm. | PR_AUTH_006 allows the action; last_action_by_id reflects the delegate; audit comment captures the delegation source (original HOD ID); PR_POST_004 advances the workflow normally; from the PR's perspective the chain progresses identically to APP-HP-01. |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| APP-PERM-01 | Approver opens a PR in their own queue (current user is in user_action.execute[] for workflow_current_stage) |
Allow read and action buttons (Approve / Send Back / Reject). PR_AUTH_002 is satisfied for the current stage. |
| APP-PERM-02 | Approver opens a PR that is at a different stage (e.g. HOD opens a PR currently at the Budget Controller stage) | Deny action, read-only. Action buttons (Approve / Send Back / Reject / Split toolbar) are disabled with an inline explanation; PR_AUTH_002 recomputes user_action.execute[] on every stage transition and the HOD is no longer in that list. List visibility itself may still allow the row to appear under All Documents depending on grant. |
| APP-PERM-03 | Approver clicks Approve at their current stage | Allow. PR_AUTH_002 passes for the current stage; PR_AUTH_003 grants approve / send-back / reject as line-level rights; PR_POST_004 (or PR_POST_005 if final) commits the transition. |
| APP-PERM-04 | Approver clicks Approve on a PR that has already advanced past their stage (race after another concurrent approver acted) | Deny. Server-side PR_AUTH_002 rejects because user_action.execute[] was recomputed for the new stage and the calling user is no longer in it; UI shows a "PR has moved to a later stage — please refresh" message and disables the action bar on refresh. |
| APP-PERM-05 | Department Head approves a PR from a different department they are not responsible for | Deny. Default-chain Stage 1 routing scopes user_action.execute[] to the requestor's department (or the HOD configured for that department in tb_workflow). A different-department HOD is not on the list; PR_AUTH_002 blocks the action. |
| APP-PERM-06 | Approver attempts to approve a PR they personally submitted (segregation of duties) | Deny. The create stage role and downstream approve stage roles are disjoint by workflow definition. Even if the same user appears in both stages by misconfig, user_action.execute[] is recomputed per stage and the create-stage owner is filtered out before the action runs (see 02-business-rules.md Section 4, default chain). |
| APP-PERM-07 | Delegate user (configured per PR_AUTH_006) acts on a delegated PR within the delegation window |
Allow. Delegation is materialised into user_action.execute[] for the delegation window; the delegate inherits approve / send-back / reject / split-reject rights for that window only. last_action_by_id records the delegate; audit comment records the delegation source. |
| APP-PERM-08 | Approver opens any PR URL with noAuth fixture / expired session |
Deny — redirect to login. Without an auth context, user_action.execute[] cannot be evaluated against the current user; PR_AUTH_002 cannot pass. Covered by the noAuthTest fixture in 301-pr.spec.ts. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| APP-VAL-01 | Approver acts on a PR they are not authorised for | Approver who is not in user_action.execute[] for workflow_current_stage calls the Approve endpoint directly (bypassing the disabled UI button) |
PR_AUTH_002 — reject with "You are not authorised to act on this PR at the current stage". The list is recomputed on every stage transition; stale clients hit this path. |
| APP-VAL-02 | Approver clicks Send Back without providing a reason | Open the send-back dialog, leave the reason textarea empty, click Confirm | Reject — reason is mandatory per the dialog contract; the Confirm button is also disabled in the UI when the reason is blank, but a direct API call is rejected at the server with "A reason is required to send back this PR". The type = system comment that PR_POST_003 writes must carry the reason text; without it the write fails. |
| APP-VAL-03 | Approver clicks Reject without providing a reason | Open the reject dialog, leave reason empty, click Confirm | Reject — PR_AUTH_004 + PR_POST_006 require an immutable system comment with the rejection reason (PR_POST_008); the server rejects an empty-reason call with "A reason is required to reject this PR". |
| APP-VAL-04 | Approver sets approved_qty = 0 and clicks Approve |
On a line, type 0 into the approved-qty input |
PR_VAL_013 — reject with "Approved quantity must be positive and may not exceed requested quantity". The rule is strict > 0, not ≥ 0. (See APP-EDGE-01 — the UI also treats approved_qty = 0 as a per-line reject when surfaced through the bulk split toolbar; the validation path below the toolbar still rejects a raw save of 0.) |
| APP-VAL-05 | Approver sets approved_qty > requested_qty |
On a line with requested_qty = 100, type 120 into the approved-qty input |
PR_VAL_013 — reject with "Approved quantity must be positive and may not exceed requested quantity". Comparison is performed after UoM conversion using approved_unit_conversion_factor. |
| APP-VAL-06 | Approver edits approved_qty but omits approved_unit_id / approved_unit_conversion_factor |
Force-save with a non-null approved_qty and a null approved_unit_id (typically only reachable via direct API call) |
PR_VAL_013 — reject with "Approved quantity must be positive and may not exceed requested quantity" (the rule also requires approved_unit_id and approved_unit_conversion_factor to be supplied with approved_qty). |
| APP-VAL-07 | Approver attempts to approve a PR whose required workflow_id is no longer active (scope changed or row soft-deleted mid-flight) |
Workflow row in tb_workflow was deactivated or its scope flipped from purchase-request after this PR was submitted; Approver clicks Approve |
Reject — PR_VAL_004 is re-evaluated on the action; "A valid PR workflow must be selected". The PR is effectively stranded until an admin re-activates a valid workflow or voids the PR (PR_AUTH_007). |
| APP-VAL-08 | Approver edits approved_qty based on a stale doc_version (another concurrent edit happened) |
Two Approvers (or an Approver and a delegate) load the same PR; A saves an approved_qty edit; B (still on the old doc_version) clicks Approve |
PR_VAL_016 — reject with "Document was modified by another user; reload and retry". Successful writes bump doc_version by 1. |
| APP-VAL-09 | Split-reject without a per-line reason on a rejected line | Mark L2 as reject but leave the per-line reason empty; click Approve at header |
Reject — line-level rejection requires a reason per PR_AUTH_003 and the per-line comment write contract (PR_POST_008 requires the comment payload). UI also disables the Approve button until every rejected line has a non-empty reason. |
| APP-VAL-10 | Approver attempts to act after the PR was voided by Finance / sys-admin | PR pr_status was just flipped to voided (PR_AUTH_007); Approver loaded the page before the void and clicks Approve |
Reject — state-machine guard: voided is terminal and freezes the document. The Approve / Send Back / Reject endpoints reject with "This PR is voided and cannot be acted on". |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| APP-EDGE-01 | approved_qty = 0 treated as a per-line rejection |
Approver uses the bulk Split toolbar to set approved_qty = 0 on L2 while leaving L1 at its requested qty |
Per the toolbar contract, approved_qty = 0 is materialised as a per-line reject with a system-generated reason; the line is saved with current_stage_status = rejected (not rejected by PR_VAL_013 because the toolbar path does not write 0 to the column — it writes null + the reject flag). Direct edit of approved_qty = 0 outside the split toolbar is still rejected by PR_VAL_013. |
| APP-EDGE-02 | approved_qty > requested_qty boundary |
requested_qty = 12.00000, Approver enters approved_qty = 12.00001 |
Rejected per PR_VAL_013 — the rule is strict ≤, evaluated at full Decimal(15, 5) precision; equality (12.00000 == 12.00000) is valid, anything above the 5th decimal is not. |
| APP-EDGE-03 | Simultaneous approval by two delegates of the same stage | HOD has delegated to two users; both open the PR within seconds and both click Approve | First Approve wins and bumps doc_version + advances workflow_current_stage; second Approve is rejected — either by PR_VAL_016 if the second client still holds the old doc_version, or by PR_AUTH_002 if the client has refreshed (the PR has already advanced past Stage 1 and the second delegate is no longer in user_action.execute[]). Last-write-wins is not the policy. |
| APP-EDGE-04 | Delegation window expires mid-decision | Delegate opens the PR at T-1m; delegation expires at T; delegate clicks Approve at T+30s |
Reject at the server. PR_AUTH_002 re-evaluates user_action.execute[] against the current clock; the expired delegate is no longer in the list. UI surfaces "Your delegation has expired — please refresh". The original Approver (or a new delegate) must act. |
| APP-EDGE-05 | Threshold boundary — header total lands exactly on the escalation threshold | tb_workflow configures Stage 4 routing at base_total_amount > 100,000.00000 ฿; Approver adjusts approved_qty such that the new base_total_amount = 100,000.00000 ฿ (exact) |
Threshold comparison is strict > per PR_AUTH_005, so the exact boundary value does not trigger Stage 4 escalation; the PR routes to the next normal stage. A value of 100,000.00001 ฿ would trigger escalation. (Specific threshold numbers are configurable per organisation — the rule is the comparison operator, not the number.) |
| APP-EDGE-06 | Send-back received by Requestor while a second Approver was loading the PR | Approver A clicks Send Back at T; Approver B (parallel reviewer / observer) was already on the detail page and clicks Approve at T+2s |
Reject for B. PR_AUTH_002 re-evaluates on action and finds workflow_current_stage has rolled back to the create stage; B is not in that stage's user_action.execute[]. UI prompts B to refresh. |
| APP-EDGE-07 | Maximum decimal precision on approved_qty adjustment |
Line has requested_qty = 1.99999; Approver enters approved_qty = 1.99998 |
Accepted — PR_VAL_013 passes (> 0 and ≤ requested_qty); the value persists exactly at 5 decimals on Decimal(15, 5); subsequent header roll-up rounds per the policy in 02-business-rules.md Section 3. |
X-PR-02 (send-back loop), X-PR-03 (split-reject), X-PR-05 (threshold escalation), X-PR-06 (reject path), X-PR-07 (Purchaser bounce-back), X-PR-10 (returned-PR round trip).PR_AUTH_001–PR_AUTH_008 — especially PR_AUTH_002 stage-membership, PR_AUTH_003 line-level actions, PR_AUTH_004 header reject, PR_AUTH_005 threshold routing, PR_AUTH_006 delegation, PR_AUTH_007 void scope), Section 2 (PR_VAL_013 on approved_qty, PR_VAL_016 on optimistic concurrency), and Section 5 (PR_POST_003–PR_POST_006, PR_POST_008 immutable system comments).../carmen-inventory-frontend-e2e/tests/303-pr-approver-journey.spec.ts — persona-journey spec (HOD primary, Finance Controller for scope contrast). TC-PR-060101..TC-PR-060104 (My Approvals dashboard), TC-PR-060201..TC-PR-060205 (PR list approver view), TC-PR-060301..TC-PR-060304 (PR detail read-only), TC-PR-060401..TC-PR-060412 (edit mode + bulk actions: approved-qty editable, vendor / unit-price read-only, bulk Approve / Reject / Send-for-Review / Split, cancel-edit discards), TC-PR-060501 (FC multi-department scope), TC-PR-060901 (golden full flow). Returned-PR send-back loop is in ../carmen-inventory-frontend-e2e/tests/311-pr-returned-flow.spec.ts. Per-action × per-role permission coverage lives in ../carmen-inventory-frontend-e2e/tests/301-pr.spec.ts (hodTest, fcTest, gmTest fixtures).../carmen/docs/purchase-request-management/testing.md (testing levels — approve-pr.spec.ts, reject-pr.spec.ts patterns), ../carmen/docs/purchase-request-management/troubleshooting.md (Section 2.1 — approval-process problems: stuck stage, missing buttons, missing history; Section 2.2 — workflow transition issues: cannot move to next stage, automatic return, wrong stage).