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
This commit is contained in:
Horux Dev
2026-05-16 14:55:10 +00:00
parent bda0a4e212
commit 414e862a44

View File

@@ -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<PerTenantResult> {
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);
});