/** * Compara Gastos del Dashboard vs Drill-down para un mes/contribuyente. * Identifica discrepancias y rompe el detalle por lado (factura/pago/NC). * * Uso: tsx scripts/validate-gastos.ts */ import { prisma, tenantDb } from '../src/config/database.js'; import { calcularEgresosPorRegimen } from '../src/services/dashboard.service.js'; const tenantRfcArg = process.argv[2] || 'DESPACHO_MO3NI6U8_B9VGG'; const contribuyenteId = process.argv[3] || 'd745a915-6a23-4818-944b-a7e1e18e536a'; const yearMonth = process.argv[4] || '2025-02'; async function main() { const tenant = await prisma.tenant.findFirst({ where: { rfc: tenantRfcArg, active: true }, select: { id: true, rfc: true, databaseName: true }, }); if (!tenant) { console.error('Tenant not found'); process.exit(1); } const pool = await tenantDb.getPool(tenant.id, tenant.databaseName); const [anio, mes] = yearMonth.split('-').map(Number); const lastDay = new Date(anio, mes, 0).getDate(); const fi = `${yearMonth}-01`; const ff = `${yearMonth}-${String(lastDay).padStart(2, '0')}`; console.log(`\n=== Contribuyente ${contribuyenteId} — ${fi} a ${ff} ===\n`); // 1. Dashboard (calcularEgresosPorRegimen) const dashboard = await calcularEgresosPorRegimen( pool, tenant.id, fi, ff, undefined, undefined, false, contribuyenteId, ); console.log('DASHBOARD calcularEgresosPorRegimen:'); console.log(` total: ${dashboard.total.toFixed(2)}`); for (const r of dashboard.porRegimen) { console.log(` ${r.regimenClave} (${r.regimenDescripcion}): ${r.monto.toFixed(2)}`); } // 2. Drill-down query (simulated — bucket=gastos uniforme) const IMP_TRAS = `COALESCE(iva_traslado_mxn,0) + COALESCE(ieps_traslado_mxn,0) + COALESCE(impuestos_locales_trasladado_mxn,0)`; const IMP_TRAS_PAGO = `COALESCE(iva_traslado_pago_mxn,0) + COALESCE(ieps_traslado_pago_mxn,0)`; const EXCL_MONTO = `COALESCE((SELECT SUM(COALESCE(cc.importe_mxn,0) - COALESCE(cc.descuento_mxn,0)) FROM cfdi_conceptos cc WHERE cc.cfdi_id = cfdis.id AND cc.clave_prod_serv IN ('84121603','93161608','85101501','85121800')), 0)`; // bucket=gastos: RECIBIDO I PUE + RECIBIDO P + RECIBIDO E PUE (excl 07) // Sumamos tomando en cuenta el signo (E resta) const { rows: drillRows } = await pool.query( `SELECT type, tipo_comprobante, metodo_pago, COALESCE(cfdi_tipo_relacion, '') AS tipo_rel, COUNT(*)::int AS n, SUM(total_mxn) AS total_bruto, SUM(COALESCE(total_mxn,0) - (${IMP_TRAS}) - (${EXCL_MONTO})) AS total_neto, SUM(COALESCE(monto_pago_mxn,0) - (${IMP_TRAS_PAGO})) AS pago_neto FROM cfdis WHERE ( (type = 'RECIBIDO' AND tipo_comprobante = 'I' AND metodo_pago = 'PUE' AND COALESCE(cfdi_tipo_relacion, '') <> '07') OR (type = 'RECIBIDO' AND tipo_comprobante = 'P') OR (type = 'RECIBIDO' AND tipo_comprobante = 'E' AND metodo_pago = 'PUE' AND COALESCE(cfdi_tipo_relacion, '') <> '07') ) AND regimen_fiscal_receptor IN ('605','606','612','621','625','626','601','603','607','608','610','611','614','615','620','622','623','624') AND status NOT IN ('Cancelado','0') AND ((tipo_comprobante='P' AND fecha_pago_p >= $1::date AND fecha_pago_p < ($2::date + interval '1 day')) OR (tipo_comprobante!='P' AND fecha_emision >= $1::date AND fecha_emision < ($2::date + interval '1 day'))) AND contribuyente_id = $3 GROUP BY type, tipo_comprobante, metodo_pago, tipo_rel ORDER BY tipo_comprobante, metodo_pago`, [fi, ff, contribuyenteId], ); console.log(`\nDRILL-DOWN bucket=gastos (filas del drill por bucket):`); let drillSumaFacturas = 0, drillSumaPagos = 0, drillSumaNC = 0; for (const r of drillRows) { const tc = r.tipo_comprobante; const valor = tc === 'P' ? Number(r.pago_neto) : Number(r.total_neto); console.log(` ${r.type} ${tc} ${r.metodo_pago || '-'} rel=${r.tipo_rel || '-'} n=${r.n} total_bruto=${Number(r.total_bruto).toFixed(2)} valor_neto=${valor.toFixed(2)}`); if (tc === 'I') drillSumaFacturas += valor; else if (tc === 'P') drillSumaPagos += valor; else if (tc === 'E') drillSumaNC += valor; } const drillTotal = drillSumaFacturas + drillSumaPagos - drillSumaNC; console.log(` → facturas=${drillSumaFacturas.toFixed(2)} pagos=${drillSumaPagos.toFixed(2)} NC=${drillSumaNC.toFixed(2)}`); console.log(` → drill total = ${drillTotal.toFixed(2)}`); // 3. Comparación const delta = dashboard.total - drillTotal; console.log(`\n=== COMPARATIVA ===`); console.log(` Dashboard: ${dashboard.total.toFixed(2)}`); console.log(` Drill-down: ${drillTotal.toFixed(2)}`); console.log(` Delta: ${delta.toFixed(2)}`); if (Math.abs(delta) < 0.01) { console.log(` ✓ CUADRAN`); } else { console.log(` ✗ NO CUADRAN — investigar`); } // 4. Régimenes del receptor que aparecen vs los ignorados const { rows: regsReceptor } = await pool.query( `SELECT DISTINCT regimen_fiscal_receptor FROM cfdis WHERE contribuyente_id = $1 AND type = 'RECIBIDO' AND fecha_emision >= $2::date AND fecha_emision < ($3::date + interval '1 day') ORDER BY regimen_fiscal_receptor`, [contribuyenteId, fi, ff], ); console.log(`\nRegímenes en CFDIs RECIBIDOS del periodo:`, regsReceptor.map(r => r.regimen_fiscal_receptor).join(', ')); await prisma.$disconnect(); } main().catch(async e => { console.error(e); await prisma.$disconnect().catch(() => {}); process.exit(1); });