---
id: ATS-ACC-BAT-017
scope: CAP-ACC-BAT-017
product_area: accounting
module: batches
revision: 2
status: proposed
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
  rhythm-v2-docs: 37405d50d1465ea0571a66095d98ebec4db04175
scenarios:
  - id: ATS-ACC-BAT-017-01
    title: Balance is enforced at commit
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-01
      - INV-DOM-ACC-BAT-004-01
    preconditions: A posting record exists.
    steps: Insert a balanced entry and an unbalanced entry.
    expected: Only the balanced entry commits and can be listed under its posting.
    negative: true
    data_needs: Provide no fixtures beyond the stated setup.
    bound_to:
      - TST-ACC-BAT-093
    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
    category: invariant
    test_evidence:
      - >-
        services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#writes
        a balanced entry, and lists it under its posting
      - >-
        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
  - id: ATS-ACC-BAT-017-02
    title: Conversion stores integer minor units
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-02
    preconditions: Existing ledger values use major units.
    steps: Run the currency-exponent conversion.
    expected: >-
      Line and schedule amounts become integer minor units and enforcement is
      restored afterward.
    negative: false
    data_needs: Provide no fixtures beyond the stated setup.
    bound_to:
      - TST-ACC-BAT-087
    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
    category: boundary
    test_evidence:
      - >-
        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
      - >-
        services-accounting:modules/batches/tests/integration/services/persistence/ledger-minor-units.integration.spec.js#leaves
        the columns bigint, the helper columns gone and the trigger enabled
  - id: ATS-ACC-BAT-017-03
    title: Ledger writes reject ordinary mutation
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-03
      - BR-CAP-ACC-BAT-017-04
      - INV-DOM-ACC-BAT-004-03
      - INV-DOM-ACC-BAT-004-04
    preconditions: A current posted entry exists.
    steps: 'Update as application and owner, then delete as application.'
    expected: >-
      All ordinary mutations fail; only guarded unposting and workspace purge
      remove rows.
    negative: true
    data_needs: Provide database roles.
    bound_to:
      - TST-ACC-BAT-093
      - TST-ACC-BAT-112
      - TST-ACC-BAT-029
    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
    category: rule
    test_evidence:
      - >-
        services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#cannot
        be updated by the application role, which holds no UPDATE on the table
      - >-
        services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#cannot
        be deleted by the application role, which holds no DELETE on the table
      - >-
        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
      - >-
        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
      - >-
        react-console:acceptance/accounting/playwright/journeys/batches-posting/ACT-ACC-0547.spec.ts#ACT-ACC-0547
  - id: ATS-ACC-BAT-017-04
    title: Ledger references preserve unit and workspace boundaries
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-05
      - BR-CAP-ACC-BAT-017-06
      - INV-DOM-ACC-BAT-004-05
    preconditions: The posting belongs to the first unit.
    steps: >-
      Use another unit’s account, duplicate a document entry and reference a
      reversal target outside the workspace.
    expected: The database refuses each invalid reference.
    negative: true
    data_needs: Provide two units.
    bound_to:
      - TST-ACC-BAT-093
    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
    category: validation
    test_evidence:
      - >-
        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
      - >-
        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
      - >-
        services-accounting:modules/batches/tests/integration/services/persistence/subledger-entries.integration.spec.js#refuses
        a reversing entry that names no entry of its tenant, by the reversal
        trigger
  - id: ATS-ACC-BAT-017-05
    title: Purge requires workspace deletion
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-07
    preconditions: A live workspace contains posting and reposting history.
    steps: Purge before and during Platform’s deleting state.
    expected: >-
      The live request removes nothing; deletion removes ledger and posting
      records before batches and configuration.
    negative: true
    data_needs: Provide platform's workspace status.
    bound_to:
      - TST-ACC-BAT-079
    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
    category: transition
    test_evidence:
      - >-
        services-accounting:modules/batches/tests/integration/services/lifecycle/ledger-purge.integration.spec.js#refuses
        the participant’s purge of a live tenant, with nothing removed
      - >-
        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
      - >-
        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
  - id: ATS-ACC-BAT-017-06
    title: Ledger reads isolate workspaces
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-08
    preconditions: The first workspace owns a posted batch.
    steps: 'From another workspace, retrieve entries by batch and document.'
    expected: Both reads return no ledger rows.
    negative: true
    data_needs: Provide two workspaces.
    bound_to:
      - TST-ACC-BAT-105
      - TST-ACC-BAT-028
    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)
    category: invariant
    test_evidence:
      - >-
        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)
      - >-
        react-console:acceptance/accounting/playwright/journeys/batches-posting/ACT-ACC-0541.spec.ts#ACT-ACC-0541
  - id: ATS-ACC-BAT-017-07
    title: Generated balances survive storage enforcement
    category: interaction
    level: api
    proves:
      - BR-CAP-ACC-BAT-015-14
      - BR-CAP-ACC-BAT-017-01
    exercises:
      - CAP-ACC-BAT-017
      - CAP-ACC-BAT-015
    workflows: []
    preconditions: A taxed invoice and its memo are eligible for posting.
    steps: Post them and sum stored debit and credit lines per entry.
    expected: >-
      Each stored entry balances and its document-line references remain
      readable.
    negative: false
    data_needs: >-
      An isolated workspace with the stated business-unit calendar, documents
      and configuration.
    bound_to: []
    provenance: proposed
    source: >-
      services-accounting:modules/batches/tests/integration/services/posting/rebuilt-documents.integration.spec.js#traces
      every line of every entry to a line of a document in the batch;
      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
  - id: ATS-ACC-BAT-017-08
    title: Guarded recovery removes only its ledger
    category: interaction
    level: api
    proves:
      - BR-CAP-ACC-BAT-016-03
      - BR-CAP-ACC-BAT-017-04
    exercises:
      - CAP-ACC-BAT-017
      - CAP-ACC-BAT-016
    workflows: []
    preconditions: Morning and Evening each have a current posting in an Open period.
    steps: Unpost Morning and list both batches’ entries including history.
    expected: Morning has no entries; Evening’s stored entries are unchanged.
    negative: false
    data_needs: >-
      An isolated workspace with the stated business-unit calendar, documents
      and configuration.
    bound_to: []
    provenance: proposed
    source: >-
      services-accounting:modules/batches/tests/integration/services/posting/unpost-deletes-entries.integration.spec.js#deletes
      the batch’s entries and their lines, and tells how many entries;
      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
  - id: ATS-ACC-BAT-017-09
    title: Immutable accounting remains readable
    category: interaction
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-03
      - BR-CAP-ACC-BAT-018-03
    exercises:
      - CAP-ACC-BAT-017
      - CAP-ACC-BAT-018
    workflows: []
    preconditions: A posted invoice entry is visible to its workspace reader.
    steps: 'Read its detail, attempt an ordinary ledger mutation and read it again.'
    expected: Mutation fails and the second detail agrees with the first.
    negative: true
    data_needs: >-
      An isolated workspace with the stated business-unit calendar, documents
      and configuration.
    bound_to: []
    provenance: proposed
    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;
      services-accounting:modules/batches/tests/e2e/subledger-entries.e2e.spec.js#names
      the number of the document each entry posts, never its id (DEF-ACT-0531)
  - id: ATS-ACC-BAT-017-10
    title: Duplicate entry creation has one committed result
    category: concurrency
    level: api
    proves:
      - BR-CAP-ACC-BAT-017-06
    preconditions: >-
      Two transactions share a current posting and the same valid document-entry
      identity.
    steps: >-
      Insert the same entry from both transactions and release their commits in
      a controlled order.
    expected: >-
      At most one transaction commits that identity and the other receives the
      database duplicate refusal.
    negative: true
    data_needs: >-
      Owned records in an isolated workspace with the stated calendar and
      accounting setup.
    bound_to: []
    provenance: proposed
    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
evidence:
  - services-accounting at f47cb263: 'Inspected references: scenario-bound tests in modules/batches/tests.'
  - react-console at baf6dae3: >-
      Inspected references:
      acceptance/accounting/playwright/journeys/batches-posting (ACT-ACC-0541,
      0547).
  - >-
    Pinned design objects, state models and workflows identify invariant and
    interaction obligations; scenario test anchors remain in test_evidence.
product_intent: proposed
coverage:
  rule: covered
  transition: covered
  validation: covered
  permission: not-applicable
  edge: 'gap: FND-ACC-BAT-071'
  failure: 'gap: FND-ACC-BAT-071'
  boundary: covered
  concurrency: covered
  invariant: covered
  interaction: covered
coverage_notes:
  permission: >-
    Ledger storage uses database roles and guarded functions rather than a
    separate caller permission in this capability.
  edge: >-
    Concurrent duplicate inserts, minimum-line and minor-unit validation
    boundaries, reversal uniqueness across generations and guarded-purge failure
    need focused proof.
  failure: >-
    Concurrent duplicate inserts, minimum-line and minor-unit validation
    boundaries, reversal uniqueness across generations and guarded-purge failure
    need focused proof.
  concurrency: >-
    Concurrent duplicate inserts, minimum-line and minor-unit validation
    boundaries, reversal uniqueness across generations and guarded-purge failure
    need focused proof.
rule_coverage:
  - rule: BR-CAP-ACC-BAT-017-01
    scenarios:
      - ATS-ACC-BAT-017-01
      - ATS-ACC-BAT-017-07
    provenance: proposed
    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
  - rule: BR-CAP-ACC-BAT-017-02
    scenarios:
      - ATS-ACC-BAT-017-02
    provenance: proposed
    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
  - rule: BR-CAP-ACC-BAT-017-03
    scenarios:
      - ATS-ACC-BAT-017-03
      - ATS-ACC-BAT-017-09
    provenance: proposed
    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
  - rule: BR-CAP-ACC-BAT-017-04
    scenarios:
      - ATS-ACC-BAT-017-03
      - ATS-ACC-BAT-017-08
    provenance: proposed
    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
  - rule: BR-CAP-ACC-BAT-017-05
    scenarios:
      - ATS-ACC-BAT-017-04
    provenance: proposed
    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
  - rule: BR-CAP-ACC-BAT-017-06
    scenarios:
      - ATS-ACC-BAT-017-04
      - ATS-ACC-BAT-017-10
    provenance: proposed
    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
  - rule: BR-CAP-ACC-BAT-017-07
    scenarios:
      - ATS-ACC-BAT-017-05
    provenance: proposed
    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
  - rule: BR-CAP-ACC-BAT-017-08
    scenarios:
      - ATS-ACC-BAT-017-06
    provenance: proposed
    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)
transition_coverage: []
permission_coverage: []
executable_coverage:
  bound_scenarios: 6
  unbound_scenarios:
    - ATS-ACC-BAT-017-07
    - ATS-ACC-BAT-017-08
    - ATS-ACC-BAT-017-09
    - ATS-ACC-BAT-017-10
  gap: FND-ACC-BAT-071
---

# Protect ledger balance, immutability and workspace isolation: Capability Test Specification

## Coverage

The scenarios define focused checks for this outcome. The coverage map distinguishes specified checks from unresolved proof; executable bindings identify inspected assertions and carry no execution result.

| Rule | Proof Specification or Gap |
|---|---|
| `BR-CAP-ACC-BAT-017-01` | `ATS-ACC-BAT-017-01`, `ATS-ACC-BAT-017-07` |
| `BR-CAP-ACC-BAT-017-02` | `ATS-ACC-BAT-017-02` |
| `BR-CAP-ACC-BAT-017-03` | `ATS-ACC-BAT-017-03`, `ATS-ACC-BAT-017-09` |
| `BR-CAP-ACC-BAT-017-04` | `ATS-ACC-BAT-017-03`, `ATS-ACC-BAT-017-08` |
| `BR-CAP-ACC-BAT-017-05` | `ATS-ACC-BAT-017-04` |
| `BR-CAP-ACC-BAT-017-06` | `ATS-ACC-BAT-017-04`, `ATS-ACC-BAT-017-10` |
| `BR-CAP-ACC-BAT-017-07` | `ATS-ACC-BAT-017-05` |
| `BR-CAP-ACC-BAT-017-08` | `ATS-ACC-BAT-017-06` |

## Interactions With Related Capabilities

Generated balances survive storage enforcement. Guarded recovery removes only its ledger. Immutable accounting remains readable. These checks use the related capability’s outcome as part of the setup or verification. Unbound interactions remain proposed until a suite executes the complete chain.
