Reference
referenceApplies the rules in order with repeated scans, tracking consumed statement and ledger ids in sets.
04 · Reconciliation
Match bank statement lines to ledger entries with exact, split, refund and duplicate rules, deterministically.
referenceApplies the rules in order with repeated scans, tracking consumed statement and ledger ids in sets.
alt_validIndexed candidate buckets and explicit remaining-id maps instead of repeated scans with consumed sets.
wrong_no_consumptionAllows one ledger payment to match multiple statement lines.
wrong_refund_absTreats negative statement amounts as payments of the absolute amount.
wrong_split_ignoredOmits aggregate split matches.
r3_latest_bookingExpect: rejectObserved: rejectedSelects the latest refund booking, violating the R3 earliest booking date tie-break.
r1_no_booking_tiebreakExpect: rejectObserved: rejectedBreaks equal distance by id before date, violating the R1 earliest booking date tie-break.
statements_by_id_onlyExpect: rejectObserved: rejectedProcesses statements by id alone, violating the (value_date,id) order.
r1_matches_refundsExpect: rejectObserved: rejectedAllows refunds as exact matches, violating the R1 payment-only rule.
r2_ledger_order_by_idExpect: rejectObserved: rejectedProcesses split payments by id alone, violating R2 (reference,booking_date,id) order.
dup_ignores_dateExpect: rejectObserved: rejectedMarks different-date statements duplicate, violating the R4 signature including value_date.
r2_excludes_negativeExpect: rejectObserved: rejectedDrops negative split lines, violating the R2 signed sum of all eligible lines rule.
alt_cleanroomExpect: acceptObserved: acceptedUses independent pools and complete passes derived from the ticket.
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 | no consumption | refund abs | split ignored |
|---|---|---|---|---|---|---|
| Duplicate statement line left unmatched | Fail | Pass | Pass | Fail * | Pass | Pass |
| Split all lines | Fail | Pass | Pass | Pass | Pass | Fail * |
| Split wrong sum and no subset | Pass | Pass | Pass | Pass | Pass | Pass |
| Refund kind and original reference | Fail | Pass | Pass | Pass | Fail * | Pass |
| Tie breaks and window | Fail | Pass | Pass | Pass | Pass | Pass |
| Ordering and candidate rules | Fail | Pass | Pass | Fail | Pass | Fail |
| Duplicate signature includes date | Pass | Pass | Pass | Fail | Pass | Pass |
| Ground truth transforms | Fail | Pass | Pass | Fail | Pass | Fail |
| Test | Starter | Reference | Alternative | no consumption | refund abs | split ignored |
|---|---|---|---|---|---|---|
| Clean one to one | Pass | Pass | Pass | Pass | Pass | Pass |
| Ticket worked example | Fail | Pass | Pass | Pass | Pass | Fail |
| Unrelated line is unmatched | Pass | Pass | Pass | Pass | Pass | Pass |
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:
(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.(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.(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.(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 id | value_date | amount | reference | Ledger id | booking_date | amount | reference | |
|---|---|---|---|---|---|---|---|---|
| s1 | 2026-01-05 | 10000 | INV-1 | l1 | 2026-01-03 | 10000 | INV-1 | |
| s2 | 2026-01-06 | 4000 | INV-2 | l2 | 2026-01-06 | 10000 | INV-2 | |
| s3 | 2026-01-07 | 6000 | INV-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.