# The Chase List — the morning list

Part of **The Engine Room** on simranjaiswal.in: automation engines that stop money going missing
between systems. This one turns a ranking of open invoices into a day's work for four collectors —
one line per customer, promises on top, a cool-down, capacity split by running rupees — runs the
list for ten mornings, and races it against the two lists most collections desks actually use:
biggest first and oldest first.

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

## What it does

Every working morning, for ten mornings:

1. **Score.** Every open invoice gets `P(slip)` = clipped logistic of
   `0.9 × prior late rate + 0.015 × days past due − 0.5` (clipped to 0.02–0.98) — the stand-in for the
   who-pays-late model — and a score = amount × P(slip). Overdue invoices are eligible; not-yet-due
   invoices only if due within 3 days and above the courtesy threshold (₹1,00,000).
2. **(a) Bundle.** One line per customer: all their eligible invoices, score = Σ, P(slip) = ₹-weighted
   mean.
3. **(b) Cool-down.** Skip customers contacted in the last 3 days, unless a promise fell due.
4. **(c) Promises.** A promise due today or overdue puts the customer at the top of the list.
5. **(d) Capacity.** 4 collectors × 18 contacts. Priority accounts go to the senior collector first;
   then each line goes to the collector with the lowest running ₹ who still has a slot (greedy, first
   minimum on ties). The balance check — every collector's ₹ within 15% of the mean — is printed, not
   enforced.
6. **(e) Roll-over.** What does not fit is on tomorrow's list, one day older (so a higher P(slip)).
7. **Outcome.** Each contact pays within 5 days with `p = 0.35 + 0.4 × (1 − P(slip))`; of the rest,
   30% promise to pay in 5 days; the remainder is nothing. One rng draw per contact, in assignment
   order; the landing day (1–5) is a second draw only when it pays. A promise follow-up that pays is
   "kept"; one that neither pays nor re-promises is broken.
8. **Tie-out.** 900 = resolved + open; today's lines = called + rolled; contacts = Σ collectors. If
   any breaks, the list is not published.

Two naive lists run alongside on the same book, the same customers and the same draws in the same
order: **BIGGEST_FIRST** (one line per invoice, sorted by amount, no cool-down, no promise priority,
round-robin across collectors) and **OLDEST_FIRST** (same, sorted by days past due). The headline is
cash landed by day 10 with the morning list minus cash landed by day 10 with biggest first.

## The synthetic book

There is no client data here. `build_book()` generates 120 customers (prior late rate
`sqrt(u) × 0.8`, 10% priority accounts, 20% "do not call before 10:00", days since last contact
0–19, 20% with an open promise due between day −3 and day 3) and 900 open invoices (amounts
lognormal `exp(normal(10.3, 0.8))` clipped ₹5,000–₹5,00,000; days past due −10..79, negative = not
yet due). 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). The book uses `seed`; the ten-morning simulation uses `seed + 1`.

## Run it

```
python3 engine.py                                    # reference: seed 42, 18 contacts/collector, cool-down 3 d, courtesy ₹1,00,000
python3 engine.py --capacity 10                      # an understaffed desk: watch the roll-over stop being zero
python3 engine.py --cooldown 0                       # nagging: more contacts, barely more cash
python3 engine.py --courtesy 250000 --seed 7
node parity.mjs                                      # proves the browser port equals results.json
```

Stdlib only. `results.json` carries the params, the book, and for each of the three lists: the
cumulative cash series (days 0–14), cash by day 10, contacts, contacts per ₹ lakh, promises made /
followed up / kept, roll-over by day, invoices resolved, per-collector load with the balance check,
the day-1 funnel and the top twelve lines of the day-1 list, and the ten-morning log.

## Reference run (seed 42)

See `results.json`. Headline: the morning list lands ₹2,86,97,512 by day 10 with 279 contacts
(0.97 per ₹ lakh); biggest first lands ₹2,15,33,591 with 720 contacts (3.34 per ₹ lakh); oldest first
₹1,07,54,573 with 720 (6.69 per ₹ lakh). Extra cash by day 10 vs biggest first: ₹71,63,921, with 441
fewer contacts. All three lists pass the ±15% balance check on this seed.

The customer response is a printed assumption and is generous (a contact that pays clears the whole
bundle; nobody pays without being asked), so read the three lists against each other, not against a
real desk.

## Where the shape comes from

The chase routine behind the collections work at a procurement startup
(https://simranjaiswal.in/work/stuck-money, https://simranjaiswal.in/work/who-pays-late) and the
payment-date model (https://simranjaiswal.in/work/invoice-payment-date). Outcome figures in those
cases are as reported there; nothing in this repository is a client's data.

MIT.
