290 lines
17 KiB
Markdown
290 lines
17 KiB
Markdown
# 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 (+ M_EMPR after 20260815160000)
|
||
policies NULL policyTypeId: 5 (0 after 20260815160000)
|
||
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` missing rows + 5 orphaned policies~~ — FIXED 2026-08-15
|
||
|
||
`policyTypeId` is `String?` with a plain relation, so Prisma's default is
|
||
`SetNull`, and `removePolicyType()` had no in-use guard — deleting a lookup row
|
||
returned 200 and silently blanked the ramo on every policy using it. That is
|
||
what happened to `M_EMPR` and its 5 `m_empr` policies.
|
||
|
||
Closed by `20260815160000_policy_type_repair` plus the guard in
|
||
`policies.service.ts`:
|
||
|
||
- `M_EMPR` restored and the 5 policies re-pointed at it, scoped to
|
||
`policyTypeId IS NULL AND legacySourceTable = 'm_empr'` so it cannot claim a
|
||
policy blanked for some other reason. Idempotent; verified against dev inside
|
||
a rolled-back transaction.
|
||
- **`INCENDIO` deliberately not recreated.** The legacy `INCENDIO` table has
|
||
1 row and it never loaded, so the type has zero policies — restoring it would
|
||
only add a dead option to the type picker.
|
||
- Deleting an in-use policy type, carrier or adjuster now **refuses** with the
|
||
name and the count. `claims.adjusterId` had the identical `SET NULL` trap and
|
||
is guarded too. `onDelete: Restrict` at the schema level was not applied —
|
||
the application guard gives a Spanish message the operator can act on, where
|
||
a raw FK error would not.
|
||
- The duplicate `ANA` carrier row (1 policy) was merged into `ANA SEGUROS`
|
||
(738), since OCR now assigns the carrier automatically and two rows would
|
||
keep splitting the book.
|
||
|
||
### 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**~~ | **BUILT 2026-10-12** (`b016e53`) as `customers.folderNumber` ("Carpeta"): a label, not a key, unique among non-archived customers, lowest free suggested. Backfilled from the utilities NUMid; insurance-only customers start blank | RECEIPT §4 |
|
||
| 3.5 | **Portal login by email for every customer** | Not started (deferred 2026-10-12). Every customer with an email gets access automatically: email → verification code → session. Token carries the email and resolves the customer set per request, so it survives the cutover REIMPORT. 30 emails are shared by 68 customers → account picker after verify. 107 active customers have no email → no access until the office adds one. Unlocks the 284 customers with no NUMid. In `my-jorgecuadros-web`: `verification_codes` is keyed `(email, numid)`, ~68 files authorize on `$tokenData['numid']` — route them through one helper | meeting 2026-10-08 |
|
||
| 3.6 | **Remove NUMid + password portal login** | Not started (deferred 2026-10-12, lands with or after 3.5). Delete `login.php`'s NUMid/password path and the legacy `PASSWD` reads; the email code becomes the only way in. Assumes no store-installed mobile app still calls `login.php` | meeting 2026-10-08 |
|
||
|
||
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).
|
||
|
||
**Captura de pólizas — desglose de primas** (built 2026-08-18)
|
||
- **IVA y prima total no existen en los datos legacy.** En Access eran
|
||
controles calculados sin campo, así que las 2,378 pólizas migradas leen
|
||
`tax` y `total` en null hasta que alguien las edite. No es recuperable: no
|
||
hay de dónde.
|
||
- **`LOCAL TAX` de A.N.A. no se captura.** El IVA (`TAX`) sí — se guarda desde
|
||
2026-08-18 — pero `LOCAL TAX` es un gravamen distinto sin columna destino y
|
||
**no** se suma al IVA: sumarlo daría una cifra que ya no divide de vuelta a
|
||
una tasa. Imprime 0.00 en todas las pólizas vistas hasta hoy; una distinta
|
||
de cero levanta la nota *"impuesto local N no capturado"* y significa que
|
||
`total` no cuadra contra `netPremium + policyFee + tax`.
|
||
- **`Policy.taxRate` queda en null por la ruta OCR.** A.N.A. imprime el monto
|
||
del IVA, no la tasa, y despejarla a la inversa inventaría una tasa que el
|
||
documento nunca declaró. El formulario resuelve una desde el ramo.
|
||
- **El recargo no se valida contra la forma de pago en datos migrados.** El
|
||
formulario lo deshabilita en ANUAL/CONTADO, pero
|
||
`backfill_policy_premium_breakdown.py` solo advierte cuando encuentra una
|
||
póliza anual con recargo; no la corrige.
|
||
- **Las parcialidades 3 y 4 no llevan desglose.** Access solo dibujó la fila
|
||
de dinero dos veces, así que una póliza trimestral capturada hoy sí puede
|
||
llenar las cuatro a mano, pero no hay nada legacy que migrar a las dos
|
||
últimas.
|
||
|
||
**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 `insuranceProviderId` beyond the two OCR carriers.** Confirm resolves
|
||
the parser's provider to an `insurance_providers` row by name, so GMX and
|
||
A.N.A. land correctly; a policy typed in by hand still gets whatever the
|
||
operator picks.
|
||
- **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 |
|