98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|