A full run_all.py pass truncates and rebuilds every table it owns from the
Access extract. That was harmless while the platform was a read-only mirror --
every row came from the extract, so wiping and rebuilding lost nothing. It
stopped being harmless once the platform started minting rows Access has never
heard of: allocated portal NUMids, customers created in the staff UI,
OCR-captured policies, app-booked ledger rows, uploaded documents.
REIMPORT is a button in /operaciones, so that was one click away.
native_guard.py counts what only exists here and exits 3; run_all.py runs it
before the first truncate and stops. Detecting an allocated NUMid needs the
staged Parquet -- the customer holds an ordinary-looking (utilities, DATGRAL,
'1172') ref, so "customer has no refs" cannot see it and only comparing against
the extract can. Missing staging is therefore treated as blocking rather than
as "nothing to protect".
The guard does not teach full mode to preserve anything: --sync already upserts
legacy rows against the existing refs and leaves the rest alone, and rebuilding
that inside full mode would re-implement it. --force-full (checkbox in the
REIMPORT confirm, recorded in the audit log) deletes them deliberately.
Verified against dev: clean before, exit 3 listing utilities/1172 with a
synthetic ref present, clean again after removing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Five documents asserted the SES_* secrets were unset in Gitea. They now
exist, so all five are corrected rather than leaving the claim to rot in
whichever one a reader opens first.
Replaces the blocker with the two things creating the secrets does NOT
establish, since both fail in ways that look identical to a missing
config: SES_FROM must be a verified identity in SES_REGION, and the
account must be out of the SES sandbox — in sandbox SES only delivers to
verified recipients, so a sweep across 815 policyholders would fail
almost every send while the configuration reads as correct.
Recommends running the first sweep with debug on, which diverts every
recipient and, on the pólizas side, leaves the avisos pending so a failed
test consumes nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Open work was spread across six documents: PLAN's per-step status,
RESUME §6, two specs' collected open questions, and the "Not built"
sections of the two OCR docs. Nothing tracked the two live data defects
except a paragraph inside INSURANCE_FEATURES_SPEC, and nothing at all
recorded that master is 14 commits and 5 migrations past the last tag.
Compiled by reading those six, then checking each claim against the code
and the dev database rather than trusting the prose — which is how the
dead-table finding surfaced and how both insurance defects were confirmed
still open.
Leads with the ship blocker: SES_* is unset in Gitea while the pólizas
sweep defaults to enabled at 06:00, so deploying current master gives a
nightly sweep that fails every run. Set the secrets or disable the
schedule before cutting v1.0.7.
Findings not previously written down anywhere:
- policy_types still holds only AUTO/LICENCIAS/MULT and 5 policies still
have a NULL policyTypeId; policyTypeId is still `String?` with Prisma's
default SetNull, so the spec's recommended Restrict was never applied.
- EmailTemplate / EmailCampaign / EmailLog have zero references in
apps/api/src or apps/web/src. Scaffolded for step 10's "email
campaigns"; notificaciones shipped against email_notification_log
instead. Either wire them or drop them.
- Customer.customerNumber does not exist, so recycling is not merely
unbuilt but unstarted at the schema level.
Linked from PLAN.md and README so it is findable from either entry point.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>