TC-PAY-009: Treasury Verify/Dispute — Maker-Checker Separation of Duties
🆕 New on stage — 2026-07-09 (#528). This TC covers the new maker-checker separation of duties for treasury reconciliation: verifying/disputing a treasury payment entry now requires a permission (
FINANCE:VERIFY:treasury) that is distinct from the permission that approves/rejects a billing payment (FINANCE:EDIT:payments). A new PermissionAction.VERIFY enum value backs this, and the admin Roles/Permissions matrix now has a VERIFY column so custom roles can split the two grants.
Module: Payments — Treasury Reconciliation / RBAC
Primary Test User: admin@rcme.membervu.com / Admin123! (holds both by default); treasurer@demo.membervu.com / Treasurer123! (FINANCE_MANAGER, holds both by default); events@demo.membervu.com / Events123! (used for the negative SoD proof)
Priority: P1 (authorization correctness / financial-controls audit requirement)
Objectives
- Confirm a checker (holding
FINANCE:VERIFY:treasury) can verify aPENDING_VERIFICATION/DISPUTEDtreasury payment entry toVERIFIED, with an audit entry written. - Confirm a checker can dispute an entry to
DISPUTED, and that a previously-VERIFIEDentry is NOT terminal — it can still be disputed for error correction. - Confirm idempotency: re-verifying an already-
VERIFIEDentry is a 200 no-op; re-disputing an already-DISPUTEDentry is a 200 no-op. - Prove the SoD split: a maker-only role (holds
FINANCE:EDIT:paymentsbut notFINANCE:VERIFY:treasury) gets403on treasury verify/dispute; a checker-only role (holdsFINANCE:VERIFY:treasurybut notFINANCE:EDIT:payments) gets403on billing payment approve/reject. - Confirm the online-payment gate: attempting treasury verify/dispute on an online/auto-verified (PayBiz) channel payment returns
400 ONLINE_PAYMENT_GATE. - Confirm no-regression: default
ADMINandFINANCE_MANAGERroles retain both permissions out of the box, so existing tenants see no behavior change until an admin deliberately splits a custom role via the Roles/Permissions matrix.
Scenario summary
| ID | Scenario | Key data assertion |
|---|---|---|
| PAY9-01 | Checker verifies a manual payment | PATCH /api/treasury/payments/:id/verify → 200; externalVerificationStatus: PENDING_VERIFICATION → VERIFIED; audit entry (PaymentAuditAction.STATUS_CHANGED) written; response is exactly { externalVerificationStatus, updatedAt }. |
| PAY9-02 | Checker disputes, then re-verifies (correction path) | .../dispute → 200, → DISPUTED. Then .../verify on that DISPUTED entry → 200, → VERIFIED — proving VERIFIED is not a terminal dead-end for DISPUTED and vice versa. |
| PAY9-03 | Idempotent no-ops | Repeat .../verify on an already-VERIFIED entry → 200, status unchanged, no duplicate audit-driven state flip. Same for repeat .../dispute on DISPUTED. |
| PAY9-04 | SoD negative — maker cannot verify/dispute | A role with ONLY FINANCE:EDIT:payments calling PATCH /api/treasury/payments/:id/verify or .../dispute → 403 Forbidden. |
| PAY9-05 | SoD negative — checker cannot approve/reject billing payments | A role with ONLY FINANCE:VERIFY:treasury calling POST /api/billing/admin/payments/:paymentId/approve or .../reject → 403 Forbidden. |
| PAY9-06 | Online-channel gate | Treasury verify/dispute on a PAYNAMICS (online, auto-verified) channel payment → 400 { code: "ONLINE_PAYMENT_GATE" }, not a silent success. |
| PAY9-07 | Nonexistent payment | PATCH /api/treasury/payments/:id/verify for a non-existent/wrong-tenant id → 404 { code: "PAYMENT_NOT_FOUND" }. |
| PAY9-08 | No-regression on default roles | Default ADMIN (via FINANCE:*:* wildcard) and default FINANCE_MANAGER (explicit grant of both keys) can still perform BOTH maker and checker actions — existing tenants are unaffected until an admin deliberately splits a custom role. |
No-regression note: out of the box,
ADMIN holds FINANCE:VERIFY:treasury via the FINANCE:*:* wildcard, and FINANCE_MANAGER (Treasurer) is granted both FINANCE:EDIT:payments and FINANCE:VERIFY:treasury explicitly. True separation of duties is opt-in — an admin must create/edit a custom role in the Roles/Permissions matrix and assign only one of the two keys to it. Testing the SoD proof (PAY9-04/05) requires that role-setup step first; see the detailed guide.
➡️ Full click-by-click steps: TC-PAY-009 (Detailed).