/** * 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); });