Customers created in the staff UI had no NUMid and so could not log in to my.jorgecuadros.com at all: the id is a CustomerLegacyRef row, not a column, and create() deliberately writes none. Allocation is a staff action (POST /customers/:id/portal-access, MANAGER) rather than part of create, because insurance is expected to move to the platform before utilities and an insurance-only customer has no reason to spend a utilities id. The audit that decides which ids are reusable took three passes. "Owns no rows" matches nobody -- migration gave all 1,171 NUMids a property and a transaction. "No transaction in N years" also matches nobody -- every customer carries a synthetic Jan-1 opening-balance row, so everyone looks active this year. Subtracting that row is what makes dormancy measurable, and it leaves 4 never-used ids and 10 dormant ones on dev. Two further traps are encoded in the queries: insurance/DATGRAL is a separate id space that reuses the sourceTable name and runs past 4,000, and ACCOUNT CANCELED is a transaction line type, not an account state -- all 8 customers carrying it have current-year activity. Recycling ships switched off (numid.recycleEmpty, default false). Every reusable id still exists in Access DATGRAL, and a --sync run reassigns refs with ON DUPLICATE KEY UPDATE customerId, so an id recycled before the utilities cutover is silently handed back to its Access owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
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, unverifiedThe 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 logFAILED— 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:
- Names match. The preflight checks
SES_REGION,SES_FROM,SES_ACCESS_KEY,SES_SECRET_KEYand warns by name if any is blank — read that warning on the next run. No_GALACTUSsuffix on any of them; one SES identity serves every deployment.SES_FROMis a verified identity inSES_REGION. An unverified sender is rejected per-send, which looks identical to a missing config in the log.- 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
debugon — 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. SeeMASS_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 ids. Last unbuilt piece of step 11 |
RECEIPT §4 |
Note 3.3 partly overlaps what 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
- 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.
providerMessageIdis captured so one can be added. - No
SKIPPED_NO_EMAILworklist (see 1.9).
Policy OCR — POLICY_OCR.md
- GMX only. The dispatcher is a
[provider, pattern]table plus a parser map, so a second carrier is one function and two entries — but no other layout has been seen, and guessing produces a parser nobody can verify. - The
reciboPDF is unread. The GMX certificate carries no premium at all; reading the separate receipt and pairing it to its certificate is what would letpostPremiumstop being a manual tick. - 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
- 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
405as205).
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.recycleEmptyinapp_settingsdefaults to false, and that default is a safety property, not a preference: every reusable id still exists in Access DATGRAL, and a--syncmigration run upserts refs withON 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 destroys every natively allocated id.
transform_customers.py:246truncatescustomersandcustomer_legacy_refs, then rebuilds the pool from Access alone. Until that is fixed — either by mandating--syncfor all future utilities loads, or by teaching the transform to preserve refs with no Access counterpart — a NUMid issued here survives only until the next full load. This is a prerequisite for the cutover, not a nice-to-have. - The empty-id rule exists twice: enforced in
numid.service.ts(EMPTY_NUMID_SQL) and reported byscripts/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-accessactivity-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.phphas 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=0because 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 |
build sequencing, locked decisions, per-step status |
../RESUME.md |
session history and §6 open items |
INSURANCE_FEATURES_SPEC.md |
§1 renewal emails (built), §2 liquidación, §3 certificate, §4 carrier APIs |
RECEIPT_CAPTURE_SPEC.md |
§1 Editor (built), §2 OCR (built), §3 multi-bank (built), §4 recycling |
MASS_EMAIL_NOTIFICATIONS.md |
mass email + schedules, as built |
STATEMENT_OCR.md · POLICY_OCR.md |
the two OCR intakes, as built |
DEPLOY_AND_MIGRATIONS.md |
release chain, galactus, known caveats |