---
id: CAP-ACC-REC-020
title: Separate duties and retain trustworthy reconciliation records
feature: FEAT-ACC-REC-07
product_area: accounting
module: reconciliation
revision: 2
status: draft
product_intent: confirmed
implementation_evidence: partially-observed
supersedes:
  - react-console acceptance inventory CAP-ACC-REC-027
  - react-console acceptance inventory CAP-ACC-REC-025 (second-person setting)
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§2: permissions (preparer and approver
    policies); idempotency of every money command)
  - 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D3'
  - 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D5'
  - 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D6 (architecture §3.4 U13)'
  - >-
    services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§4: the architecture §6 invariants hold;
    INV-01, INV-03)
purpose: 'Controllers need permissions, retry safeguards and history that support accountable financial work.'
outcomes:
  - Roles can separate preparation from sensitive approval.
  - Keyed retries preserve committed financial effects.
  - Committed activity and workspace isolation support accountable review.
concepts:
  - CON-ACC-REC-003
  - CON-ACC-REC-001
  - CON-ACC-REC-002
  - CON-ACC-REC-004
actors:
  - ACT-ACC-019
  - ACT-ACC-017
  - ACT-ACC-018
  - ACT-ACC-007
  - ACT-ACC-011
surfaces:
  - console
  - api
preconditions: []
postconditions: []
permissions:
  - key: accounting.application.access
    where_checked: api
    scope: tenant
  - key: accounting.payouts.read
    where_checked: api
    scope: tenant
  - key: accounting.reconciliation-batches.finalize
    where_checked: api
    scope: tenant
  - key: accounting.reconciliation-batches.reverse
    where_checked: api
    scope: tenant
  - key: accounting.settlement-items.resolve
    where_checked: api
    scope: tenant
  - key: accounting.reconciliation-exceptions.accept
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-ACC-REC-020-01
    proved_by: [ATS-ACC-REC-020-01, TST-ACC-REC-097]
    statement: >-
      The catalogue defines sixteen reconciliation keys: read-tier inspection, user-tier ordinary changes and
      four sensitive administrator approvals for finalize, reverse, resolve and accept, without bulk keys.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-accounting:tests/unit/reconciliation-permissions.spec.ts#makes every approval tier admin and
      sensitive; services-accounting:tests/unit/reconciliation-permissions.spec.ts#publishes exactly the
      reconciliation keys of the architecture, no bulk among them;
      services-accounting:tests/unit/reconciliation-permissions.spec.ts#makes each read tier read, so the Read
      Only policy carries it; services-accounting:tests/unit/reconciliation-permissions.spec.ts#makes the
      ordinary writes tier user: reconciliation’s records are records, not configuration
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§2: permissions)'
  - id: BR-CAP-ACC-REC-020-02
    proved_by: [ATS-ACC-REC-011-14, ATS-ACC-REC-016-10, ATS-ACC-REC-020-02, ATS-ACC-REC-020-11, ATS-ACC-REC-020-13, TST-ACC-REC-097]
    statement: >-
      Declared preparer and approver policies separate duties, with preparers reading and doing ordinary work
      and approvers adding the four sensitive approvals; a role must assign a policy before it grants access.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-accounting:tests/unit/reconciliation-permissions.spec.ts#lets the preparer record payouts and
      prepare reconciliations, and approve nothing;
      services-accounting:tests/unit/reconciliation-permissions.spec.ts#lets the approver do what the preparer
      does and the four approvals; services-accounting:tests/unit/reconciliation-permissions.spec.ts#files
      both policies under the accounting application
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§2: preparer and approver policies)'
  - id: BR-CAP-ACC-REC-020-03
    proved_by: [ATS-ACC-REC-020-03, TST-ACC-REC-016, TST-ACC-REC-026]
    statement: >-
      Permission grants operate throughout the workspace, while financial records retain their unit context
      for accounting.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: observed
    source: 'services-accounting:aws/accounting.service.ts:ReconciliationBuiltInPolicies'
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D5'
  - id: BR-CAP-ACC-REC-020-04
    proved_by: [ATS-ACC-REC-020-04, TST-ACC-REC-017, TST-ACC-REC-031, TST-ACC-REC-032]
    statement: >-
      Keyed creation, payout void or verification, manual addition, finalize, reverse, split, resolution and
      acceptance require a header key up to 255 characters, scope retries to caller and operation, reject
      changed bodies and retain keys until purge.
    kind: validation
    enforced_at:
      - api
      - service
      - database
    must_prove: true
    provenance: observed
    source: >-
      services-accounting:modules/reconciliation/services/persistence/idempotency-keys.ts:requireIdempotencyKey;
      services-accounting:modules/reconciliation/aws/api/payouts.routes.ts;
      services-accounting:modules/reconciliation/aws/api/reconciliation.routes.ts;
      services-accounting:modules/reconciliation/aws/api/matching.routes.ts;
      services-accounting:modules/reconciliation/aws/api/batch-lifecycle.routes.ts;
      services-accounting:modules/reconciliation/aws/api/outstanding.routes.ts
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§2: idempotency of every money command)'
  - id: BR-CAP-ACC-REC-020-05
    proved_by: [ATS-ACC-REC-001-14, ATS-ACC-REC-016-11, ATS-ACC-REC-020-05, ATS-ACC-REC-020-12, ATS-ACC-REC-020-14, TST-ACC-REC-038, TST-ACC-REC-064]
    statement: >-
      Audit events keep the actual actor and bounded detail in the command transaction, with keys where
      applicable; bulk claims may emit several, repeat no-ops and replay may emit none, and rollback emits
      none, while history permits deletion only in purge.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: observed
    source: >-
      services-accounting:modules/reconciliation/services/persistence/reconciliation-events.ts:recordEvent;
      services-accounting:modules/reconciliation/services/persistence/reconciliation-allocations.ts;
      services-accounting:modules/reconciliation/db/reconciliation-events.table.ts;
      services-accounting:modules/reconciliation/services/persistence/idempotency-keys.ts
    origin: >-
      services-accounting:docs/reconciliation/brief.md#Controls (Andrew’s product brief, 2026-10-15): audit
      trails
  - id: BR-CAP-ACC-REC-020-06
    proved_by: [ATS-ACC-REC-020-06, TST-ACC-REC-029, TST-ACC-REC-093]
    statement: Refusals carry a stable machine code independently of their displayed wording.
    kind: policy
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/e2e/refusal-codes.e2e.spec.js#keeps the code a refusal
      carries: a create without an Idempotency-Key answers 400 idempotency_key_required;
      services-accounting:modules/reconciliation/tests/e2e/refusal-codes.e2e.spec.js#answers a payout edit
      whose body breaks its schema 400 validation_failed;
      services-accounting:modules/reconciliation/tests/e2e/refusal-codes.e2e.spec.js#answers an unknown payout
      404 not_found; services-accounting:modules/reconciliation/tests/e2e/refusal-codes.e2e.spec.js#answers a
      payout edit at a version already moved on 409 stale_version;
      services-accounting:modules/reconciliation/tests/e2e/refusal-codes.e2e.spec.js#answers a cursor the list
      did not answer 400 invalid_request;
      services-accounting:modules/reconciliation/tests/unit/rules/refusal-codes.spec.js#keeps the code a
      refusal carries; services-accounting:modules/reconciliation/tests/unit/rules/refusal-codes.spec.js#names
      a state refusal without a refusal word illegal_operation
    origin: none
  - id: BR-CAP-ACC-REC-020-07
    proved_by: [ATS-ACC-REC-004-09, ATS-ACC-REC-020-07, TST-ACC-REC-018, TST-ACC-REC-033]
    statement: Workspace isolation prevents reading or writing another customer’s reconciliation records.
    kind: invariant
    enforced_at:
      - database
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/db/payouts.table.integration.spec.js#refuses
      a row written for another tenant;
      services-accounting:modules/reconciliation/tests/integration/db/payouts.table.integration.spec.js#hides
      them from another tenant;
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0610.spec.ts#ACT-ACC-0610
    origin: >-
      services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§4: the architecture §6 invariants hold;
      INV-01)
  - id: BR-CAP-ACC-REC-020-08
    proved_by: [ATS-ACC-REC-020-03, TST-ACC-REC-016, TST-ACC-REC-026]
    statement: 'Missing authentication fails before access, and denied Accounting access prevents reconciliation reads.'
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/e2e/reconciliation-lists.e2e.spec.js#refuses a request
      without a security context with 401;
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0608.spec.ts#ACT-ACC-0608;
      services-accounting:modules/reconciliation/tests/e2e/reconciliation-lists.e2e.spec.js#refuses a
      principal denied accounting.application.access with 403
    origin: none
  - id: BR-CAP-ACC-REC-020-09
    proved_by: [ATS-ACC-REC-020-08, TST-ACC-REC-087]
    statement: >-
      Amounts use integer minor units, reject arithmetic beyond the exact safe range and display the
      currency’s decimal places.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/unit/rules/money.spec.js#refuses a sum past the
      safe-integer range; services-accounting:modules/reconciliation/tests/unit/rules/money.spec.js#refuses a
      fraction, naming what it expected;
      services-accounting:modules/reconciliation/tests/unit/rules/money.spec.js#formats an exponent of 0 with
      no decimal point
    origin: >-
      services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§4: the architecture §6 invariants hold;
      INV-03)
  - id: BR-CAP-ACC-REC-020-10
    proved_by: [ATS-ACC-REC-020-09]
    statement: >-
      Second-person finalization defaults on and can be represented as waived in settings, but no current
      product operation or screen changes that setting.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-accounting:modules/books/services/persistence/settings.ts:toSettings'
    origin: >-
      services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D3 (turning it off is an audited
      administrator’s change)
  - id: BR-CAP-ACC-REC-020-11
    proved_by: []
    statement: >-
      Console action gates cover creation, finalize, reverse, verification, resolution and acceptance with
      permission hints; the exception-note button checks text presence without update permission.
    kind: permission
    enforced_at:
      - console
    must_prove: false
    provenance: observed
    source: 'react-console:components/accounting/reconciliation/exceptions-screen.tsx:ExceptionDetail'
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D6 (architecture §3.4 U13)'
  - id: BR-CAP-ACC-REC-020-12
    proved_by: []
    statement: >-
      Exports retain reconciliation record identities and relationships while excluding operational retry keys
      and integration connection or execution references.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: observed
    source: 'services-accounting:modules/reconciliation/export/reconciliation.dataset.ts:reconciliationExport'
    origin: none
  - id: BR-CAP-ACC-REC-020-13
    proved_by: [ATS-ACC-REC-020-10]
    statement: >-
      Evidence exports contain asset references without copyable service addresses, so a retained reference
      alone does not prove that the package contains the document.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-accounting:modules/reconciliation/export/reconciliation.dataset.ts:reconciliationExport'
    origin: none
scenarios:
  - id: SC-CAP-ACC-REC-020-01
    given: 'People hold preparer, approver and access-only roles.'
    when: Exercise reconciliation under each role.
    then: 'Preparation and approval follow their distinct grants, while access-only cannot read records.'
    provenance: test
    source: >-
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0608.spec.ts#ACT-ACC-0608
  - id: SC-CAP-ACC-REC-020-02
    given: A finalize already succeeded under its request key.
    when: Retry the identical request.
    then: The saved answer returns with no further ledger request.
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/e2e/batch-lifecycle.e2e.spec.js#replays the same answer
      for the same Idempotency-Key
edge_cases:
  - id: SC-CAP-ACC-REC-020-03
    given: Two colleagues choose identical key text for payout creation.
    when: Submit as each caller.
    then: They remain separate requests and payout duplicate checks govern the second creation.
    provenance: test
    source: >-
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0609.spec.ts#ACT-ACC-0609
constraints: []
depends_on: []
related:
  - CAP-ACC-REC-011
  - CAP-ACC-REC-013
  - CAP-ACC-REC-016
journeys:
  - JNY-ACC-REC-006
open_questions: []
evidence:
  - >-
    Examined supporting service material: aws/accounting.service.ts (PayoutActions,
    ReconciliationBatchActions, SettlementItemActions, ReconciliationExceptionActions,
    ReconciliationBuiltInPolicies); tests/unit/reconciliation-permissions.spec.ts;
    modules/reconciliation/services/persistence/idempotency-keys.ts, reconciliation-events.ts,
    rules/refusal-codes.ts, rules/money.ts; db/idempotency-keys.table.ts, reconciliation-events.table.ts;
    export/reconciliation.dataset.ts, reconciliation.reader.ts; modules/books/services/persistence/settings.ts
    (dependency: the second-person setting); tests integration db idempotency-keys and reconciliation-events
    table specs, reconciliation-events.integration.spec.js, e2e refusal-codes, unit refusal-codes and money
    specs.
  - >-
    Examined supporting console material: lib/authz/permissions.ts, lib/authz/backend-aliases.ts
    (reconciliation aliases); acceptance journeys ACT-ACC-0608, ACT-ACC-0609, ACT-ACC-0610, ACT-ACC-0628,
    ACT-ACC-0629.
controls: [TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-IAM-006, 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-DAT-002, TC-DAT-003, TC-DAT-004, TC-DAT-005, TC-LOG-001, TC-LOG-002, TC-LOG-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-005, TC-AI-005, TC-PRV-001, TC-PRV-002]
exercised_by: [FAS-ACC-REC-07-01, FAS-ACC-REC-07-02, FAS-ACC-REC-07-03, FAS-ACC-REC-07-04, FAS-ACC-REC-07-05]
suites: [TST-ACC-REC-016, TST-ACC-REC-017, TST-ACC-REC-018, TST-ACC-REC-026, TST-ACC-REC-029, TST-ACC-REC-031, TST-ACC-REC-032, TST-ACC-REC-033, TST-ACC-REC-038, TST-ACC-REC-064, TST-ACC-REC-087, TST-ACC-REC-093, TST-ACC-REC-097]
---

# Separate duties and retain trustworthy reconciliation records

## Purpose

Controllers need permissions, retry safeguards and history that support accountable financial work.

## How It Works

A controller assigns roles using the declared preparation and approval policies. Rhythm evaluates those grants at service boundaries and narrows applicable screen actions. Keyed financial decisions can be retried without repeating their committed effect. History retains committed activity while isolation keeps workspace records separate. Missing waiver administration, incomplete file export and the ungated note control remain design concerns.

## Relationships

Platform owns role assignment, Books stores the approval setting and reconciliation workflows use the shared permission, key and activity controls.
