# The Eligibility Engine — who gets which letter

Part of **The Engine Room** on simranjaiswal.in: five automation engines that stop money going
missing between systems. This one is the logic of a supplier's arrears path: every night it decides
which customer in debt gets which letter, and which must never get one. The exclusions run first,
in a fixed order; the routing runs on whatever is left; a tie-out refuses to publish if the columns
don't add up.

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

## What it does

1. **Exclusions, in order, first match wins.** Each account is tested against eight rules and the
   first one that matches is recorded on the account as its outcome:
   `E01` deceased / insolvent → hold; `E02` on the vulnerability register → specialist team, never a
   letter; `E03` open dispute → hold until resolved; `E04` payment plan kept → no letter; `E05` paid in
   the last 14 days → wait; `E06` a letter within the minimum gap (default 21 days) → minimum gap;
   `E07` balance below the threshold (default £50) → below threshold; `E08` "no letters" contact
   preference → other channel.
2. **Routing by days in arrears** for accounts no rule caught: under 7 days → `NOT_YET_DUE`;
   7–21 → `A` first reminder; 22–45 → `B` second reminder; 46–90 → `C` formal notice if the balance
   is at least £100, otherwise `B` again; 91+ → `D` final notice / handover. A broken payment plan
   routes by age like any other account but carries a "plan broken" marker that adds a line to the letter.
3. **Tie-out.** accounts = Σ exclusions + A + B + C + D + not yet due, and every account has exactly
   one outcome. If either check fails nothing goes to the mailhouse.
4. **Harm avoided.** For each rule: the accounts it caught that would have been routed to a letter by
   age alone (`prevented`), and how many of those it guarded *alone* (`sole` — no later rule would have
   caught them either). `letters_prevented` = letters with no exclusions − letters with all of them.

## The synthetic base

There is no client data here. `build_base()` generates 2,000 accounts in arrears from the printed
rules: balance lognormal exp(normal(5.6, 0.9)) clipped £20–£5,000 and rounded to the penny, days in
arrears 1–160, and flags drawn independently (vulnerability register 6%, deceased/insolvent 0.8%,
open dispute 4%, active plan 15% of which 80% kept, payment in the last 14 days 12%, a letter within
the last 21 days 22% with the days since it drawn so the gap slider has something to bite on, "no
letters" preference 3%). Money is held in integer pence so sums tie out exactly. 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, all eight rules on, threshold £50, gap 21 d
python3 engine.py --off E01 --off E02 --off E03 --off E04 --off E05 --off E06 --off E07 --off E08   # no exclusions: the harm
python3 engine.py --threshold 100 --gap 30        # tighter
```

Stdlib only. `results.json` carries the base and its flag counts, the exclusion waterfall (count, pence,
prevented, sole guard per rule), routing per script with balance and plan-broken markers, the tie-out,
letters by age band, and a twelve-account audit sample with a one-line reason each.

## Reference run (seed 42)

See `results.json`. Headline: 2,000 accounts, 980 excluded, 1,020 to routing, 989 letters going out,
954 letters prevented (1,943 would have gone out with no exclusions), tie-out 2,000 = 2,000.

## Where the shape comes from

The eligibility logic and the tie-out discipline of a UK energy supplier's arrears path, anonymised
(https://simranjaiswal.in/work/arrears-path). The rule list, thresholds, rates and every figure in
this repository are synthetic; nothing here is a client's data or a client's rule book.

MIT.
