Extract
I built it for the easy supplier first. The second one is where it had to earn its keep.
Supplier one Electricity: the template
One known invoice layout (two minor historical variants), one file per billing period. A manifest tracks what has already been processed; every run downloads only new or changed PDFs, extracts the billing text, pulls the fields, and appends fact rows. Skip logic first, extraction second, so a rerun with nothing new touches nothing.
Supplier two Gas: the real test
The second supplier's folder is not a folder of gas invoices. It is a folder of everything the supplier has ever sent: sales invoices, monthly rental invoices, credit adjustment notes, purchase orders, statements. Four different invoice layouts mixed in among them.
A pipeline that assumes every PDF in the folder is an invoice breaks the first time it hits a credit note. This one doesn't assume that.
The design decision that mattered: classify by the document's actual extracted text, never by filename. Filenames are inconsistent; text is not. Every document is routed to one of three outcomes: parse as an invoice, skip as non-target, or flag for review, and even the ones it skips are recorded in the manifest, so a purchase order doesn't get re-read and re-rejected on every single run, forever.
One batch: 120 PDFs in, 112 real invoices correctly classified and parsed, 8 correctly skipped, 0 needing manual review.
The second supplier is where a pattern proves itself. Anyone can build a script that reads one layout. The question is what happens when the folder isn't clean.