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>
105 lines
5.8 KiB
SQL
105 lines
5.8 KiB
SQL
-- One row per portal NUMid, with every signal that says whether the id is in use.
|
|
-- Consumed by scripts/numid-audit.mjs, which applies the tier rules.
|
|
--
|
|
-- POOL. customer_legacy_refs where sourceSystem='utilities' AND sourceTable='DATGRAL'.
|
|
-- That pair IS the portal "Security Number" the login screen asks for.
|
|
-- insurance/DATGRAL is a DIFFERENT id space running to 4000 and sharing the same
|
|
-- sourceTable name; drawing from it would hand out an id the portal cannot resolve.
|
|
--
|
|
-- WHY THE OBVIOUS RULES FIND NOTHING.
|
|
-- "every owned row count is zero" -> 0 of 1,171. Migration gave every NUMid
|
|
-- at least one property and one transaction.
|
|
-- "no transaction in the last N years" -> 0 of 1,171. Every customer carries a
|
|
-- synthetic Jan-1 opening-balance row, so
|
|
-- everyone looks active in the current year.
|
|
-- The opening-balance row has to be subtracted before any of this means anything,
|
|
-- which is what `bf` below does and why `real_tx` exists.
|
|
--
|
|
-- BALANCE-FORWARD DETECTION IS TWO-SHAPED ON PURPOSE.
|
|
-- transform_transactions.py:120 mints a transaction type literally named
|
|
-- 'BALANCE FORWARD'. Databases loaded before that change carry the same rows with
|
|
-- typeId NULL, dated Jan 1, legacySourceTable='datos2' -- 1,170 of them, exactly one
|
|
-- per customer. Matching the type name alone floors nothing on such a database, and
|
|
-- every balance below silently becomes a raw lifetime sum: the same double-count that
|
|
-- read the whole book as +20.6M MXN in credit before d173c9e. Match both shapes.
|
|
--
|
|
-- Balances otherwise follow BillingService exactly -- voided out, outstanding out,
|
|
-- superseded rows out (BALANCE_FLOOR_JOIN / NOT_SUPERSEDED, billing.service.ts:179-210).
|
|
|
|
WITH bf AS (
|
|
SELECT t.id, t.customerId, t.transactionDate
|
|
FROM transactions t
|
|
LEFT JOIN type_transactions tt ON tt.id = t.typeId
|
|
WHERE t.voidedAt IS NULL
|
|
AND (
|
|
tt.nameEn = 'BALANCE FORWARD'
|
|
OR (t.typeId IS NULL AND MONTH(t.transactionDate) = 1 AND DAY(t.transactionDate) = 1
|
|
AND t.legacySourceTable = 'datos2')
|
|
)
|
|
),
|
|
bfloor AS (
|
|
SELECT customerId, MAX(transactionDate) AS floorDate FROM bf GROUP BY customerId
|
|
),
|
|
real_tx AS (
|
|
SELECT t.* FROM transactions t
|
|
WHERE t.voidedAt IS NULL AND t.id NOT IN (SELECT id FROM bf)
|
|
),
|
|
pool AS (
|
|
SELECT CAST(r.legacyId AS UNSIGNED) AS numid,
|
|
c.id AS cid,
|
|
REPLACE(REPLACE(COALESCE(c.name,''),'\n',' '),'\t',' ') AS name,
|
|
IF(c.archivedAt IS NULL,0,1) AS archived,
|
|
IF(c.email IS NULL OR c.email='',0,1) AS hasEmail
|
|
FROM customer_legacy_refs r
|
|
JOIN customers c ON c.id = r.customerId
|
|
WHERE r.sourceSystem='utilities' AND r.sourceTable='DATGRAL'
|
|
)
|
|
SELECT
|
|
p.numid, p.cid AS customerUuid, p.name, p.archived, p.hasEmail,
|
|
-- EXISTS, not a join: 16 customers hold more than one insurance ref (several
|
|
-- insurance rows folded into one customer), and joining them fans this result
|
|
-- out past one row per NUMid — 1,188 rows for a 1,171-id pool.
|
|
EXISTS(SELECT 1 FROM customer_legacy_refs i
|
|
WHERE i.customerId=p.cid AND i.sourceSystem='insurance') AS insRef,
|
|
|
|
COALESCE((SELECT ROUND(SUM(t.amount),2) FROM transactions t
|
|
LEFT JOIN bfloor f ON f.customerId=t.customerId
|
|
WHERE t.customerId=p.cid AND t.voidedAt IS NULL AND t.outstanding=0
|
|
AND t.currency='MXN'
|
|
AND (f.floorDate IS NULL OR t.transactionDate>=f.floorDate)),0) AS balMxn,
|
|
COALESCE((SELECT ROUND(SUM(t.amount),2) FROM transactions t
|
|
LEFT JOIN bfloor f ON f.customerId=t.customerId
|
|
WHERE t.customerId=p.cid AND t.voidedAt IS NULL AND t.outstanding=0
|
|
AND t.currency='USD'
|
|
AND (f.floorDate IS NULL OR t.transactionDate>=f.floorDate)),0) AS balUsd,
|
|
|
|
(SELECT COUNT(*) FROM transactions t
|
|
WHERE t.customerId=p.cid AND t.voidedAt IS NULL AND t.outstanding=1) AS nopago,
|
|
(SELECT COUNT(*) FROM real_tx t WHERE t.customerId=p.cid) AS realTx,
|
|
(SELECT COUNT(*) FROM real_tx t WHERE t.customerId=p.cid
|
|
AND t.transactionDate >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)) AS realTx12m,
|
|
(SELECT COUNT(*) FROM real_tx t WHERE t.customerId=p.cid
|
|
AND t.transactionDate >= DATE_SUB(CURDATE(), INTERVAL 36 MONTH)) AS realTx36m,
|
|
(SELECT DATE(MAX(t.transactionDate)) FROM real_tx t WHERE t.customerId=p.cid) AS lastRealTx,
|
|
|
|
(SELECT COUNT(*) FROM properties pr WHERE pr.customerId=p.cid AND pr.archivedAt IS NULL) AS props,
|
|
-- services are counted BOTH ways: an inactive service is still a record of the id
|
|
-- having been used, so the auto tier requires zero of any kind.
|
|
(SELECT COUNT(*) FROM property_services ps JOIN properties pr ON pr.id=ps.propertyId
|
|
WHERE pr.customerId=p.cid AND pr.archivedAt IS NULL) AS anySvc,
|
|
(SELECT COUNT(*) FROM property_services ps JOIN properties pr ON pr.id=ps.propertyId
|
|
WHERE pr.customerId=p.cid AND pr.archivedAt IS NULL AND ps.active=1) AS activeSvc,
|
|
(SELECT COUNT(*) FROM policies po WHERE po.customerId=p.cid AND po.archivedAt IS NULL
|
|
AND (po.policyTo IS NULL OR po.policyTo >= CURDATE())) AS activePol,
|
|
(SELECT COUNT(*) FROM policies po WHERE po.customerId=p.cid AND po.archivedAt IS NULL) AS anyPol,
|
|
(SELECT COUNT(*) FROM vehicles v WHERE v.customerId=p.cid) AS veh,
|
|
(SELECT COUNT(*) FROM trust_accounts ta JOIN properties pr ON pr.id=ta.propertyId
|
|
WHERE pr.customerId=p.cid) AS trust,
|
|
(SELECT COUNT(*) FROM statement_documents s WHERE s.matchedCustomerId=p.cid) AS stmt,
|
|
(SELECT COUNT(*) FROM policy_ocr_documents o WHERE o.matchedCustomerId=p.cid) AS ocr,
|
|
(SELECT COUNT(*) FROM email_notification_log e WHERE e.customerId=p.cid) AS enl,
|
|
(SELECT COUNT(*) FROM email_log e WHERE e.customerId=p.cid) AS elog,
|
|
(SELECT COUNT(*) FROM account_status_history a WHERE a.customerId=p.cid) AS ash
|
|
FROM pool p
|
|
ORDER BY p.numid;
|