From 414e862a441431e31649ad6b4fd17fb37121cfec Mon Sep 17 00:00:00 2001 From: Horux Dev Date: Sat, 16 May 2026 14:55:10 +0000 Subject: [PATCH] feat(cfdi): backfill codigo_postal_receptor desde xml_original Script: apps/api/scripts/backfill-cp-receptor.ts - Escanea 93,617 CFDIs con xml_original y codigo_postal_receptor IS NULL - Extrae DomicilioFiscalReceptor via parseXml() (misma logica que sync SAT) - Actualiza 53,858 registros en 6 tenants activos - 0 fallos de parseo --- apps/api/scripts/backfill-cp-receptor.ts | 178 +++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 apps/api/scripts/backfill-cp-receptor.ts diff --git a/apps/api/scripts/backfill-cp-receptor.ts b/apps/api/scripts/backfill-cp-receptor.ts new file mode 100644 index 0000000..d422d89 --- /dev/null +++ b/apps/api/scripts/backfill-cp-receptor.ts @@ -0,0 +1,178 @@ +/** + * 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); +});