At a Glance
Persona: Purchaser (Procurement Officer — PR-to-PO bridge) · Module: purchase-request · Scenarios: ~33
Categories: Happy Path · Permission · Validation · Edge Case
E2E coverage: maps totests/304-pr-purchaser-journey.spec.ts,tests/310-pr-template.spec.ts, andtests/301-pr.spec.ts(purchaseTest fixture) in../carmen-inventory-frontend-e2e/
This page captures the test scenarios that the Purchaser persona directly drives in the purchase-request module. The Purchaser (also titled Procurement Officer) is the bridge persona between the upstream PR side and the downstream PO side of the procure-to-pay chain; they do not approve PR content. By the time a PR reaches their queue it has already cleared the entire approval chain and pr_status = approved (PR_POST_005). Scenarios are grouped into the happy paths described in 03-user-flow-purchaser.md Section 2 (validate vendor allocation, run Allocate Vendor, look up pricelist deviation, consolidate by vendor + currency, convert to PO — full and partial), the RBAC boundary enforced by PR_AUTH_008 (only enum_stage_role = purchase may convert), the cross-module / PO conversion rules in 02-business-rules.md Section 6 (bridge table tb_purchase_order_detail_tb_purchase_request_detail, consolidation by (vendor_id, currency_id), partial conversion, FX rate snapshot at conversion), and a small set of bridge-integrity and concurrency edges. Cross-persona handoffs that pivot off the Purchaser (X-PR-01, X-PR-04, X-PR-07) live in the parent overview, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| PUR-HP-01 | Open an approved PR from the Approved PRs queue | PR pr_status = approved (final approve fired PR_POST_005); Purchaser purchaser@blueledgers.com logged in with enum_stage_role = purchase; PR has at least one unbridged line |
1. Sidebar → Purchase Request → Approved PRs queue. 2. Apply filter (vendor, currency, requested delivery window, department, store). 3. Open the PR by clicking the pr_no row. 4. Confirm the detail page is read-mostly for the Purchaser — header (PR type, requestor, department, pr_date, delivery_date, currency_id, exchange_rate, justification, attachments) is non-editable; only vendor allocation, pricelist selection, and per-line conversion checkbox are interactive. |
PR detail loads; queue row shows unbridged_lines / total_lines; header fields are read-only; vendor, pricelist, and conversion-qty controls are enabled; user_action.execute[] contains the Purchaser; no write side-effects on tb_purchase_request. |
| PUR-HP-02 | Validate vendor allocation against current vendor master | PR approved with a single line that already has vendor_id, vendor_name, pricelist_detail_id, pricelist_price snapshotted from PR submit |
1. Open the PR → walk to the line. 2. Confirm the snapshotted vendor_id / vendor_name. 3. The system pulls live vendor-master data (is_active, payment terms, credit limit, blacklist flags) from tb_vendor for context. 4. No issues found → keep the allocation. |
The PR detail row is unchanged (no write); the live-lookup indicator on the line shows "vendor active, no blacklist"; conversion remains eligible; the snapshotted fields stay frozen (pricelist_price, pricelist_no, pricelist_unit, pricelist_type). |
| PUR-HP-03 | Reassign vendor via Allocate Vendor dialog | PR approved with one line where vendor_id IS NULL (Requestor did not pick a preferred vendor); active pricelist rows exist for the product / location / required date |
1. Open the line → click Allocate Vendor. 2. Dialog ranks candidate vendors by pricelist match against product_id, location_id, and delivery_date, showing current price, lead time, and historical performance. 3. Pick the top vendor and confirm. |
The line's vendor_id, vendor_name, pricelist_detail_id, pricelist_no, pricelist_unit, pricelist_price, and pricelist_type (enum_pricelist_compare_type) snapshots are written to the tb_purchase_request_detail row; PR stays in approved (no re-approval needed per PR_AUTH_008); line is now eligible for conversion. |
| PUR-HP-04 | Look up pricelist deviation within tolerance | PR approved with one line where the snapshotted pricelist_price = 100.00000 ฿; the current active pricelist row for the same (product_id, vendor, location, effective_date) is 102.00000 ฿ (+2 %, inside the configured ±5 % tolerance) |
1. Open the line → the deviation panel resolves the current pricelist row. 2. Compare snapshot vs current. 3. Deviation +2 % is below the ±5 % band — indicator is green. 4. Accept the snapshot and proceed. |
Deviation indicator green; no warning raised; the snapshotted pricelist_price is carried into PO conversion unchanged; the PR detail row is untouched. |
| PUR-HP-05 | Consolidate two PRs that share vendor + currency into one PO | Two approved PRs (PR-A 1 line, PR-B 1 line) where both lines target vendor_id = V1 and currency_id = THB; both lines unbridged |
1. Open the Convert to PO workbench. 2. Tick the line on PR-A and the line on PR-B. 3. The workbench auto-groups by (vendor_id, currency_id) → one draft-PO group for V1 / THB containing both lines. 4. Review the group preview (line count = 2, subtotal, tax, discount, grand total). 5. Run Convert to PO. |
One tb_purchase_order row created for V1 / THB with two tb_purchase_order_detail lines; one bridge row per (po_line, pr_line) pair written to tb_purchase_order_detail_tb_purchase_request_detail; both source PRs flip from approved to completed (PR_POST_007); Requestors of PR-A and PR-B notified that their PRs are now linked to a PO. |
| PUR-HP-06 | Convert to PO — full (single PR, all lines bridged in one round) | PR approved with two lines (L1, L2) both fully allocated and within tolerance; both unbridged |
1. From the PR detail page tick both lines (default), leaving convert-qty at the open quantity (= approved_base_qty). 2. Open the workbench → one draft-PO group (vendor_id, currency_id). 3. Run Convert to PO → confirm in summary dialog (PO count = 1, source-PR count = 1). |
One PO created with two PO detail lines; two bridge rows written; sum of bridge-linked PO quantities = approved_base_qty per line; PR_POST_007 fires and pr_status flips approved → completed; soft budget commitment converts to a hard commitment on the new PO; type = system comment written to source PR (PR_POST_008); PR drops from the Approved PRs queue. |
| PUR-HP-07 | Convert to PO — partial (one line, partial quantity) — bridge table records linkage | PR approved with one line L1, approved_base_qty = 100 PCS, fully allocated; Purchaser only needs 60 PCS now |
1. Open L1 → set the convert qty to 60 (instead of the default 100). 2. Tick L1, run Convert to PO. 3. Confirm summary. |
One PO created with one PO line for 60 PCS; one bridge row written linking po_line ↔ pr_line with converted_qty = 60; the source PR's L1 retains 100 - 60 = 40 PCS open quantity; PR_POST_007 does not flip pr_status to completed (open quantity > 0); PR stays in approved, still visible in the Approved PRs queue with unbridged_lines = 1; soft commitment for the remaining 40 PCS persists. |
| PUR-HP-08 | FX rate snapshot at conversion (PR rate vs PO rate divergence) | PR approved three weeks ago with currency_id = USD, PR exchange_rate = 35.50000 (immutable per PR_CALC_006); today's organisation FX rate for USD is 36.20000; one unbridged line |
1. Open the line → run Convert to PO. 2. Confirm summary. | The new PO's exchange_rate is snapshotted at 36.20000 (the rate at the moment of vendor commitment), not 35.50000; the PR's exchange_rate stays 35.50000; the bridge row stores the PR-line link; PR-side base_total_amount and PO-side total in base currency may diverge — this is expected per Section 6 of 02-business-rules.md and documented on the PR detail for traceability. |
| PUR-HP-09 | Bounce-back to Requestor for vendor / spec clarification | PR approved with one line where the vendor cannot meet the requested delivery_date and no alternative vendor is acceptable; clarification reason text available |
1. Open the PR → click Send Back to Requestor (or the Bounce-back action). 2. Mandatory reason dialog — type "Vendor V1 lead time 21 days, requested delivery in 7 days. Please revise delivery_date or scope.". 3. Confirm. |
PR_POST_003 fires: workflow_current_stage re-opens to the Requestor's create stage; pr_status returns to draft; last_action = reviewed; soft budget commitment released; Requestor notified with the reason text; type = system comment written on the PR (PR_POST_008); the PR drops out of the Purchaser's Approved PRs queue and re-enters once the Requestor resubmits and the approver chain re-clears. |
| # | Scenario | Expected behaviour (allow/deny + reason) |
|---|---|---|
| PUR-PERM-01 | Purchaser opens an approved PR (current user has enum_stage_role = purchase and is in user_action.execute[] for the purchase stage) |
Allow read and the conversion / vendor-allocation actions (Allocate Vendor / Convert to PO / Send Back to Requestor). PR_AUTH_002 is satisfied for the purchase stage; PR_AUTH_008 grants conversion rights. |
| PUR-PERM-02 | Purchaser runs Convert to PO on an approved PR |
Allow. PR_AUTH_008 (enum_stage_role = purchase owns vendor allocation and PO conversion); PR_POST_007 runs the bridge writes and pr_status transition. The PO module (purchase-order) creates the new tb_purchase_order rows. |
| PUR-PERM-03 | Purchaser attempts to convert a PR currently in pr_status = in_progress (still mid-approval chain) |
Deny — wrong status. PR_AUTH_008 and PR_POST_007 both require the source PR to be in approved (or partially-converted approved with open lines). The Convert-to-PO endpoint rejects an in_progress source PR with "This PR has not completed approval and cannot be converted to a PO". The workbench filters such PRs out of the selection pool. |
| PUR-PERM-04 | Purchaser attempts to edit a PR header (e.g. delivery_date, description, currency_id) |
Deny — edit reserved. Header edits are scoped to the Requestor on draft (PR_AUTH_001) and approvers on in_progress for the narrow approved_qty field only (PR_AUTH_003). The Purchaser's detail page is read-mostly: header inputs are disabled; a direct API call to update tb_purchase_request header columns is rejected with "This PR is in approved status and only conversion-related fields may be modified". |
| PUR-PERM-05 | Purchaser attempts to edit PR line content (requested_qty, approved_qty, description, product_id) |
Deny — edit reserved. Line approved_qty was set by the Approver chain (PR_AUTH_003 + PR_VAL_013) and is frozen at approved. The Purchaser may only update vendor / pricelist snapshots on the line and write a converted-qty into the bridge table — not edit the source line itself. Direct API call rejected with "This field is read-only for the purchase stage role". |
| PUR-PERM-06 | Purchaser attempts to approve the PR (use an approval action like approve / send-back-to-prior-approver) |
Deny — different role. Per PR_AUTH_008, the purchase role is distinct from the approve role. The approval action endpoints reject the call with "You are not authorised to approve at this stage"; PR_AUTH_002 fails because the Purchaser is not in any approve stage's user_action.execute[]. The bounce-back to Requestor (PUR-HP-09) is a separate, allowed path. |
| PUR-PERM-07 | Purchaser triggers Send Back to Requestor (bounce-back) on an approved PR |
Allow. The bounce-back is a documented Purchaser-side handoff (X-PR-07); it routes the PR to the Requestor via the standard send-back path (PR_POST_003). The Purchaser does not edit content — they route back with a reason. |
| PUR-PERM-08 | Non-purchase user (Requestor / HOD / Finance) opens an approved PR and clicks Convert to PO |
Deny — wrong role. PR_AUTH_008 restricts the action to roles with enum_stage_role = purchase. The Convert-to-PO endpoint rejects the call regardless of where the user came from. UI hides or disables the button for non-purchase roles; a direct API call returns "You are not authorised to convert this PR to a PO". |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| PUR-VAL-01 | Consolidate two PRs that share vendor but differ in currency | Select line A (vendor_id = V1, currency_id = THB) and line B (vendor_id = V1, currency_id = USD); attempt to merge into a single draft PO group |
Reject — the workbench grouping key is (vendor_id, currency_id) per Section 6 of 02-business-rules.md. The two lines materialise as two separate draft POs (one per currency). Forcing them into a single group via API call is rejected with "Cannot consolidate lines with mismatched currency on a single PO". |
| PUR-VAL-02 | Consolidate two PRs that share currency but differ in vendor | Select line A (vendor_id = V1, currency_id = THB) and line B (vendor_id = V2, currency_id = THB); attempt to merge into a single draft PO group |
Reject — (vendor_id, currency_id) grouping requires vendor_id to match. The lines materialise as two draft POs (one per vendor). API call to merge rejected with "Cannot consolidate lines with mismatched vendor on a single PO". |
| PUR-VAL-03 | Partial-conversion qty exceeds approved_base_qty |
Line L1 has approved_base_qty = 100, no prior bridge rows; Purchaser sets convert-qty to 120 and runs Convert to PO |
Reject — partial-conversion qty per line must satisfy 0 < convert_qty ≤ approved_base_qty − Σ(prior bridge-linked qty). Server rejects with "Convert quantity exceeds the open quantity on this PR line"; bridge integrity guard refuses to write a row whose sum would over-bridge the source line. |
| PUR-VAL-04 | Partial-conversion qty exceeds remaining open qty after a prior partial conversion | Line L1 approved_base_qty = 100; prior bridge row already records converted_qty = 60; Purchaser sets convert-qty to 50 (60 + 50 = 110 > 100) |
Reject — same rule as PUR-VAL-03; server rejects with "Convert quantity exceeds the open quantity on this PR line" and shows the open-qty value (40) in the error toast. |
| PUR-VAL-05 | Pricelist deviation beyond tolerance triggers warning / forces decision | Line snapshotted pricelist_price = 100.00000 ฿; current active pricelist row is 108.00000 ฿ (+8 %, outside the configured ±5 % tolerance) |
The deviation panel shows a red warning. The Purchaser must explicitly choose one of three paths (per 03-user-flow-purchaser.md Section 3): (a) Accept snapshot — confirm and proceed at 100.00000 ฿; (b) Refresh to current — pull 108.00000 ฿ onto the PO line; (c) Raise concern / send back — bounce the PR to the Requestor. Convert-to-PO is blocked until one path is chosen. Direct API call without an explicit deviation decision is rejected with "Pricelist deviation exceeds tolerance — explicit accept/refresh/send-back required before conversion". |
| PUR-VAL-06 | Missing vendor allocation blocks conversion | Line L1 has vendor_id IS NULL (Requestor did not allocate, Purchaser has not yet run Allocate Vendor); Purchaser ticks L1 and runs Convert to PO |
Reject — bridge target needs a vendor. Server rejects with "Cannot convert: line has no vendor allocation. Run Allocate Vendor first."; the workbench flags the line in red and excludes it from the group until vendor allocation is set. |
| PUR-VAL-07 | PR already converted (pr_status = completed) cannot be reopened for further conversion |
Source PR was fully bridged on a prior round (PR_POST_007 flipped to completed); Purchaser opens it from the completed filter and clicks Convert to PO via a stale UI / direct API call |
Reject — completed is terminal for conversion. Server rejects with "This PR has already been fully converted and cannot be reopened"; the workbench filters completed PRs out of the selection pool. |
| PUR-VAL-08 | PR was voided by Finance / sys-admin during the Purchaser's session | Purchaser loaded an approved PR; in the background PR_AUTH_007 flipped pr_status to voided; Purchaser clicks Convert to PO |
Reject — state-machine guard: voided is terminal. The conversion endpoint rejects with "This PR is voided and cannot be converted". The PR's soft budget commitment was released by PR_POST_006. |
| PUR-VAL-09 | Vendor master flipped inactive between PR submit and conversion | The vendor V1 snapshotted on the line was active at PR submit; today tb_vendor.is_active = false (or blacklist flag set); Purchaser clicks Convert to PO |
Reject — live vendor-master lookup invalidates the allocation. Server rejects with "Vendor V1 is no longer active and cannot be used for new POs. Run Allocate Vendor to pick an alternative."; the Purchaser must reassign vendor before retry. |
| PUR-VAL-10 | Bounce-back to Requestor without providing a reason | Open the Send-Back-to-Requestor dialog, leave the reason textarea empty, click Confirm | Reject — reason is mandatory per PR_POST_008 (immutable system comment requires payload) and the dialog contract. UI disables the Confirm button when reason is blank; a direct API call is rejected with "A reason is required to send back this PR". |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| PUR-EDGE-01 | Zero-line conversion attempt (no lines selected) | Purchaser opens the Convert-to-PO workbench, ticks no lines, and clicks Convert to PO | Reject — the conversion endpoint requires at least one bridge row. Server rejects with "Select at least one line to convert"; UI also disables the Convert-to-PO button when no lines are checked. No tb_purchase_order row is created. |
| PUR-EDGE-02 | All lines selected but each with convert-qty = 0 | Purchaser ticks every line but sets convert_qty = 0 on every row, then clicks Convert to PO |
Reject — convert_qty > 0 per line is required (mirrors PUR-VAL-03's lower bound). Server rejects with "Convert quantity must be positive"; UI flags every zero-qty row in red and disables Convert to PO until at least one row has a positive convert-qty. |
| PUR-EDGE-03 | FX rate snapshot exactly equal to PR rate (no divergence) | PR exchange_rate = 35.50000; today's organisation FX rate is also 35.50000; one unbridged line |
Accepted — the PO snapshots exchange_rate = 35.50000 (same as the PR). Bridge row written; PR-side and PO-side base-currency totals match exactly. The snapshot rule is unchanged regardless of whether the rate moved (PUR-HP-08) or held steady (this case). |
| PUR-EDGE-04 | Bridge-table integrity when partial conversion fully completes the last open line | Source PR has three lines (L1, L2, L3); L1 and L2 were fully bridged in a prior round (pr_status stayed approved because L3 still had open qty 40); Purchaser now bridges L3 for the remaining 40 |
Bridge row written; Σ bridge-linked qty for L3 now equals L3.approved_base_qty; all three lines are now fully bridged. PR_POST_007 fires and flips pr_status from approved to completed; soft commitment for L3 hardens into the new PO; PR drops from the Approved PRs queue. |
| PUR-EDGE-05 | Concurrent conversion attempts on the same PR line | Purchaser A and Purchaser B both load approved PR L1 (approved_base_qty = 100, no prior bridge); A ticks L1 at 60 and clicks Convert to PO at T; B ticks L1 at 60 and clicks Convert to PO at T+1s (after A's bridge row commits but before B's client refreshes) |
First conversion wins: a bridge row is written for A with converted_qty = 60, the source line's open qty is now 40. Second conversion (B) is rejected at the server — the bridge-integrity guard re-checks Σ(prior bridge-linked qty) + new converted_qty ≤ approved_base_qty against current state and rejects with "Convert quantity exceeds the open quantity on this PR line" (same path as PUR-VAL-04). B's UI prompts a refresh. No double-bridge is created. |
| PUR-EDGE-06 | Maximum-precision convert qty | Line L1 has approved_base_qty = 1.99999 PCS (Decimal(15, 5)); Purchaser sets convert_qty = 1.99998 |
Accepted — convert_qty validation passes (> 0 and ≤ open_qty at full 5-dp precision); bridge row written with converted_qty = 1.99998; line retains 0.00001 PCS open; PR stays in approved until the residual 0.00001 is also bridged or the PR is explicitly cancelled. |
X-PR-01 (full happy path through to conversion), X-PR-04 (partial conversion), X-PR-07 (Purchaser bounce-back to the Requestor).PR_AUTH_008 — enum_stage_role = purchase owns vendor allocation and PO conversion; PR_AUTH_007 — void scope), Section 5 (PR_POST_005 final approve → approved, PR_POST_007 convert to PO → bridge writes + completed, PR_POST_008 immutable system comments), Section 6 (PR → PO bridge tb_purchase_order_detail_tb_purchase_request_detail, consolidation by (vendor_id, currency_id), partial conversion, FX rate snapshot at PO creation, soft → hard budget commitment).tb_purchase_order_detail_tb_purchase_request_detail (many-to-many PR↔PO line linkage supporting consolidation and partial conversion); tb_purchase_request_detail snapshot columns (vendor_id, vendor_name, pricelist_detail_id, pricelist_no, pricelist_unit, pricelist_price, pricelist_type).../carmen-inventory-frontend-e2e/tests/304-pr-purchaser-journey.spec.ts — persona-journey spec for the Purchaser. Covers list scoping to the Purchase stage, edit-mode permissions (vendor / unit-price / discount / tax-profile editable, approved-qty read-only), Auto Allocate vendors, bulk approve / reject / send-for-review / split, and the TC-PR-070901 golden full-flow scenario (PR approved → Convert to PO → PR completed). Per-action × per-role permission coverage lives in ../carmen-inventory-frontend-e2e/tests/301-pr.spec.ts (purchaseTest fixture). PR Template create / edit coverage adjacent to Purchaser scope is in ../carmen-inventory-frontend-e2e/tests/310-pr-template.spec.ts.../carmen/docs/purchase-request-management/PR-User-Experience.md (Allocate Vendor dialog, Convert-to-PO workbench UX, pricelist-deviation panel), ../carmen/docs/purchase-request-management/purchase-request-module-prd.md (consolidation grouping by vendor + currency, partial-conversion behaviour), ../carmen/docs/purchase-request-management/testing.md (testing levels — convert-to-po and allocate-vendor spec patterns), ../carmen/docs/purchase-request-management/troubleshooting.md (Section 2.3 — PO conversion problems: missing vendor allocation, pricelist deviation, partial-conversion bridge-integrity errors, FX rate snapshot mismatches).PR_POST_007.