Files
jorgecuadros-platform/docs/BACKLOG.md
T
rmancinasandClaude Opus 5 d645ba51d3
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m39s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m21s
feat(policy-ocr): read A.N.A. Seguros' two policy faces
A.N.A. is the Rosarito office's tourist auto book and the second carrier
the policy OCR pipeline reads. It ships two unrelated faces, and the split
is different from GMX's: GMX ships two documents about one policy, A.N.A.
ships two products.

  AUTOMOBILE (SPECIAL POLICY FOR TOURISTS)  insures a car; vehicle table,
                                            9 numbered sections, one
                                            LIMIT OF LIABILITY column
  DRIVER'S POLICY (the office: "licencia")  insures up to 5 named drivers;
                                            no vehicle at all, 6 unnumbered
                                            sections in a different order,
                                            SUM INSURED + PREMIUM columns

The four automobile products the office sells (amplia / responsabilidad
civil, annual / by-the-day) are the same layout with different numbers, so
they get one parser rather than four.

These are born-digital portal PDFs, so pdftotext -layout returns exact
columns and the driver's-policy parser uses that: its two value columns
print the same shape (100,000.00 usd. / 18.70 usd.) with no per-row label,
so horizontal position is the only thing separating them. The split comes
from the header's own offsets, not a constant, because they shift between
products; when it can't be read every amount is reported as a sum insured
and the reviewer is told, rather than half the premiums being filed as
coverage limits.

Three things the layout will punish a naive read for:

- Each PDF prints its face two or three times (ORIGINAL, AGENT COPY, then
  a receipt and three travel cards) and the pipeline concatenates every
  page before parsing. The coverage walk is bounded to the first copy and
  the driver list to the first POLICY HOLDER block. Unbounded, the licencia
  returns the same person three times, which reads as a three-driver policy
  rather than as a bug.
- The money row is read positionally off its header. An unused DISCOUNT
  prints as a bare "-", so "find the six amounts" shifts every value one
  column left on a discounted policy.
- Two five-digit numbers sit in the header band and only one is the agent
  clave; the agent's street address is "BENITO JUAREZ 25 No.50 INT 38",
  three lines above the No. cell holding the policy number.

Sections 6-8 print a PREMIUM where the others print a limit, so
ParsedCoverage gains an optional `premium` (GMX never fills it) and the
review table a column: $40 is what legal aid cost, not a $40 liability
limit. Exclusions follow the GMX rule and go in the risk label with a null
amount -- which matters more here, since a responsabilidad-civil policy
prints 0.00 for material damage and the two are identical on the page.

Also in this change:

- coveragePeriodDays is parsed and written. A.N.A. sells 3- and 4-day
  policies; Policy.coveragePeriodDays defaults to 365, so a weekend policy
  left at the default sits in the renewals window a year out. Derived from
  the dates, cross-checked against the printed DAYS cell, disagreement
  noted not resolved.
- Vehicles and named drivers are parsed, shown read-only in review, and
  written as Vehicle / InsuredDriver rows on confirm, skipping any already
  on the policy (VIN then plate; licence then name). The case that forces
  the skip is confirming a renewal onto an existing policy. Nothing is ever
  updated or deleted -- a changed plate lands as a second row for a human.
- Batch.provider is set from what the parsers actually claimed instead of
  being hardcoded "GMX", so a mixed upload is labelled as mixed and the
  header can never contradict its own documents. PolicyDocument.documentType
  follows the same rule (was hardcoded GMX_POLICY).
- matchNote becomes TEXT. It was VARCHAR(191) and the note trail was sliced
  to 190 chars, which cut the tail notes -- the "could not read X" ones.
- The policy detail page renders an array coveragesJson as a table. Both
  shapes have always been possible there, but the object renderer was the
  only one, so an OCR-confirmed policy showed a row per array index labelled
  "0", "1", "2" with [object Object] as the value. ANA makes that routine.

GMX is untouched behaviourally; its two parsers now spread a shared empty
base instead of listing every null field. 29 new parser cases against
verbatim pdftotext output of three real ANA PDFs, 53 in the suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:09:16 -07:00

250 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backlog — what is pending, missing, and not yet built
One place for work that is known-outstanding. Compiled 2026-08-02 from
`PLAN.md`, `RESUME.md`, the four specs and the two OCR docs, then **checked
against the code and the dev database** rather than trusted — several items in
those documents had already been closed, and two defects they describe are
still live.
This file is an index, not a replacement. Each item points at the document that
carries the reasoning. Close an item *there* as well as here, or the two drift.
**Verified against dev at compile time** (re-run before trusting the numbers):
```
policy_types: AUTO, LICENCIAS, MULT
policies NULL policyTypeId: 5
policies pending liquidación: 226
customers: 1536
last tag: v1.0.6 (2026-08-02 02:06 UTC) — 14 commits, 5 migrations behind HEAD
```
---
## 0. Ship-blocked — read before the next deploy
**Everything from the notificaciones arc is unreleased.** `v1.0.6` predates it.
Five migrations are waiting:
```
20260801120000_ocr_batch_discarded
20260801130000_renewal_email_notifications
20260801200000_mass_email_notifications
20260802120000_renewal_notices_unified_log
20260802140000_app_settings
```
Plus three backup-pipeline fixes that have never reached prod (`860d483`,
`567b033`, `898cf48` — the last prod run went green through the whole chain and
died on the final step wanting `deploy/.env.prod`).
> ### `SES_*` secrets created in Gitea 2026-08-02 — unblocked, unverified
>
> The variables were wired through both deploy workflows and the app stack but
> had never been set. **They now exist.** What that clears: the production
> image runs `NODE_ENV=production`, which disables the stdout dev fallback, so
> a blank config made every send fail and log `FAILED` — and the pólizas sweep
> defaults to **enabled, 06:00 America/Tijuana**, so the failure would have
> repeated nightly.
>
> **Not yet confirmed, and the first deploy is what confirms it:**
>
> 1. **Names match.** The preflight checks `SES_REGION`, `SES_FROM`,
> `SES_ACCESS_KEY`, `SES_SECRET_KEY` and warns by name if any is blank —
> read that warning on the next run. No `_GALACTUS` suffix on any of them;
> one SES identity serves every deployment.
> 2. **`SES_FROM` is a verified identity in `SES_REGION`.** An unverified
> sender is rejected per-send, which looks identical to a missing config in
> the log.
> 3. **The AWS account is out of the SES sandbox.** This is the one that would
> hurt: in sandbox, SES only delivers to *verified* recipients, so a renewal
> sweep across 815 policyholders would fail almost every send while the
> config looks entirely correct. Check before letting a real sweep run.
>
> Until 2 and 3 are confirmed, run the first sweep with `debug` on — it diverts
> every recipient to the override inbox, and on the pólizas side it also leaves
> the avisos pending, so nothing is consumed by a failed test. See
> [`MASS_EMAIL_NOTIFICATIONS.md`](MASS_EMAIL_NOTIFICATIONS.md) "Send flags".
Also outstanding on the deploy path: every pre-existing database still needs
its one-time `prisma migrate resolve --applied 0000_init`.
---
## 1. Blocked on a decision from Jorge
Nothing here is a build problem. Each one makes the work either impossible or a
guess.
| # | Question | Blocks | Source |
|---|---|---|---|
| 1.1 | What "garantías" refers to | §2 liquidación's exclusion filter | INSURANCE §2 |
| 1.2 | What "Solicitud Atlas" is — application form or certificate | §3 entirely | INSURANCE §3 |
| 1.3 | Carrier API **direction**: outbound quote/issue (ANA supports today) or inbound portfolio sync (no evidence either carrier offers it) | whether §4 is buildable at all | INSURANCE §4 |
| 1.4 | CFE amount: the rounded barcode figure (`$268`, what is paid at the window) or the exact breakdown total (`$268.88`) | the parser currently takes the barcode | STATEMENT_OCR / RECEIPT §2 |
| 1.5 | The Seguros USD bank's name, currency and details | multi-bank is built; that account does not exist yet | RECEIPT §3 |
| 1.6 | Whether recycling ever means a true data purge. The *triggers* are now settled and built (see §5 "NUMid allocation"); what is still open is whether a recycled id's old rows are ever deleted rather than left attached to the previous customer | nothing — the allocator ships without a purge | RECEIPT §4 |
| 1.7 | Notice body in Spanish or English | `Customer` carries no language preference | INSURANCE §1 |
| 1.8 | How to model `TRASPASOS PAYPAL` — a clearing account, not a customer, carrying 7.03M MXN over 309 movements and therefore topping the adeudo worklist | deliberately not special-cased in code | RESUME §6 |
| 1.9 | The 78 policyholders with no email — skip silently or produce a print worklist | recommendation is the worklist | INSURANCE §1 |
1.3 also needs the practical half: WSDL + credentials for
`server.anaseguros.com.mx/ananetws/service.asmx`, whether a cartera download
exists for an agent's own book, whether GMX daños has any machine interface at
all, and whether one Grupo Valore credential spans both carriers. All four go
in the same phone call — (55) 5480-4000.
---
## 2. Live data defects — open, and confirmed open today
### 2.1 `policy_types` is missing `INCENDIO` and `M_EMPR`, and 5 policies are orphaned
`policyTypeId` is `String?` with a plain relation, so Prisma's default is
`SetNull`. The spec's recommended `onDelete: Restrict` was **never applied**.
Five `m_empr` policies lost their ramo; four of them are pending liquidación
and are invisible to every ramo-filtered query — including the pending report
§2 is supposed to produce.
Fix alongside the liquidación work (3.1), since it distorts that feature's own
report. Source: INSURANCE "Two defects found while verifying this spec".
### 2.2 ≤41 MULT second settlements were dropped in migration
`MULT`/`INCENDIO` carry two settlement slots and `M EMPR` carries four; `Policy`
collapses to one. Spec recommends moving settlement onto
`PolicyPaymentInstallment` rather than adding a second slot. Open sub-question:
whether to backfill the lost rows.
### 2.3 Three dead tables
`EmailTemplate`, `EmailCampaign` and `EmailLog` exist in the schema with
**zero references anywhere in `apps/api/src` or `apps/web/src`**. They were
scaffolded for plan step 10's "email campaigns"; notificaciones shipped against
`email_notification_log` instead. Either wire them or drop them — a schema that
carries tables nothing writes teaches the next reader the wrong thing.
---
## 3. Spec'd, not built
| # | Item | State | Source |
|---|---|---|---|
| 3.1 | **Liquidación batch workflow** | ~70% of the fields already wired end to end. **226 policies pending.** Needs the ramo-parameterized pending report + `POST /policies/liquidate-batch` under a new `policy:liquidate` (MANAGER). Smallest remaining piece of step 12 | INSURANCE §2 |
| 3.2 | **Certificate rendering** | The report half is buildable now off the same `format: "letter"` machinery as `aviso-renovacion`. Portal delivery waits on steps 8/9. Whole section waits on 1.2 | INSURANCE §3 |
| 3.3 | **Carrier API integration** | Blocked on 1.3. ANA's SOAP service is real with a known operation list; GMX publishes nothing machine-readable and writes the larger half of this book | INSURANCE §4 |
| 3.4 | **Customer-number recycling** | Not started. `Customer.customerNumber` **does not exist in the schema**. Backfill needs care: ~140 utilities rows and all insurance-only customers carry synthetic `rownum_N`/`insrow_N` placeholders, not real `NUM id`s. Last unbuilt piece of step 11 | RECEIPT §4 |
Note 3.3 partly overlaps what [`POLICY_OCR.md`](POLICY_OCR.md) already does —
an OCR path that turns a carrier PDF into a `Policy` row covers some of what
the API was wanted for, and unlike the API it is not waiting on a phone call.
---
## 4. Roadmap steps with no spec
| # | Item | State |
|---|---|---|
| 4.1 | **Step 8 — VPS provisioning** | Not started. Provider undecided (Hetzner vs DigitalOcean), size, Tailscale + MySQL replica. Pure ops; the design is settled. RESUME calls this *the only genuinely blocking item left on the roadmap* |
| 4.2 | **Step 9 — sync worker** | Not built. Unblocked now that `utility_dbo` and the portal code are on disk, but depends on 4.1. Portal write points to poll: `peticion_gas`, PayPal payments, `notifications_settings`, `verification_codes` |
| 4.3 | **Step 10 — reports / campaigns / admin** | Mostly done by other work. `/reportes` exists; "email campaigns" landed as `/notificaciones` against a different table (see 2.3) |
| 4.4 | **Phase B sync in production** | Verified 32/32 against dev, never run from the `/operaciones` UI (the `OpsService` path) nor against a prod-shaped database |
---
## 5. Gaps in features that already shipped
Each of these is a known, deliberate stopping point rather than a bug.
**Notificaciones** — [`MASS_EMAIL_NOTIFICATIONS.md`](MASS_EMAIL_NOTIFICATIONS.md)
- No multi-replica lock on the servicios sweep (pólizas has one via
`scheduled_job_states`). Safe only while the deployment stays single-replica.
- No per-recipient preview of a sent body in the UI.
- No SNS bounce/complaint webhook. `providerMessageId` is captured so one can
be added.
- No `SKIPPED_NO_EMAIL` worklist (see 1.9).
**Policy OCR** — [`POLICY_OCR.md`](POLICY_OCR.md)
- **GMX and A.N.A. only.** The dispatcher is a `[provider, pattern]` table plus
a parser map, so a third carrier is one function and two entries — but no
other layout has been seen, and guessing produces a parser nobody can verify.
- **The `recibo` PDF is unread.** The GMX certificate carries no premium at
all; reading the separate receipt and pairing it to its certificate is what
would let `postPremium` stop being a manual tick. A.N.A. prints its premium
on the face, so this is a GMX-only gap.
- **No `policyTypeId` from OCR.** A.N.A.'s two faces are distinguishable in the
parser (automobile vs driver's policy) and the platform has a `PolicyType`
discriminator, but confirm never sets one — partly because the `policy_types`
rows are themselves incomplete (INSURANCE §live defects).
- **No versioning.** A re-issued policy arrives as a new certificate with the
same number and confirm updates the existing row. Nothing records that this
is the 2027 issue of that policy.
**Statement OCR** — [`STATEMENT_OCR.md`](STATEMENT_OCR.md)
- **CFE / CESPT / Telnor have no unit suite.** They predate the gas/predial
extension and were verified end to end against the 46-page corpus only.
Close this if those parsers are ever touched.
- No way to re-run a corrected parser over a stored batch, though the source
PDFs are kept precisely so it is possible.
- Handwritten folder numbers are deliberately not an input to matching
(Tesseract read `405` as `205`).
**NUMid allocation**`POST /customers/:id/portal-access` assigns the portal
"Security Number", on a staff action rather than at create time, because an
insurance-only customer has no reason to hold a utilities id.
- **Recycling is built but switched off.** `numid.recycleEmpty` in `app_settings`
defaults to false, and that default is a safety property, not a preference:
every reusable id still exists in Access DATGRAL, and a `--sync` migration run
upserts refs with `ON DUPLICATE KEY UPDATE customerId`
(`transform_customers.py:327`), so an id recycled today is handed back to its
Access owner on the next sync and the customer given it loses portal access.
**Flip it on after utilities cuts over**, or for ids deleted at the source.
- **A full re-import would destroy every natively allocated id — now guarded.**
`transform_customers.py:246` truncates `customers` and `customer_legacy_refs`
(and the other transforms truncate everything they own), then rebuild from
Access alone. `migration/native_guard.py` runs before any of it and refuses
when the target holds rows Access has never seen; `run_all.py --force-full`,
or the checkbox in the REIMPORT confirm, overrides and deletes them. **`--sync`
remains the correct path for any database with native rows** — the guard stops
the loss, it does not make full mode preserve anything.
- **The empty-id rule exists twice**: enforced in `numid.service.ts`
(`EMPTY_NUMID_SQL`) and reported by `scripts/numid-audit.sql`. They agree today
(both return 1089, 1094, 1134, 1143 on dev); they are not mechanically kept in
step, so change them together.
- **No un-assign.** Nothing removes a NUMid once given, and nothing reports which
ids were recycled from whom beyond the `customer.portal-access` activity-log
entry.
**Bank** — the concept→ramo classifier is **won't-build**, not pending.
`concepto` is a payee name (0 of 22,354 match a category) and TABLA RAMODOS is
a property-management expense chart, not the business-line split it was assumed
to be. `/banco` intentionally has no category dimension. Recorded here only
because `bank_transactions.categoryId` being null on every row otherwise reads
as unfinished work.
---
## 6. Security / hygiene
- **The old repo's `dbConnection.php` has a plaintext MySQL password committed
to git history.** Not carried into this platform, but the credential is
already exposed and has not been rotated. Rotate regardless of this repo.
- The pre-migrate backup step sets `NODE_TLS_REJECT_UNAUTHORIZED=0` because
Portainer serves a self-signed certificate. Scoped to that one step; the real
fix is replacing the certificate.
---
## Source documents
| Document | What it carries |
|---|---|
| [`../PLAN.md`](../PLAN.md) | build sequencing, locked decisions, per-step status |
| [`../RESUME.md`](../RESUME.md) | session history and §6 open items |
| [`INSURANCE_FEATURES_SPEC.md`](INSURANCE_FEATURES_SPEC.md) | §1 renewal emails (built), §2 liquidación, §3 certificate, §4 carrier APIs |
| [`RECEIPT_CAPTURE_SPEC.md`](RECEIPT_CAPTURE_SPEC.md) | §1 Editor (built), §2 OCR (built), §3 multi-bank (built), §4 recycling |
| [`MASS_EMAIL_NOTIFICATIONS.md`](MASS_EMAIL_NOTIFICATIONS.md) | mass email + schedules, as built |
| [`STATEMENT_OCR.md`](STATEMENT_OCR.md) · [`POLICY_OCR.md`](POLICY_OCR.md) | the two OCR intakes, as built |
| [`DEPLOY_AND_MIGRATIONS.md`](DEPLOY_AND_MIGRATIONS.md) | release chain, galactus, known caveats |