docs: document policy OCR capture, the feature no spec proposed
Policy OCR shipped 2026-08-01 (5e9cb12) and was documented nowhere. It is
not in INSURANCE_FEATURES_SPEC.md because it did not come from that
meeting — it came out of building the utility statement OCR pipeline in
RECEIPT_CAPTURE_SPEC.md §2 and noticing the same shape fits carrier
policy PDFs. A reader had no way to find that lineage.
New docs/POLICY_OCR.md covers it end to end, with weight on the three
things that are not obvious from the statement side:
- **One PDF = one policy.** Statements arrive bundled one customer per
page, so there a page is a document. A GMX certificate is one policy
across two pages, so the pages are concatenated and the parser runs
once per file — which is why `pageNumber` is a file ordinal and
`storageKey` is the source PDF, not a page image.
- **The GMX certificate carries no premium at all** — it lives on a
separate recibo PDF. Hence the null-preserving confirm and the
double-gated ledger write.
- **OcrModule was extracted out of StatementsModule to make this
possible**, and that was blocking rather than cosmetic.
Cross-referenced from RECEIPT_CAPTURE_SPEC.md §2 (where it came from),
INSURANCE_FEATURES_SPEC.md (which never proposed it, and whose §4 carrier
API it partly overlaps), PLAN.md step 11, README and RESUME.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -527,6 +527,82 @@ Implementation notes worth keeping:
|
||||
(`$268`, what is paid at the window — what the parser uses today) or the exact
|
||||
breakdown total (`$268.88`). One question for Jorge.
|
||||
|
||||
## Policy OCR capture (`/polizas/captura`) — DONE 2026-08-01, unplanned
|
||||
|
||||
**This feature was not in any spec.** It is what the statement OCR work above
|
||||
turned into once the pipeline existed. Having built render → OCR → parse →
|
||||
match → review for CFE/CESPT/Telnor receipts, the same shape obviously fits
|
||||
the *other* stack of paper this office keys in by hand every week: the carrier
|
||||
policy PDFs behind every `Policy` row. Full write-up in `docs/POLICY_OCR.md`.
|
||||
|
||||
The pipeline was reused rather than copied. `OcrModule` was **extracted out of
|
||||
`StatementsModule`** in the same commit so `PolicyOcrModule` could inject
|
||||
`OCR_PROVIDER` without taking on the statement pipeline — that extraction was
|
||||
blocking, not tidying; the policy module could not resolve the provider at all
|
||||
until it existed. `StatementsModule` imports it now and binds nothing itself,
|
||||
so the Tesseract-vs-managed-API decision stays one line in one file for both
|
||||
features.
|
||||
|
||||
Screens mirror Captura exactly: `/polizas/nuevo` is the manual tab,
|
||||
`/polizas/captura` the automática one, both rendering `PolicyCaptura.tsx`, with
|
||||
the batch review queue at `/polizas/captura/[id]`. Abilities `policy:ingest` /
|
||||
`policy:ocr-review`, both STAFF — same trust tier as statement OCR, and for the
|
||||
same reason: nothing reaches the books unconfirmed.
|
||||
|
||||
**The statement pipeline's central assumption inverts here, and that is the
|
||||
thing to remember.** Utility statements arrive bundled *one customer per page*,
|
||||
so there a page is a document and the parser runs per page. A policy PDF is the
|
||||
opposite: the GMX certificate is one policy spread across two pages (contract
|
||||
header on page 1, the per-coverage table on page 2). So every page's text is
|
||||
concatenated and the parser and matcher run **once per file**. Consequences:
|
||||
`PolicyOcrDocument.pageNumber` is repurposed as the file ordinal within the
|
||||
batch (the `(batchId, pageNumber)` unique constraint still holds), `ocrConfidence`
|
||||
is the mean across the file's pages, and a file that fails to parse yields
|
||||
exactly one `OCR_FAILED` row.
|
||||
|
||||
`storageKey` points at the **source PDF**, not a rendered page image, so the
|
||||
review screen embeds the exact artifact the office received and gets the
|
||||
browser's native PDF scrolling, zoom and text selection for free. The page PNGs
|
||||
are still written for future re-OCR, but nothing treats them as the document's
|
||||
identity. (The statement side is the reverse, because there a page *is* the
|
||||
document.)
|
||||
|
||||
Findings worth keeping:
|
||||
|
||||
- **The GMX certificate has no premium on it at all.** Not intermittently
|
||||
missing — the figure lives on GMX's separate `recibo` PDF. The parser leaves
|
||||
the premium fields null and pushes a note saying so, confirm never overwrites
|
||||
an existing `Policy.netPremium` with null, and the optional ledger write is
|
||||
gated on staff ticking `postPremium` *and* a premium actually parsing.
|
||||
Without that second gate a premium-less certificate would book a $0 charge on
|
||||
every confirm.
|
||||
- **Match on `Policy.policyNumber`, never the printed insured name.** Same
|
||||
registrant-vs-current-owner drift that rules names out on the utility side.
|
||||
Zero hits means a new policy and confirm creates the row under a picked
|
||||
customer; more than one hit is surfaced for a human, never auto-picked —
|
||||
duplicate numbers across related parties do occur.
|
||||
- Deductible and loss participation are stored as **strings** (`"5%"`,
|
||||
`"USD 1,000"`): they are printed as a mix of percentages, amounts and free
|
||||
text, and normalising them would lose the distinction.
|
||||
- Carrier-portal PDFs are usually **born-digital**, so the text layer wins and
|
||||
no OCR runs at all most of the time — same precedence rule as the statement
|
||||
pipeline.
|
||||
- The digit-confusion map and the amount-by-separator-position parser are
|
||||
**duplicated on purpose** rather than imported, to keep the module
|
||||
self-contained. Fix a bug in one, check the other.
|
||||
|
||||
8/8 parser tests, all against verbatim text from one real document
|
||||
(`HC_Folio_000767_Traduccion.pdf`).
|
||||
|
||||
**Open:** GMX is the only carrier implemented — the dispatcher is a
|
||||
`[provider, pattern]` table plus a parser map, so a second carrier is a
|
||||
function and two entries, but no other layout has been seen. Reading the
|
||||
premium off the separate `recibo` PDF and pairing it to its certificate is the
|
||||
obvious next piece; it is what would let `postPremium` stop being a manual
|
||||
tick. And nothing versions a re-issued policy — confirm updates the existing
|
||||
row, so there is no record that this is the 2027 issue of that number.
|
||||
|
||||
|
||||
## Notificaciones (`/notificaciones`) — DONE 2026-08-01 → 08-02
|
||||
|
||||
Two features that were spec'd separately turned out to be one screen. The four
|
||||
|
||||
Reference in New Issue
Block a user