04 · Reconciliation

Reconcile synthetic statements deterministically

Match bank statement lines to ledger entries with exact, split, refund and duplicate rules, deterministically.

Pythonhard

What the hardened grader checks

Solutions

Reference

reference

Applies the rules in order with repeated scans, tracking consumed statement and ledger ids in sets.

Naive: accepted (3/3)Hardened: accepted (18/18)

Alternative

alt_valid

Indexed candidate buckets and explicit remaining-id maps instead of repeated scans with consumed sets.

Naive: accepted (3/3)Hardened: accepted (18/18)

wrong_no_consumption

Allows one ledger payment to match multiple statement lines.

Naive: accepted (3/3)Hardened: rejected (14/18)

wrong_refund_abs

Treats negative statement amounts as payments of the absolute amount.

Naive: accepted (3/3)Hardened: rejected (17/18)

wrong_split_ignored

Omits aggregate split matches.

Naive: rejected (2/3)Hardened: rejected (13/18)

Held-out solutions

Tests × solutions

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.

Hardened · 18 recorded cases · 8 test rows
TestStarterReferenceAlternativeno consumptionrefund abssplit ignored
Duplicate statement line left unmatchedFailPassPassFail *PassPass
Split all linesFailPassPassPassPassFail *
Split wrong sum and no subsetPassPassPassPassPassPass
Refund kind and original referenceFailPassPassPassFail *Pass
Tie breaks and windowFailPassPassPassPassPass
Ordering and candidate rulesFailPassPassFailPassFail
Duplicate signature includes datePassPassPassFailPassPass
Ground truth transformsFailPassPassFailPassFail
Naive grader
Naive · 3 recorded cases · 3 test rows
TestStarterReferenceAlternativeno consumptionrefund abssplit ignored
Clean one to onePassPassPassPassPassPass
Ticket worked exampleFailPassPassPassPassFail
Unrelated line is unmatchedPassPassPassPassPassPass

The ticket

Ticket: reconcile a synthetic statement to ledger entries

Keep recon.engine.reconcile(statement: list[StatementLine], ledger: list[LedgerEntry]) -> ReconResult. Public frozen dataclasses (also exported from recon.engine):

  • StatementLine(id: str, value_date: date, amount_minor: int, reference: str)
  • LedgerEntry(id: str, booking_date: date, amount_minor: int, reference: str, kind: "payment"|"refund" = "payment", original_reference: str|None = None)
  • Match(statement_ids: frozenset[str], ledger_ids: frozenset[str], rule: str)
  • ReconResult(matches: list[Match], unmatched_statement: dict[str,str], unmatched_ledger: set[str])

All amounts are integer minor units in one synthetic currency (CAD, 2 dp); statement amounts are signed, ledger payment/refund amounts positive. Dates are calendar dates. IDs are unique within each input, strings sort lexicographically. Inputs are valid and must not be mutated. Apply complete passes in the following order; each id can appear in at most one match:

  1. R1: process statements by (value_date,id). Match an unmatched ledger payment with equal reference and equal signed amount, date difference <=3 calendar days. Choose smallest absolute date difference, then earliest booking_date, then smallest id.
  2. R2: process remaining ledger payments by (reference,booking_date,id). For each payment, collect all unmatched statement lines of the same reference within +/-3 days of that payment. If at least two lines and their signed sum equals its amount, match the whole group. Do not search subsets. Negative lines, if present, contribute their signed amount to this pass.
  3. R3: process remaining negative statements by (value_date,id). Match an unmatched ledger refund, with original_reference equal to the statement reference and positive amount equal to abs(statement amount). There is no date restriction in R3. Choose earliest booking_date, then smallest id. The refund's own reference need not equal the original reference.
  4. R4: each remaining statement identical in (value_date,amount_minor,reference) to any matched statement gets reason duplicate; otherwise no_match. Unused ledger ids form unmatched_ledger.

Worked example (CAD minor units; every reference is a synthetic invoice id):

Statement idvalue_dateamountreferenceLedger idbooking_dateamountreference
s12026-01-0510000INV-1l12026-01-0310000INV-1
s22026-01-064000INV-2l22026-01-0610000INV-2
s32026-01-076000INV-2

s1 matches l1 by R1 (two days apart, inside the window). s2 and s3 are the only unmatched lines for INV-2 inside the window of l2 and sum to 10000, so R2 matches both to l2. Nothing is left unmatched.

Use rule strings R1, R2, R3. Match-list order is not graded. Exact membership, reasons, partitions and no reuse are graded. Empty inputs produce empty collections. Run from starter: python -m pytest tests -q.

Source