← SIMRANJAISWAL.INTHE ENGINE ROOM · 04 · ARREARS

Who gets which letter

A supplier's arrears path: the logic that decides which customer in debt gets which letter tonight, and which must never get one. Eight exclusions, evaluated in a fixed order where the first match wins; four scripts routed by age; a tie-out that refuses to send anything if the columns don't add up. It is running below, in your browser, on a 2,000-account base you can make as unforgiving as you like.

◆ LIVE ENGINE · RUNS IN YOUR BROWSER · PYTHON REFERENCE PUBLIC
0ACCOUNTS IN ARREARS IN THE REFERENCE BASE · £ SYNTHETIC
0EXCLUSIONS IN A FIXED ORDER · FIRST MATCH WINS · 4 SCRIPTS
0LETTERS PREVENTED · ACCOUNTS A RULE PROTECTED TONIGHT · REFERENCE RUN
0LETTERS TO THE MAILHOUSE, EACH WITH A RULE TRAIL
01 · THE JOB

On the arrears path the danger was never the letter that went out late. It was the letter that should never have gone out at all: to a customer on the vulnerability register, to an estate, to someone who paid on Tuesday. Zero escalations is not luck. It is an exclusion list that runs before the routing, every night, in the same order.

A collections letter is the one piece of customer contact a company sends at scale to people who are, by definition, having a bad month. Most of them should get one; the routing for those is easy and boring. The whole difficulty is the rest: the accounts that must not get a letter tonight, whatever their age or balance says. A dispute that is still open. A payment plan that is being kept. A "no letters" preference that was recorded two years ago and honoured ever since. In the arrears case the discipline that held those lines was a reconciliation: every account in, every account out, exactly once, and no file to the mailhouse if the sum broke. The Eligibility Engine is that discipline written down as rules and run on a schedule — SEAL and WATCH of the Leak Ledger, pointed at harm rather than cash.

02 ·THE ENGINEEXCLUSIONS FIRST, THEN ROUTING, THEN THE TIE-OUT
THE NIGHTLY BUILD · EVERY ACCOUNT IN ARREARS PASSES THROUGH THE EXCLUSIONS IN ORDER; THE FIRST RULE THAT MATCHES OWNS THE ACCOUNT AND IT LEAVES THE PATH · WHATEVER IS LEFT IS ROUTED BY DAYS IN ARREARS TO ONE OF FOUR SCRIPTS · THE VULNERABILITY REGISTER NEVER REACHES THE MAILHOUSE; IT GOES TO PEOPLE.

The engine is deliberately boring: no scoring model, no "propensity", no rule that depends on a rule further down. Each exclusion is a sentence a complaints handler can read and disagree with, the order is printed, and the two numbers a business could argue about — the balance under which a letter isn't worth sending, and the minimum gap between letters — are sliders, not constants. That is what makes the output trustworthy: when the engine says an account gets a formal notice, the trail says which seven rules looked at it first and why none of them fired.

03 ·THE RULESWHAT "NO LETTER" MEANS, EXACTLY
STEPRULEWHAT HAPPENS
E01Deceased or insolventHold. Nothing goes out; the account leaves the path for the estates process.
E02On the vulnerability registerSpecialist team, never a letter. The one branch that goes to people, not the mailhouse.
E03Open disputeHold until resolved. You do not chase a balance the customer is contesting.
E04Active payment plan, being keptNo letter. A broken plan is not excluded: it routes by age with a "plan broken" line added to the letter.
E05A payment in the last 14 daysWait. The customer is moving; the next build will see whether the balance cleared.
E06A letter sent within the minimum gap (default 21 days)Minimum gap. One letter at a time; the slider decides how long "a time" is.
E07Balance below the threshold (default £50)Below threshold. Not worth the stamp or the phone call it triggers.
E08Contact preference "no letters"Other channel. Honoured every night, not just the night it was recorded.
RoutingUnder 7 days → not yet due · 7–21 → A first reminder · 22–45 → B second reminder · 46–90 → C formal notice if the balance is ≥ £100, else B again · 91+ → D final notice / handoverOnly accounts no rule caught. Every account ends in exactly one place.
Tie-outaccounts = Σ exclusions + A + B + C + D + not yet due, and each account has one outcomeIf it breaks, nothing goes to the mailhouse. It never should; the check runs anyway.
# the exclusions, in order; the first match owns the account (engine.py)
def hits(a, rules_on, threshold_p, gap):
    h = []
    if rules_on["E01"] and a["deceased"]: h.append("E01")
    if rules_on["E02"] and a["vulnerable"]: h.append("E02")
    if rules_on["E03"] and a["dispute"]: h.append("E03")
    if rules_on["E04"] and a["plan"] and a["plan_kept"]: h.append("E04")
    if rules_on["E05"] and a["paid14"]: h.append("E05")
    if rules_on["E06"] and a["since_letter"] <= gap: h.append("E06")
    if rules_on["E07"] and a["pence"] < threshold_p: h.append("E07")
    if rules_on["E08"] and a["no_letters"]: h.append("E08")
    return h                      # h[0] fires; len(h) == 1 means it guarded alone

def route(a):                        # only for accounts no rule caught
    d = a["days"]
    if d < 7: return "NOT_YET_DUE"
    if d <= 21: return "A"
    if d <= 45: return "B"
    if d <= 90: return "C" if a["pence"] >= FORMAL_MIN_P else "B"   # a formal notice needs £100
    return "D"
04 ·RUN ITTONIGHT'S BUILD, LIVE, ON A BASE YOU CONTROL
SEED
A 2,000-ACCOUNT BASE IS GENERATED FROM THE PRINTED RULES AND DECIDED IN YOUR BROWSER. NOTHING LEAVES THIS PAGE.
LETTERS PREVENTED TONIGHT

Run the build to see who the rules protected.

THE WATERFALL

Two things to try. Press NO EXCLUSIONS and the prevented count becomes the letters that would have gone out: every account on the vulnerability register gets a reminder, and the estate of a customer who has died gets a final notice. That is what "the routing is fine" looks like without the gate in front of it. Then switch only E06 off and watch the sole-guard column in the table below: most of the accounts it protects are protected by nothing else — a minimum gap is not politeness, it is the rule that stops the same person getting three letters in a fortnight.

05 ·WHAT IT FINDSTHE REFERENCE RUN · SEED 42

The exclusion waterfall

2,000 IN → EACH RULE TAKES ITS SHARE, IN ORDER → THE REST ARE ROUTED

Letters by script and age band

ACCOUNTS PER BAND · WHO GOT WHICH SCRIPT, WHO WAS EXCLUDED

Balance by script

£ IN ARREARS BEHIND EACH LETTER · MEAN PER ACCOUNT

Letters prevented, by rule

CAUGHT · WOULD HAVE GOT A LETTER · GUARDED ALONE

The audit trail, twelve accounts

The first account each rule caught, then the first account sent to each script. Every account in the base has a line like this; a complaints handler can pull it by id.

06 ·WATCHHOW IT RUNS WHEN NOBODY IS LOOKING
WHENWHAT HAPPENSWHO SEES IT
22:00 nightlyPull the base as of close of business: balances, ages, the vulnerability register, disputes, plans, payments, letter history, preferences. Refuse to run if the register extract is older than 26 hours or any flag count moved more than 20% day-on-day.A failed pull pages the owner. A stale register never becomes a letter to someone it should have protected.
22:03Exclusions in order, then routing, then the tie-out: accounts = Σ exclusions + A + B + C + D + not yet due; every account has one outcome and a reason line.The controller. A tie-out failure is the engine finding a bug in itself.
22:04The no-publish rule. If the tie-out breaks, or a single account has two outcomes or none, no file goes to the mailhouse. Yesterday's letters do not go out twice; tonight's do not go out at all until a human has read the diff.Operations gets a "no file tonight" message with the diff attached, never a silent gap.
06:00The mailhouse file lands with a manifest: letters by script, the count the tie-out signed, the list of ids. The specialist team's queue lands separately, with the E02 accounts and nothing else.The mailhouse prints; the specialist team calls.
MonthlyTwo tables on one page: letters prevented by rule, with the sole-guard column, and complaints per thousand letters by script. A rule that guards alone is a rule nobody gets to switch off in a hurry.Leadership — the WATCH stage of the Leak Ledger, pointed at harm.
07 · THE TAKEAWAY

The most important letter is the one that never goes out. On the reference base the routing alone would post 1,943 letters tonight; the eight rules in front of it stop 954 of them, 116 to people on a register that exists precisely so they are not written to. Put the exclusions first, print the order, make the thresholds decisions rather than constants, and let the tie-out refuse to send — and zero escalations stops being a good month and becomes the design.

THE BASE IS SYNTHETIC AND GENERATED FROM THE RULES PRINTED IN engine.py (SEED 42; RATES AS SHOWN). NO CLIENT DATA, NO CLIENT RULE BOOK: THE RULE LIST, THE THRESHOLDS AND EVERY FIGURE ON THIS PAGE ARE INVENTED. THE ENGINE LOGIC IS THE ANONYMISED SHAPE OF THE ELIGIBILITY AND TIE-OUT WORK SIMRAN DOES ON A UK ENERGY SUPPLIER'S ARREARS PATH; "ZERO ESCALATIONS" IS AS REPORTED IN THAT CASE. THE BROWSER ENGINE IS A LINE-FOR-LINE PORT OF THE PYTHON AND REPRODUCES results.json EXACTLY ON THE REFERENCE SETTINGS — THE STATUS LINE ABOVE SAYS SO WHEN IT DOES.

Letters, notices or nudges going out at scale? An eligibility engine is a two-to-four-week SEAL: your rules, in your order, with your thresholds as decisions — running every night with a tie-out that refuses to send the one letter you would have to apologise for.

Start with a TRACE → engine.py README results.json The case it came from →