A distributor was approving every supplier invoice on human judgement alone. Duplicates and short deliveries were being paid, and nobody could see it happening.
The Problem
Three documents have to agree before a supplier is paid: what was ordered, what arrived, and what was billed.
A small team checked every invoice against a purchase order and a goods receipt by hand. At the volume arriving each month, what actually happened is that they checked the easy ones carefully and waved the rest through. Suppliers were paid twice, short deliveries were paid in full, and nobody in the business could say how much it was costing.
Money left the business unnoticed
Duplicate invoices, deliveries billed in full after arriving short, and quiet price drift between the agreed rate and the invoiced one.
Early settlement discounts lost
The approval cycle ran long enough that discounts for prompt payment expired before an invoice cleared.
No answer to a simple question
Nobody could say what invoice processing cost, how often it was wrong, or which suppliers accounted for the errors.
Constraints
The non negotiables that ruled out the obvious approach.
An incorrect automatic approval sends cash to a supplier for goods never received. The system is tuned to abstain, and is measured on false approvals rather than on coverage.
Tolerances change per vendor and per category, often monthly. If changing a tolerance needed a developer, the system would be abandoned inside a quarter.
Goods receipt happens on a loading dock at pace. The system observes that process and never becomes a step that can block it.
Finance would not allow automatic payment on day one. The design assumed a long shadow period where it predicted and was scored against the humans doing the job.
How It Works
Match at the line level, keep the running state on the purchase order rather than the invoice, and make every exception arrive with a specific reason.
Supplier email attachments, an EDI feed from the larger vendors, and scanned paper from the smaller ones, all landing in one queue with duplicate detection at intake.
Totals agree by coincidence more often than people expect. Every line is extracted with its own confidence, so a short delivery hidden inside a correct looking total is still caught.
The supplier's description never matches your SKU. A per vendor mapping is built from the first corrections a buyer makes, then reused, so each vendor gets easier over time.
Received and invoiced quantities accumulate against the purchase order line. This is what makes partial deliveries and split invoices work in any order.
Price and quantity tolerances per vendor and category, edited in the application by the finance team, versioned so a past decision can be explained.
An exception names the line and the disagreement: ordered 400, received 380, billed 400. A generic failure would just move the manual work somewhere else.
The Hard Part
The naive model attaches a match to an invoice: find the order, find the receipt, compare. It survives about a week of real data. A single purchase order becomes several partial deliveries and several invoices, they arrive out of sequence, and one invoice routinely spans two deliveries.
Moving the state onto the purchase order line fixed it. Each line carries an ordered quantity and accumulates received and invoiced quantities as events land against it. An invoice is then not matched in isolation, it is applied to the line and either fits within what remains or does not.
That reframing made ordering irrelevant. An invoice arriving before its goods receipt is no longer an error, it is simply a line where invoiced exceeds received, which parks until the receipt lands or ages into an exception.
It also made overbilling visible for the first time. When the cumulative invoiced quantity exceeds the cumulative received quantity on a line, that is a precise, defensible finding a buyer can take to a supplier, rather than a suspicion.
Technical Decisions
| Choice | Why | Instead of |
|---|---|---|
| State on the PO line | Partial deliveries and split invoices make invoice centric matching unworkable. Accumulating received and invoiced quantities per line makes arrival order irrelevant. | Matching each invoice against a receipt in isolation |
| Append only event log | Every receipt and invoice is an event applied to a line. A disputed payment can be replayed exactly as it was decided, which is what finance asks for first. | Overwriting quantities in place on the order |
| Tolerances as versioned data | Finance edits tolerances themselves, and old decisions still explain themselves because the version in force at the time is retained. | Thresholds in configuration files owned by engineering |
| Shadow mode as a first class feature | It predicted while humans decided, and the disagreements were the tuning signal. It is still used whenever a vendor changes format. | A pilot on a subset of low value vendors |
| n8n for approval routing | Approval chains change with staff and delegation. Operations edits the flow visually rather than requesting a deployment. | Approval logic embedded in application code |
Outcome
What changed for the business.
Anything that reconciles cleanly against the order and the receipt posts without a person, so attention goes to the ones that do not.
Near duplicate detection at intake catches the repeat submissions that previously slipped through as ordinary invoices.
Matching at the line level means a partial delivery billed in full is held with the specific line named, rather than discovered at stock count.
Tolerances are edited in the application and versioned, so a past decision still explains itself and no change needs a deployment.
In Hindsight
What I would do differently.
I modelled the match against the invoice first, which is the obvious reading of the problem and is wrong. Real time was lost before moving state onto the purchase order line, and every hard case afterwards became straightforward.
Shadow mode was added because finance insisted, not because I planned it. It turned out to be the most valuable feature in the build, and I would now write it first on anything that touches money.
The SKU mapping learns from buyer corrections, but I did not show buyers that their corrections were teaching it. Once the interface said so, correction rates rose sharply and coverage improved with them.
Get in Touch