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

  1. Confirm a checker (holding FINANCE:VERIFY:treasury) can verify a PENDING_VERIFICATION/DISPUTED treasury payment entry to VERIFIED, with an audit entry written.
  2. Confirm a checker can dispute an entry to DISPUTED, and that a previously-VERIFIED entry is NOT terminal — it can still be disputed for error correction.
  3. Confirm idempotency: re-verifying an already-VERIFIED entry is a 200 no-op; re-disputing an already-DISPUTED entry is a 200 no-op.
  4. Prove the SoD split: a maker-only role (holds FINANCE:EDIT:payments but not FINANCE:VERIFY:treasury) gets 403 on treasury verify/dispute; a checker-only role (holds FINANCE:VERIFY:treasury but not FINANCE:EDIT:payments) gets 403 on billing payment approve/reject.
  5. Confirm the online-payment gate: attempting treasury verify/dispute on an online/auto-verified (PayBiz) channel payment returns 400 ONLINE_PAYMENT_GATE.
  6. Confirm no-regression: default ADMIN and FINANCE_MANAGER roles 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

IDScenarioKey data assertion
PAY9-01Checker verifies a manual paymentPATCH /api/treasury/payments/:id/verify200; externalVerificationStatus: PENDING_VERIFICATION → VERIFIED; audit entry (PaymentAuditAction.STATUS_CHANGED) written; response is exactly { externalVerificationStatus, updatedAt }.
PAY9-02Checker disputes, then re-verifies (correction path).../dispute200, → DISPUTED. Then .../verify on that DISPUTED entry → 200, → VERIFIED — proving VERIFIED is not a terminal dead-end for DISPUTED and vice versa.
PAY9-03Idempotent no-opsRepeat .../verify on an already-VERIFIED entry → 200, status unchanged, no duplicate audit-driven state flip. Same for repeat .../dispute on DISPUTED.
PAY9-04SoD negative — maker cannot verify/disputeA role with ONLY FINANCE:EDIT:payments calling PATCH /api/treasury/payments/:id/verify or .../dispute403 Forbidden.
PAY9-05SoD negative — checker cannot approve/reject billing paymentsA role with ONLY FINANCE:VERIFY:treasury calling POST /api/billing/admin/payments/:paymentId/approve or .../reject403 Forbidden.
PAY9-06Online-channel gateTreasury verify/dispute on a PAYNAMICS (online, auto-verified) channel payment → 400 { code: "ONLINE_PAYMENT_GATE" }, not a silent success.
PAY9-07Nonexistent paymentPATCH /api/treasury/payments/:id/verify for a non-existent/wrong-tenant id404 { code: "PAYMENT_NOT_FOUND" }.
PAY9-08No-regression on default rolesDefault 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).