fix(migration): make Phase B additive sync actually work + verify end-to-end
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m37s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m9s

The --sync path had never been run and was broken in several ways. Fixed and
verified against the dev DB (two consecutive syncs, both exit 0, 32/32
assertions: stable PKs, manual-row preservation, changed-row updates,
legacy-delete, no child duplication, zero FK orphans; idempotent).

- policies/properties: reuse each legacy row's existing id (by provenance)
  BEFORE building child rows, so children no longer point at a discarded fresh
  uuid; rebuild legacy-owned children via scoped delete + reinsert.
- customers: replace zip(customers, refs) (mispaired almost every row) with a
  ref-grouped id remap; names now restore and no spurious customers appear.
- drop the invalid Vehicle @@unique(legacySourceTable, legacyId) — one legacy
  policy row carries up to 3 vehicles sharing a legacyId; handle via delete+reinsert.
- upsert lookup tables (policy_types, insurance_providers, type_transactions,
  adjusters) by natural name and remap child FKs instead of inserting fresh
  uuids that nothing points at.
- transactions: drop updatedAt=NOW() (no such column); guard report formatting
  on NULL legacySourceTable (manual rows). Same report guard in bank.
- add manual-safe prune (prune_empty_customers.py --sync, in SYNC_STEPS): prune
  only legacy-owned empties, never manually-added customers.

web: customer-detail mini tx list now strikes voided rows with an "(anulado)"
tag (was the last void-UI rendering gap; /estado-cuenta already handled it).

docs: RESUME.md updated — Phase B sync marked verified end-to-end, void-UI
browser pass recorded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 13:37:22 -07:00
co-authored by Claude Opus 4.8
parent 8802f08d4f
commit 1b79b43a54
11 changed files with 211 additions and 76 deletions
+24 -7
View File
@@ -50,11 +50,21 @@ def main() -> None:
ap.add_argument("--env", default="dev", help="target environment (reads deploy/.env.<env>)")
ap.add_argument("--dry-run", action="store_true",
help="report and write the audit CSV, but delete nothing")
ap.add_argument("--sync", action="store_true",
help="only prune legacy-owned empties; never touch manually-added "
"customers (those with no customer_legacy_refs row)")
args = ap.parse_args()
conn = connect(args.env)
cur = conn.cursor()
print(f"[prune] target env: {args.env}")
print(f"[prune] target env: {args.env}{' (sync: legacy-owned only)' if args.sync else ''}")
# In sync mode a manually-added customer (no legacy ref) with no records yet
# is a legitimate new row, not Access-era dead weight — so restrict the prune
# to customers that carry a legacy ref.
where = EMPTY_WHERE + (
"\nAND EXISTS (SELECT 1 FROM customer_legacy_refs lr WHERE lr.customerId = c.id)"
if args.sync else "")
cur.execute("SELECT COUNT(*) FROM customers")
before = cur.fetchone()[0]
@@ -67,7 +77,7 @@ def main() -> None:
ORDER BY r.sourceSystem SEPARATOR ' | ')
FROM customers c
LEFT JOIN customer_legacy_refs r ON r.customerId = c.id
WHERE {EMPTY_WHERE}
WHERE {where}
GROUP BY c.id
ORDER BY c.nameMissing, c.name
""")
@@ -86,11 +96,18 @@ def main() -> None:
if args.dry_run:
print(f" dry run — {len(rows)} would be pruned, nothing deleted")
else:
cur.execute(f"DELETE r FROM customer_legacy_refs r JOIN customers c ON c.id = r.customerId "
f"WHERE {EMPTY_WHERE}")
refs_deleted = cur.rowcount
cur.execute(f"DELETE c FROM customers c WHERE {EMPTY_WHERE}")
deleted = cur.rowcount
# Delete by the exact id set selected above (which already carries the
# sync guard). Deleting via the id list avoids referencing the delete
# target table inside its own WHERE (MySQL error 1093) and keeps refs +
# customers on the same set regardless of delete order.
ids = [r[0] for r in rows]
refs_deleted = deleted = 0
if ids:
fmt = ",".join(["%s"] * len(ids))
cur.execute(f"DELETE FROM customer_legacy_refs WHERE customerId IN ({fmt})", ids)
refs_deleted = cur.rowcount
cur.execute(f"DELETE FROM customers WHERE id IN ({fmt})", ids)
deleted = cur.rowcount
conn.commit()
print(f" deleted {deleted} customers, {refs_deleted} legacy refs")