On this page

Ledgers that reconcile

Finance systems are tested on data that cannot be shown to the testers, or on random numbers that never balance. Neither exercises the code that matters: the close, the reconciliation, the report that ties one ledger to another. Here the books balance by construction and the balances you care about are declared.

Who: finance engineering, audit analytics, ERP and fintech teams. Time: under 2 seconds for 44,440 rows.

The declaration#

YAML
# misata.yaml
name: general_ledger
seed: 8
tables:
  accounts:
    rows: 40
    columns:
      account_id: {type: int, unique: true, min: 1000, max: 1039}
      account_type: {type: categorical, choices: [asset, liability, equity, revenue, expense], probabilities: [0.3, 0.2, 0.05, 0.15, 0.3]}
  journal_entries:
    rows: 8000
    columns:
      entry_id: {type: int, unique: true, min: 1, max: 8000}
      source: {type: categorical, choices: [billing, payroll, payables, manual], probabilities: [0.45, 0.15, 0.3, 0.1]}
      posted_on: {type: date, start: "2024-01-01", end: "2025-12-31"}
  journal_lines:
    rows: 36000
    columns:
      line_id: {type: int, unique: true, min: 1, max: 36000}
      entry_id: {type: foreign_key, sampling: uniform}
      account_id: {type: foreign_key}
      debit: {type: float, distribution: lognormal, mean: 2400, std: 5200, min: 0.01, max: 400000, decimals: 2}
      credit: {type: float, distribution: lognormal, mean: 2400, std: 5200, min: 0.01, max: 400000, decimals: 2}
    constraints:
      - name: double_entry
        type: balanced_ledger
        group_by: [entry_id]
        debit_column: debit
        credit_column: credit
  receivables_rollforward:
    rows: 400
    columns:
      period: {type: categorical, choices: ["2024-Q1", "2024-Q2", "2024-Q3", "2024-Q4", "2025-Q1", "2025-Q2", "2025-Q3", "2025-Q4"]}
      movement_type: {type: categorical, choices: [invoiced, collected, written_off]}
      amount: {type: float, min: 1, max: 250000, decimals: 2}
relationships:
  - {parent: journal_entries.entry_id, child: journal_lines.entry_id, min_children: 2}
  - accounts.account_id -> journal_lines.account_id
waterfalls:
  - table: receivables_rollforward
    period_column: period
    type_column: movement_type
    amount_column: amount
    starting_value: 1250000
    inflow_shares: {invoiced: 1.0}
    outflow_shares: {collected: 0.96, written_off: 0.04}
    outflow_rate: 0.55
    points:
      - {period: "2024-Q1", ending_value: 1318400.00}
      - {period: "2024-Q2", ending_value: 1402750.50}
      - {period: "2024-Q3", ending_value: 1377210.25}
      - {period: "2024-Q4", ending_value: 1461980.75}
      - {period: "2025-Q1", ending_value: 1512006.10}
      - {period: "2025-Q2", ending_value: 1590044.80}
      - {period: "2025-Q3", ending_value: 1566310.35}
      - {period: "2025-Q4", ending_value: 1648902.40}
  • balanced_ledger makes every line one-sided (a debit or a credit) and every journal entry's debits equal its credits to the cent.
  • min_children: 2 gives every entry at least two lines, because a one-line entry cannot balance.
  • The waterfalls declaration is an accounts receivable roll-forward: opening balance, plus invoices, minus collections and write-offs, lands on the declared closing balance every quarter for two years, with collections 96% and write-offs 4% of what leaves.
Declared receivables at each quarter end
2024-Q1: 1,318,400.001,318,400.002024-Q12024-Q2: 1,402,750.502024-Q22024-Q3: 1,377,210.252024-Q32024-Q4: 1,461,980.752024-Q42025-Q1: 1,512,006.102025-Q12025-Q2: 1,590,044.802025-Q22025-Q3: 1,566,310.352025-Q32025-Q4: 1,648,902.401,648,902.402025-Q4
Show the numbers
QuarterClosing balance
2024-Q11,318,400.00
2024-Q21,402,750.50
2024-Q31,377,210.25
2024-Q41,461,980.75
2025-Q11,512,006.10
2025-Q21,590,044.80
2025-Q31,566,310.35
2025-Q41,648,902.40

Source: waterfalls in the misata.yaml above, opening at 1,250,000.00; every quarter closes exactly

Run it#

Shell
misata explain misata.yaml
misata generate -c misata.yaml -o books --attest

The check#

Shell
python books/verify.py
Text
ok    4 file(s) match their attested sha256
ok    52 of 52 invariant(s) hold
InvariantCountWhat it proves
ledger_entry_balance1no journal entry where debits differ from credits
ledger_trial_balance1total debits equal total credits: 111,369,276.72 each
min_children1every entry has at least two lines (here 2 to 15, median 4)
waterfall_balance8each quarter closes on its declared balance
waterfall_net8each quarter's movements net to the change in balance
waterfall_component24invoiced, collected and written off, per quarter
keys, domains, uniques9every line points at a real entry and account

What to test with it#

  • The close. Every entry balances, so every slice of entries does: the trial balance is zero for 2024 (4,020 entries), for 2025 (3,980), for any month and any source. A report that does not tie out has a bug in the report.
  • Reconciliations. The receivables roll-forward opens at 1,250,000.00 and closes 2025-Q4 at 1,648,902.40. A reconciliation job that computes a different closing balance from the movements is wrong, and you know the right answer before running it.
  • Known-answer questions. misata evalpack -c misata.yaml turns the same declarations into SQL questions with answers, for testing an agent or a BI tool on accounting data.

Files#

The recipe folder runs as it is. Its files on GitHub:

Prefer generating data visually?

Misata Studio lets you turn natural language prompts into relational schemas with exact outcome invariants, live preview tables, and one-click database seeding without writing code.

Last updated Edit this page.md