/** * Backfill de cfdis.codigo_postal_receptor desde xml_original para CFDIs * pre-migracion 044. * * Criterio: WHERE xml_original IS NOT NULL AND codigo_postal_receptor IS NULL. * Re-usa `parseXml()` para mantener la logica de extraccion identica al sync. * * Uso: * pnpm --filter @horux/api exec tsx scripts/backfill-cp-receptor.ts # ejecuta * pnpm --filter @horux/api exec tsx scripts/backfill-cp-receptor.ts --dry # reporta sin escribir */ import { prisma, tenantDb } from '../src/config/database.js'; import { parseXml } from '../src/services/sat/sat-parser.service.js'; const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run'); interface PerTenantResult { tenantId: string; rfc: string; databaseName: string; scanned: number; parsedOk: number; parseFailed: number; withCp: number; updated: number; error?: string; } async function backfillTenant( tenantId: string, rfc: string, databaseName: string, ): Promise { const result: PerTenantResult = { tenantId, rfc, databaseName, scanned: 0, parsedOk: 0, parseFailed: 0, withCp: 0, updated: 0, }; const pool = await tenantDb.getPool(tenantId, databaseName); const { rows } = await pool.query<{ id: number; uuid: string; type: string; xml_original: string | null; }>( `SELECT id, uuid, type, xml_original FROM cfdis WHERE xml_original IS NOT NULL AND codigo_postal_receptor IS NULL ORDER BY id`, ); result.scanned = rows.length; if (rows.length === 0) return result; const client = await pool.connect(); try { await client.query('BEGIN'); for (const row of rows) { if (!row.xml_original) continue; const downloadType = row.type === 'EMITIDO' ? 'emitidos' : 'recibidos'; let parsed; try { parsed = parseXml(row.xml_original, downloadType); } catch { result.parseFailed++; continue; } if (!parsed) { result.parseFailed++; continue; } result.parsedOk++; if (!parsed.codigoPostalReceptor) continue; result.withCp++; await client.query( `UPDATE cfdis SET codigo_postal_receptor = $2, actualizado_en = NOW() WHERE id = $1`, [row.id, parsed.codigoPostalReceptor], ); result.updated++; } if (DRY_RUN) { await client.query('ROLLBACK'); } else { await client.query('COMMIT'); } } catch (err: any) { await client.query('ROLLBACK').catch(() => {}); result.error = err?.message || String(err); } finally { client.release(); } return result; } async function main() { console.log(`=== Backfill codigo_postal_receptor ${DRY_RUN ? '(DRY RUN — no writes)' : ''} ===\n`); const tenants = await prisma.tenant.findMany({ where: { active: true }, select: { id: true, rfc: true, databaseName: true }, orderBy: { rfc: 'asc' }, }); console.log(`Tenants activos: ${tenants.length}\n`); const results: PerTenantResult[] = []; for (const t of tenants) { process.stdout.write(`[${t.rfc}] (${t.databaseName}) ... `); try { const r = await backfillTenant(t.id, t.rfc, t.databaseName); results.push(r); if (r.error) { console.log(`ERROR: ${r.error}`); } else if (r.scanned === 0) { console.log(`sin CFDIs candidatos (skip)`); } else { console.log( `scan=${r.scanned} parsed=${r.parsedOk} fail=${r.parseFailed} conCP=${r.withCp} upd=${r.updated}`, ); } } catch (err: any) { console.log(`FATAL: ${err?.message || err}`); results.push({ tenantId: t.id, rfc: t.rfc, databaseName: t.databaseName, scanned: 0, parsedOk: 0, parseFailed: 0, withCp: 0, updated: 0, error: err?.message || String(err), }); } } const totalScanned = results.reduce((s, r) => s + r.scanned, 0); const totalUpdated = results.reduce((s, r) => s + r.updated, 0); const totalParseFailed = results.reduce((s, r) => s + r.parseFailed, 0); const tenantsTouched = results.filter(r => r.updated > 0).length; const tenantsFailed = results.filter(r => r.error).length; console.log(`\n=== Resumen ===`); console.log(` Tenants procesados: ${results.length}`); console.log(` Tenants con backfill: ${tenantsTouched}`); console.log(` CFDIs escaneados: ${totalScanned}`); console.log(` CFDIs actualizados: ${totalUpdated}${DRY_RUN ? ' (rolled back)' : ''}`); if (totalParseFailed > 0) console.log(` CFDIs parse fallo: ${totalParseFailed}`); if (tenantsFailed > 0) console.log(` Tenants con error: ${tenantsFailed}`); await prisma.$disconnect(); process.exit(tenantsFailed > 0 ? 1 : 0); } main().catch(async (err) => { console.error('Fatal:', err); await prisma.$disconnect().catch(() => {}); process.exit(1); });