Update: nueva version Horux Despachos
This commit is contained in:
160
apps/api/scripts/validate-metricas.ts
Normal file
160
apps/api/scripts/validate-metricas.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user