# The Reconciliation Engine — three ledgers, one truth

Part of **The Engine Room** on simranjaiswal.in: five automation engines that stop money going
missing between systems. This one ties four records of the same event together every morning —
orders, deliveries, invoices and the bank feed — and turns every disagreement into an exception
with a cause, a value, an age and an owner.

Live case, with the engine running in the browser: https://simranjaiswal.in/work/reconciliation-engine

## What it does

1. **Invoices → deliveries.** T1: exact PO reference, delivered, amounts within ₹1 (a second invoice
   claiming a claimed delivery is `DUPLICATE_INVOICE`). T2: same customer, an unclaimed delivery within
   the tolerance (default 0.5%), invoiced 0–30 days after delivery, nearest amount wins — logged, so the
   reference gets fixed upstream. Anything else is `INVOICE_WITHOUT_DELIVERY`.
2. **Deliveries nobody billed.** Delivered more than 7 days ago and claimed by no invoice →
   `UNINVOICED_DELIVERY`. This is the leak.
3. **Payments → invoices.** P1 by reference; P2 by amount within tolerance (closest, oldest first);
   P3 as a part-payment against the smallest open invoice it fits inside; else `UNMATCHED_PAYMENT`.
4. **Aftermath.** Paid but a balance above tolerance remains for 14+ days → `SHORT_PAY` (a deduction
   nobody approved). Unpaid past terms → `OVERDUE_OPEN` (the chase list).
5. **Tie-out.** deliveries = claimed + uninvoiced + too-recent; invoices = T1 + T2 + duplicates +
   unmatched. If either fails, the queue is not published — the engine has found a bug in itself.

"Money found" = uninvoiced deliveries + duplicate invoices + short-pay deductions.

## The synthetic book

There is no client data here. `build_book()` generates 1,200 orders for 60 customers over 180 days
from the printed rules (terms 15/30/45, habitual lag, 12% of customers short-pay three times as
often, 2% never delivered, rounding noise, one payment in ten with no reference, 5% paid in two parts)
with the mess rates as parameters: deliveries never invoiced, mistyped PO references, short payments.
Randomness is mulberry32, a 32-bit generator the browser port implements bit-for-bit, so the page's
reference run reproduces `results.json` exactly (the page's status line says so when it does).

## Run it

```
python3 engine.py                        # reference: seed 42, uninvoiced 5%, typos 6%, short-pays 8%, tolerance 0.5%
python3 engine.py --uninvoiced 0.15 --typo 0.15 --shortpay 0.2   # chaos
python3 engine.py --tol 2.0             # watch short-pays get forgiven
```

Stdlib only. `results.json` carries the book, the match ladder, exceptions by cause, ageing of the
found money, the customers it belongs to and the top of the queue.

## Reference run (seed 42)

See `results.json`. Headline: the match ladder (T1 / T2 / duplicates / unmatched), exceptions by
cause with value, money found as a share of billed, and overdue exposure.

## Where the shape comes from

The reconciliation Simran built on a procurement startup's Zoho + MySQL ledgers
(https://simranjaiswal.in/work/stuck-money) and the tie-out discipline from the arrears work
(https://simranjaiswal.in/work/arrears-path). Outcome figures in those cases are as reported there;
nothing in this repository is a client's data.

MIT.
