const { Pool } = require('pg'); const { XMLParser } = require('fast-xml-parser'); const DB_NAME = process.argv[2] || 'horux_roem691011ez4'; const BATCH_SIZE = parseInt(process.argv[3]) || 100; const pool = new Pool({ host: 'localhost', port: 5432, user: 'postgres', password: 'ZxHMrmnwanvLfLDdNJdRthFjWF2Lj1Rb', database: DB_NAME, }); const xmlParser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', removeNSPrefix: true, }); function toArray(val) { if (!val) return []; return Array.isArray(val) ? val : [val]; } function pf(v) { const n = parseFloat(v); return isNaN(n) ? 0 : n; } function extractConceptos(comprobante) { const conceptosNode = comprobante.Conceptos?.Concepto; if (!conceptosNode) return []; const conceptos = toArray(conceptosNode); return conceptos.map((c) => { const trasladosC = toArray(c.Impuestos?.Traslados?.Traslado); const retencionesC = toArray(c.Impuestos?.Retenciones?.Retencion); let ivaTraslado = 0, iepsTraslado = 0; for (const t of trasladosC) { const importe = pf(t['@_Importe']); if (t['@_Impuesto'] === '002') ivaTraslado += importe; else if (t['@_Impuesto'] === '003') iepsTraslado += importe; } let isrRetencion = 0, ivaRetencion = 0, iepsRetencion = 0; for (const r of retencionesC) { const importe = pf(r['@_Importe']); if (r['@_Impuesto'] === '001') isrRetencion += importe; else if (r['@_Impuesto'] === '002') ivaRetencion += importe; else if (r['@_Impuesto'] === '003') iepsRetencion += importe; } return { claveProdServ: c['@_ClaveProdServ'] || null, noIdentificacion: c['@_NoIdentificacion'] || null, descripcion: c['@_Descripcion'] || '', cantidad: pf(c['@_Cantidad']) || 1, claveUnidad: c['@_ClaveUnidad'] || null, unidad: c['@_Unidad'] || null, valorUnitario: pf(c['@_ValorUnitario']), importe: pf(c['@_Importe']), descuento: pf(c['@_Descuento']), isrRetencion, ivaTraslado, ivaRetencion, iepsTraslado, iepsRetencion, }; }); } function parseXml(xmlContent) { try { const result = xmlParser.parse(xmlContent); const comprobante = result.Comprobante; if (!comprobante) return null; const tc = pf(comprobante['@_TipoCambio']) || 1; return { tipoCambio: tc, conceptos: extractConceptos(comprobante) }; } catch (e) { console.error('Parse error:', e.message); return null; } } async function run() { let totalProcessed = 0; let totalConceptos = 0; let batch = 0; while (true) { batch++; const { rows: cfdis } = await pool.query(` SELECT c.id, c.xml_original FROM cfdis c LEFT JOIN cfdi_conceptos cc ON cc.cfdi_id = c.id WHERE c.xml_original IS NOT NULL AND cc.id IS NULL LIMIT $1 `, [BATCH_SIZE]); if (cfdis.length === 0) break; let batchConceptos = 0; for (const row of cfdis) { try { const parsed = parseXml(row.xml_original); if (!parsed || !parsed.conceptos || parsed.conceptos.length === 0) continue; const tc = parsed.tipoCambio || 1; const m = (v) => v * tc; for (const c of parsed.conceptos) { await pool.query(` INSERT INTO cfdi_conceptos ( cfdi_id, clave_prod_serv, no_identificacion, descripcion, cantidad, clave_unidad, unidad, valor_unitario, valor_unitario_mxn, importe, importe_mxn, descuento, descuento_mxn, isr_retencion, isr_retencion_mxn, iva_traslado, iva_traslado_mxn, iva_retencion, iva_retencion_mxn, ieps_traslado, ieps_traslado_mxn, ieps_retencion, ieps_retencion_mxn ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23) `, [ row.id, c.claveProdServ, c.noIdentificacion, c.descripcion, c.cantidad, c.claveUnidad, c.unidad, c.valorUnitario, m(c.valorUnitario), c.importe, m(c.importe), c.descuento, m(c.descuento), c.isrRetencion, m(c.isrRetencion), c.ivaTraslado, m(c.ivaTraslado), c.ivaRetencion, m(c.ivaRetencion), c.iepsTraslado, m(c.iepsTraslado), c.iepsRetencion, m(c.iepsRetencion), ]); batchConceptos++; } } catch (err) { console.error(`Error CFDI ${row.id}:`, err.message); } } totalProcessed += cfdis.length; totalConceptos += batchConceptos; console.log(`Batch ${batch}: procesados ${cfdis.length}, conceptos ${batchConceptos}, total procesados ${totalProcessed}, total conceptos ${totalConceptos}`); if (cfdis.length < BATCH_SIZE) break; } console.log(`\nBackfill completado. Total CFDIs procesados: ${totalProcessed}, total conceptos insertados: ${totalConceptos}`); await pool.end(); } run().catch((e) => { console.error(e); process.exit(1); });