# The Invoice Validator — the invoice that cannot be rejected

Part of **The Engine Room** on simranjaiswal.in: automation engines that stop money going missing
between systems. This one checks every invoice against the customer's own rules before it is sent,
so the customer's AP portal has nothing to reject. A rejected invoice is not a billing error; it is a
restart of the payment clock, and the engine measures exactly how many days of cash that costs.

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

## What it does

1. **A requirements profile per customer.** Forty customers, each with printed rules: needs a PO number
   (70% of customers) and, if so, the PO must be open with remaining value; a valid GSTIN on the invoice
   (100%); the GSTIN's state must match the bill-to state, the place-of-supply rule (100%); a cap on the
   invoice value per PO line (30%); the bill-to address must match the master (100%); line-level HSN codes
   (50%); invoice date within 7 days of delivery (40%); the delivery note number (35%); portal submissions
   on working days only (25%).
2. **Ten checks, in order, all of them.** `PO_MISSING`, `PO_EXHAUSTED`, `GSTIN_INVALID` (format only, no
   checksum: `^[0-9]{2}[A-Z0-9]{13}$` with a state code 01–37), `STATE_MISMATCH`, `ADDRESS_DRIFT`,
   `HSN_MISSING`, `DATE_WINDOW`, `DN_MISSING`, `AMOUNT_VS_PO` (more than 0.5% over the PO line),
   `DUPLICATE_NUMBER`. A check only counts if that customer requires it. An invoice with any failure is
   **HELD** with the full fix list; the fix takes 1–2 days and it goes out clean.
3. **The world without the validator.** Every invoice goes out as drafted. The customer's portal rejects
   it 4–13 days later with probability 0.85 per applicable failure (the rest slip through), the invoice is
   re-issued after the same 1–2 day fix, and the customer pays terms (30 days) plus their habitual lag
   (normal(8, 6) days) after the re-issue. The clock restarts.
4. **What it costs, in both worlds.** Days delayed per invoice against a clean send, cash delayed as
   Σ ₹ × days (shown as ₹ lakh-days), days added to DSO (value-weighted), mean days to cash, the
   days-to-cash distribution, failures by reason, rejections by customer, and the 40 profiles.
5. **Tie-out.** 800 = clean + held (+ rejected + slipped when the validator is off), 800 = clean +
   rejected + slipped without it, and held = defective. If any of those break, nothing is published.

Headline = cash delay avoided (without minus with), in ₹ lakh-days, and the same thing as days off
the mean days-to-cash.

## The synthetic book

There is no client data here. `build_customers()` draws the 40 profiles and `build_invoices()` generates
800 invoices over 60 days (PO line values lognormal, clipped ₹3,000–3,00,000; raised 0–3 days after
delivery) with the ten defects injected independently at printed rates: PO missing 8%, PO exhausted 4%,
GSTIN missing/invalid 3%, state mismatch 5%, address drift 6%, HSN missing 7%, date outside window 6%,
delivery note missing 9%, amount over the PO line 4%, duplicate invoice number 1%. The sliders scale
those rates as three groups (document, PO, tax). Every random number either world needs later — the fix
delay, the portal's delay, the rejection roll, the payment lag — is drawn at generation time, so both
worlds see the same draws in the same order. 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 rate multipliers 1.0, portal p 0.85
python3 engine.py --no-validator                   # everything goes out as drafted; the portal decides
python3 engine.py --doc 0.3 --po 0.3 --tax 0.3     # clean billing: what is left for the validator to do
python3 engine.py --po 3 --portal 1.0              # a PO-obsessed customer base with a strict portal
node parity.mjs                                    # proves the browser port reproduces results.json
```

Stdlib only. `results.json` carries the parameters, the book, both worlds, the headline, the ten checks
with who requires them / defects injected / failures / rejections caused, the 40 profiles, the top-10
customers by rejection, and the first twelve holds with their fix lists.

## Reference run (seed 42)

800 invoices, ₹2.90 crore billed, 233 defective (28 with more than one failure). With the validator: 233
held, 0 rejected, 0.4 days added to DSO. Without: 201 rejected, 32 slipped through, 2.6 days added to
DSO. Avoided: 616 ₹ lakh-days of cash delay, 2.1 days off the mean days-to-cash, 201 rejections.
The full numbers are in `results.json`.

## Where the shape comes from

The rejected-invoice loop in the procurement-startup collections work
(https://simranjaiswal.in/work/stuck-money) and the payment-date model
(https://simranjaiswal.in/work/invoice-payment-date), where a portal rejection was the single largest
predictor of a late payment because it restarts the clock. Outcome figures in those cases are as reported
there; nothing in this repository is a client's data. The portal's rejection probability, the 4–13 day
rejection delay and the 1–2 day fix are assumptions, printed and adjustable.

MIT.
