v1.0.24
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9973488330 |
fix(billing): folio typos hid two double-booked receipts from the audit
Matching the datos2 `CN` reference against EFECTIVO folio `N` finds pairs only where both were keyed correctly. Jorge Cuadros Jr's account carries `C13647` against EFECTIVO folio `13649` — same day, same 3,500.00 MXN, one receipt — and POWERS carries `C135808` against `13508`. Folio matching alone calls both accounts clean, which is exactly backwards: an account used as a validator reporting a false negative is worse than no audit. A second pass now sweeps the C-refs the first pass left orphaned, on proximity alone (same customer, same three-day window), and both passes are judged by the same money rules. The folio is demoted to a lead: it can be wrong in either direction, so it never decides anything on its own. 278 confirmed pairs, up from 276 — 989,740.00 MXN and 88,392.00 USD on the EFECTIVO side. Of the 97 orphaned C-refs only 3 had any EFECTIVO row nearby, so the remaining 94 are datos2-only postings rather than misses. Rejections rise to 3. RAMIREZ, SUSANA pairs 3,320.00 MXN against 18,000.00 the next day; that is a partial application, not a duplicate, and it needs a human rather than a rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
75dcbc11b8 |
feat(billing): audit the two populations the balance floor misses
Read-only. Reports what a corte would touch without touching it. The platform inherited legacy's BALANCE FORWARD rows (1,170, all dated 2026-01-01) but not the yearly process that produces them, and BillingService floors balances per customer on those rows. Two populations fall outside that floor, and they are unrelated defects that happen to surface as the same symptom — a customer whose balance reads as a credit the office does not owe. A. 102 customers have no BALANCE FORWARD row, so their balance is a raw lifetime sum. Only the current-year charge ledger (datos2) was migrated; the per-year charge tables stayed in DreamHost. What survives before the cutover is the EFECTIVO cash journal, and it shows: 199 of their 201 pre-cutover rows are credits. Flooring them at 2026-01-01 moves the book by -82,297.78 MXN and -52,020.20 USD. 91 of the 102 are left with no rows at all, so their balance becomes zero — an assertion, not a figure recovered from anywhere, which is why this script proposes and does not apply. B. 276 cash receipts booked twice in 2026 — once in EFECTIVO under folio N, once in datos2 as reference CN — across 120 customers, 986,240.00 MXN and 88,252.00 USD on the EFECTIVO side. Both rows sit after the floor so both count. The statement already hides them via STATEMENT_EXCLUDED_SOURCE_TABLES; the balances worklist, the movement browser and the /clientes/:id card do not. The folio alone does not establish a pair — folios are reused. Every pair is corroborated on money too: equal amounts when both legs share a currency, or an implied USD->MXN rate inside the band exchange_rates observed that year. Two of 278 folio matches fail that test and are reported apart rather than counted, both same-day partial applications that need a human. Matches the balance-forward row in both shapes, as numid.service.ts does. Databases imported before the type was minted carry those rows with typeId NULL, and name-only matching reports every customer as floorless on such a copy — including the dev database, which the API itself currently reads as +20,653,109.15 MXN against a floored -9,194.61. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
17d83291c3 |
feat(migration): refuse a full re-import that would delete native rows
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> |
||
|
|
6a97242fc3 |
feat(customers): allocate portal NUMids, with an audit for reusable ones
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> |
||
|
|
4ee7ec71f0 |
feat(deploy): prisma migration history, /version, galactus standalone deploy
Closes the gap between "what tag did I deploy" and "what is actually running", and gives the schema a history that can be reasoned about across releases. Migrations - Baseline the existing schema as 0000_init (migrate diff --from-empty). The schema had only ever been applied with `prisma db push`, so no history existed and schema state was disconnected from app version. Existing databases must be baselined once with `migrate resolve --applied 0000_init`; the workflows print this remedy on P3005. - Run `prisma migrate deploy` as a deploy STEP, not the container CMD — as a CMD, N replicas would race each other applying the same migration. Version reporting - GET /version on the API reports the APP_VERSION / GIT_SHA / BUILD_DATE that build.yml already baked into both images but nothing ever read. - The web footer shows the web build and flags an api/web mismatch. The two cannot drift at build time (one matrix run) but can at deploy time. - Both deploy workflows now fail if the running API does not report the tag that was dispatched — a stack naming a tag is not proof of what is running. - scripts/set-version.mjs stamps every package.json, which had all sat at 0.1.0 while real releases shipped as v1.x. Pre-migrate backup - deploy/scripts/pre-migrate-backup.mjs dumps the database from INSIDE the still-running old API container over Portainer's Docker API, so the file lands in the volume the Operaciones restore screen reads. A dump taken on the CI runner would be unreachable by the only restore path we have. Verifies the artefact with `gzip -t` before letting the migration proceed. galactus - deploy/galactus/*.compose.yml: standalone-Docker ports of the Swarm stacks. Plain compose silently ignores `deploy:`, so restart_policy becomes `restart: unless-stopped` — without it nothing returns after a host reboot. - .gitea/workflows/deploy-galactus.yml drives endpoint 3 with its own secrets. Fixes - deploy.yml passed `endpoint_id` and `pull_image` to cssnr/portainer-stack-deploy-action, which has no such inputs (they are `endpoint` and `pull`). The endpoint was silently never set. docs/DEPLOY_AND_MIGRATIONS.md documents expand/contract as the rule for schema changes: Prisma has no down-migrations, so a code rollback never rolls the schema back, and restoring the replication master from a dump diverges every replica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |