98f7aa8a2dd91d3512cd144578cd8bc158e1984b
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d5008b545 |
feat(statements): OCR intake for scanned utility bills
Staff key 300+ utility statements per company per month by hand. This adds the ingest -> split -> OCR -> match -> review pipeline that proposes customer and amount per page instead (RECEIPT_CAPTURE_SPEC §2), posting through the existing BillingService.createBatch seam with source=OCR and a per-document captureRef so machine and hand capture share one write path and audit trail. Everything was designed against 10 real scanned statements (46 pages of CFE, CESPT and Telnor bills) rather than from the sample-free spec. The scans have no text layer at all — they are camera images — so OCR is mandatory, and they arrive bundled one customer per page. Measured on those pages the parser identifies the provider 46/46 and reads an account reference 43/46; against the dev database that is 39/46 (85%) exact auto-match, 40/46 identified, with the rest genuine review cases. That closes the OCR-provider question in favour of self-hosted Tesseract: it clears the bar for a queue where a human confirms every row, and OcrProvider keeps a managed API a one-line swap. The samples corrected three things the spec had wrong or unknown: - Clave catastral is NOT predial. DATMEX.clave (934 rows) is what CESPT and predial bills print; DATMEX.predial, which PROPERTY_TAX.accountNumber holds, has 663 distinct values across 1135 rows and appears on no statement. The clave now lives on Property.cadastralKey as the matcher's secondary key; predial is left untouched. This had been blocking predial matching. - Gas was recoverable: 160 of 334 DATMEX.gas values are real account numbers (the rest are ESTACIONARIO/CILINDRO descriptors), now in GAS.meterNumber. - Phone is one billed line per property (534/18/1 across phone1/2/3), so the new TELEPHONE ServiceKind backfills from phone1 only, not three rows. Matching is scoped to one column per service kind and never reads the customer name — a CESPT receipt prints ARNAIZ ROSAS ELSA AURORA for an account this office holds under CATT, RANDY, because the printed name is the registrant, not the current owner. Where a provider prints a payment barcode it beats the printed label (one CFE label OCR'd a digit too many while its barcode was correct) and the two cross-check, with disagreement forcing review. Confirming a document whose service had no reference writes it back, so gas and any other cold start is a one-time cost rather than a permanent queue. Verified end to end against the live dev API and MinIO: real scans uploaded over HTTP, matched, confirmed against a check, and the resulting rows checked in MySQL (negative amounts, captureSource=OCR, concept derived from the batch kind, captureRef linking back to each page). Re-confirming a posted batch is refused. Test data was removed afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2c6a6bf60b |
feat(billing): shared statements module across both business lines
Plan step 6 — the payoff of the unified customer record: a utility charge and an insurance payment finally sit on the same page, under the same person, with a running balance. API (apps/api/src/billing/): - GET /billing — cross-customer movement browser. Search over customer, referencia, cheque, concepto and periodo; filters for business line, currency, charge-vs-credit, concept, origin table and a from/to date range; 5 sorts. Returns totals for the whole filtered set, not just the page, so a filtered view can't be misread as the full ledger. - GET /billing/balances — per-customer receivables worklist with owing/credit/settled buckets and 4 sorts. Raw SQL (parameterized via Prisma.sql): needs conditional sums per currency and per direction in one pass plus ordering and pagination on a computed balance, none of which groupBy expresses. - GET /billing/stats, /billing/facets, /billing/customers/:id. Web: - /estado-cuenta — two views over the same ledger, because staff ask two different questions: "Saldos por cliente" (who owes what) and "Movimientos" (every charge and credit). - /estado-cuenta/[id] — the statement: balance per currency, the same balance split by business line, charges broken out by concept, and the full movement list with a running balance. - Cross-linked from the customer and property detail pages. Two data findings shape the whole module: 1. transactions.amount is a signed ledger. Every charge type is negative without exception (WATER 3115/3117, ELECTRIC 2191/2191, PROPERTY TAXES 926/926, TRUST FEE 188/188) and every deposit type positive (CHECK and CASH DEPOSIT, PAYPAL, all of EFECTIVO). So SUM(amount) is the balance and negative means the customer owes the office. 2. Currency is not summable. 912 of the 1269 customers with a ledger move in both MXN and USD, the charge side is MXN-only while receipts arrive in both, and no per-movement exchange rate was ever stored. A single "total balance" would be a figure that never existed in the books, so every total is reported per currency and the balance filter/sort takes a currency argument rather than collapsing. Also: type_transactions.nameEs is entirely null (the legacy TYPE OF TRX ESPAÑOL column is empty in all 79 rows), so Spanish concept names come from a label map in labels.ts; the entries that are payee names rather than categories fall through untranslated, which is correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |