Adds four parsers to the statement intake — GAS TIJUANA plus one per
municipality, because Tijuana, Rosarito and Ensenada issue three
completely different predial documents — and a text-layer fast path for
the born-digital invoices the gas company sends.
Measured against a new corpus of 14 documents / 29 pages: provider read
on 29/29, amount on 26/29, and 21/29 auto-matched against the dev
database (22/29 identified). The eight review cases are all legitimate.
Five things the corpus forced:
- Not every statement is a scan. The gas invoices are born-digital CFDIs
whose text layer is exact; rasterising them only loses information (one
sample turned `MEDIDOR: VM01014426` into `ar (LTR): 014420`). The new
`OcrProvider.textPages` reads the embedded layer via `pdftotext
-bbox-layout` — same poppler package as `pdftoppm`, so no new
dependency — and OCR stays the fallback for real scans. Poppler's own
`<line>` grouping follows text flow rather than the page, so words are
regrouped by vertical position; without that, a two-column header
leaves every label separated from the value printed beside it.
- The clave catastral is not two letters and six digits. Position three
is a letter in 15 of the 932 stored claves, and digitising the whole
tail mapped a real `MMB01041` to a nonexistent `MM801041`.
- Tijuana predial prints no clave at all. Its only identifier is an
8-digit municipal account carried in a 32-digit payment barcode, which
the legacy database never held, so it goes in `meterNumber` alongside
gas — `accountNumber` holds `DATMEX.predial`, which is not a
per-property key and must not be overwritten. Those pages start cold
and are taught by the first confirm.
- On Rosarito and Ensenada the clave is the primary key, not a fallback:
those receipts print nothing else, so a unique hit auto-matches. On a
utility bill that merely happens to print one it stays a review hint.
- A misread `$` is the dangerous failure. An Ensenada receipt for
$2,203.00 OCR'd as `82,203.00`, which would post a charge 37x too large
and look ordinary in the ledger. Predial amounts now require a literal
`$` and a page that cannot produce one goes to review.
The scoped match field is now one exported function rather than three
copies of `kind === "GAS" ? ... : ...`, since the lookup, the
blank-service fill and the confirm write-back have to agree or a
reference gets learned into a column nothing searches.
First tests in this package: 23 specs over the parsers and the text-layer
reader, every fixture a verbatim OCR excerpt from a real receipt. Adds
the jest config they need and a build tsconfig so they stay out of dist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scanning a stack of bills and keying them in are the same daily job, ending
in the same ledger path, so OCR intake becomes a mode of the capture screen
instead of a second menu entry:
- components/Captura.tsx holds the mode switch; the manual check form moves
verbatim to components/ManualCheckCapture.tsx and the OCR intake to
components/StatementIntake.tsx.
- /estado-cuenta/lote opens on manual, /recibos on automatic — both render
Captura, so batch-review links and old bookmarks still land right.
- Nav drops "Recibos (OCR)"; "Captura" covers both, with a NavLink.aliases
field so /recibos still highlights it.
Also fixes the "El almacenamiento de documentos no está configurado" failure
staff hit on upload. Uploading with no object storage configured used to
succeed, then die on the first put minutes later, leaving a FAILED batch
whose only explanation was that string. createBatch now refuses up front,
GET /statements/status reports storageAvailable alongside ocrAvailable, and
the intake tab explains the situation instead of offering an upload that
cannot work. S3_* documented in .env.example (deploy stacks already set it).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>