At a Glance
Persona: Fulfiller (Store Keeper / Warehouse Supervisor at source) · Module: store-requisition · Scenarios: ~27
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: maps totests/701-sr.spec.ts(TC-SR-120001..120004+ Issuance) in../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Fulfiller persona (the Store Keeper / Warehouse Supervisor at the source location) directly drives in the store-requisition module. The Fulfiller's involvement begins when an SR transitions to the fulfilment stage of in_progress (after all approvals complete and at least one line has approved_qty > 0) and ends at commit — either through in_progress → completed (the single posting event) or through a pre-commit interruption (system / SoD failure) that returns the SR to the fulfiller queue. Scenarios are grouped into happy paths (full fulfilment matching approved_qty, single-lot pick, multi-lot pick on a single line, partial fulfilment at-issue stock-out, sr_type = transfer paired with destination on-hand increment, sr_type = issue with destination cost-centre debit), RBAC (Approver ≠ Fulfiller SoD per SR_AUTH_012, stage-gated authorization, location-scoped fulfiller permission), validation (negative tests against SR_VAL_008 — issued_qty > approved_qty, SR_VAL_011–SR_VAL_014 — commit-time gates), and edge cases around closed-period commit, optimistic concurrency at commit, lot-data completeness for perishable items, and multi-line mixed outcomes (some lines fully issued, others short, others zero). Cross-persona handoffs that pivot off the Fulfiller (Scenarios 1, 2, 6, 8, 9, 10 in the parent overview) live in 04-test-scenarios.md, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| FUL-HP-01 | Full fulfilment — sr_type = issue kitchen pull |
SR SR-A at doc_status = in_progress, fulfilment stage; line 1: approved_qty = 10, source on-hand at WH-Central = 100; Fulfiller purchase@blueledgers.com in user_action.execute; Fulfiller ≠ Approver. |
1. Open the fulfilment dashboard. 2. Click SR-A. 3. Verify destination Main-Kitchen (tb_location.location_type = 'direct') and sr_type = issue. 4. Pick 10 units physically. 5. Enter issued_qty = 10.000. 6. (Non-lot-controlled item — no lot sub-form.) 7. Click Commit. |
All commit-time rules pass (SR_VAL_011 ≥1 approved line, SR_VAL_013 live source on-hand check, SR_VAL_014 open period); doc_status = in_progress → completed; tb_inventory_transaction row inserted (inventory_doc_type = store_requisition) with one tb_inventory_transaction_detail child carrying cost_per_unit from source cost-layer; tb_store_requisition_detail.inventory_transaction_id stamped; source tb_inventory_status[WH-Central, Rice-1kg].quantity_on_hand decremented 100 → 90; no destination on-hand increment (issue lands as expense, not stock); journal entry: Dr Main-Kitchen cost-centre expense, Cr WH-Central inventory account, balanced at 10 × cost_per_unit. Maps to TC-SR-120001. |
| FUL-HP-02 | Full fulfilment — sr_type = transfer warehouse-to-warehouse |
SR SR-B: source WH-Central, destination WH-Pool (both tb_location.location_type = 'inventory'); sr_type = transfer; line 1: approved_qty = 50; source on-hand = 200; destination on-hand = 30. |
1. Open SR-B. 2. Pick 50 units. 3. Enter issued_qty = 50.000. 4. Commit. |
SR completed; tb_inventory_transaction writes a paired OUT row at source + IN row at destination (or two related rows depending on [inventory](/en/inventory/inventory) implementation); source on-hand 200 → 150; destination on-hand 30 → 80 (incremented by the same quantity); cost-layer at source consumed per FIFO / moving-average; cost-layer at destination appended (new FIFO layer or absorbed into moving-average per destination's costing method); journal entry: Dr WH-Pool inventory, Cr WH-Central inventory, balanced. Maps to TC-SR-120002 (Transfer Issuance). |
| FUL-HP-03 | Multi-lot pick on a single line (lot-controlled perishable) | SR SR-C: lot-controlled product Milk-1L, flagged perishable; line 1: approved_qty = 20; source has two active lots — Lot-A (exp 2026-05-20, qty 12), Lot-B (exp 2026-05-25, qty 30); rotation policy FIFO-by-expiry. |
1. Open SR-C. 2. Open the lot sub-form on the line. 3. Select Lot-A: pick 12 units (consumes the whole lot, oldest expiry). 4. Select Lot-B: pick 8 units. 5. Confirm lot-sum = 20 = issued_qty. 6. Commit. |
One tb_inventory_transaction row written for the line with two tb_inventory_transaction_detail children: row 1 for Lot-A (qty 12, expiry 2026-05-20, cost_per_unit from Lot-A's cost-layer), row 2 for Lot-B (qty 8, expiry 2026-05-25, cost_per_unit from Lot-B's cost-layer). SR_VAL_012 passes because every issued unit has lot data on the linked transaction. Display unit_cost on the line is the blended cost across the two lots. Maps to TC-SR-120004 (Issuance with multi-lot). |
| FUL-HP-04 | Partial fulfilment — at-issue stock-out, fulfiller short-fulfils | SR SR-D line 1: approved_qty = 10, source on-hand at approval time was 12, but at commit time another SR has consumed 4, leaving on-hand = 8. |
1. Open SR-D. 2. Live on-hand check shows 8 < approved_qty = 10. 3. Enter issued_qty = 8.000. 4. Write per-line comment "issued 8 of 10; 2 short due to concurrent consumption on SR-X". 5. Commit. |
SR_VAL_013 re-runs at commit and passes (since issued_qty = 8 ≤ live_on_hand = 8); SR completed; source on-hand 8 → 0; per-line fulfilment_gap = approved_qty − issued_qty = 2 recorded (computed, not persisted as a column); variance event raised for outlet reporting (SR_POST_008); per-line history JSON appended with the partial commit; Requester and Inventory Controller alerted in parallel for the partial; the SR does NOT auto-create a follow-up — that is the Requester's decision. Maps to TC-SR-120003 (Partial Issuance). |
| FUL-HP-05 | Mixed multi-line outcomes — some full, some partial, some zero | SR SR-E with 3 lines: line 1 (approved_qty = 10, on-hand 100; full); line 2 (approved_qty = 15, on-hand 12; partial); line 3 (approved_qty = 5, on-hand 0; zero). |
1. Open SR-E. 2. Line 1: issued_qty = 10. 3. Line 2: issued_qty = 12 with system comment "issued 12 of 15; 3 short due to stock-out". 4. Line 3: issued_qty = 0 with system comment "could not fulfil — source stock-out". 5. Commit. |
SR completed; per-line: line 1 fully issued (10 = 10), line 2 partially issued (12 of 15, gap 3), line 3 zero-issued (0 of 5, gap 5 = approved_qty); inventory transactions written for line 1 and line 2 only (line 3 with issued_qty = 0 and inventory-type product produces no transaction, per SR_POST_006); source on-hand: rice -10, flour -12, sugar unchanged; variance events raised on lines 2 and 3 for outlet reporting. Per SR_POST_012 option (a) for line 2 and option (b) for line 3. |
| FUL-HP-06 | Commit succeeds despite SoD relaxation for low-value SR | SR SR-F total value = ฿2,500; tenant SoD-relaxation threshold = "Approver = Fulfiller allowed for SRs < ฿5,000"; Approver and Fulfiller are the same user. |
1. Same user (who approved) opens SR-F. 2. Records issued_qty per line. 3. Commits. |
Commit passes — SR_AUTH_012 is relaxed for this SR per tenant config (value below threshold); a system comment records the SoD relaxation event for audit. Above threshold the relaxation would not apply (FUL-PERM-04 below). |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| FUL-PERM-01 | Fulfiller in user_action.execute at fulfilment stage acts on SR at their source |
Allow. Pick, enter issued_qty, select lots, write per-line comments, and Commit are all enabled while doc_status = in_progress and workflow_current_stage = fulfilment stage. Per SR_AUTH_007. |
| FUL-PERM-02 | Fulfiller from a different source location attempts to act | Deny — location scope. Fulfiller authority is per-location: a fulfiller at WH-North cannot act on an SR sourced from WH-Central. Direct API call rejects with "You are not authorized to fulfil requisitions from source location WH-Central." Per SR_AUTH_007 location constraint + SR_AUTH_014 stage gating (the fulfiller is not in user_action.execute). |
| FUL-PERM-03 | Fulfiller attempts to set issued_qty > approved_qty |
Deny — value cap. Per SR_VAL_008. The screen blocks the entry client-side; the server rejects with "Quantities must satisfy 0 ≤ issued_qty ≤ approved_qty ≤ requested_qty." Fulfiller must cap at approved_qty or escalate to the Inventory Controller to amend approval (which requires reopening the SR — typically not permitted; instead, raise a fresh SR for the excess). |
| FUL-PERM-04 | Approver attempts to fulfil own-approved SR above SoD threshold | Deny — SoD Approver ≠ Fulfiller. Per SR_AUTH_012. SR SR-G total value = ฿15,000 (above the ฿5,000 relaxation threshold). Same user holds both Approver and Fulfiller rights but is blocked at commit with "You approved a line on this requisition; another user must issue the goods." SR stays at in_progress; a deputy fulfiller or shift handover is required. |
| FUL-PERM-05 | Fulfiller attempts to act on completed SR |
Deny — terminal state. Per the global rule, completed is locked across all personas. The Fulfiller may inspect historical lots and quantities for audit but cannot re-commit or amend. Post-commit corrections go through [inventory-adjustment](/en/inventory/inventory-adjustment). |
| FUL-PERM-06 | Fulfiller attempts to skip lot selection for a lot-controlled item | Deny at commit. Per SR_VAL_012. If the product is lot-controlled or perishable and the linked tb_inventory_transaction_detail has no lot_no (or no expiry_date for perishable), commit rejects with "Issue posting requires lot information for lot-controlled items and a valid unit cost on every issued line; line <seq> is missing data on the linked inventory transaction." Fulfiller must complete the lot sub-form before re-attempting commit. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| FUL-VAL-01 | issued_qty > approved_qty on a line |
Fulfiller enters issued_qty = 12 on a line with approved_qty = 10. |
Reject — SR_VAL_008. Server returns "Quantities must satisfy 0 ≤ issued_qty ≤ approved_qty ≤ requested_qty." Common cause: typo / data-entry mistake. |
| FUL-VAL-02 | issued_qty < 0 on a line |
Fulfiller accidentally enters a negative quantity. | Reject — SR_VAL_008. Server returns the same invariant error. |
| FUL-VAL-03 | Commit with no approved lines | All lines have approved_qty = 0 (e.g. all approvers rejected, but the auto-cancel did not fire — system bug or race). |
Reject at commit — SR_VAL_011. Server returns "SR must contain at least one approved line (approved quantity > 0) before it can be committed." Inventory Controller investigates the SoR; typically the SR should already be cancelled. |
| FUL-VAL-04 | Lot data missing on linked inventory transaction (lot-controlled item) | Fulfiller commits without populating the lot sub-form on a lot-controlled product (skipped, deferred, or removed by accident). | Reject at commit — SR_VAL_012. Server returns "Issue posting requires lot information for lot-controlled items and a valid unit cost on every issued line; line <seq> is missing data on the linked inventory transaction." Fulfiller opens the lot sub-form, picks lot(s), re-commits. |
| FUL-VAL-05 | Source on-hand below issued_qty at commit (live check) |
Live SR_VAL_013 check: another SR consumed source on-hand between Fulfiller's lot pick and commit; on-hand now < issued_qty. |
Reject at commit — SR_VAL_013. Server returns "Source stock-out at issue: line <seq> requires <issued_qty> but only <on_hand> is available at <from_location_name>. Reduce issued_qty to the available quantity or cancel the line." Fulfiller reduces issued_qty to live on-hand, writes a system comment, re-commits (the FUL-HP-04 partial-fulfilment pattern). |
| FUL-VAL-06 | Closed-period commit attempt | Posting date for the commit (typically now()) falls in a closed accounting period. |
Reject at commit — SR_VAL_014. Server returns "Cannot commit SR <sr_no>: posting date falls in a closed accounting period." Fulfiller escalates to Finance for period reopen or to the Inventory Controller for SR void; SR stays in_progress. |
| FUL-VAL-07 | Optimistic-concurrency mismatch at commit | Another user (concurrent fulfiller in a different tab, or background system) has updated doc_version on the SR between read and commit. |
Reject at commit — generic optimistic-concurrency. Server returns "This SR was modified by another user. Please refresh and re-apply your changes." Fulfiller refreshes, re-enters any unsaved per-line state, re-commits. |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| FUL-EDGE-01 | Decimal precision on issued_qty and multi-lot split |
Lot-controlled product, approved_qty = 5.500 (3-dp); Fulfiller splits across two lots: Lot-A = 2.250, Lot-B = 3.250; conversion factor 1.000000 (base UoM). |
Accepted at storage. Per-lot quantities stored at 5dp precision per Decimal(20, 5); sum must exactly equal issued_qty = 5.500 (the lot-sub-form validates this client-side and server-side); no rounding drift carried into totals. Display rounded half-up to 3dp per SR_CALC_007. |
| FUL-EDGE-02 | Lot data partially present (lot picked but no expiry for perishable) | Fulfiller picked a lot but the lot's expiry_date is null in the source data (data-quality issue at receipt time). |
Reject at commit — SR_VAL_012. The check looks for non-null expiry_date for perishable products. Fulfiller escalates to Inventory Controller; the issue is at source-receipt time, not at SR time. Pre-commit fix: Inventory Controller posts an inventory-adjustment to populate the lot's expiry; then SR commit re-attempted. |
| FUL-EDGE-03 | Stock-OUT event in the inventory-transaction sub-ledger reconciliation | After commit, the inventory sub-ledger query Σ outQty for inventory_doc_type = store_requisition, document_id = SR-A.id is reconciled against the GL credit to the source inventory account. |
Match within tolerance. The reconciliation is the Finance / Inventory Controller's responsibility (Audit / Config persona); the Fulfiller's role is correct entry of issued_qty and lot selection. Per SR_XMOD_003 (enum_inventory_doc_type = store_requisition). |
| FUL-EDGE-04 | Commit triggers cost-layer FIFO consumption from multiple lots | Source uses FIFO costing; line has issued_qty = 10, oldest layer has remaining 4 units, next layer has remaining 30. |
Two cost-layer consumption rows. The costing module ([costing](/en/inventory/costing)) consumes 4 units from the oldest layer at its cost_per_unit, then 6 units from the next layer at its cost_per_unit; the line's display unit_cost is the weighted blend; the GL entry uses the blended total; per SR_XMOD_004. |
| FUL-EDGE-05 | Pre-commit lot selection saved as draft state | Fulfiller has entered issued_qty and selected lots for 2 of 3 lines, then their shift ends. |
Lot selection persisted. Pre-commit per-line state (issued qty + lot selection) is saved in-place on tb_store_requisition_detail and the staged tb_inventory_transaction_detail rows (the inventory transaction may be created in a staged / non-committed state, then finalised at commit). The next fulfiller (deputy / next shift) opens the SR, sees the in-progress state, completes the remaining lines, and commits. |
| FUL-EDGE-06 | Transfer-type SR with destination location currently inactive | sr_type = transfer; destination tb_location is marked is_active = false (administrative inactivation) between approval and commit. |
Reject at commit — SR_VAL_002 (locations must reference active rows). Server returns "Destination location is no longer active; cannot transfer." Fulfiller escalates to Inventory Controller to either reactivate the location or void the SR. |
| FUL-EDGE-07 | UoM conversion at issue (issue in a different UoM than approval) | Product has two UoMs (Bottle and Case = 12 Bottle); approved in Case = 5; Fulfiller picks in Bottle (60 bottles total = 5 cases). |
Conversion handled at the inventory-transaction layer. Per SR_VAL_008, the value cap is in the line's UoM (typically the requested UoM); the conversion to base UoM happens on the inventory transaction (received_unit_conversion_factor on tb_inventory_transaction_detail). The Fulfiller enters the base-UoM equivalent; the SR line issued_qty reflects the requested-UoM value. The cost-layer consumption is in base UoM. |
| FUL-EDGE-08 | Batch commit (multiple SRs in one transaction window) | Inventory Manager-equivalent role (Fulfiller with elevated permission) commits multiple in_progress SRs at end of shift. |
Per-SR evaluation in batch. Each SR is evaluated independently against SR_VAL_011–SR_VAL_014; failures on one SR (e.g. closed-period block) leave that SR at in_progress; successful SRs all commit in the same transaction window; activity log records each per-SR result. Note: the SR module's batch pattern is less common than GRN's; tenant config decides. |
issue), Scenario 2 (full happy path transfer), Scenario 6 (at-issue stock-out partial), Scenario 8 (multi-lot pick), Scenario 9 (SoD violation at commit), Scenario 10 (closed-period commit block).SR_VAL_008 (quantity invariant issued_qty ≤ approved_qty), SR_VAL_011 (at-least-one-approved-line at commit), SR_VAL_012 (lot info on linked inventory transaction), SR_VAL_013 (source on-hand live check at commit), SR_VAL_014 (open period); Section 4 — SR_AUTH_007 (Fulfiller authority), SR_AUTH_012 (Approver ≠ Fulfiller SoD); Section 5 — SR_POST_005–SR_POST_008 (commit posting effects: inventory, GL, variance).../carmen-inventory-frontend-e2e/tests/701-sr.spec.ts — canonical Playwright spec. Fulfiller-relevant test groups: TC-SR-120001..120004+ (Issuance — FUL-HP-01, FUL-HP-02 transfer, FUL-HP-03 multi-lot, FUL-HP-04 partial, FUL-HP-05 mixed-line outcomes, FUL-VAL-01..VAL-05). Permission-denial coverage uses the requestor@blueledgers.com fixture.tb_inventory_transaction / tb_inventory_transaction_detail are the canonical store of lot, expiry, and cost-layer data.unit_cost; FUL-EDGE-04 shows the cost-layer consumption pattern.sr_no for matching.