---
id: WF-ACC-REC-002
title: Record a payout once
trigger: >-
  Staff submit a manually entered payout from the console or another authorized
  client.
product_area: accounting
module: reconciliation
revision: 3
status: draft
design_intent: confirmed
origin: >-
  ACCT-M8 plan §2 scope (manual payouts with duplicate rules; idempotency of
  every money command) and D8 (manual entry; processor import later), approved
  by Andrew 2026-10-22
implementation_evidence: observed
inference: []
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
implements:
  - CAP-ACC-REC-001
  - CAP-ACC-REC-020
moves:
  - STATE-ACC-REC-001
  - STATE-ACC-REC-002
crosses:
  - SVC-ACC-REC-reconciliation
idempotency: >-
  Claim-first request keys are scoped to caller and operation. Identical
  completed bodies replay; changed bodies fail, and leased claims can delay
  retry after an uncertain commit.
concurrency: >-
  Merchant locks serialize duplicate checks for one account. Across accounts,
  numbering retries unique conflicts under savepoints rather than locking a
  single allocator.
hops:
  - 'n': 1
    actor_or_service: Bookkeeper in the console
    action: >-
      Collect merchant, amount, deposit day, reference and optional payout
      details.
    anchor: 'react-console:components/accounting/reconciliation/add-payout-sheet.tsx'
    seam: sync
    on_failure: Show the service error and retain the entry sheet.
    left_inconsistent: No payout is created by a refused request.
    observer: The staff member sees the toast.
    provenance: observed
    source: 'react-console:lib/accounting/reconciliation/store.tsx:createPayout'
  - 'n': 2
    actor_or_service: Reconciliation request-key ledger
    action: Acquire the caller’s create key or return its retained result.
    anchor: >-
      services-accounting:modules/reconciliation/services/persistence/idempotency-keys.ts:withIdempotencyKey
    seam: sync
    on_failure: Reject reused bodies or a request still in progress.
    left_inconsistent: An uncertain outcome can retain the key until its lease expires.
    observer: The response code identifies retry state.
    provenance: observed
    source: >-
      services-accounting:modules/reconciliation/services/persistence/idempotency-keys.ts
  - 'n': 3
    actor_or_service: Payout persistence
    action: >-
      Lock the merchant, validate accounts and apply reference and look-alike
      duplicate checks.
    anchor: >-
      services-accounting:modules/reconciliation/services/persistence/payouts.ts:createPayout
    seam: sync
    on_failure: >-
      Refuse incompatible accounts or unacknowledged duplicates; release a
      failed effect claim.
    left_inconsistent: The business transaction creates no payout.
    observer: The response identifies duplicate candidates or invalid accounts.
    provenance: observed
    source: >-
      services-accounting:modules/reconciliation/services/persistence/payouts.ts:createPayout
  - 'n': 4
    actor_or_service: Payout persistence
    action: >-
      Allocate a number and commit payout, evidence, creation activity and the
      completed answer together.
    anchor: >-
      services-accounting:modules/reconciliation/services/persistence/payouts.ts:createPayout
    seam: sync
    on_failure: >-
      Rollback removes the business writes; uncertain completion can retain the
      earlier key claim.
    left_inconsistent: 'No partial payout commits, though request-key state may remain unresolved.'
    observer: The API response or retry exposes the outcome.
    provenance: observed
    source: >-
      services-accounting:modules/reconciliation/services/persistence/payouts.ts:createPayout
  - 'n': 5
    actor_or_service: Console store
    action: Reload the unit’s records and navigate to the new payout.
    anchor: 'react-console:lib/accounting/reconciliation/store.tsx:run'
    seam: sync
    on_failure: A failed reload shows an error and may leave the browser’s previous state.
    left_inconsistent: The created payout can exist while the screen is stale.
    observer: The staff member sees the reload or command error.
    provenance: observed
    source: 'react-console:lib/accounting/reconciliation/store.tsx:run'
branches:
  - The sheet does not submit look-alike acknowledgement.
  - Duplicate preview is a separate read that creates no payout.
waits: []
evidence:
  - >-
    services-accounting at f47cb263:
    modules/reconciliation/services/persistence/payouts.ts, idempotency-keys.ts,
    payout-numbers.ts, payout-refusals.ts; aws/api/payouts.routes.ts.
  - >-
    react-console at baf6dae3:
    components/accounting/reconciliation/add-payout-sheet.tsx;
    lib/accounting/reconciliation/store.tsx, client.ts.
controls: [TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-DAT-001, TC-LOG-001, TC-LOG-002, TC-LOG-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-002, TC-PI-003]
---
# Record a payout once

## What It Does

Staff enter a bank deposit and the service checks duplicate evidence before recording it. The successful payout and its retained command answer commit together.

## The Flow

```mermaid
sequenceDiagram
  participant P as Bookkeeper (console)
  participant K as Request-key ledger
  participant S as Payout persistence
  participant D as Accounting database
  P->>K: create with a request key
  K-->>P: replay or refuse a repeated key
  K->>S: key claimed
  S->>D: lock merchant account, check duplicates
  alt duplicate reference or unacknowledged look-alike
    S-->>P: refused, with candidates
  else clear
    S->>D: number, insert, events, key completed
    D-->>P: the payout
  end
```

## When Something Fails

Business failure rolls back payout creation. Retrying an uncertain result with the same key can recover the retained answer; a new console key instead invokes duplicate checks and is not guaranteed to replay.

## Open Design Questions

Console retry keys and look-alike acknowledgement need an implementation decision.
