A disputed invoice is money that has stopped moving and a conversation nobody owns. "Pricing will look at it" is not an owner; "soon" is not a deadline. This engine gives every dispute a cause, a team and a working-day clock, escalates the ones that pass it, and counts the cash each resolution lets go. It runs below, in your browser, on 300 disputes — twice: once with the clock, once with the same disputes and nobody watching.
A dispute is the one state of an invoice where chasing makes things worse. The customer has said "this is wrong"; until someone on your side says "here is the answer", every reminder is an insult and every day is free for them. In the procurement startup's ledger and the logistics receivables, the disputed pile was small in count and large in age — the same handful of invoices, months old, each waiting for a team that did not know it was waiting for them. The fix was not a nicer email. It was a clock. Route the dispute by its cause to the team that can answer it, give that team a deadline in working days, escalate the moment the deadline passes, and count the cash the answer releases. The Dispute Clock is that, written down as rules — TRACE and SEAL of the Leak Ledger — and run against the world without it.
There is no judgement in the engine about who is right. It does not decide the dispute; it decides who must decide it and by when, and it refuses to let the question sit in nobody's queue. The cause code is the routing key, the SLA is a number printed per lane, escalation is a rule and not a favour, and the measure is not "disputes closed" but cash unlocked by day — because a dispute resolved as a credit still releases the rest of the invoice, and an invoice upheld after ninety days has already cost most of what it was worth.
| STEP | RULE | WHAT IT DOES |
|---|---|---|
| LOGGED | A dispute arrives on a day with an invoice amount and a cause: PRICE, QUANTITY, QUALITY, PO_MISMATCH, TAX or OTHER | The cause is the routing key. No cause, no route — the desk assigns one. |
| TRIAGE | The desk opens up to 6 disputes per working day, oldest first; each takes 1–2 working days to confirm the cause and hand over | The queue in front of the desk is the first place disputes hide. Its capacity is a slider. |
| ROUTE | PRICE → PRICING (SLA 3 wd) · QUANTITY → OPS (5) · QUALITY → QA (7) · PO_MISMATCH → SALES (4) · TAX → FINANCE (3) · OTHER → the desk itself (5) | One team, one deadline, printed. The SLA multiplier scales all six. |
| THE CLOCK | The owner's time to answer is drawn around its SLA — exp(normal(log SLA, 0.45)) working days, times the multiplier | A shorter clock is a faster owner: the deadline shapes the behaviour, which is the whole bet. |
| ESCALATE | Still open at the deadline → the owner's manager is paged; from then the resolution hazard doubles (the remaining time is halved, rounded up) | Counted, per team. A lane that escalates every second dispute has the wrong SLA or the wrong staffing — the table says which. |
| RESOLVE | CREDIT (a partial credit, 5–100% of the invoice), RE_INVOICE or UPHELD, with probabilities per cause (PRICE .55 / .15 / .30 · QUANTITY .60 / .20 / .20 · QUALITY .50 / .10 / .40 · PO_MISMATCH .10 / .70 / .20 · TAX .20 / .60 / .20 · OTHER .30 / .30 / .40) | Cash unlocked on the day of the answer = invoice − credit. A re-invoice and an upheld invoice both release the full amount. |
| NO CLOCK | The same 300 disputes, the same draws: desk capacity 2 a day, no deadline, no escalation, every owner duration × 2.2 | The baseline. Its "within SLA" is measured against the clock it never had, so the two columns compare. |
| Tie-out | 300 = resolved by day 120 + with an owner + still in triage, for each policy; cash unlocked = Σ (amount − credit) over the resolved | If the sum breaks, nothing is published. It never should; the strip shows the check. |
# the clock — the sentence an ops lead can disagree with (engine.py) dur = max(1, rhu(d["raw"] * mult)) # the owner's duration, working days sla = max(1, rhu(SLA[d["cause"]] * sla_mult)) if clock else SLA[d["cause"]] esc = clock and escalation and dur > sla esc_day = add_wd(w, sla) if esc else None # the deadline: SLA working days after handover actual = sla + (dur - sla + 1) // 2 if esc else dur # past it, the remainder is halved (rounded up) r = add_wd(w, actual) # resolved; amount - credit lands this day
Run the clock to see what it releases.
Three things to try. Press NO CLOCK: the sliders become the world without one — a two-a-day desk, no deadline, nobody paged — and the big number goes to zero, because now both worlds are the same one. Press DOUBLE THE TRIAGE DESK and almost nothing moves: at six a day the desk was never the bottleneck, the owner's clock was. Then drag the SLA multiplier to ×0.5 and watch escalations fall while the cash comes sooner — a tighter clock is not more paging, it is faster answers. And switch escalation off with the SLA left alone: the deadline still exists, nobody enforces it, and the p90 tells you what a deadline is worth without a consequence.
| WHEN | WHAT HAPPENS | WHO SEES IT |
|---|---|---|
| 09:00 daily | New disputes are logged from the mailbox and the portal; the desk opens the oldest first, confirms the cause, routes each to its lane. A dispute with no cause after two working days is a desk failure, not a customer one. | The desk. The customer gets one line: who owns it and by when. |
| Hourly | Every dispute with an owner is checked against its clock. Past the SLA → the owner's manager is paged with the invoice, the cause and the days late. Nobody has to notice; the clock notices. | The manager, once. A lane that pages daily is staffed wrong or clocked wrong. |
| 09:05 daily | Publish the open list: one row per dispute — owner, cause, days with owner, days to deadline, invoice value held. Tie-out first: logged = resolved + with owner + in triage, or no publish. | Collections, so they stop chasing what is disputed; sales, so they see their lane. |
| Weekly | Two lines on one chart: cash unlocked this week, value still held by age. SLAs are revisited only when a lane's hit rate moves — not when someone complains about the paging. | Leadership — the WATCH stage of the Leak Ledger. |
A dispute is not a difficult customer; it is an unanswered question with a value attached. Give the question an owner and a clock and most of them get answered in a week, and the money behind them moves. Leave it to goodwill and the same questions age quietly in six inboxes until the invoice is too old to matter. The engine does not make the answer better. It makes the answer arrive.
Disputes sitting in six inboxes? A dispute clock is a two-to-three-week SEAL: your cause codes, your lanes, your SLAs — with a desk queue, an escalation that pages, and a weekly line that shows the cash it released.
Start with a TRACE → engine.py README results.json The case it came from →