/** * Validación Tanda A: para cada contribuyente con datos en metricas_mensuales, * toma 5 filas al azar y compara contra el cálculo on-the-fly usando los * servicios canónicos (dashboard, impuestos). Reporta PASS/FAIL por celda. * * Uso: * pnpm --filter @horux/api exec tsx scripts/validate-metricas.ts */ import { prisma, tenantDb } from '../src/config/database.js'; import { calcularIngresosPorRegimen, calcularEgresosPorRegimen, } from '../src/services/dashboard.service.js'; import { getResumenIva } from '../src/services/impuestos.service.js'; const TOL = 0.01; // tolerancia de $0.01 para redondeo decimal interface StoredRow { contribuyente_id: string; anio: number; mes: number; regimen_fiscal: string | null; ingresos_cobrados: string; egresos_pagados: string; iva_trasladado_total: string; iva_acreditable: string; iva_retenido_cobrado: string; iva_resultado: string; cfdis_emitidos_count: number; cfdis_recibidos_count: number; cfdis_cancelados_count: number; } 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 validateRow( tenantId: string, row: StoredRow, ): Promise<{ pass: boolean; diffs: string[] }> { const tenant = await prisma.tenant.findUnique({ where: { id: tenantId }, select: { databaseName: true }, }); if (!tenant) return { pass: false, diffs: ['tenant no encontrado'] }; const pool = await tenantDb.getPool(tenantId, tenant.databaseName); const fi = `${row.anio}-${String(row.mes).padStart(2, '0')}-01`; const lastDay = new Date(row.anio, row.mes, 0).getDate(); const ff = `${row.anio}-${String(row.mes).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; // Ejecutamos secuencial para evitar interferencia entre queries bajo el pool // limit del tenant (max 3 conexiones). Con Promise.all concurrente, algunas // queries compartidas de getResumenIva devolvían valores parciales. const ingresos = await calcularIngresosPorRegimen(pool, tenantId, fi, ff, [], undefined, false, row.contribuyente_id); const egresos = await calcularEgresosPorRegimen(pool, tenantId, fi, ff, [], undefined, false, row.contribuyente_id); const resumenIva = await getResumenIva(pool, fi, ff, tenantId, false, row.contribuyente_id); const reg = row.regimen_fiscal; const ingOtf = ingresos.porRegimen.find(r => r.regimenClave === reg)?.monto || 0; const egrOtf = egresos.porRegimen.find(r => r.regimenClave === reg)?.monto || 0; const trasOtf = resumenIva.trasladadoPorRegimen.find(r => r.regimenClave === reg)?.monto || 0; const acrOtf = resumenIva.acreditablePorRegimen.find(r => r.regimenClave === reg)?.monto || 0; const retOtf = resumenIva.retenidoPorRegimen.find(r => r.regimenClave === reg)?.monto || 0; const resOtf = trasOtf - acrOtf - retOtf; const diffs: string[] = []; const ingStored = Number(row.ingresos_cobrados); const egrStored = Number(row.egresos_pagados); const trasStored = Number(row.iva_trasladado_total); const acrStored = Number(row.iva_acreditable); const retStored = Number(row.iva_retenido_cobrado); const resStored = Number(row.iva_resultado); if (!cmp(ingStored, ingOtf)) diffs.push(`ingresos: tabla=${fmt(ingStored)} vs otf=${fmt(ingOtf)}`); if (!cmp(egrStored, egrOtf)) diffs.push(`egresos: tabla=${fmt(egrStored)} vs otf=${fmt(egrOtf)}`); if (!cmp(trasStored, trasOtf)) diffs.push(`ivaTras: tabla=${fmt(trasStored)} vs otf=${fmt(trasOtf)}`); if (!cmp(acrStored, acrOtf)) diffs.push(`ivaAcr: tabla=${fmt(acrStored)} vs otf=${fmt(acrOtf)}`); if (!cmp(retStored, retOtf)) diffs.push(`ivaRet: tabla=${fmt(retStored)} vs otf=${fmt(retOtf)}`); if (!cmp(resStored, resOtf)) diffs.push(`ivaResultado: tabla=${fmt(resStored)} vs otf=${fmt(resOtf)}`); return { pass: diffs.length === 0, diffs }; } async function main() { console.log('=== Validación metricas_mensuales (5 muestras aleatorias por contribuyente) ===\n'); const tenants = await prisma.tenant.findMany({ where: { active: true }, select: { id: true, rfc: true, databaseName: true }, }); let totalMuestras = 0; let totalPass = 0; let totalFail = 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(`\n[${t.rfc}] ${contribs.length} contribuyentes con datos`); for (const c of contribs) { const { rows: samples } = await pool.query( `SELECT contribuyente_id::text, anio, mes, regimen_fiscal, ingresos_cobrados, egresos_pagados, iva_trasladado_total, iva_acreditable, iva_retenido_cobrado, iva_resultado, cfdis_emitidos_count, cfdis_recibidos_count, cfdis_cancelados_count FROM metricas_mensuales WHERE contribuyente_id = $1 ORDER BY random() LIMIT 5`, [c.entidad_id], ); console.log(` ${c.nombre} (${samples.length} muestras):`); for (const s of samples) { totalMuestras++; const { pass, diffs } = await validateRow(t.id, s); const mesLabel = `${s.anio}-${String(s.mes).padStart(2, '0')}`; const reg = s.regimen_fiscal || 'null'; if (pass) { totalPass++; console.log(` ✓ ${mesLabel} reg=${reg} ingresos=$${fmt(Number(s.ingresos_cobrados))}`); } else { totalFail++; console.log(` ✗ ${mesLabel} reg=${reg} DIFFS:`); for (const d of diffs) console.log(` - ${d}`); } } } } console.log(`\n=== Resumen ===`); console.log(` Muestras totales: ${totalMuestras}`); console.log(` PASS: ${totalPass}`); console.log(` FAIL: ${totalFail}`); await prisma.$disconnect(); process.exit(totalFail > 0 ? 1 : 0); } main().catch(async (err) => { console.error('Fatal:', err); await prisma.$disconnect().catch(() => {}); process.exit(1); });