feat(customers): a shelf folder number the office can hand back out
The office files every customer in a numbered paper folder and reuses the number once an account closes. That number was the legacy NUMid, which is also an import key and the portal login, so reusing it meant re-pointing an identity. It is now customers.folderNumber: a plain label nothing joins on. - Unique only among non-archived customers, checked in CustomersService (create, update, restore). Archived customers keep theirs as history. - GET /customers/folder/next suggests the lowest free number; the new customer form pre-fills it. - Backfilled from the utilities NUMid (dev: 1,171 folders, 0 shared); the importer copies it on load. Insurance-only customers start without one. - Reports, the printed statement and the customer file show "Carpeta" instead of NUMid. The list searches by it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
031b13f8c8
commit
b016e53893
15 files changed
+261
-58
No files matched your search
@@ -166,12 +166,17 @@ def resolve_name(raw, nid, index) -> tuple[str, str | None]:
|
||||
|
||||
# ------------------------------- record builders ---------------------------- #
|
||||
def customer_from_utilities(row, name_index) -> dict:
|
||||
name, name_source = resolve_name(row["nombre"], norm_id(row["num_id"]), name_index)
|
||||
nid = norm_id(row["num_id"])
|
||||
name, name_source = resolve_name(row["nombre"], nid, name_index)
|
||||
return dict(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
nameSource=name_source,
|
||||
nameMissing=int(name == NO_NAME),
|
||||
# The NUMid is the number on the customer's shelf folder. It is copied
|
||||
# into a plain label so the office can reuse it without it ever being
|
||||
# an identity; customer_legacy_refs keeps the NUMid itself.
|
||||
folderNumber=int(nid) if nid and nid.isdigit() else None,
|
||||
addressLine1=s(row["direccion"]),
|
||||
addressLine2=s(row["colonia"]),
|
||||
city=s(row["ciudad"]),
|
||||
@@ -205,6 +210,7 @@ def customer_from_insurance(row, name_index) -> dict:
|
||||
name=name,
|
||||
nameSource=name_source,
|
||||
nameMissing=int(name == NO_NAME),
|
||||
folderNumber=None, # insurance ids are a separate space, not a shelf folder
|
||||
addressLine1=s(row["direccion_1"]),
|
||||
addressLine2=s(row["direccion_2"]),
|
||||
city=s(row["ciudad"]),
|
||||
@@ -228,7 +234,7 @@ def customer_from_insurance(row, name_index) -> dict:
|
||||
|
||||
|
||||
_CUST_COLS = [
|
||||
"id", "name", "nameSource", "nameMissing", "addressLine1", "addressLine2", "city", "state", "zipCode",
|
||||
"id", "name", "nameSource", "nameMissing", "folderNumber", "addressLine1", "addressLine2", "city", "state", "zipCode",
|
||||
"country", "phone", "mobile", "fax", "email", "notes", "identificationType",
|
||||
"identificationNumber", "identificationExpiration", "customerSince",
|
||||
"status", "feeAmount", "minimumBalance", "updatedAt",
|
||||
@@ -322,7 +328,7 @@ def main() -> None:
|
||||
remap[rec["id"]] = stable or rec["id"]
|
||||
for rec in customers:
|
||||
rec["id"] = remap[rec["id"]]
|
||||
cur.execute(f"INSERT INTO customers ({','.join(f'`{c}`' for c in _CUST_COLS)}) VALUES ({placeholders}) ON DUPLICATE KEY UPDATE name=VALUES(name),nameSource=VALUES(nameSource),nameMissing=VALUES(nameMissing),addressLine1=VALUES(addressLine1),addressLine2=VALUES(addressLine2),city=VALUES(city),state=VALUES(state),zipCode=VALUES(zipCode),country=VALUES(country),phone=VALUES(phone),mobile=VALUES(mobile),fax=VALUES(fax),email=VALUES(email),notes=VALUES(notes),identificationType=VALUES(identificationType),identificationNumber=VALUES(identificationNumber),identificationExpiration=VALUES(identificationExpiration),customerSince=VALUES(customerSince),status=VALUES(status),feeAmount=VALUES(feeAmount),minimumBalance=VALUES(minimumBalance),updatedAt=VALUES(updatedAt)", tuple(rec[c] for c in _CUST_COLS))
|
||||
cur.execute(f"INSERT INTO customers ({','.join(f'`{c}`' for c in _CUST_COLS)}) VALUES ({placeholders}) ON DUPLICATE KEY UPDATE name=VALUES(name),nameSource=VALUES(nameSource),nameMissing=VALUES(nameMissing),folderNumber=VALUES(folderNumber),addressLine1=VALUES(addressLine1),addressLine2=VALUES(addressLine2),city=VALUES(city),state=VALUES(state),zipCode=VALUES(zipCode),country=VALUES(country),phone=VALUES(phone),mobile=VALUES(mobile),fax=VALUES(fax),email=VALUES(email),notes=VALUES(notes),identificationType=VALUES(identificationType),identificationNumber=VALUES(identificationNumber),identificationExpiration=VALUES(identificationExpiration),customerSince=VALUES(customerSince),status=VALUES(status),feeAmount=VALUES(feeAmount),minimumBalance=VALUES(minimumBalance),updatedAt=VALUES(updatedAt)", tuple(rec[c] for c in _CUST_COLS))
|
||||
for ref in refs:
|
||||
cur.execute("INSERT INTO customer_legacy_refs (id,customerId,sourceSystem,sourceTable,legacyId) VALUES (%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE customerId=VALUES(customerId)",
|
||||
(ref[0], remap[ref[1]], ref[2], ref[3], ref[4]))
|
||||
@@ -374,6 +380,12 @@ def main() -> None:
|
||||
recovered = cur.fetchall()
|
||||
cur.execute("SELECT COUNT(*) FROM customers WHERE name = %s", (NO_NAME,))
|
||||
still_unnamed = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(folderNumber) FROM customers")
|
||||
with_folder = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(*) FROM (SELECT folderNumber FROM customers "
|
||||
"WHERE archivedAt IS NULL AND folderNumber IS NOT NULL "
|
||||
"GROUP BY folderNumber HAVING COUNT(*) > 1) d")
|
||||
shared_folders = cur.fetchone()[0]
|
||||
|
||||
print("=== Customer load complete ===")
|
||||
print(f" utilities DATGRAL rows : {len(util)}")
|
||||
@@ -390,6 +402,8 @@ def main() -> None:
|
||||
print(f" from {src:16} : {n}")
|
||||
print(f" of which via the linked insurance record : {from_ins_side}")
|
||||
print(f" still {NO_NAME} : {still_unnamed}")
|
||||
print(f" folder numbers : {with_folder} (one per numeric utilities NUMid)")
|
||||
print(f" held by >1 open customer (must be 0) : {shared_folders}")
|
||||
if not sync_mode:
|
||||
# Full-load invariants only: sync upserts into an already-loaded (and
|
||||
# pruned) table, so these exact counts don't hold.
|
||||
|
||||
Reference in new issue
Block a user