-- 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: one customer can hold several insurance refs (DATGRAL and -- COBRO3 both), and joining them fans this result out past one row per NUMid. 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;