At a Glance
Persona: Approver (Department Head + higher-tier approvers) · Module: store-requisition · Scenarios: ~27
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: maps totests/701-sr.spec.ts(TC-SR-060001..060005, 070001..070003, 080001..080002, 090001+, 100001+, 110001+) in../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Approver persona (the Department Head at the first approval stage, and any higher-tier approver — Operations Manager / Cost Controller — at later stages) directly drives in the store-requisition module. The Approver's involvement begins when an SR transitions to in_progress and the workflow lands at an approval stage where the Approver is in user_action.execute; it ends when all lines have been actioned (advancing the workflow to fulfilment) or when all lines are rejected (auto-cancellation per SR_POST_004 tail). Scenarios are grouped into happy paths (per-line approve in full, trim down with approved_message, reject with reject_message, send-back with review_message, mixed outcomes on a single SR, multi-tier escalation, delegation), RBAC (segregation-of-duties Requester ≠ Approver per SR_AUTH_011, stage-gated authorization per SR_AUTH_014, delegated authority), validation (negative tests against SR_VAL_010 — approved_qty > requested_qty, empty reject_message), and edge cases around concurrent approvers on different lines, time-out / SLA escalation, second-stage approver value threshold, and the all-lines-rejected auto-cancel boundary. Cross-persona handoffs that pivot off the Approver (Scenarios 1, 3, 4, 5 in the parent overview) live in 04-test-scenarios.md, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| APR-HP-01 | Approve all lines in full | SR SR-A at doc_status = in_progress, workflow_current_stage = 'approval-stage-1'; three lines, each with requested_qty > 0 and approved_qty = 0; Approver bann@blueledgers.com is in user_action.execute; Requester ≠ Approver. |
1. Open the approvals dashboard. 2. Click SR SR-A. 3. For each line, set approved_qty = requested_qty. 4. Click Submit Approval Decision. |
All three lines get approved_by_id = bann, approved_by_name = "Bann", approved_date_at = now(); per-line history appended with { status: 'approved' }; workflow advances workflow_current_stage to the fulfilment stage; user_action.execute populated with fulfillers at from_location_id; SR stays in_progress (status does not change at approval — only the workflow stage does). Maps to TC-SR-070001. |
| APR-HP-02 | Trim one line, approve others in full | SR SR-B: line 1 (requested_qty = 25), line 2 (requested_qty = 15), line 3 (requested_qty = 10); source on-hand on line 2 is only 12. |
1. Open SR-B. 2. Line 1: approved_qty = 25 (full). 3. Line 2: approved_qty = 12, approved_message = "trimmed to source on-hand". 4. Line 3: approved_qty = 10 (full). 5. Submit decision. |
Per-line: line 1 fully approved at 25, line 2 trimmed to 12 with approved_message, line 3 fully approved at 10; all three lines get the approve signature; workflow advances to fulfilment; the Fulfiller sees the trim with the reason; the variance requested_qty − approved_qty = 3 on line 2 is the approver's trim, not yet a fulfilment gap (that happens at issue). Maps to TC-SR-090001 (Adjust Approved Quantity). |
| APR-HP-03 | Reject a line with a reason | SR SR-C: line 1 (requested_qty = 50 for "Champagne-VintageX", outlet asks for 50 bottles for a non-existent event). |
1. Open SR-C. 2. Line 1: click Reject. 3. Enter reject_message = "no banquet event scheduled this week; quantity unjustified". 4. Confirm. 5. Submit decision. |
Line 1: approved_qty = 0, reject_by_id = bann, reject_by_name = "Bann", reject_date_at = now(), reject_message populated; per-line history appended with { status: 'rejected' }. If this was the only line, the SR moves to cancelled automatically (SR_POST_004 tail); if there are other approved lines, the SR continues to fulfilment with line 1 dropped. Maps to TC-SR-110001 (Reject Requisition). |
| APR-HP-04 | Send a line back for correction | SR SR-D: line 1 (requested_qty = 25 for rice, par level is 18, unclear rationale). |
1. Open SR-D. 2. Line 1: click Send Back for Review. 3. Enter review_message = "please justify the 25-unit rice request — par level is 18". 4. Confirm. 5. Submit decision. |
Line 1: review_by_id = bann, review_by_name = "Bann", review_date_at = now(), review_message populated; per-line history appended with { status: 'review', to_stage: 'requester' }; approved_qty remains 0 (the line was not approved); workflow routes the SR back to the requester stage; the requester sees the line in their queue with the review note. Maps to TC-SR-100001 (Request Review). |
| APR-HP-05 | Mixed outcomes on a single SR — approve, trim, reject, send-back | SR SR-E with 4 lines: line 1 (requested_qty = 10, fine), line 2 (requested_qty = 20, over source on-hand of 15), line 3 (requested_qty = 5, unjustified), line 4 (requested_qty = 25, missing rationale). |
1. Open SR-E. 2. Line 1: approve at 10. 3. Line 2: trim to 15 with approved_message. 4. Line 3: reject with reject_message. 5. Line 4: send back with review_message. 6. Submit decision. |
The system permits mixed outcomes per line (SR_AUTH_006). Workflow advances back to requester stage because of the send-back on line 4; lines 1, 2 stay approved (with their signatures), line 3 stays rejected (preserved through the round-trip), line 4 awaits requester response. When the requester amends and resubmits, the approver re-actions line 4 only; the others do not re-enter the approval stage. Maps to TC-SR-080001 (Approve Item-level — happy path with mixed lines). |
| APR-HP-06 | Multi-tier escalation to second-stage approver | SR SR-F total value (Σ requested_qty × catalog_price) = ฿15,000; tenant has a second tier above ฿10,000 (Operations Manager ops-mgr@blueledgers.com); first-tier Approver bann@blueledgers.com approves all lines. |
1. First-tier Approver opens SR-F. 2. Approves all lines. 3. Submits decision. |
Workflow advances to the second approval stage (not fulfilment); user_action.execute is now Operations Manager; first-tier signatures preserved on each line; second-tier Approver sees the first-tier signature as context and may further trim, reject, or approve. Per-line stages_status JSON tracks which stage each line is at. |
| APR-HP-07 | Approver delegation — deputy acts | Department Head bann@blueledgers.com is on leave; workflow has been configured to delegate to deputy deputy-bann@blueledgers.com; SR SR-G lands in the deputy's queue. |
1. Deputy opens SR-G. 2. Approves all lines. 3. Submits decision. |
Per-line approved_by_id = deputy, approved_by_name = "Deputy Bann"; a system comment is appended on the SR noting the delegation chain ("Delegated from bann@blueledgers.com — on leave 2026-05-10..2026-05-20"); workflow advances normally; Auditor can trace the delegation in the audit log. |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| APR-PERM-01 | Approver in user_action.execute at current stage acts on SR |
Allow. Approve / trim / reject / send-back actions are all enabled on lines while doc_status = in_progress and workflow_current_stage matches. Per SR_AUTH_005. |
| APR-PERM-02 | User NOT in user_action.execute attempts to approve |
Deny — workflow-stage gating. Per SR_AUTH_014. User sees the SR in the activity feed but the Approve button is hidden / disabled; direct API call rejects with "You are not assigned to act on this requisition at the current stage." No approved_qty write. Maps to TC-SR-070002 (Unauthorized User Attempts to Approve). |
| APR-PERM-03 | Requester attempts to approve own SR | Deny — SoD Requester ≠ Approver. Per SR_AUTH_011. The same user holds both Requester and Approver rights but is blocked on their own SR with "You raised this requisition; another user must approve it." No write. Workflow remains stuck until a different approver acts (or until the Inventory Controller administratively voids). |
| APR-PERM-04 | Approver attempts to set approved_qty > requested_qty |
Deny — value cap. Per SR_VAL_010. The screen blocks the entry client-side; the server rejects via "Approved quantity cannot exceed requested quantity; to grant more, the requester must amend and resubmit." Approver must either trim to requested_qty or send the line back asking the requester to amend. |
| APR-PERM-05 | Approver rejects line without reject_message |
Deny — mandatory reason. Per SR_VAL_010 second clause. The Reject dialog requires non-empty reject_message; server rejects empty submissions with "Rejection reason is required." |
| APR-PERM-06 | Approver attempts to edit requested_qty on a line |
Deny — Approver scope. Approver can set approved_qty (and the signature columns) but not requested_qty (which is requester scope and locked at submit). The line's requested_qty field is read-only on the approval screen. To change the requested quantity, the Approver sends the line back for correction (APR-HP-04). |
| APR-PERM-07 | Approver attempts to act on completed SR |
Deny — terminal state. Per the global rule, completed is locked across all personas. The Approver may inspect the historical signature for audit but cannot re-approve or revoke. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| APR-VAL-01 | approved_qty > requested_qty per line |
Approver attempts approved_qty = 30 on a line with requested_qty = 25. |
Reject — SR_VAL_010 first clause. Server returns "Approved quantity cannot exceed requested quantity; to grant more, the requester must amend and resubmit." |
| APR-VAL-02 | approved_qty < 0 |
Approver attempts a negative approved quantity (data-entry error or API mis-use). | Reject — SR_VAL_008 (the quantity invariant rejects negatives at every stage). Server returns "Quantities must satisfy 0 ≤ issued_qty ≤ approved_qty ≤ requested_qty." |
| APR-VAL-03 | Reject line with empty reject_message |
Approver clicks Reject and submits without entering a reason. | Reject — SR_VAL_010 second clause. Server returns "Rejection reason is required." |
| APR-VAL-04 | Send-back with empty review_message |
Approver clicks Send Back and submits without entering a reason. | Reject — same family as VAL-03. Server returns "Review message is required when sending an SR back for correction." |
| APR-VAL-05 | Approver acts on line outside their stage | Approver is in user_action.execute for stage 1, but tries to act on a line whose current_stage_status is at stage 2 (already moved past stage 1 — unusual sequencing). |
Reject — SR_AUTH_014. Server returns "You are not assigned to act on this requisition at the current stage." |
| APR-VAL-06 | Budget exceeded warning at approval | Outlet's cost-centre is within ฿1,000 of monthly budget; the SR's Σ approved_qty × cost_per_unit would push it over. |
Warn (not block) — the screen surfaces a budget-exceeded warning if Finance has wired the budget module to the approver view; Approver may either proceed (record acknowledgement) or trim. Server permits the approval but logs a system comment for Finance follow-up. Maps to TC-SR-070003 (Budget Exceeded During Approval). |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| APR-EDGE-01 | Concurrent approvers on different lines | Two approvers (both in user_action.execute for the same stage) open the same SR and each click approve on different lines simultaneously. |
Per-line locking. The system locks at the line level via optimistic concurrency (doc_version on the detail). Each line accepts the first approver who acts; the second approver acting on the same line gets a "line already actioned" message and is asked to refresh. No double-write, no lost signature. Workflow advances when all lines have been actioned by some approver. |
| APR-EDGE-02 | All lines rejected → automatic SR cancel | Approver rejects every active line on the SR (each with its own reject_message); submits decision. |
Automatic SR cancel. After the per-line writes, Σ approved_qty = 0 across active lines triggers the SR_POST_004 tail; doc_status = in_progress → cancelled automatically; the SR is terminal; requester is notified per line. The Approver does not have to manually cancel — the system detects the all-rejected state. |
| APR-EDGE-03 | Time-out / SLA escalation | SR sits in the Approver's queue for > tenant SLA window (e.g. 48h) without action. | Workflow escalation. Tenant config decides: (a) workflow auto-advances to a higher-tier approver with the original Approver notified; (b) workflow notifies the Inventory Controller; (c) workflow does nothing, leaving the SR stale (manual intervention required). The Approver's authority is not lost — but the SR is flagged in the variance dashboard for stale-approval review. |
| APR-EDGE-04 | Approver trims to exactly source on-hand at approval time | Source on-hand visible to Approver = 15; Approver trims approved_qty = 15 (matches on-hand). |
Approval accepted. By commit time, source on-hand may have moved (other SRs / consumption). SR_VAL_013 runs at commit against live on-hand — if on-hand has fallen below 15, the Fulfiller short-fulfils; if it has risen, no impact (the Fulfiller is capped at approved_qty = 15 anyway). The Approver does NOT see the at-issue stock-out risk at approval time — that is the Fulfiller's responsibility. |
| APR-EDGE-05 | Multi-tier with second-stage further trim | First-tier Approver approves line at 20; second-tier Approver further trims to 15 with their own approved_message. |
Second signature overlays first. The per-line signature columns (approved_by_id, approved_by_name, approved_date_at, approved_message) are last-writer-wins, but the per-line history JSON timeline captures both signatures in sequence ({seq:1, name:'stage1', by: bann, qty: 20}, {seq:2, name:'stage2', by: ops-mgr, qty: 15}). The Fulfiller acts against the final approved_qty = 15. Auditor reads the full chain from history. |
| APR-EDGE-06 | Approver acts then immediately edits within the same session | Approver approves line 1 at 25, then realises they meant to trim; tries to re-set approved_qty = 20. |
Within-session re-decision permitted before workflow advance. As long as the workflow stage has not yet advanced (typically because other lines on the stage are not yet actioned), the Approver may update their own signature on a line; the approved_date_at updates, the latest approved_qty is the canonical value, the history JSON records the change. Once the workflow advances (all lines actioned, decision submitted), the line is locked at this stage. |
| APR-EDGE-07 | Bulk approve from approvals list | Approver selects multiple SRs in the list and bulk-approves (all lines on each SR at requested_qty). |
Bulk approve fans out to per-SR per-line writes. Each SR is processed in turn; per-line rules (SR_VAL_010) apply to each; failures on one SR (e.g. approved_qty > requested_qty on a line marked for manual) do not stop the others; the Approver sees a per-SR result summary. Maps to TC-SR-060003 (Bulk Action — Export Selected Requisitions; bulk approve is a sibling pattern). |
SR_VAL_008 (quantity invariant), SR_VAL_010 (approval cap + reject-message presence); Section 4 — authorization rules SR_AUTH_005 (Approver authority), SR_AUTH_006 (split & reject), SR_AUTH_011 (Requester ≠ Approver SoD), SR_AUTH_014 (workflow-stage gating); Section 5 — posting rules SR_POST_003 (approve), SR_POST_004 (reject / send-back), SR_POST_009 (auto-cancel).../carmen-inventory-frontend-e2e/tests/701-sr.spec.ts — canonical Playwright spec for the SR module. Approver-relevant test groups: TC-SR-060001..060005 (Approver list actions — APR-HP-01 setup, APR-EDGE-07 bulk approve, delegation), TC-SR-070001..070003 (Approve — APR-HP-01, APR-PERM-02, APR-VAL-06), TC-SR-080001..080002 (Approve Item-level — APR-HP-05, mixed outcomes; APR-VAL-05 insufficient-stock scenario), TC-SR-090001+ (Adjust approved quantity — APR-HP-02), TC-SR-100001+ (Request Review — APR-HP-04), TC-SR-110001+ (Reject — APR-HP-03, APR-EDGE-02). Permission-denial coverage uses the requestor@blueledgers.com fixture.info.recipe_id; the Approver sees the recipe context as part of the per-line decision.