> ## Documentation Index
> Fetch the complete documentation index at: https://docs.compuzign.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The four disbursement controls

> The four controls that wrap fund release: maker-checker dual control, AML and sanctions screening, per-officer limits, and optional auto-disbursement.

Four controls sit around the moment funds are released: maker-checker dual control, AML and sanctions screening, a per-officer limit, and optional auto-disbursement. Each is off by default, so a workspace that configures nothing keeps the simple release path.

## Dual control (maker-checker)

A disbursement above the workspace threshold needs a second person to approve it before it can be released.

The threshold is `TenantConfig.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.

<Note>
  The approval status enum is `NOT_REQUIRED`, `PENDING`, `APPROVED`, `REJECTED`. Every disbursement below the threshold reads `NOT_REQUIRED` and skips the second sign-off.
</Note>

## 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 HTTP `sanctions-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.       |

The rollup is dominance-based: any `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 by `TenantConfig.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 (`approvalStatus` is `NOT_REQUIRED`).
* Screening is satisfied. When the workspace requires screening, the rollup must be `CLEAR`.
* The officer is within their personal limit.

When all hold, the disbursement executes immediately and a `disbursement.auto_executed` event fires. Auto-disbursement only fires at or below `TenantConfig.autoDisburseThresholdCents`, which is the gate.

<Note>
  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.
</Note>

## Configuring the controls

The thresholds, flags, and per-officer limits are set by the `admin.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.
