Reference
referenceValidates lines in Python, then posts or reverses inside one PostgreSQL transaction with row locks.
05 · Ledger
Run a double-entry journal where every post balances per currency and is all-or-nothing.
referenceValidates lines in Python, then posts or reverses inside one PostgreSQL transaction with row locks.
alt_validSet-based JSON recordset validation and INSERT SELECT reversal, with an independent journal schema.
wrong_cross_currency_sumBalances the total across currencies instead of each currency.
wrong_double_reverseAllows the same original transaction to be reversed twice.
wrong_non_atomicCommits individual lines before all account validation completes.
trial_excludes_reversalsExpect: rejectObserved: rejectedExcludes reversal lines from turnover, violating trial_balance totals including reversals.
reverse_without_row_lockExpect: rejectObserved: rejectedChecks and reverses without serialization, violating at-most-once reversal under concurrency.
account_currency_overwriteExpect: rejectObserved: rejectedOverwrites an existing account currency, violating rejection of re-creation with a different currency.
account_unvalidatedExpect: rejectObserved: rejectedAccepts empty codes and unsupported currencies, violating create_account validation.
accounts_before_amountsExpect: rejectObserved: rejectedChecks account existence before line validity, violating amount/side-before-accounts error priority.
trial_inner_joinExpect: rejectObserved: rejectedOmits currencies without postings, violating the (0,0) entry for every created account currency.
alt_running_totalsExpect: acceptObserved: acceptedMaintains denormalized account and currency totals and enforces one reversal through a unique index.
Each row groups a test’s parameterized cases; any failing case marks the row as Fail. * marks a wrong solution’s declared must_fail test.
| Test | Starter | Reference | Alternative | cross currency sum | double reverse | non atomic |
|---|---|---|---|---|---|---|
| Cross currency cannot net | Fail | Pass | Pass | Fail * | Pass | Fail |
| Unknown account is atomic | Fail | Pass | Pass | Pass | Pass | Fail * |
| Reverse once | Fail | Pass | Pass | Pass | Fail * | Pass |
| Multiple currencies and persistence | Pass | Pass | Pass | Pass | Pass | Pass |
| Invalid amount unchanged | Pass | Pass | Pass | Pass | Pass | Pass |
| Rejections leave no trace | Fail | Pass | Pass | Pass | Pass | Fail |
| Create account validation | Pass | Pass | Pass | Pass | Pass | Pass |
| Create account idempotence and currency mismatch | Pass | Pass | Pass | Pass | Pass | Pass |
| Post validation priority | Fail | Pass | Pass | Pass | Pass | Pass |
| Concurrent reversals once | Fail | Pass | Pass | Pass | Fail | Fail |
| Journal state machine | Fail | Pass | Pass | Fail | Fail | Fail |
| Test | Starter | Reference | Alternative | cross currency sum | double reverse | non atomic |
|---|---|---|---|---|---|---|
| Balanced post | Pass | Pass | Pass | Pass | Pass | Pass |
| Unbalanced rejected | Fail | Pass | Pass | Pass | Pass | Pass |
| Ticket example post then reverse | Fail | Pass | Pass | Pass | Pass | Pass |
ledger.service.LedgerService(database_url) operates a synthetic PostgreSQL ledger. Expose frozen Line(account_code: str, side: "debit"|"credit", amount_minor: int) and: create_account(code,currency), post(lines: list[Line],memo: str)->int, reverse(txn_id:int,memo:str)->int, balance(account_code)->int, trial_balance()->dict[str,tuple[int,int]].
Money uses integer minor units. Currencies: CAD/USD/EUR/GBP=2 dp, JPY=0, KWD/BHD=3; no exchange or cross-currency netting. Errors derive from LedgerError: UnbalancedTransaction, InvalidAmount, UnknownAccount, AlreadyReversed, CannotReverseReversal.
(total_debits,total_credits) across all posted lines, including reversal lines. A currency without postings has (0,0). balance of an unknown account raises UnknownAccount.Worked example (CAD accounts cash and revenue, synthetic): post([Line("cash","debit",5000), Line("revenue","credit",5000)], "sale") returns id 1; afterwards balance("cash") == 5000, balance("revenue") == -5000 and trial_balance()["CAD"] == (5000, 5000). reverse(1, "undo sale") returns a different id; both balances are then 0 and the CAD trial balance is (10000, 10000). Reversing id 1 again raises AlreadyReversed.
The service may create its own tables; tests observe only the declared methods. Run python -m pytest tests -q from starter with DATABASE_URL pointing at a disposable database.