Files
HoruxDespachos/apps/api/scripts/backfill-facturapi-cfdis.ts
2026-04-27 22:09:36 -06:00

127 lines
4.9 KiB
TypeScript

/**
* Backfill one-shot: completa los campos de emisor/subtotal/IVA/XML en las
* filas de `cfdis` con `source='facturapi'` que fueron insertadas por la
* versión buggy del controller (previo al fix 2026-04-24).
*
* Descarga el XML real de Facturapi, lo parsea con el mismo parser SAT,
* upsertea la fila de `rfcs` del emisor, y actualiza la fila de `cfdis`.
*/
import { prisma, tenantDb } from '../src/config/database.js';
import { downloadXmlContribuyente } from '../src/services/contribuyente-facturapi.service.js';
import * as facturapiService from '../src/services/facturapi.service.js';
import { parseXml } from '../src/services/sat/sat-parser.service.js';
const TENANT_RFC = process.argv[2] || 'DESPACHO_MO3NI6U8_B9VGG';
async function main() {
const tenant = await prisma.tenant.findFirst({
where: { rfc: TENANT_RFC },
select: { id: true, databaseName: true },
});
if (!tenant) {
console.log(`Tenant ${TENANT_RFC} no encontrado`);
return;
}
const pool = await tenantDb.getPool(tenant.id, tenant.databaseName);
const { rows: pendientes } = await pool.query<{
id: number;
uuid: string;
facturapi_id: string;
contribuyente_id: string | null;
rfc_emisor: string | null;
}>(
`SELECT id, uuid, facturapi_id, contribuyente_id, rfc_emisor
FROM cfdis
WHERE source = 'facturapi'
AND (COALESCE(rfc_emisor, '') = '' OR xml_original IS NULL OR subtotal = 0)
ORDER BY fecha_emision ASC`,
);
console.log(`Encontradas ${pendientes.length} CFDIs Facturapi a backfillear en ${TENANT_RFC}\n`);
let ok = 0;
let fail = 0;
for (const row of pendientes) {
try {
console.log(`\n[${row.uuid}] facturapi_id=${row.facturapi_id} contrib=${row.contribuyente_id}`);
const xmlBuffer = row.contribuyente_id
? await downloadXmlContribuyente(pool, row.contribuyente_id, row.facturapi_id)
: await facturapiService.downloadXml(tenant.id, row.facturapi_id);
const xmlString = xmlBuffer.toString('utf-8');
const parsed = parseXml(xmlString, 'emitidos');
if (!parsed) {
console.log(` ⚠️ Parser retornó null — skip`);
fail++;
continue;
}
console.log(` emisor=${parsed.rfcEmisor} (${parsed.nombreEmisor}, régimen ${parsed.regimenFiscalEmisor})`);
console.log(` receptor=${parsed.rfcReceptor} (${parsed.nombreReceptor}, régimen ${parsed.regimenFiscalReceptor})`);
console.log(` subtotal=${parsed.subtotal} total=${parsed.total} iva_traslado=${parsed.ivaTraslado}`);
// Upsert rfcs emisor
const { rows: [emisorRow] } = await pool.query(
`INSERT INTO rfcs (rfc, razon_social, regimen_fiscal) VALUES ($1, $2, $3)
ON CONFLICT (rfc) DO UPDATE SET
razon_social = COALESCE(NULLIF($2, ''), rfcs.razon_social),
regimen_fiscal = CASE WHEN $3 IS NOT NULL AND $3 != '' THEN $3 ELSE rfcs.regimen_fiscal END
RETURNING id`,
[parsed.rfcEmisor, parsed.nombreEmisor || null, parsed.regimenFiscalEmisor || null],
);
// Upsert rfcs receptor
const { rows: [receptorRow] } = await pool.query(
`INSERT INTO rfcs (rfc, razon_social, regimen_fiscal) VALUES ($1, $2, $3)
ON CONFLICT (rfc) DO UPDATE SET
razon_social = COALESCE(NULLIF($2, ''), rfcs.razon_social),
regimen_fiscal = CASE WHEN $3 IS NOT NULL AND $3 != '' THEN $3 ELSE rfcs.regimen_fiscal END
RETURNING id`,
[parsed.rfcReceptor, parsed.nombreReceptor || null, parsed.regimenFiscalReceptor || null],
);
await pool.query(
`UPDATE cfdis SET
fecha_cert_sat = $2,
rfc_emisor_id = $3, rfc_emisor = $4, nombre_emisor = $5,
regimen_fiscal_emisor = $6,
rfc_receptor_id = $7, rfc_receptor = $8, nombre_receptor = $9,
regimen_fiscal_receptor = $10,
subtotal = $11, subtotal_mxn = $11,
total = $12, total_mxn = $12,
iva_traslado = $13, iva_traslado_mxn = $13,
iva_retencion = $14, iva_retencion_mxn = $14,
xml_original = $15,
serie = COALESCE($16, serie), folio = COALESCE($17, folio)
WHERE id = $1`,
[
row.id,
parsed.fechaCertSat,
emisorRow.id, parsed.rfcEmisor, parsed.nombreEmisor,
parsed.regimenFiscalEmisor,
receptorRow.id, parsed.rfcReceptor, parsed.nombreReceptor,
parsed.regimenFiscalReceptor,
parsed.subtotal,
parsed.total,
parsed.ivaTraslado,
parsed.ivaRetencion,
xmlString,
parsed.serie, parsed.folio,
],
);
console.log(` ✅ actualizada fila id=${row.id}`);
ok++;
} catch (e: any) {
console.log(` ❌ error: ${e?.message || String(e)}`);
fail++;
}
}
console.log(`\n=== Resumen: ${ok} actualizadas, ${fail} fallidas ===`);
await prisma.$disconnect();
}
main().catch(async e => { console.error(e); await prisma.$disconnect().catch(() => {}); process.exit(1); });