/** * Valida la alineación dashboard ≡ impuestos tras refactor de getResumenIva. * Para 5 muestras aleatorias por contribuyente, compara: * dashboard.calcularIvaBalancePorRegimen().total vs * impuestos.getResumenIva().resultado * * Deben coincidir céntimo por céntimo (Resultado = Trasladado − Acreditable − Retenido, * usando los mismos 6 buckets del dashboard). * * Uso: * pnpm --filter @horux/api exec tsx scripts/validate-dashboard-impuestos.ts * METRICAS_BYPASS_CACHE=1 pnpm --filter @horux/api exec tsx scripts/validate-dashboard-impuestos.ts */ import { prisma, tenantDb } from '../src/config/database.js'; import * as dashboard from '../src/services/dashboard.service.js'; import { getResumenIva } from '../src/services/impuestos.service.js'; const TOL = 0.01; function cmp(a: number, b: number): boolean { return Math.abs(a - b) <= TOL; } function fmt(n: number): string { return n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } async function main() { console.log('=== Validación dashboard.balance ≡ impuestos.resultado ==='); console.log(` BYPASS_CACHE=${process.env.METRICAS_BYPASS_CACHE === '1' ? 'YES' : 'no'}\n`); const tenants = await prisma.tenant.findMany({ where: { active: true }, select: { id: true, rfc: true, databaseName: true }, }); let total = 0; let pass = 0; let fail = 0; for (const t of tenants) { const pool = await tenantDb.getPool(t.id, t.databaseName); const { rows: contribs } = await pool.query<{ entidad_id: string; nombre: string }>( `SELECT c.entidad_id, eg.nombre FROM contribuyentes c JOIN entidades_gestionadas eg ON eg.id = c.entidad_id WHERE EXISTS (SELECT 1 FROM metricas_mensuales m WHERE m.contribuyente_id = c.entidad_id)`, ); if (contribs.length === 0) continue; console.log(`[${t.rfc}] ${contribs.length} contribuyentes`); for (const c of contribs) { const { rows: samples } = await pool.query<{ anio: number; mes: number }>( `SELECT anio, mes FROM ( SELECT DISTINCT anio, mes FROM metricas_mensuales WHERE contribuyente_id = $1 ) t ORDER BY random() LIMIT 5`, [c.entidad_id], ); console.log(` ${c.nombre}:`); for (const s of samples) { total++; const fi = `${s.anio}-${String(s.mes).padStart(2, '0')}-01`; const lastDay = new Date(s.anio, s.mes, 0).getDate(); const ff = `${s.anio}-${String(s.mes).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; const bal = await dashboard.calcularIvaBalancePorRegimen( pool, t.id, fi, ff, [], undefined, false, c.entidad_id, ); const resumen = await getResumenIva(pool, fi, ff, t.id, false, c.entidad_id); const mesLabel = `${s.anio}-${String(s.mes).padStart(2, '0')}`; if (cmp(bal.total, resumen.resultado)) { pass++; console.log(` ✓ ${mesLabel} balance=$${fmt(bal.total)} resultado=$${fmt(resumen.resultado)}`); } else { fail++; const delta = bal.total - resumen.resultado; console.log(` ✗ ${mesLabel} balance=$${fmt(bal.total)} resultado=$${fmt(resumen.resultado)} Δ=$${fmt(delta)}`); console.log(` T=$${fmt(resumen.trasladado)} A=$${fmt(resumen.acreditable)} R=$${fmt(resumen.retenido)}`); } } } } console.log(`\n=== Resumen ===`); console.log(` Muestras: ${total}`); console.log(` PASS: ${pass}`); console.log(` FAIL: ${fail}`); await prisma.$disconnect(); process.exit(fail > 0 ? 1 : 0); } main().catch(async (err) => { console.error('Fatal:', err); await prisma.$disconnect().catch(() => {}); process.exit(1); });