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>
This commit is contained in:
+27
-1
@@ -83,7 +83,7 @@ guess.
|
||||
| 1.3 | Carrier API **direction**: outbound quote/issue (ANA supports today) or inbound portfolio sync (no evidence either carrier offers it) | whether §4 is buildable at all | INSURANCE §4 |
|
||||
| 1.4 | CFE amount: the rounded barcode figure (`$268`, what is paid at the window) or the exact breakdown total (`$268.88`) | the parser currently takes the barcode | STATEMENT_OCR / RECEIPT §2 |
|
||||
| 1.5 | The Seguros USD bank's name, currency and details | multi-bank is built; that account does not exist yet | RECEIPT §3 |
|
||||
| 1.6 | Recycling triggers — exact "1 year inactive" / "cancelled" definitions, and whether recycling ever means true data purge | §4 recycling | RECEIPT §4 |
|
||||
| 1.6 | Whether recycling ever means a true data purge. The *triggers* are now settled and built (see §5 "NUMid allocation"); what is still open is whether a recycled id's old rows are ever deleted rather than left attached to the previous customer | nothing — the allocator ships without a purge | RECEIPT §4 |
|
||||
| 1.7 | Notice body in Spanish or English | `Customer` carries no language preference | INSURANCE §1 |
|
||||
| 1.8 | How to model `TRASPASOS PAYPAL` — a clearing account, not a customer, carrying −7.03M MXN over 309 movements and therefore topping the adeudo worklist | deliberately not special-cased in code | RESUME §6 |
|
||||
| 1.9 | The 78 policyholders with no email — skip silently or produce a print worklist | recommendation is the worklist | INSURANCE §1 |
|
||||
@@ -184,6 +184,32 @@ Each of these is a known, deliberate stopping point rather than a bug.
|
||||
- Handwritten folder numbers are deliberately not an input to matching
|
||||
(Tesseract read `405` as `205`).
|
||||
|
||||
**NUMid allocation** — `POST /customers/:id/portal-access` assigns the portal
|
||||
"Security Number", on a staff action rather than at create time, because an
|
||||
insurance-only customer has no reason to hold a utilities id.
|
||||
|
||||
- **Recycling is built but switched off.** `numid.recycleEmpty` in `app_settings`
|
||||
defaults to false, and that default is a safety property, not a preference:
|
||||
every reusable id still exists in Access DATGRAL, and a `--sync` migration run
|
||||
upserts refs with `ON DUPLICATE KEY UPDATE customerId`
|
||||
(`transform_customers.py:327`), so an id recycled today is handed back to its
|
||||
Access owner on the next sync and the customer given it loses portal access.
|
||||
**Flip it on after utilities cuts over**, or for ids deleted at the source.
|
||||
- **A full re-import destroys every natively allocated id.**
|
||||
`transform_customers.py:246` truncates `customers` and `customer_legacy_refs`,
|
||||
then rebuilds the pool from Access alone. Until that is fixed — either by
|
||||
mandating `--sync` for all future utilities loads, or by teaching the transform
|
||||
to preserve refs with no Access counterpart — a NUMid issued here survives only
|
||||
until the next full load. This is a prerequisite for the cutover, not a
|
||||
nice-to-have.
|
||||
- **The empty-id rule exists twice**: enforced in `numid.service.ts`
|
||||
(`EMPTY_NUMID_SQL`) and reported by `scripts/numid-audit.sql`. They agree today
|
||||
(both return 1089, 1094, 1134, 1143 on dev); they are not mechanically kept in
|
||||
step, so change them together.
|
||||
- **No un-assign.** Nothing removes a NUMid once given, and nothing reports which
|
||||
ids were recycled from whom beyond the `customer.portal-access` activity-log
|
||||
entry.
|
||||
|
||||
**Bank** — the concept→ramo classifier is **won't-build**, not pending.
|
||||
`concepto` is a payee name (0 of 22,354 match a category) and TABLA RAMODOS is
|
||||
a property-management expense chart, not the business-line split it was assumed
|
||||
|
||||
Reference in New Issue
Block a user