Dual control (maker-checker)
A disbursement above the workspace threshold needs a second person to approve it before it can be released. The threshold isTenantConfig.dualControlThresholdCents. When the net to member is at or above the threshold, the instruction’s approvalStatus is set to PENDING and a disbursement.approval.requested event fires. The comparison is net >= threshold. A null threshold means dual control is off, which is the default.
The approver must be a different user from the officer who scheduled the disbursement. The scheduler is recorded as scheduledByUserId, the maker. If the same user tries to approve, the platform refuses with a SELF_APPROVAL error. Maker is never checker.
Approval is gated by the disbursements.approve capability. The Credit Manager role holds it. The Disbursement role does not hold it. That separation is deliberate: the Disbursement team makes the disbursement, and a different role, the Credit Manager, checks it.
Every approve or reject records a required rationale on an immutable DisbursementApproval row, alongside the amount snapshot and the deciding user. An empty rationale is refused.
The audit events are exact: disbursement.approval.requested, disbursement.approval.approved, and disbursement.approval.rejected. A rejected disbursement cannot be released.
The approval status enum is
NOT_REQUIRED, PENDING, APPROVED, REJECTED. Every disbursement below the threshold reads NOT_REQUIRED and skips the second sign-off.AML and sanctions screening
Screening checks every payee against sanctions and PEP lists before funds move, a Proceeds of Crime Act obligation. Screening runs through a provider abstraction. The directory ships a deterministic mock as the default in development and test, a real HTTPsanctions-list stub for production, an optional recording wrapper, and a factory that picks one from configuration. Swapping the vendor is a configuration change, never a code change.
screenInstructionPayees runs the provider over every payee on the instruction, writes one immutable PayeeScreening row per payee, and rolls the per-payee verdicts up onto the instruction. Each row carries the provider, the verdict, a numeric score, and the list of matches. Re-screening appends new rows; the latest set drives the rollup.
The rollup status is the PayeeScreeningStatus enum, four values:
| Status | Meaning |
|---|---|
NOT_SCREENED | No screen has run, or a payee is unscreened. |
CLEAR | No match. Release may proceed. |
REVIEW | A possible match. Needs a human decision. |
BLOCKED | A confirmed match. Release is refused. |
BLOCKED payee makes the instruction BLOCKED, then any REVIEW makes it REVIEW, and only when every payee is CLEAR is the instruction CLEAR.
Screening is required only when TenantConfig.amlScreeningRequired is true. When required, execute is gated on a CLEAR rollup; anything else blocks release with a SCREENING_REQUIRED or SCREENING_NOT_CLEAR error. Screening is run by the disbursements.screen capability, which the Disbursement role holds. The events are disbursement.screening.completed and disbursement.screening.blocked.
Per-officer limits
Each officer can carry a personal ceiling,Membership.disbursementLimitCents. When set, execute refuses a net above that officer’s ceiling with an OFFICER_LIMIT error, and a higher-tier officer must release it. A null limit means no ceiling for that officer.
Auto-disbursement
A workspace can opt in to release a scheduled disbursement automatically when it clears every gate. It is off by default. Auto-disbursement is governed byTenantConfig.autoDisburseEnabled and TenantConfig.autoDisburseThresholdCents. The decision is a pure gate, shouldAutoDisburse, and it fires only when all of these hold:
- The feature is enabled.
- A threshold is configured and the net is at or below it.
- No approval is outstanding (
approvalStatusisNOT_REQUIRED). - Screening is satisfied. When the workspace requires screening, the rollup must be
CLEAR. - The officer is within their personal limit.
disbursement.auto_executed event fires. Auto-disbursement only fires at or below TenantConfig.autoDisburseThresholdCents, which is the gate.
Auto-disbursement is consulted at schedule time, before screening has run, so the schedule path passes the screening rollup as
NOT_SCREENED. A workspace with amlScreeningRequired set to true therefore never auto-disburses: the screening gate cannot be satisfied yet. Auto-disbursement fires for workspaces that do not require screening.Configuring the controls
The thresholds, flags, and per-officer limits are set by theadmin.disbursement.config capability, which the Credit Manager role holds. The tenant.disbursement_config.updated event is registered for the config-change path. No in-app config editor ships yet, so today these values are set out of band and that event is not emitted.