---
id: CAP-ACC-BAT-017
title: Protect ledger balance, immutability and workspace isolation
feature: FEAT-ACC-BAT-04
product_area: accounting
module: batches
revision: 1
status: draft
product_intent: proposed
implementation_evidence: observed
supersedes:
- inventory CAP-ACC-BAT-013 (the store half)
- inventory RUL-ACC-BAT-024
- inventory RUL-ACC-BAT-049
- inventory RUL-ACC-BAT-050
- journey ACT-ACC-0547
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
intent_sources:
- services-accounting:docs/architecture/accounting-2.0/02-decisions.md#A-1
- services-accounting:docs/architecture/accounting-2.0/02-decisions.md#D-04
- services-accounting:docs/accounting/batches.md#Unposting deletes the entries (Andrew, Reporting D7, 2026-10-21)
- 'services-accounting:docs/migrations/batch-posting/README.md#Merge readiness (Andrew''s D9 of 2026-10-15: tenant
  deletion removes the posted ledger)'
purpose: Ledger safeguards prevent unbalanced, mutable or cross-workspace accounting records.
outcomes:
- Ledger protections refuse invalid writes and restrict deletion to the guarded recovery and purge paths.
concepts:
- CON-ACC-BAT-004
actors:
- ACT-ACC-007
- ACT-ACC-014
surfaces:
- job
permissions: []
rules:
- id: BR-CAP-ACC-BAT-017-01
  proved_by: [ATS-ACC-BAT-015-13, ATS-ACC-BAT-017-01, ATS-ACC-BAT-017-07, TST-ACC-BAT-093]
  statement: Every stored entry needs at least two lines and equal debits and credits in base currency and each
    transaction currency, with the database checking balance at commit.
  kind: invariant
  enforced_at:
  - database
  - service
  must_prove: true
  provenance: test
  source: services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#refuses
    an entry whose debits and credits differ, at commit, by the balance constraint
  origin: services-accounting:docs/architecture/accounting-2.0/02-decisions.md#A-1
- id: BR-CAP-ACC-BAT-017-02
  proved_by: [ATS-ACC-BAT-017-02, TST-ACC-BAT-087]
  statement: Amounts are nonnegative whole minor units with their direction expressed as debit or credit.
  kind: invariant
  enforced_at:
  - database
  must_prove: true
  provenance: test
  source: services-accounting:modules/batches/tests/integration/services/persistence/ledger-minor-units.integration.spec.js#converts
    each line with its unit's base exponent and its transaction currency's, and each schedule entry with its schedule's
  origin: services-accounting:docs/architecture/accounting-2.0/02-decisions.md#D-04
- id: BR-CAP-ACC-BAT-017-03
  proved_by: [ATS-ACC-BAT-017-03, ATS-ACC-BAT-017-09, ATS-ACC-BAT-018-08, TST-ACC-BAT-029, TST-ACC-BAT-093, TST-ACC-BAT-112]
  statement: Stored headers and lines reject updates even from their owner, and the application cannot delete them
    directly.
  kind: invariant
  enforced_at:
  - database
  must_prove: true
  provenance: test
  source: 'services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#cannot
    be updated even by the table owner: the append-only trigger refuses it, naming the table'
  origin: services-accounting:docs/architecture/accounting-2.0/02-decisions.md#A-1
- id: BR-CAP-ACC-BAT-017-04
  proved_by: [ATS-ACC-BAT-016-12, ATS-ACC-BAT-017-03, ATS-ACC-BAT-017-08, TST-ACC-BAT-029, TST-ACC-BAT-093, TST-ACC-BAT-112]
  statement: Removal is restricted to guarded deletion of a current ordinary posting within its workspace or workspace
    purge.
  kind: invariant
  enforced_at:
  - database
  must_prove: true
  provenance: test
  source: 'services-accounting:modules/batches/tests/integration/services/posting/unpost-deletes-entries.integration.spec.js#is
    the only way to delete a ledger row: the application role is refused a direct delete'
  origin: services-accounting:docs/accounting/batches.md#Unposting deletes the entries (Andrew, Reporting D7, 2026-10-21)
- id: BR-CAP-ACC-BAT-017-05
  proved_by: [ATS-ACC-BAT-017-04, TST-ACC-BAT-093]
  statement: Each line account must belong to its entry’s business unit.
  kind: invariant
  enforced_at:
  - database
  must_prove: true
  provenance: test
  source: services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#refuses
    a line whose G/L account belongs to another business unit, by the same-unit foreign key
  origin: none
- id: BR-CAP-ACC-BAT-017-06
  proved_by: [ATS-ACC-BAT-017-04, ATS-ACC-BAT-017-10, TST-ACC-BAT-093]
  statement: A posting cannot contain duplicate entries for one document or schedule entry, and an original entry
    permits only one reversal referencing that workspace’s entry.
  kind: invariant
  enforced_at:
  - database
  must_prove: true
  provenance: test
  source: services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#refuses
    a second entry of the same transaction in the same posting, by the no-duplicates key
  origin: none
- id: BR-CAP-ACC-BAT-017-07
  proved_by: [ATS-ACC-BAT-017-05, TST-ACC-BAT-079]
  statement: During workspace deletion, the ledger participant purges ledger, postings, schedules and jobs before
    batches and configuration and refuses purge while the workspace remains live.
  kind: lifecycle
  enforced_at:
  - database
  - service
  must_prove: true
  provenance: test
  source: services-accounting:modules/batches/tests/integration/services/lifecycle/ledger-purge.integration.spec.js#removes
    every batch posting table’s rows, the ledger’s first among them, and answers done with nothing left
  origin: 'services-accounting:docs/migrations/batch-posting/README.md#Merge readiness (Andrew''s D9 of 2026-10-15:
    tenant deletion removes the posted ledger)'
- id: BR-CAP-ACC-BAT-017-08
  proved_by: [ATS-ACC-BAT-017-06, TST-ACC-BAT-028, TST-ACC-BAT-105]
  statement: Workspace isolation prevents reading another workspace’s ledger.
  kind: invariant
  enforced_at:
  - database
  must_prove: true
  provenance: test
  source: services-accounting:modules/batches/tests/integration/services/posting/rebuilt-documents.integration.spec.js#answers
    another tenant none of the entries, by batch or by transaction (the row-level policy)
  origin: none
scenarios:
- id: SC-CAP-ACC-BAT-017-01
  given: A ledger entry is stored under a posting.
  when: An application or the table owner changes its amount.
  then: The update is refused and the stored accounting remains intact.
  provenance: test
  source: 'services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#cannot
    be updated even by the table owner: the append-only trigger refuses it, naming the table'
- id: SC-CAP-ACC-BAT-017-02
  given: Platform is deleting a workspace containing posting history.
  when: The ledger participant purges and configuration purge follows.
  then: No rows for that workspace remain in the examined accounting tables.
  provenance: test
  source: services-accounting:modules/batches/tests/integration/services/lifecycle/ledger-purge.integration.spec.js#lets
    configuration be purged once the posting tables are gone, leaving no row of the tenant in any tenant table
edge_cases: []
constraints:
- Platform coordinates workspace deletion and this module contributes ledger purge.
depends_on: []
related:
- CAP-ACC-BAT-015
- CAP-ACC-BAT-016
- CAP-ACC-BAT-018
journeys: []
open_questions: []
evidence:
- 'Pinned services-accounting (f47cb263), examined sources: db/subledger-entries.table.ts, db/subledger-entry-lines.table.ts,
  db/batch-postings.table.ts, services/persistence/subledger-entries.ts, ledger-records.ts, services/lifecycle/batch-posting-purge-order.ts;
  tests integration/services/persistence/subledger-entries.integration.spec.js, ledger-minor-units.integration.spec.js,
  integration/services/posting/unpost-deletes-entries.integration.spec.js, rebuilt-documents.integration.spec.js,
  integration/services/lifecycle/ledger-purge.integration.spec.js; docs/accounting/batches.md, docs/migrations/batch-posting/README.md
  ("Merge readiness").'
controls: [TC-IAM-001, TC-IAM-005, TC-IAM-006, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-TEN-004, TC-DAT-004, TC-LOG-001, TC-LOG-002, TC-LOG-003, TC-LOG-005, TC-PI-005]
exercised_by: [FAS-ACC-BAT-04-01, FAS-ACC-BAT-04-02, FAS-ACC-BAT-04-03, FAS-ACC-BAT-04-04, FAS-ACC-BAT-04-05]
suites: [TST-ACC-BAT-028, TST-ACC-BAT-029, TST-ACC-BAT-079, TST-ACC-BAT-087, TST-ACC-BAT-093, TST-ACC-BAT-105, TST-ACC-BAT-112]
---

# Protect ledger balance, immutability and workspace isolation

## Purpose

Ledger safeguards prevent unbalanced, mutable or cross-workspace accounting records.

## How It Works

Posting writes entries through the ledger’s constrained persistence path. The database checks balanced totals and account ownership. Stored records reject ordinary updates and application deletion. Controlled unposting removes an ordinary posting’s entries through the guarded function. Fiscal reopening instead retains its system entries and adds their mirrors. Workspace deletion uses the purge participant, which refuses to act on a live workspace.

## Relationships

- Related outcome: Produce document entries using the applicable accounting method, which addresses the connected batch work.
- Related outcome: Unpost a batch for correction and replacement posting, which addresses the connected batch work.
- Related outcome: Inspect and report on posted ledger entries, which addresses the connected batch work.
