161 lines
6.3 KiB
TypeScript
161 lines
6.3 KiB
TypeScript
/**
|
|
* 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<StoredRow>(
|
|
`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);
|
|
});
|