---
id: CAP-ACC-REC-011
title: Finalize a balanced explanation with eligible approval
feature: FEAT-ACC-REC-03
product_area: accounting
module: reconciliation
revision: 2
status: draft
product_intent: confirmed
implementation_evidence: observed
supersedes:
  - react-console acceptance inventory CAP-ACC-REC-005
  - react-console acceptance inventory CAP-ACC-REC-025 (second-person finalize)
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D3'
  - 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D7'
  - 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D2'
  - >-
    services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§4: the architecture §6 invariants hold;
    INV-07, INV-08, INV-12, INV-15, INV-16)
purpose: Approvers need a protected financial record of a prepared deposit explanation.
outcomes:
  - Exact recomputed balance is required for finalization.
  - Independent approval is required by default and waived decisions stay marked.
  - Generated accounting distinguishes the transfer from individual recognition effects.
concepts:
  - CON-ACC-REC-003
  - CON-ACC-REC-001
actors:
  - ACT-ACC-018
surfaces:
  - console
  - api
preconditions: []
postconditions: []
permissions:
  - key: accounting.reconciliation-batches.finalize
    where_checked: api
    scope: tenant
  - key: accounting.reconciliation-batches.finalize
    where_checked: console
    scope: tenant
rules:
  - id: BR-CAP-ACC-REC-011-01
    proved_by: [ATS-ACC-REC-011-01, TST-ACC-REC-023, TST-ACC-REC-049]
    statement: >-
      Finalize checks the request key and read version, replays a successful identical call without another
      ledger request and rejects a fresh-key attempt on the finalized record.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      another key after success, naming the state (DEF-ACT-0596);
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#replays
      the stored answer for the same key and request;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#asks
      the ledger for nothing more on the replay;
      services-accounting:modules/reconciliation/tests/e2e/batch-lifecycle.e2e.spec.js#refuses a request
      without an Idempotency-Key with 400;
      services-accounting:modules/reconciliation/tests/e2e/batch-lifecycle.e2e.spec.js#refuses a body whose
      version is not a number with 400
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§2: idempotency of every money command)'
  - id: BR-CAP-ACC-REC-011-02
    proved_by: [ATS-ACC-REC-011-02, ATS-ACC-REC-011-12, TST-ACC-REC-007, TST-ACC-REC-023, TST-ACC-REC-036, TST-ACC-REC-049]
    statement: >-
      Finalize recomputes the difference from actual claims and payout links, rejects any nonzero amount with
      an audited refusal and leaves the reconciliation open.
    kind: calculation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a batch whose counters balance but whose rows do not;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a batch one minor unit short, naming the difference;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#leaves
      the refused batch open;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#records
      the refusal as batch.finalize-refused with the difference;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#finalizes
      a batch whose rows balance whatever its counters say, writing the rows’ totals;
      services-accounting:modules/reconciliation/tests/e2e/batch-lifecycle.e2e.spec.js#refuses an unbalanced
      batch with 409, its code and its difference;
      services-accounting:modules/reconciliation/tests/integration/db/reconciliation-batches.table.integration.spec.js#refuses
      a finalized batch whose difference is not zero (INV-07);
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0597.spec.ts#ACT-ACC-0597
    origin: >-
      services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§4: the architecture §6 invariants hold;
      INV-07)
    examples:
      positive: A reconciliation one cent short is refused with a difference of -1.
      negative: Counters that read zero over rows that do not are refused.
  - id: BR-CAP-ACC-REC-011-03
    proved_by: [ATS-ACC-REC-009-12, ATS-ACC-REC-011-03, ATS-ACC-REC-011-11, ATS-ACC-REC-011-12, ATS-ACC-REC-011-14, ATS-ACC-REC-020-11, TST-ACC-REC-001, TST-ACC-REC-007, TST-ACC-REC-023, TST-ACC-REC-049, TST-ACC-REC-094]
    statement: >-
      The default second-person policy excludes the creator and anyone who claimed, released, added manual
      items or linked payouts, recording a refused self-approval.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a person who claimed items in it;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      the person who created the batch;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#records
      the refusal as batch.finalize-refused;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#lets
      the preparer finalize where the tenant does not require a second person, waived;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#records
      segregation.waived for that finalize;
      services-accounting:modules/reconciliation/tests/unit/rules/segregation.spec.js#refuses a preparer while
      a second person is required;
      services-accounting:modules/reconciliation/tests/unit/rules/segregation.spec.js#records any finalize as
      waived when the tenant does not require a second person;
      services-accounting:modules/reconciliation/tests/e2e/batch-lifecycle.e2e.spec.js#refuses the preparer
      with 403 segregation-required;
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0590.spec.ts#ACT-ACC-0590;
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0597.spec.ts#ACT-ACC-0597
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D3'
  - id: BR-CAP-ACC-REC-011-04
    proved_by: [ATS-ACC-REC-011-03, ATS-ACC-REC-011-14, ATS-ACC-REC-020-11, TST-ACC-REC-023, TST-ACC-REC-049, TST-ACC-REC-094]
    statement: >-
      With the second-person requirement disabled, preparers can finalize and every finalization records that
      the control was waived.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#records
      segregation.waived for that finalize;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      the person who created the batch;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a person who claimed items in it;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#records
      the refusal as batch.finalize-refused;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#lets
      the preparer finalize where the tenant does not require a second person, waived;
      services-accounting:modules/reconciliation/tests/unit/rules/segregation.spec.js#refuses a preparer while
      a second person is required;
      services-accounting:modules/reconciliation/tests/unit/rules/segregation.spec.js#records any finalize as
      waived when the tenant does not require a second person;
      services-accounting:modules/reconciliation/tests/e2e/batch-lifecycle.e2e.spec.js#refuses the preparer
      with 403 segregation-required
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D3'
  - id: BR-CAP-ACC-REC-011-05
    proved_by: [ATS-ACC-REC-011-04, ATS-ACC-REC-011-12, TST-ACC-REC-007, TST-ACC-REC-049]
    statement: >-
      Finalization rejects a voided reconciliation, absent payout links, voided linked payouts and resolved
      claimed items.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a batch whose item was resolved after it was claimed;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a batch whose payout was voided after it was linked;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a batch that explains no payout;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a voided batch;
      react-console:acceptance/accounting/playwright/journeys/reconciliation-close/ACT-ACC-0597.spec.ts#ACT-ACC-0597
    origin: none
  - id: BR-CAP-ACC-REC-011-06
    proved_by: [ATS-ACC-REC-009-12, ATS-ACC-REC-010-12, ATS-ACC-REC-011-05, ATS-ACC-REC-011-11, ATS-ACC-REC-014-11, TST-ACC-REC-001, TST-ACC-REC-049, TST-ACC-REC-091]
    statement: >-
      Finalize creates a single transfer request containing nonzero net lines per bank and a clearing total
      when nonzero, plus required unrecognized manual or imported fee, reserve and adjustment requests; an
      all-zero transfer is omitted.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-accounting:modules/reconciliation/services/batch-lifecycle/finalize-batch.ts:plansOf'
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D7'
    examples:
      positive: 'A 9,725.00 deposit with a 225.00 fee and 50.00 reserve asks for three balanced entries.'
      negative: A zero payout asks for its fee only.
  - id: BR-CAP-ACC-REC-011-07
    proved_by: [ATS-ACC-REC-011-06, TST-ACC-REC-049, TST-ACC-REC-051]
    statement: >-
      Missing default accounts stop finalization before writes and identify the account purpose requiring
      setup.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a fee in a unit with no processor_fee_expense default;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-posts-to-subledger.integration.spec.js#is
      refused naming the missing default;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-posts-to-subledger.integration.spec.js#asks
      the ledger for nothing, so batch posting has nothing to post for it
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D7 (refused by name when a default is missing)'
  - id: BR-CAP-ACC-REC-011-08
    proved_by: [ATS-ACC-REC-010-12, ATS-ACC-REC-011-05, ATS-ACC-REC-014-11, TST-ACC-REC-049, TST-ACC-REC-091]
    statement: >-
      After operational batch posting, reconciliation entries offset the posted captures and refunds it
      explains in processor clearing.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-posts-to-subledger.integration.spec.js
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D2'
  - id: BR-CAP-ACC-REC-011-09
    proved_by: [ATS-ACC-REC-011-07, ATS-ACC-REC-011-13, TST-ACC-REC-049]
    statement: >-
      Finalized holdings, notes, totals and payout amounts remain sealed; reversal is the supported way to
      release them.
    kind: lifecycle
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#locks
      the finalized batch against updates and deletes;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      releasing one of its claims (RHF30);
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      changing its payout’s amount (RHF30);
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      changing the batch itself (RHF30);
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#resolves
      the payout’s unbalanced-payout exception naming the batch;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#records
      batch.finalized by the approver with the posting dates, batch and keys
    origin: >-
      services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§4: the architecture §6 invariants hold;
      INV-08)
  - id: BR-CAP-ACC-REC-011-10
    proved_by: [ATS-ACC-REC-011-07, ATS-ACC-REC-011-13, TST-ACC-REC-049]
    statement: >-
      Finalize closes open unbalanced-payout exceptions and records the finalizer, operational batch, dates
      and ledger keys.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#resolves
      the payout’s unbalanced-payout exception naming the batch;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#locks
      the finalized batch against updates and deletes;
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      releasing one of its claims (RHF30);
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      changing its payout’s amount (RHF30);
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      changing the batch itself (RHF30);
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#records
      batch.finalized by the approver with the posting dates, batch and keys
    origin: >-
      services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D6 (architecture §3.4 U10: finalize resolves
      only exceptions whose condition it cleared)
  - id: BR-CAP-ACC-REC-011-11
    proved_by: [ATS-ACC-REC-011-08, ATS-ACC-REC-011-15, ATS-ACC-REC-018-13, TST-ACC-REC-049, TST-ACC-REC-050]
    statement: >-
      Period locking serializes finalize with close, marks closed-period effects late and uses the
      default-selected operational batch’s date only when that batch accepts work in an open period, without
      automatically selecting a later batch.
    kind: policy
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: observed
    source: >-
      services-accounting:modules/reconciliation/services/persistence/reconciliation-ledger-postings.ts:recordPostings
    origin: >-
      services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D10 (§4: the architecture §6 invariants hold;
      INV-16)
  - id: BR-CAP-ACC-REC-011-12
    proved_by: [ATS-ACC-REC-011-09, TST-ACC-REC-023, TST-ACC-REC-073, TST-ACC-REC-082]
    statement: >-
      The sensitive finalize permission and preparation guards determine whether finalize is offered,
      including an explanation when preparation prevents it.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/reconciliation-batches/batch-views.integration.spec.js#names
      why finalize is withheld from the person who prepared it;
      services-accounting:modules/reconciliation/tests/e2e/batch-lifecycle.e2e.spec.js#refuses a principal
      without accounting.reconciliation-batches.finalize with 403;
      services-accounting:modules/reconciliation/tests/integration/services/reconciliation-batches/batch-views.integration.spec.js#offers
      finalize to a person who did not prepare it;
      services-accounting:modules/reconciliation/tests/integration/services/reconciliation-batches/batch-views.integration.spec.js#does
      not offer finalize to the person who prepared it, the tenant requiring a second person;
      services-accounting:modules/reconciliation/tests/integration/services/reconciliation-batches/batch-views.integration.spec.js#offers
      finalize to the preparer once the tenant turns the second-person rule off;
      services-accounting:modules/reconciliation/tests/unit/rules/batch-actions.spec.js#offers finalize only
      while balanced; services-accounting:modules/reconciliation/tests/unit/rules/batch-actions.spec.js#keeps
      only the actions whose permission the caller holds;
      services-accounting:modules/reconciliation/tests/unit/rules/batch-actions.spec.js#offers a finalized
      batch its reversal alone
    origin: 'services-accounting:docs/milestones/plans/ACCT-M8-plan.md#D3'
  - id: BR-CAP-ACC-REC-011-13
    proved_by: [ATS-ACC-REC-011-10, TST-ACC-REC-022]
    statement: >-
      Console finalize is hidden from a disqualified preparer, saves current draft changes first and labels a
      waived result as finalized without second approval.
    kind: policy
    enforced_at:
      - console
    must_prove: false
    provenance: observed
    source: 'react-console:components/accounting/reconciliation/workspace/workspace.tsx:ReconWorkspace'
    origin: none
scenarios:
  - id: SC-CAP-ACC-REC-011-01
    given: The sample explanation balances with fee and reserve entries prepared by one person.
    when: 'Have that preparer attempt approval, then an independent approver finalize.'
    then: >-
      Preparation prevents self-approval and the independent decision creates the required transfer and
      recognitions.
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/e2e/reconciliation-scenario.e2e.spec.js#step 7: posts
      the fee 22500, the reserve 5000 and the bank 972500 against the clearing account, each balanced
  - id: SC-CAP-ACC-REC-011-02
    given: The explanation differs by one minor unit.
    when: Attempt finalize.
    then: The difference is reported and the draft stays open.
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#refuses
      a batch one minor unit short, naming the difference
edge_cases:
  - id: SC-CAP-ACC-REC-011-03
    given: A fee-only deposit is negative.
    when: Finalize its explanation.
    then: Accounting requests the transfer from bank back to clearing.
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#posts
      a bank debit from the bank to clearing
  - id: SC-CAP-ACC-REC-011-04
    given: No operational batch accepts the generated requests.
    when: Attempt finalize.
    then: Rollback leaves the open version intact without a request or event.
    provenance: test
    source: >-
      services-accounting:modules/reconciliation/tests/integration/services/batch-lifecycle/finalize-batch.integration.spec.js#writes
      nothing when refused for want of a batch: the batch open at its version, no request, no event
constraints: []
depends_on:
  - CAP-ACC-REC-009
  - CAP-ACC-REC-010
  - CAP-ACC-REC-014
related:
  - CAP-ACC-REC-013
  - CAP-ACC-REC-020
journeys:
  - JNY-ACC-REC-001
  - JNY-ACC-REC-006
open_questions: []
evidence:
  - >-
    Examined supporting service material: batch-lifecycle.routes.ts (finalizeReconciliationBatch),
    batch-lifecycle.schemas.ts, services/batch-lifecycle/finalize-batch.ts,
    persistence/reconciliation-batch-lifecycles.ts, rules/segregation.ts, rules/posting-plans.ts,
    rules/lifecycle-refusals.ts, reconciliation-batches/batch-views.ts; tests integration finalize-batch,
    finalize-concurrency, finalize-posts-to-subledger, batch-views, unit segregation and posting-plans specs,
    e2e batch-lifecycle.e2e.spec.js and reconciliation-scenario.e2e.spec.js;
    docs/reconciliation/architecture.md §18.2, §24a.
  - >-
    Examined supporting console material: components/accounting/reconciliation/workspace/workspace.tsx;
    lib/accounting/reconciliation/store.tsx (finalizeRecon, finalizeBlocker); acceptance journeys
    ACT-ACC-0590, ACT-ACC-0597, ACT-ACC-0599, ACT-ACC-0624.
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-DAT-002, TC-DAT-003, 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-03-01, FAS-ACC-REC-03-02, FAS-ACC-REC-03-03, FAS-ACC-REC-03-04, FAS-ACC-REC-07-01, FAS-ACC-REC-07-03]
suites: [TST-ACC-REC-001, TST-ACC-REC-007, TST-ACC-REC-022, TST-ACC-REC-023, TST-ACC-REC-036, TST-ACC-REC-049, TST-ACC-REC-050, TST-ACC-REC-051, TST-ACC-REC-073, TST-ACC-REC-082, TST-ACC-REC-091, TST-ACC-REC-094]
---

# Finalize a balanced explanation with eligible approval

## Purpose

Approvers need a protected financial record of a prepared deposit explanation.

## How It Works

The approver inspects the saved draft and its supporting choices. Rhythm recomputes balance from actual holdings rather than trusting displayed counters. It evaluates approval eligibility and accounting setup before completing the decision. Successful finalization protects the explanation and creates the applicable requests for later posting. A failed prerequisite leaves preparation open, and console draft saving can change the approver’s eligibility.

## Relationships

Claims and manual entries prepare the balance, accounting requests reach posting and reversal supplies the supported correction path.
