At a Glance
Persona: Finance (Officer / AP Clerk + Finance Manager) · Module: purchase-order · Scenarios: ~25
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: maps to401-po.spec.ts,403-po-finance-ap-match.spec.tsin../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Finance persona (the Finance Officer / Accounts Payable clerk who runs the day-to-day three-way match and posts the AP liability, plus the Finance Manager who exercises pre-transmission financial sign-off on high-value or FX-sensitive POs) directly drives in the purchase-order module. Finance has two distinct touch points on the PO lifecycle: a pre-transmission review while the PO is still at po_status = in_progress (Finance Manager signs off currency, exchange rate, tax codes, and totals before PO_POST_004 transmits the PO to the vendor), and a post-receipt three-way match after GRN posting (Finance Officer captures the vendor invoice, looks up the matching PO and GRN(s), and runs the match under PO_POST_008 / PO_POST_009). The three-way match is the key Finance activity — on success, AP clears the GRN accrual and posts the vendor liability; on failure, the invoice is held in dispute and the discrepancy is flagged back to the Purchaser for resolution. The PO itself is not transitioned by the three-way match (PO_POST_008 is explicit on this) — the match outcome lives on the invoice record, and the PO retains whatever fulfilment status it reached (partial, completed, or closed). Scenarios are grouped into the happy paths described in 03-user-flow-finance.md, the RBAC boundary enforced by PO_AUTH_009 (Finance Officer scope — read-only across the PO + match + post AP on the AP side) and the Finance Manager pre-transmission approver right under PO_AUTH_011 (stage-gated approval), the validation rules around qty / price tolerance, missing GRN, FX, closed-period, and duplicate-invoice guards, and a small set of edge cases around tolerance boundaries, FX no-op cases, multi-invoice matching, concurrency, and decimal precision on AP amounts. Cross-persona handoffs that pivot off Finance (X-PO-05 pre-transmission send-back, X-PO-07 three-way-match dispute bounce-back, X-PO-09 AP posting close-out) live in the parent overview, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| FIN-HP-01 | Finance Manager pre-transmission review signs off a high-value PO | PO PO-HV at po_status = in_progress with total_amount = ฿1,250,000 (above the tenant high-value threshold); workflow cursor at the Finance review stage; Finance Manager finance.manager@blueledgers.com in user_action.execute per PO_AUTH_011; vendor's contracted currency, FX rate, tax codes, and credit term all consistent with vendor master |
1. Open PO-HV from the review queue → Financial Details tab. 2. Verify currency_id, exchange_rate against tenant FX policy, payment terms, prepayment flag. 3. Verify per-line tax_id / tax rate against product tax profile + vendor tax registration; tally Σ line.total_price against header total_amount per PO_CALC_008–PO_CALC_011. 4. Click Sign off. |
Stage approval posted per PO_POST_003; workflow_history appended; workflow_current_stage advances to the next stage (or directly to final approval). On final-stage approval, PO_POST_004 fires: po_status = in_progress → sent, approval_date = now(), last_action = approved; transmission to vendor triggered via the email/transmit layer. Finance Manager's signoff entry persisted in tb_purchase_order_comment (type system). |
| FIN-HP-02 | Clean three-way match (qty + price both within tolerance) posts AP liability | PO PO-A at po_status = completed (order_qty = 10, received_qty = 10 across one GRN GRN-A1); vendor invoice INV-A1 received for qty 10 @ unit price matching PO unit_price; invoice vendor and currency match tb_purchase_order.vendor_id / currency_id; Finance Officer finance@blueledgers.com logged in with PO_AUTH_009 read on PO and AP post right on the AP side |
1. Capture INV-A1 in AP — invoice number, date, vendor, currency, line items, tax, total. 2. Index against PO-A via printed po_no. 3. System resolves matching GRN GRN-A1. 4. Run three-way match per PO_POST_008. 5. Confirm AP posting. |
All lines match on qty (invoice qty 10 ↔ GRN accepted_qty 10) and price (invoice unit price ↔ PO unit_price) within tolerance. PO_POST_008 fires: AP module clears the GRN inventory-receipt accrual (debit GRN-accrual / credit AP), and posts the AP liability against the vendor (debit inventory-clearing / credit vendor payable) in transaction currency with the base-currency FX equivalent captured at invoice date. Invoice moves to approved for payment. tb_purchase_order.po_status unchanged (remains completed); matched-but-unbilled position on PO-A is now zero. |
| FIN-HP-03 | Partial GRN match — invoice matched against received quantity only | PO PO-B at po_status = partial (order_qty = 10, received_qty = 6 via GRN-B1, pending balance 4); vendor invoice INV-B1 for qty 6 only @ matching unit price; invoice references the same PO |
1. Capture INV-B1 in AP. 2. Index against PO-B. 3. System resolves GRN-B1 (only the posted GRN exists for the partial receipt). 4. Run three-way match per PO_POST_008. 5. Confirm AP posting. |
Match runs against GRN-B1 only (the 4-unit pending balance is not invoiced yet and is irrelevant to this match). Qty matches (invoice 6 ↔ GRN accepted_qty 6), price within tolerance. PO_POST_008 fires: AP clears GRN-B1's accrual + posts AP liability for the 6 units in transaction currency. Invoice approved for payment. tb_purchase_order.po_status unchanged (remains partial); a follow-up invoice against the remaining 4 units (once GRN posts) will re-enter this flow. |
| FIN-HP-04 | Price within tolerance — auto-pass with PPV entry | PO PO-T at po_status = completed (qty 100 @ PO unit_price = ฿50.00); vendor invoice INV-T1 for qty 100 @ ฿50.40; tenant price tolerance = 1.5%; invoice currency matches PO currency |
1. Capture INV-T1. 2. Index against PO-T. 3. Run three-way match. |
Price variance (50.40 − 50.00) / 50.00 = 0.8% falls inside the 1.5% band. Qty matches. PO_POST_008 fires: AP posts at the invoiced price (฿50.40); the variance (50.40 − 50.00) × 100 = ฿40.00 is captured as a purchase price variance (PPV) entry on the AP posting (debit / credit PPV account per tenant GL mapping); GRN accrual cleared at the GRN-recorded valuation. Invoice approved for payment. PO unit_price remains the contracted price; no PO amendment is generated. |
| FIN-HP-05 | Currency mismatch within tenant FX tolerance — FX adjustment posted | PO PO-F at po_status = completed with currency_id = USD, exchange_rate = 36.50 (against base THB); vendor invoice INV-F1 received in EUR per dual-currency contract; tenant policy permits EUR ↔ USD for this vendor; invoice-date EUR → USD rate = 1.08 and invoice-date USD → THB rate = 36.65 |
1. Capture INV-F1 in EUR. 2. Index against PO-F. 3. System recognises dual-currency vendor; applies invoice-date FX cross-rate EUR → USD → THB. 4. Run three-way match. 5. Confirm AP posting. |
Match runs on qty + (FX-adjusted) price within tolerance. PO_POST_008 fires: AP posts the invoice in EUR (transaction currency on the invoice) with an FX adjustment entry against the PO's contracted currency (USD); the base-currency THB value is computed at the invoice-date EUR → THB cross-rate, NOT the PO's stale exchange_rate = 36.50. The delta between the PO-snapshot FX value and the invoice-date FX value is posted as a realised FX gain/loss entry per tenant GL mapping. Invoice approved for payment. PO unchanged. |
| FIN-HP-06 | AP liability posted with correct GL account hits on clean match | PO PO-G at po_status = completed (qty 50 @ ฿200.00, tax_rate = 7%, GRN posted); vendor invoice INV-G1 exactly matches PO + GRN; tenant GL mapping: GRN-accrual = 2110-GRN, AP liability = 2100-AP-Trade, input VAT = 1310-VAT-Input |
1. Capture INV-G1 (net = ฿10,000, tax = ฿700, total = ฿10,700). 2. Index against PO-G. 3. Run three-way match. 4. Confirm AP posting. |
PO_POST_008 fires with the following GL entries on AP posting: Dr 2110-GRN ฿10,000 (clear GRN accrual), Dr 1310-VAT-Input ฿700 (recover input VAT), Cr 2100-AP-Trade ฿10,700 (post vendor payable). Net inventory-receipt position becomes zero on 2110-GRN; vendor payable increases by ฿10,700; input VAT becomes recoverable. Invoice approved for payment. tb_purchase_order_comment appended with system entry confirming AP posting (invoice ref + GL batch ref). PO unchanged. |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| FIN-PERM-01 | Finance Officer runs three-way match on a PO at po_status ∈ {partial, completed, closed} |
Allow. Per PO_AUTH_009 (Finance Officer read-only on the PO) and AP-side permission to capture + match invoices. The match algorithm under PO_POST_008 executes; on success, PO_POST_008 posts AP; on failure, PO_POST_009 parks the invoice in dispute. The PO record itself is not mutated by the Finance Officer (read-only). |
| FIN-PERM-02 | Finance Officer posts the AP liability on three-way-match success | Allow. AP posting is an AP-module action driven by PO_POST_008 (cross-module rule PO_XMOD_007). The Finance Officer holds AP-post right on the AP side; the PO module is only consulted (read-only) for matching reference data — vendor_id, currency_id, line unit_price, GRN linkage. GL entries are written to AP / inventory-clearing / VAT accounts per tenant mapping. |
| FIN-PERM-03 | Finance Officer attempts to override a failed three-way match (force-post AP despite qty / price discrepancy) | Deny — Manager-only. Override of a failed match requires the Finance Manager role (or AP Manager per tenant configuration). The Finance Officer's UI hides / disables the Override and Post action on a failed match; the invoice stays in PO_POST_009 dispute state. A direct API call returns "Override of failed three-way match requires the Finance Manager role." |
| FIN-PERM-04 | Finance Manager signs off a high-value PO at the pre-transmission review stage | Allow per PO_AUTH_011 (stage-gated approval) when the user appears in tb_purchase_order.user_action.execute at the current workflow_current_stage. The signoff posts PO_POST_003 (within in_progress) or, on the final stage, fires PO_POST_004 (in_progress → sent). Below the high-value threshold, the workflow may bypass the Finance stage entirely. |
| FIN-PERM-05 | Finance Officer attempts to post AP without a successful three-way match | Deny. AP posting is gated on PO_POST_008 success — the AP module rejects a direct post against an unmatched / failed-match invoice. Server returns "Cannot post AP liability — three-way match has not succeeded on invoice <invoice_no>. Resolve qty / price discrepancy or escalate to the Finance Manager." GL entries are not written; invoice remains in pending_match or dispute state. |
| FIN-PERM-06 | Finance Officer attempts to modify PO header (vendor, currency, price, qty) | Deny — Purchaser only. Per PO_AUTH_002 (Purchaser scope) and PO_AUTH_010 (segregation of duties), the Finance Officer has no edit right on the PO record itself — only read access under PO_AUTH_009. The PO detail page in the Finance Officer's view is read-only; the AP-module screens are the only place Finance can act, and those screens cannot mutate the PO. A direct API edit call returns "You are not authorised to edit this PO at the current stage." |
| FIN-PERM-07 | Finance Manager sends back a PO at the pre-transmission review stage | Allow. Per PO_POST_005, the Finance Manager (or any stage approver) may reject the PO at the review stage with reason text; po_status = in_progress → draft; workflow_current_stage resets; last_action = rejected; rejection comment persisted in tb_purchase_order_comment (type system); ownership returns to the Purchaser to correct and resubmit. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| FIN-VAL-01 | Qty discrepancy outside tolerance — flag back to Purchaser | PO PO-Q1 at po_status = completed (qty 10 received via GRN); vendor invoice INV-Q1 for qty 12 @ matching unit price; tenant qty tolerance = 0% |
Reject the match per PO_POST_009. Server returns "Three-way match failed — invoice qty (12) exceeds GRN accepted qty (10) outside tenant tolerance (0%)." Invoice held in dispute state; AP posting blocked; system comment appended on the PO recording the failure (line, dimension = qty, magnitude); deviation record opened on the vendor / vendor-pricelist side; Purchaser notified via activity log to pursue credit note (over-invoicing) or supplementary GRN (under-receipt). |
| FIN-VAL-02 | Price discrepancy outside tolerance — flag back to Purchaser | PO PO-P1 at po_status = completed (qty 100 @ PO unit_price = ฿50.00); vendor invoice INV-P1 for qty 100 @ ฿55.00; tenant price tolerance = 1.5% (actual variance = 10%) |
Reject the match per PO_POST_009. Server returns "Three-way match failed — invoice unit price (฿55.00) is outside the price-tolerance band (±1.5% of PO unit price ฿50.00)." Invoice held in dispute; AP posting blocked; system comment appended on PO + deviation on vendor pricelist; Purchaser pursues credit note (over-billing) or PO amendment under PO_VAL_016 (price changes post-sent are restricted). |
| FIN-VAL-03 | Missing GRN — cannot match without it | PO PO-N1 at po_status = sent (no GRN posted yet); vendor invoice INV-N1 received |
Reject the match. Server returns "Three-way match cannot run — no GRN posted against PO <po_no>. Confirm receipt with the Receiver / Purchaser before re-presenting the invoice." Invoice parked in pending_match (not in dispute); AP posting blocked; if vendor delivered but GRN is not yet posted, Receiver / Purchaser notified to chase the GRN — once posted, the match re-runs automatically. If vendor invoiced ahead of delivery, the invoice is bounced back to the vendor with a non-receipt notice. |
| FIN-VAL-04 | Invoice for a voided PO — rejected | PO PO-V1 at po_status = voided (voided by Procurement Manager per PO_AUTH_007); vendor invoice INV-V1 arrives referencing the voided po_no |
Reject the match. Server returns "PO <po_no> is at status voided — no invoice can be matched against this PO." Invoice flagged for return to vendor; AP posting blocked; activity log records the attempt; resolution path is vendor correction (vendor must withdraw the invoice) — no PO-side action available since voided is terminal per PO_POST_010. |
| FIN-VAL-05 | Currency mismatch beyond FX tolerance — rejected | PO PO-C1 at po_status = completed with currency_id = USD; vendor invoice INV-C1 received in EUR but tenant policy does not permit EUR invoicing for this vendor (single-currency contract per PO_VAL_013) |
Reject the match. Server returns "Invoice currency (EUR) does not match PO currency (USD); dual-currency invoicing is not permitted for this vendor per tenant policy." Invoice held in dispute; AP posting blocked; system comment appended on PO; Purchaser flagged to contact vendor for re-invoicing in USD. Distinct from the FIN-HP-05 happy path where dual-currency is permitted. |
| FIN-VAL-06 | Duplicate invoice match attempt — rejected | PO PO-D1 already matched + AP-posted against invoice INV-D1 (state = approved for payment); a second invoice INV-D2 with the same vendor invoice number is captured against the same PO |
Reject at AP-capture stage (before the match runs). Server returns "Duplicate invoice — vendor invoice number <invoice_no> from <vendor> has already been captured and matched against PO <po_no>." Second invoice is not written; AP posting blocked; activity log records the attempt; Purchaser / vendor notified for re-issue if the second invoice was a legitimate amendment (in which case the vendor must use a distinct invoice number, typically with a -CN or -AM suffix). |
| FIN-VAL-07 | AP posting attempted against a closed accounting period — rejected | PO PO-CP at po_status = completed; vendor invoice INV-CP matched cleanly; current GL period for the invoice date (2026-04-15) is closed (period-close cutoff applied) |
Reject at AP-post stage. Server returns "Cannot post AP — accounting period <yyyy-mm> is closed. Either re-date the invoice to the open period or request period re-open from the Finance Manager." GL entries are not written; invoice held at pending_post state; resolution path is Finance Manager period re-open (audit-logged) or invoice re-dating with vendor concurrence. PO unchanged. |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| FIN-EDGE-01 | Exact tolerance boundary on both price and qty | PO PO-B1 at po_status = completed (qty 100 @ ฿50.00); vendor invoice INV-B1 for qty 102 @ ฿50.75; tenant qty tolerance = 2% (upper bound = 102); tenant price tolerance = 1.5% (upper bound = ฿50.75) |
Accepted — both dimensions land exactly on the boundary. PO_POST_008 fires: qty variance (102 − 100) / 100 = 2% and price variance (50.75 − 50.00) / 50.00 = 1.5% both equal the tolerance, not exceed it (inclusive boundary per tenant configuration). AP posts at invoiced values; the qty over-receipt is reconciled against the GRN over-receipt tolerance posted by the Receiver; price variance flows to PPV. Crossing either bound (qty = 103 or price = ฿50.76) would trigger PO_POST_009 per FIN-VAL-01 / FIN-VAL-02. |
| FIN-EDGE-02 | FX rate exactly matches PO snapshot — no FX adjustment posted | PO PO-NX at po_status = completed with currency_id = USD, exchange_rate = 36.50; vendor invoice INV-NX received in USD (single-currency match); invoice-date USD → THB rate happens to be exactly 36.50 (matches the PO snapshot to 5 dp) |
Accepted, no FX entry. PO_POST_008 fires: match passes on qty + price; AP posts the invoice in USD; the base-currency THB value uses the invoice-date rate 36.50 — which equals the PO exchange_rate snapshot — so the realised FX gain/loss entry is ฿0.00 and is not written to GL (no-op suppression per tenant policy). Distinct from FIN-HP-05 where the cross-rate differs from the snapshot. |
| FIN-EDGE-03 | Multi-invoice partial-then-full match against same PO | PO PO-MI at po_status = partial (order_qty = 10, two GRNs: GRN-MI1 for qty 4, GRN-MI2 for qty 6); vendor sends two invoices: INV-MI1 for qty 4 at first, then INV-MI2 for qty 6 later, both at matching unit price |
Each invoice is matched independently. First pass: INV-MI1 matches GRN-MI1 (qty 4) → PO_POST_008 fires → AP posts INV-MI1 liability for 4 units; matched-but-unbilled on PO drops from 10 to 6. Second pass: INV-MI2 matches GRN-MI2 (qty 6) → PO_POST_008 fires again → AP posts INV-MI2 liability for 6 units; matched-but-unbilled drops to 0. PO po_status follows GRN-driven transitions independently (sent → partial → completed); each invoice is approved for payment in turn; no cumulative-match logic — the model is GRN-by-GRN match, not running-totals. |
| FIN-EDGE-04 | Concurrent invoice-match posts on the same PO from two AP sessions | Finance Officer A and B both capture invoices against PO-X (po_status = completed, single GRN posted, fully receipted); A captures INV-XA for the full qty at T; B captures a duplicate-vendor-invoice INV-XB for the same qty at T + 500ms; both attempt to match |
First match wins atomically: A's match runs → PO_POST_008 posts AP for INV-XA → vendor invoice number INV-XA is now recorded against PO-X. B's match attempt is then reconciled: either (a) the duplicate-invoice guard (FIN-VAL-06) catches B with "Duplicate invoice — vendor invoice <INV-XB> has already been matched..." if INV-XB == INV-XA, or (b) if INV-XB is a distinct invoice number but matches the same GRN, the GRN-level optimistic-concurrency guard rejects B with "This GRN's invoice match was claimed by another user. Please refresh and re-capture." No double-post; no 2100-AP-Trade over-credit; B retries after refresh. |
| FIN-EDGE-05 | Decimal precision on AP amount — line totals reconcile to header to 2 dp | PO PO-DP with two lines: L1 (qty 7 @ ฿14.33, tax 7%), L2 (qty 3 @ ฿49.99, tax 7%); vendor invoice INV-DP matches exactly |
Line calculations per PO_CALC_001–PO_CALC_005 use the rounded-prior-step rule from PR_046–PR_055. L1: subtotal Round(14.33 × 7, 2) = ฿100.31, tax Round(100.31 × 0.07, 2) = ฿7.02, total ฿107.33. L2: subtotal Round(49.99 × 3, 2) = ฿149.97, tax Round(149.97 × 0.07, 2) = ฿10.50, total ฿160.47. Header per PO_CALC_008–PO_CALC_010: total_price = ฿250.28, total_tax = ฿17.52, total_amount = ฿267.80. AP posts to GL: Dr GRN-accrual ฿250.28, Dr VAT-input ฿17.52, Cr AP-Trade ฿267.80. No rounding drift between line sum and header; Decimal(20, 5) storage precision retained internally; 2-dp display rounding applied on the AP voucher only. |
X-PO-05 (pre-transmission send-back from Finance Manager → Purchaser), X-PO-07 (three-way-match dispute bounce-back from Finance Officer → Purchaser), X-PO-09 (AP posting close-out — PO terminal commercial position).received_qty and accepted_qty that the three-way match consumes.PO_POST_003 stage approval; PO_POST_004 final approval → in_progress → sent; PO_POST_005 reject → in_progress → draft; PO_POST_008 three-way match success — AP clears GRN accrual + posts vendor invoice, PO not status-changed; PO_POST_009 three-way match failure — invoice held in dispute, system comment + vendor-pricelist deviation, PO not auto-voided; PO_POST_010 void — terminal; PO_AUTH_009 Finance Officer read-only; PO_AUTH_011 workflow-derived stage-gated approval), Section 6 (PO_XMOD_007 AP / Three-way match — GRN posting raises inventory accrual, cleared only on PO_POST_008 success; PO closure alone does not clear AP accrual).received_qty / accepted_qty that drive the qty side of the match.PO_XMOD_008.PO_POST_009, feeding pricelist-deviation analytics.../carmen-inventory-frontend-e2e/tests/401-po.spec.ts — shared / mixed-persona coverage; no dedicated Finance E2E spec exists at this time. The three-way match, AP posting, and FX adjustment behaviours are exercised at the API / integration level (cross-module AP service) rather than through the PO UI; a dedicated 403-po-finance-ap-match.spec.ts is a roadmap item. The SKIP_NOTE_BACKEND annotation in 401-po.spec.ts explicitly flags GRN-sync and AP-integration behaviour as out-of-scope for UI E2E coverage.