import 'dotenv/config'; import { Client } from 'pg'; // Backfill NO destructivo de contribuyentes.regimen_fiscal desde la CSF ya // almacenada en constancias_situacion_fiscal (tabla del tenant). Replica la // lógica de matching de consultarConstanciaContribuyente para que el resultado // sea idéntico al de una descarga fresca. Solo toca filas con regimen_fiscal // NULL o '' (COALESCE semantics). APPLY=1 para escribir; default = dry-run. const APPLY = process.env.APPLY === '1'; const PG = { host: 'localhost', port: 5432, user: 'postgres', password: process.env.PGPASSWORD, }; const CONTROL_DB = 'horux360'; // Réplica exacta del `norm` de consultarConstanciaContribuyente: // NFD + strip de diacríticos (U+0300–U+036F) + lowercase + trim. const DIACRITICS_RE = new RegExp('[̀-ͯ]', 'g'); const norm = (s: string) => s.normalize('NFD').replace(DIACRITICS_RE, '').toLowerCase().trim(); interface CatalogoReg { clave: string; descripcion: string; } function clavesFromCsfRegimenes( regimenes: Array<{ nombre: string; fechaFin?: string | null }> | undefined | null, catalogo: CatalogoReg[], ): string[] { if (!regimenes?.length) return []; const found: string[] = []; for (const reg of regimenes) { if (reg.fechaFin) continue; // solo activos const regNorm = norm(reg.nombre); let best: { clave: string; score: number } | null = null; for (const r of catalogo) { const catNorm = norm(r.descripcion); if (regNorm === catNorm || regNorm.includes(catNorm) || catNorm.includes(regNorm)) { const score = catNorm.length; if (!best || score > best.score) best = { clave: r.clave, score }; } } if (best) found.push(best.clave); } return [...new Set(found)]; } async function main() { const control = new Client({ ...PG, database: CONTROL_DB }); await control.connect(); const { rows: catalogo } = await control.query( `SELECT clave, descripcion FROM regimenes WHERE activo = true`, ); const { rows: tenants } = await control.query<{ id: string; database_name: string; rfc: string | null }>( `SELECT id, database_name, rfc FROM tenants WHERE active = true AND database_name IS NOT NULL AND database_name NOT LIKE '%\\_deleted\\_%' ORDER BY database_name`, ); let totalContrib = 0; let totalUpdated = 0; const report: Array<{ db: string; rfc: string; claves: string; applied: boolean }> = []; for (const t of tenants) { const tdb = new Client({ ...PG, database: t.database_name }); try { await tdb.connect(); } catch (e: any) { console.log(`SKIP ${t.database_name}: no se pudo conectar (${e.message})`); continue; } try { const { rows: hasTbl } = await tdb.query( `SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='constancias_situacion_fiscal' LIMIT 1`, ); if (hasTbl.length === 0) continue; const { rows: contribs } = await tdb.query<{ entidad_id: string; rfc: string }>( `SELECT entidad_id, rfc FROM contribuyentes WHERE regimen_fiscal IS NULL OR regimen_fiscal = ''`, ); if (contribs.length === 0) continue; for (const c of contribs) { totalContrib++; const { rows: csfRows } = await tdb.query<{ regimenes: any }>( `SELECT datos->'regimenes' AS regimenes FROM constancias_situacion_fiscal WHERE UPPER(rfc) = UPPER($1) ORDER BY fecha_consulta DESC LIMIT 1`, [c.rfc], ); const regimenes = csfRows[0]?.regimenes; const claves = clavesFromCsfRegimenes(regimenes, catalogo); if (claves.length === 0) { report.push({ db: t.database_name, rfc: c.rfc, claves: '(sin match en CSF)', applied: false }); continue; } const csv = claves.join(','); let applied = false; if (APPLY) { const { rowCount } = await tdb.query( `UPDATE contribuyentes SET regimen_fiscal = $1 WHERE entidad_id = $2 AND (regimen_fiscal IS NULL OR regimen_fiscal = '')`, [csv, c.entidad_id], ); applied = (rowCount ?? 0) > 0; if (applied) totalUpdated++; } report.push({ db: t.database_name, rfc: c.rfc, claves: csv, applied }); } } catch (e: any) { console.log(`ERR ${t.database_name}: ${e.message}`); } finally { await tdb.end().catch(() => {}); } } await control.end(); console.log(`\n=== ${APPLY ? 'APPLY' : 'DRY-RUN'} regimen_fiscal backfill ===`); console.log(`catálogo regimenes activos: ${catalogo.length}`); console.log(`contribuyentes con regimen_fiscal vacío: ${totalContrib}`); for (const r of report) { const tag = APPLY ? (r.applied ? 'UPDATED' : 'skip') : 'would-update'; console.log(` [${tag}] ${r.db} ${r.rfc} -> ${r.claves}`); } if (APPLY) console.log(`\nTOTAL ACTUALIZADOS: ${totalUpdated}`); else console.log(`\n(dry-run: ninguna escritura. Corre con APPLY=1 para aplicar.)`); } main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); });