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
+60 -23
View File
@@ -37,7 +37,7 @@ from pathlib import Path
import pandas as pd
from dbenv import connect, env_arg
from sync import parse_mode
from sync import parse_mode, existing_ids, delete_missing
STG = Path(__file__).parent / "output" / "stg_seguros"
LEGACY_DB = "SEGUROS 16_be"
@@ -180,6 +180,14 @@ def main():
c.execute("SELECT legacyId, customerId FROM customer_legacy_refs WHERE sourceSystem='insurance'")
cust = {r[0]: r[1] for r in c.fetchall()}
# Sync mode reuses each legacy policy's existing id (keyed by provenance) so
# its PK is stable and every child row built below points at the right
# parent. New legacy policies fall through to a fresh uuid.
existing_pol = existing_ids(
c, "policies", ("legacySourceDb", "legacySourceTable", "legacyId"),
"WHERE legacyId IS NOT NULL") if sync_mode else {}
pol_keys: set = set()
policies, insts, vehicles, drivers = [], [], [], []
polno_to_id = {} # policy number -> a policyId (for BENEF/DATOS linking)
providers, ptypes = set(), set()
@@ -198,7 +206,10 @@ def main():
if not cid:
skipped += 1
continue
pid = str(uuid.uuid4())
legacy_pid = str(int(row["_row_num"]))
pkey = (LEGACY_DB, table, legacy_pid)
pol_keys.add(pkey)
pid = existing_pol.get(pkey) or str(uuid.uuid4())
comp = s(row.get(F.get("comp", ""), None)) if F.get("comp") else None
if comp:
providers.add(comp)
@@ -316,20 +327,60 @@ def main():
dt(r["fecha_cheque"]), s(r["num_cheque"]),
1 if truthy(r["concluido"]) else 0, s(r["resolucion"])))
pol_cols = ("id,policyNumber,customerId,policyTypeId,insuranceProviderId,agentName,policyDate,"
"policyFrom,policyTo,netPremium,policyFee,commission,total,currency,observations,"
"coveragesJson,liquidated,liquidationNumber,liquidationDate,legacySourceDb,"
"legacySourceTable,legacyId,updatedAt")
ph = ",".join(["%s"] * 23)
pol_upsert = (
f"INSERT INTO policies ({pol_cols}) VALUES ({ph}) ON DUPLICATE KEY UPDATE "
"customerId=VALUES(customerId),policyNumber=VALUES(policyNumber),policyTypeId=VALUES(policyTypeId),"
"insuranceProviderId=VALUES(insuranceProviderId),agentName=VALUES(agentName),policyDate=VALUES(policyDate),"
"policyFrom=VALUES(policyFrom),policyTo=VALUES(policyTo),netPremium=VALUES(netPremium),policyFee=VALUES(policyFee),"
"commission=VALUES(commission),total=VALUES(total),currency=VALUES(currency),observations=VALUES(observations),"
"coveragesJson=VALUES(coveragesJson),liquidated=VALUES(liquidated),liquidationNumber=VALUES(liquidationNumber),"
"liquidationDate=VALUES(liquidationDate),updatedAt=VALUES(updatedAt),archivedAt=NULL")
if sync_mode:
c.execute("SELECT id,name FROM policy_types")
# policy_types / providers: upsert by their name unique, keep ids stable.
c.execute("SELECT name,id FROM policy_types")
ptype_ids = dict(c.fetchall())
for n in ptypes:
ptype_ids.setdefault(n, str(uuid.uuid4()))
c.executemany("INSERT INTO policy_types (id,name) VALUES (%s,%s) ON DUPLICATE KEY UPDATE name=VALUES(name)", [(i, n) for n, i in ptype_ids.items()])
c.execute("SELECT id,name FROM insurance_providers")
c.execute("SELECT name,id FROM insurance_providers")
prov_ids = dict(c.fetchall())
for n in providers:
prov_ids.setdefault(n, str(uuid.uuid4()))
c.executemany("INSERT INTO insurance_providers (id,name) VALUES (%s,%s) ON DUPLICATE KEY UPDATE name=VALUES(name)", [(i, n) for n, i in prov_ids.items()])
for p in policies:
p = list(p); p[3] = ptype_ids[p[3]]; p[4] = prov_ids.get(p[4])
c.execute(f"INSERT INTO policies ({pol_cols}) VALUES ({','.join(['%s'] * 23)}) ON DUPLICATE KEY UPDATE customerId=VALUES(customerId),policyNumber=VALUES(policyNumber),policyTypeId=VALUES(policyTypeId),insuranceProviderId=VALUES(insuranceProviderId),agentName=VALUES(agentName),policyDate=VALUES(policyDate),policyFrom=VALUES(policyFrom),policyTo=VALUES(policyTo),netPremium=VALUES(netPremium),policyFee=VALUES(policyFee),commission=VALUES(commission),total=VALUES(total),currency=VALUES(currency),observations=VALUES(observations),coveragesJson=VALUES(coveragesJson),liquidated=VALUES(liquidated),liquidationNumber=VALUES(liquidationNumber),liquidationDate=VALUES(liquidationDate),updatedAt=VALUES(updatedAt),archivedAt=NULL", tuple(p))
# Adjusters carry no provenance key, so they can't be upserted by one.
# Resolve claims against the adjusters already in the DB (manual + prior
# loads), inserting only names not present yet — keeps manual adjusters
# and every claim's adjusterId FK valid.
c.execute("SELECT id,name FROM adjusters")
db_adj = {(nm or "").upper(): i for i, nm in c.fetchall()}
adj_id_name = {aid: (nm or "").upper() for aid, comp, city, nm, tel, bp in adj_rows}
new_adj = []
for aid, comp, city, nm, tel, bp in adj_rows:
if nm and nm.upper() not in db_adj:
db_adj[nm.upper()] = aid
new_adj.append((aid, comp, city, nm, tel, bp))
if new_adj:
c.executemany("INSERT INTO adjusters (id,company,city,name,phone,beeper) VALUES (%s,%s,%s,%s,%s,%s)", new_adj)
claims = [(cl[0], cl[1], *cl[2:6], db_adj.get(adj_id_name.get(cl[6])) if cl[6] else None, *cl[7:]) for cl in claims]
# Rebuild every child of a legacy-owned policy before re-inserting the
# children below (manual rows survive: vehicles by their own null
# provenance, the rest by their parent policy's null provenance).
c.execute("DELETE FROM vehicles WHERE legacyId IS NOT NULL")
for tbl in ("policy_payment_installments", "insured_drivers", "policy_beneficiaries", "claims"):
c.execute(f"DELETE ch FROM {tbl} ch JOIN policies p ON p.id=ch.policyId WHERE p.legacyId IS NOT NULL")
pol_rows = [tuple([p[0], p[1], p[2], ptype_ids.get(p[3]), prov_ids.get(p[4]), *p[5:]]) for p in policies]
c.executemany(pol_upsert, pol_rows)
# Drop legacy policies that vanished from source (children already gone).
delete_missing(c, "policies", ("legacySourceDb", "legacySourceTable", "legacyId"), pol_keys, "WHERE legacyId IS NOT NULL")
else:
c.execute("SET FOREIGN_KEY_CHECKS=0")
for t in ("policy_payment_installments", "vehicles", "insured_drivers",
@@ -342,22 +393,8 @@ def main():
prov_ids = {n: str(uuid.uuid4()) for n in providers}
c.executemany("INSERT INTO insurance_providers (id,name) VALUES (%s,%s)", [(i, n) for n, i in prov_ids.items()])
c.executemany("INSERT INTO adjusters (id,company,city,name,phone,beeper) VALUES (%s,%s,%s,%s,%s,%s)", adj_rows)
pol_cols = ("id,policyNumber,customerId,policyTypeId,insuranceProviderId,agentName,policyDate,"
"policyFrom,policyTo,netPremium,policyFee,commission,total,currency,observations,"
"coveragesJson,liquidated,liquidationNumber,liquidationDate,legacySourceDb,"
"legacySourceTable,legacyId,updatedAt")
if not sync_mode:
fixed = []
for p in policies:
p = list(p)
p[3] = ptype_ids.get(p[3])
p[4] = prov_ids.get(p[4])
fixed.append(tuple(p))
ph = ",".join(["%s"] * 23)
c.executemany(f"INSERT INTO policies ({pol_cols}) VALUES ({ph})", fixed)
else:
c.executemany("INSERT INTO policies (id,policyNumber,customerId,policyTypeId,insuranceProviderId,agentName,policyDate,policyFrom,policyTo,netPremium,policyFee,commission,total,currency,observations,coveragesJson,liquidated,liquidationNumber,liquidationDate,legacySourceDb,legacySourceTable,legacyId,updatedAt) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE customerId=VALUES(customerId),policyNumber=VALUES(policyNumber),policyTypeId=VALUES(policyTypeId),insuranceProviderId=VALUES(insuranceProviderId),agentName=VALUES(agentName),policyDate=VALUES(policyDate),policyFrom=VALUES(policyFrom),policyTo=VALUES(policyTo),netPremium=VALUES(netPremium),policyFee=VALUES(policyFee),commission=VALUES(commission),total=VALUES(total),currency=VALUES(currency),observations=VALUES(observations),coveragesJson=VALUES(coveragesJson),liquidated=VALUES(liquidated),liquidationNumber=VALUES(liquidationNumber),liquidationDate=VALUES(liquidationDate),updatedAt=VALUES(updatedAt),archivedAt=NULL", [tuple([p[0],p[1],p[2],ptype_ids.get(p[3]),prov_ids.get(p[4]),*p[5:]]) for p in policies])
pol_rows = [tuple([p[0], p[1], p[2], ptype_ids.get(p[3]), prov_ids.get(p[4]), *p[5:]]) for p in policies]
c.executemany(f"INSERT INTO policies ({pol_cols}) VALUES ({ph})", pol_rows)