---
id: TST-ACC-TRX-034
title: Credit memos suite
kind: integration
product_area: accounting
module: transactions
revision: 1
status: draft
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
repository: services-accounting
files:
  - >-
    modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js
implements:
  - ATS-ACC-TRX-003-01
  - ATS-ACC-TRX-003-02
  - ATS-ACC-TRX-003-03
  - ATS-ACC-TRX-003-05
  - ATS-ACC-TRX-003-08
  - ATS-ACC-TRX-003-10
implements_parts:
  - scenario: ATS-ACC-TRX-003-01
    assertion: it is refused 409 invoice_not_draft
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      is refused 409 invoice_not_draft
  - scenario: ATS-ACC-TRX-003-01
    assertion: >-
      it is refused 400 business_unit_mismatch for a line of another invoice of
      the unit (the ledger trigger’s check)
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      is refused 400 business_unit_mismatch for a line of another invoice of the
      unit (the ledger trigger’s check)
  - scenario: ATS-ACC-TRX-003-01
    assertion: >-
      it is refused 400 business_unit_mismatch for a line of another unit’s
      invoice (the composite key)
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      is refused 400 business_unit_mismatch for a line of another unit’s invoice
      (the composite key)
  - scenario: ATS-ACC-TRX-003-02
    assertion: >-
      it answers the memo whole: issued, numbered from 1,000,000, its line and
      total, the invoice’s bill-to
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      answers the memo whole: issued, numbered from 1,000,000, its line and
      total, the invoice’s bill-to
  - scenario: ATS-ACC-TRX-003-03
    assertion: it is refused 400 over_application
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      is refused 400 over_application
  - scenario: ATS-ACC-TRX-003-03
    assertion: 'it writes nothing: no memo, no ledger line, no fact, no event'
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      writes nothing: no memo, no ledger line, no fact, no event
  - scenario: ATS-ACC-TRX-003-05
    assertion: >-
      it makes the credited line cancelled and leaves the invoice open with the
      other line’s 80.00
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      makes the credited line cancelled and leaves the invoice open with the
      other line’s 80.00
  - scenario: ATS-ACC-TRX-003-05
    assertion: it makes the header cancelled once a second memo credits the other line
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      makes the header cancelled once a second memo credits the other line
  - scenario: ATS-ACC-TRX-003-05
    assertion: 'it makes every line and the header paid, not cancelled'
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      makes every line and the header paid, not cancelled
  - scenario: ATS-ACC-TRX-003-08
    assertion: >-
      it answers the memo void, its negating lines after the originals, each
      naming the line it reverses
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      answers the memo void, its negating lines after the originals, each naming
      the line it reverses
  - scenario: ATS-ACC-TRX-003-08
    assertion: it returns the invoice’s balances to what they were before the memo
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      returns the invoice’s balances to what they were before the memo
  - scenario: ATS-ACC-TRX-003-08
    assertion: >-
      it writes a credit_memo_voided fact of plus each line’s amount, dated
      today
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      writes a credit_memo_voided fact of plus each line’s amount, dated today
  - scenario: ATS-ACC-TRX-003-08
    assertion: 'it refuses a second void 409 stale, the memo being void'
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      refuses a second void 409 stale, the memo being void
  - scenario: ATS-ACC-TRX-003-10
    assertion: it refuses another unit’s batch 409 batch_not_open
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      refuses another unit’s batch 409 batch_not_open
  - scenario: ATS-ACC-TRX-003-10
    assertion: >-
      it refuses no batch in a unit without batch assignment settings 409
      batch_required
    provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js#it
      refuses no batch in a unit without batch assignment settings 409
      batch_required
environment: local
implementation_state: implemented
latest_result: not-run
latest_run_at: null
latest_run_ref: null
conformance: not-verified
estimated_minutes: 3
isolation: >-
  Suite-marked records in the prepared local test database; fixture teardown
  removes owned records.
provenance: test
source: >-
  services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js
evidence:
  - provenance: test
    source: >-
      services-accounting:modules/transactions/tests/integration/services/change-memos/credit-memos.integration.spec.js
    assertion_titles:
      - credit memos (integration)
      - on a draft invoice
      - is refused 409 invoice_not_draft
      - 'crediting a line exactly, then the other'
      - >-
        answers the memo whole: issued, numbered from 1,000,000, its line and
        total, the invoice’s bill-to
      - >-
        makes the credited line cancelled and leaves the invoice open with the
        other line’s 80.00
      - >-
        writes one credit_memo fact of minus the amount on the line, dated the
        memo’s accounting date
      - records the create as the memo’s document event
      - leaves the as-of read of the facts equal to the live balance
      - makes the header cancelled once a second memo credits the other line
      - crediting more than a line owes
      - is refused 400 over_application
      - 'writes nothing: no memo, no ledger line, no fact, no event'
      - leaves the invoice as issued
      - naming a line that is not the invoice’s
      - >-
        is refused 400 business_unit_mismatch for a line of another invoice of
        the unit (the ledger trigger’s check)
      - >-
        is refused 400 business_unit_mismatch for a line of another unit’s
        invoice (the composite key)
      - leaves both invoices of alpha owing their whole totals
      - the retroactive coupon’s memo (RB-5114)
      - 'makes every line and the header paid, not cancelled'
      - voiding a memo
      - read the as-of balance equal to the live one while the memo stood
      - >-
        answers the memo void, its negating lines after the originals, each
        naming the line it reverses
      - returns the invoice’s balances to what they were before the memo
      - 'writes a credit_memo_voided fact of plus each line’s amount, dated today'
      - 'records the void after the create, with its reason'
      - leaves the as-of read of the facts equal to the live balance
      - 'refuses a second void 409 stale, the memo being void'
      - reads the memo as the void answered it
      - a replay
      - answers the first memo
      - writes nothing again
      - refuses the same key with another request 409 idempotency_key_reused
      - the batch
      - refuses another unit’s batch 409 batch_not_open
      - >-
        refuses no batch in a unit without batch assignment settings 409
        batch_required
      - an invoice the tenant does not have
      - is refused 404 not_found
    imports:
      - 'const { expect } = require(''chai'');'
      - >-
        const { uuidV7 } =
        require('@rhythmsoftware/macroservice-records/uuid-v7');
      - 'const { stage } = require(''@rhythmsoftware/macroservice-testing/stage'');'
      - >-
        const { ready, testId } =
        require('../../../../../books/tests/integration/support');
      - >-
        const { createCreditMemo, voidCreditMemo } =
        require('../../../../services/change-memos/memo-commands');
      - >-
        const { getCreditMemo } =
        require('../../../../services/change-memos/memo-reads');
      - '} = require(''../../memos-fixtures'');'
      - "\t\t\tworld.require();"
      - "\t\t\tconst invoice = draft.require();"
      - "\t\t\tworld.require();"
      - "\t\t\tcredit(creditRequest(invoice.require(), 'cm-exact-1', [[0, 12000]])),"
      - "\t\t\tconst memo = first.require();"
      - "\t\t\tconst issued = invoice.require();"
      - "\t\t\tfirst.require();"
      - "\t\t\texpect(await balancesOf(TENANT, invoice.require().id)).to.deep.equal({"
      - "\t\t\tconst memo = first.require();"
      - "\t\t\t\t\topen_item_id: invoice.require().id,"
      - "\t\t\t\t\topen_item_line_id: invoice.require().lines[0].id,"
      - "\t\t\texpect(await eventsOf(TENANT, first.require().id)).to.deep.equal(["
      - "\t\t\tfirst.require();"
      - "\t\t\texpect(await asOfAndLive(TENANT, invoice.require().id)).to.deep.equal({ as_of: 8000, live: 8000 });"
      - "\t\t\tfirst.require();"
      - "\t\t\tconst issued = invoice.require();"
      - "\t\t\tworld.require();"
      - "\t\t\t\t\tcreditRequest(invoice.require(), 'cm-over-1', ["
      - "\t\t\texpect(refusalShapeOf(attempt.require().refusal)).to.deep.equal({"
      - "\t\t\t\tmessage: `InvalidRequest: Credit memo ${attempt.require().refusal.message.match(/Credit memo (\\S+)/)[1]} takes more from a line than it still owes (over_application: the amount applied exceeds what the invoice or line still owes)`,"
      - "\t\t\tconst { before, after } = attempt.require();"
      - "\t\t\tattempt.require();"
      - "\t\t\texpect(await balancesOf(TENANT, invoice.require().id)).to.deep.equal({"
      - "\t\t\tworld.require();"
      - "\t\t\tconst { alpha, other } = invoices.require();"
      - "\t\t\tconst { alpha, beta } = invoices.require();"
      - "\t\t\tconst { alpha, other } = invoices.require();"
      - "\t\t\tworld.require();"
      - "\t\t\texpect(await balancesOf(TENANT, invoice.require().id)).to.deep.equal({"
      - "\t\t\tworld.require();"
      - "\t\t\texpect(voided.require().credited).to.deep.equal({ as_of: 12000, live: 12000 });"
      - "\t\t\tconst { memo, answer, issued } = voided.require();"
      - "\t\t\texpect(await balancesOf(TENANT, voided.require().issued.id)).to.deep.equal({"
      - "\t\t\tconst { memo, answer } = voided.require();"
      - "\t\t\t\t(await eventsOf(TENANT, voided.require().memo.id)).map((event) => ["
      - "\t\t\texpect(await asOfAndLive(TENANT, voided.require().issued.id)).to.deep.equal({ as_of: 20000, live: 20000 });"
      - "\t\t\tconst { memo } = voided.require();"
      - "\t\t\tconst { memo, answer } = voided.require();"
      - "\t\t\tworld.require();"
      - "\t\t\tconst { first, again } = replay.require();"
      - "\t\t\tconst { before, after } = replay.require();"
      - "\t\t\tconst { request } = replay.require();"
      - "\t\t\tworld.require();"
      - "\t\t\tconst refusal = await refusalOf(credit(creditRequest(invoice.require(), 'cm-batch-1', [[0, 100]], { batch_id: 'beta-2026-10' })));"
      - "\t\t\tconst refusal = await refusalOf(credit(creditRequest(invoice.require(), 'cm-batch-2', [[0, 100]], { batch_id: null })));"
      - "\t\t\tworld.require();"
    test_double_declarations: []
---

# Credit memos suite

## Verification Boundary

The suite exercises transaction commands and database protections in the local prepared test environment. Registration records the existing executable file; it does not report a run or establish product approval.

## What This Suite Proves

Its cited assertions contribute evidence for memo targets must belong to eligible invoices; saving memos issues them immediately; excessive credit leaves no financial footprint; memo-derived standing matches the treatment; memo void preserves opposing lines; memo batch selection stays within the unit. The structured assertion bindings identify which parts of combined scenarios it executes.

## What It Does Not Prove

Local commands and route invocations do not establish deployed routing, live processor guarantees or the complete staff browser experience. An unbound feature chain remains proposed, and this registration carries no execution result.

## Real Dependencies and Test Doubles

The suite uses the imported commands, route handlers or persistence code against the local test database. Fixture helpers supply accounting context; any stepped clock, dummy processor or substituted external client is retained in the evidence declarations.

## Must Not Be Doubled

The command or persistence behavior under assertion and its relevant database protections must remain real. Substituting the final financial result removes evidence of transactional effects.
