Engineering7 min read
The invoice never says which job it's for
Reading a PDF is solved. Deciding which of a company's forty open jobs it belongs to is the whole problem, and a confident wrong answer is worse than no answer.
Written by OZP Studios, Seattle, WA
A representative document from the first week of Verdant Ledger: a photo of a paper ticket, taken at an angle in a truck, for a supplier whose name appears three different ways in the company's own records, with a handwritten note in the margin that says “north lot.” There is no job number on it. There is no PO. The company has two jobs that could plausibly be called the north lot and one of them closed in November.
Extracting text off that is a solved problem now and has been for a while. Deciding where the money goes is not, and that's the part the client is actually paying for.
Why the model doesn't get to name the job
The first version we built asked a model to read the document and return a job code. It worked well enough in a demo to be dangerous. Sometimes it invented a code with the right shape that didn't exist, which is fine, you catch that on a lookup. The bad case is when it returns a code that does exist and is wrong.
A wrong invoice doesn't look like an error. It looks like a budget. It sits in a total, the total looks reasonable, and the job it should have hit looks healthier than it is until somebody closes the books and has to explain a number. Miscoding is worse than a backlog, because a backlog is visible and this isn't.
Retrieval first, then a multiple choice question
So the model never sees an open field. Before it's asked anything, we pull candidates out of the company's own history: jobs this vendor has billed to before, jobs open on the invoice date, jobs matching any site or foreman or PO string we found in the document, and the lines that look most like this one from the past two years. Usually that's somewhere between three and eight rows.
Then the question is: here is the document, here are these eight real jobs, pick one or say none of them. The model is good at that. It's much less good at remembering a company it has never worked at.
“None of them” is a first-class answer and we spent real effort making sure the system prefers it. Below a confidence threshold, or when the top two candidates are close, nothing gets filed. It lands in a queue instead.
The number that actually matters
About eight in ten invoices file themselves without anyone touching them. That's the number people ask for and it's the less interesting one.
The metric we watch is how many auto-filed lines get re-coded by a human afterward, because that's the count of times the system was confidently wrong. It's been sitting near 3%, and when it moves we go look at why, usually a new vendor or a job naming convention someone changed without telling anyone. The morning queue takes an operator about fifteen minutes. That trade — a quarter hour of certain work to avoid an invisible error — is the actual product.
Two things that stayed dumb on purpose
Vendor identity. The same supplier arrives as “ABC Supply,” “ABC Supply Co,” “Abc Supply Company LLC,” and once as “ABC Suppy” from a phone keyboard. We do not fuzzy-match those at file time. A person confirms a new vendor once, the system remembers the alias forever, and after the first couple of months this basically stops happening. Automatic merging was a bad week for us and we took it out.
Duplicates. The check is vendor plus amount within a five-day window, plus a hash of the file. It flags, it doesn't block, because identical monthly charges are real. Equipment rental bills the same number on the same day every month and that's not a mistake, it's a rental.
Still unsolved
One invoice covering four jobs on a single line. A person has to split it, and we have built three different interfaces for splitting and disliked all of them. The current one is a table with an amount field per job and a running remainder that turns red when it doesn't reconcile, which works and looks like something from 2009. It's been eighteen months in production and that screen is still the thing we'd redo first.