Three reports a CPA expects that the engine could not produce: - general_ledger(start, end): the canonical general ledger, organized by account in code order. Each account has its beginning balance, every posting in the period (date, entry, description, debit, credit, running balance on the normal side) and its ending balance, which ties to the trial balance. Each posting names its split: the one account on the opposite side of its entry, or "-Split-" when that side has several, so a compound entry (one debit, several credits) reads correctly from every account it touches. The entry-ordered listing the exports called a "general ledger" is really the general journal. - trial_balance_as_of(as_of): a trial balance is always as of a date. - members_equity(start, end, members): the statement of changes in members' equity for a multi-member LLC taxed as a partnership. Each member's capital roll-forward: beginning (capital plus their share of undistributed earnings), contributions, allocated net income (loss), distributions, ending. Allocation is by ownership share to the cent, the last member absorbing rounding, so totals are exact and the ending total equals balance-sheet equity; any equity change the roll-forward can't explain shows as "other" instead of disappearing. Shares must be positive and sum to exactly 1; capital accounts must be distinct equity accounts (LedgerError::InvalidMembers). - CashFlowLine.account: the account a line is about, so renderers can name members instead of account codes. Tests (every expectation computed by hand): trial balance as of Jan 31; February general ledger with beginning balances, running balances, a card that nets to zero, an inactive account still listed, and every ending balance tied to the trial balance; a compound entry's splits from all three sides; a two-member partnership where 666.67 and 100.01 split unevenly (333.34/333.33, 50.01/50.00), with and without closing January, tying to the balance sheet each month; bad member configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
137 lines
7.7 KiB
Markdown
137 lines
7.7 KiB
Markdown
# Requirements — accounting semantics (Phase 1)
|
||
|
||
Numbered requirements for the engine. `R-N` is current scope; `R-FUTURE-N`
|
||
is explicitly out of scope for now but designed for. Agents: if code and a
|
||
requirement conflict, the requirement wins.
|
||
|
||
## Core double-entry
|
||
|
||
- **R-1 — Balanced entries.** Every journal entry has at least two lines and
|
||
total debits exactly equal total credits (`Decimal` equality, no epsilon).
|
||
Unbalanced entries are rejected at post time with the offending totals.
|
||
- **R-2 — Exact arithmetic.** All money is `Decimal`. No binary floats in any
|
||
money path. No silent rounding: amounts are exact end to end. (Presentation
|
||
rounding, when the UI arrives, is half-up to the currency's minor unit and
|
||
is a display concern only.)
|
||
- **R-3 — Immutability.** Posted entries cannot be edited or deleted. The only
|
||
correction mechanism is a reversing entry (same lines, sides flipped)
|
||
linked via `reversal_of`. Draft → Posted is one-way.
|
||
- **R-4 — Chart of accounts.** Five closed types (Asset, Liability, Equity,
|
||
Revenue, Expense) with fixed normal sides (debit-normal: Asset, Expense;
|
||
credit-normal: Liability, Equity, Revenue). Codes are unique and non-empty.
|
||
Accounts may have parents (hierarchy). Placeholder (header) accounts
|
||
organize the chart and reject postings.
|
||
- **R-5 — Single currency per ledger (day one).** Every account and every
|
||
journal line uses the ledger's base currency. Anything else is rejected,
|
||
not converted. Multi-currency with translation is R-FUTURE-1.
|
||
- **R-6 — Line hygiene.** Zero-amount lines are rejected, not ignored.
|
||
Unknown accounts are rejected. Single-line entries are rejected.
|
||
|
||
## Reports
|
||
|
||
- **R-7 — Trial balance.** Every non-zero, non-placeholder account appears
|
||
with its debit or credit balance, sorted by code. Total debits must equal
|
||
total credits (the `balanced()` check).
|
||
- **R-8 — Balance sheet.** Assets, liabilities, equity grouped by account
|
||
type. The accounting equation holds exactly: assets = liabilities + equity.
|
||
Current earnings (revenue minus expenses not yet closed to retained
|
||
earnings) are folded into equity so the equation holds on a live book.
|
||
`balance_sheet_as_of` gives the statement as of any date, a line per
|
||
account, counting every entry dated on or before it (closing entries
|
||
included: after a close the earnings sit in retained earnings).
|
||
- **R-9 — Profit and loss.** Revenue and expenses with net income.
|
||
`profit_and_loss_for_period` scopes it to a date range (optionally one
|
||
class), a line per account, and **excludes closing entries**: a close moves
|
||
earnings into retained earnings and must not make a closed period read as
|
||
break-even. The cash flow statement excludes closing entries for the same
|
||
reason. Period P&Ls partition: split a range anywhere and the halves sum
|
||
to the whole, and a range's net income equals the change in current
|
||
earnings across it (property-tested).
|
||
- **R-9a — General ledger.** `general_ledger(start, end)`: by account in
|
||
code order, each with its beginning balance, every posting in the period
|
||
(date, entry, description, debit, credit, running balance on the account's
|
||
normal side), and its ending balance, which ties to the trial balance as
|
||
of `end`. Each posting names its *split*: the one account on the entry's
|
||
opposite side, or "-Split-" when that side has several (the QuickBooks
|
||
convention). The entry-ordered listing is the *general journal*, a
|
||
different report.
|
||
- **R-9b — Trial balance as of a date.** `trial_balance_as_of(as_of)`.
|
||
- **R-9c — Statement of changes in members' equity.** For a multi-member LLC
|
||
(partnership): each member's capital roll-forward — beginning,
|
||
contributions (credits to their capital account), allocated net income
|
||
(loss), distributions (debits), ending — plus a total that equals
|
||
balance-sheet equity. Earnings not yet distributed (retained earnings and
|
||
unclosed income) and the period's income are allocated by ownership share
|
||
to the cent, the last member absorbing rounding, so every row's total is
|
||
exact. Any equity change the roll-forward doesn't explain is shown as
|
||
"other", never hidden.
|
||
- **R-10 — Canonical snapshots.** `reports::snapshot()` renders the three
|
||
core reports as deterministic JSON (decimal strings, sorted lines, fixed
|
||
keys). Two engines fed the same books must produce identical output. This
|
||
is the oracle interchange format.
|
||
|
||
## Auditability
|
||
|
||
- **R-11 — Append-only log.** Posted entries are kept in posting order. The
|
||
log plus the chart fully determine every report.
|
||
- **R-12 — Command surface.** All mutations go through `Ledger` methods.
|
||
There is no second way to change the books.
|
||
|
||
## Receipt policy (MCP phase)
|
||
|
||
- **R-13a — Receipt corrections.** Posted entries are immutable (R-3); their
|
||
evidence is not frozen but is append-only. A receipt is replaced by
|
||
attaching the new one and unlinking the old with a reason; unwanted
|
||
receipts and documents are withdrawn with a reason. Unlinking may never
|
||
leave an entry the policy (R-13) requires a receipt for with none. Every
|
||
step is a journal event; no blob is deleted.
|
||
- **R-13 — Receipt enforcement.** The MCP layer (not the engine — the engine
|
||
stays policy-free) enforces a configurable receipt policy: postings at or
|
||
above a threshold (`require_receipts_above`, default `"25.00"` in base
|
||
currency) must have at least one attached receipt, with per-account
|
||
overrides. `post_entry` with `confirmed: true` refuses with a typed
|
||
`receipt_required` error otherwise. The draft preview always includes the
|
||
receipt checklist (`attached: [...]`, `required_but_missing: bool`), so the
|
||
human sees it before confirming. Full spec: `docs/mcp-interface.md`.
|
||
|
||
## Explicitly future work
|
||
|
||
- **R-FUTURE-6 — Budgets and scheduled transactions.** (Budgets explicitly
|
||
declined as bloat; scheduled transactions remain future.)
|
||
|
||
## Promoted from future (done)
|
||
|
||
- **R-? — Cash flow statement** (indirect method). Was R-FUTURE-3.
|
||
`ledger-core::reports::cash_flow` + `books.cash_flow`.
|
||
- **R-? — QuickBooks-compatible import/export.** Was R-FUTURE-4.
|
||
`ledger-import` crate: CSV bank/chart import, IIF and XLSX export.
|
||
- **R-? — Multi-currency (foreign-currency lines).** Was R-FUTURE-1.
|
||
`JournalLine` carries optional foreign currency/amount; `amount` is the
|
||
base-currency equivalent. `Ledger::add_line_fx`, `Ledger::set_fx_rate`.
|
||
Reports work in base currency. (Full per-account currencies and translation
|
||
remain future.)
|
||
- **R-? — Periods and close.** Was R-FUTURE-2.
|
||
`ledger-core::periods`: `create_period`, `close_period` (posts closing
|
||
entry to `3999` Retained Earnings), closed periods reject new entries.
|
||
- **R-? — A/R and A/P workflows.** Was R-FUTURE-5.
|
||
`ledger-core::arap`: customers, vendors, invoices, bills, payments,
|
||
AR/AP aging. MCP tools for the full workflow.
|
||
- **R-? — Bank reconciliation.** Cleared flags on posted lines,
|
||
`reconciliation_report` with uncleared-line detail.
|
||
- **R-? — Payroll (accounting only).** `ledger-core::payroll`:
|
||
`run_payroll` posts one balanced entry (Dr 6100 gross, Dr 6110 employer
|
||
FICA, Cr 2110–2150 payables, Cr cash net); `remit_payroll_taxes` clears
|
||
the payables. No tax tables — the caller supplies every number.
|
||
- **R-? — Tracking classes (divisional P&L).** `ledger-core::classes`:
|
||
`JournalLine.class_id`, `add_class`, `set_line_class` (drafts only),
|
||
`profit_and_loss_filtered`, `profit_and_loss_by_class`. Shared costs are
|
||
split explicitly across lines at entry time.
|
||
- **R-? — CPA handoff package.** `books.export_cpa_package`: zip with
|
||
trial-balance XLSX, general-ledger XLSX (every posted line), P&L and
|
||
balance-sheet PDFs (printpdf, MIT).
|
||
|
||
## Non-goals (all phases of the engine)
|
||
|
||
The engine never does I/O, networking, wall-clock time sourcing, or UI. It is
|
||
a pure domain library: data in, validated books out.
|