Update: nueva version Horux Despachos
This commit is contained in:
159
apps/api/scripts/invalidate-metricas-all.ts
Normal file
159
apps/api/scripts/invalidate-metricas-all.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Invalida TODAS las entradas en `metricas_mensuales` — marca para recompute
|
||||
* cada (contribuyente_id, anio, mes) que tenga datos cacheados. Diseñado para
|
||||
* usarse después de un cambio de fórmula que afecta resultados históricos
|
||||
* (ej. 2026-04-23: NC tipo E con TipoRelacion=07 dejan de restar en Grupo 1).
|
||||
*
|
||||
* El cron `metricas-invalidations.job` (cada 15min) procesa el backlog.
|
||||
* Para acelerar: `pnpm --filter @horux/api exec tsx -e "import { runProcessInvalidations } from './src/jobs/metricas-invalidations.job.js'; runProcessInvalidations().then(()=>process.exit(0))"`
|
||||
*
|
||||
* Uso:
|
||||
* pnpm --filter @horux/api exec tsx scripts/invalidate-metricas-all.ts # ejecuta
|
||||
* pnpm --filter @horux/api exec tsx scripts/invalidate-metricas-all.ts --dry # reporta sin escribir
|
||||
*/
|
||||
import { prisma, tenantDb } from '../src/config/database.js';
|
||||
|
||||
const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run');
|
||||
const REASON = process.argv.find(a => a.startsWith('--reason='))?.slice(9) || 'FORMULA_CHANGE_E07_GRUPO1';
|
||||
|
||||
interface PerTenantResult {
|
||||
tenantId: string;
|
||||
rfc: string;
|
||||
databaseName: string;
|
||||
metricasRows: number;
|
||||
marcadasNuevas: number;
|
||||
marcadasUpdate: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function invalidateTenant(
|
||||
tenantId: string,
|
||||
rfc: string,
|
||||
databaseName: string,
|
||||
): Promise<PerTenantResult> {
|
||||
const result: PerTenantResult = {
|
||||
tenantId,
|
||||
rfc,
|
||||
databaseName,
|
||||
metricasRows: 0,
|
||||
marcadasNuevas: 0,
|
||||
marcadasUpdate: 0,
|
||||
};
|
||||
|
||||
const pool = await tenantDb.getPool(tenantId, databaseName);
|
||||
|
||||
// Cuenta filas existentes en metricas_mensuales para reportar
|
||||
const { rows: cnt } = await pool.query<{ n: number }>(
|
||||
`SELECT COUNT(DISTINCT (contribuyente_id, anio, mes))::int AS n FROM metricas_mensuales`,
|
||||
);
|
||||
result.metricasRows = cnt[0]?.n || 0;
|
||||
if (result.metricasRows === 0) return result;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Insert-or-update: si ya estaba marcada, sobrescribe reason y marcado_at
|
||||
// para que el cron la re-procese con el motivo correcto.
|
||||
const { rows: inserted } = await client.query<{
|
||||
contribuyente_id: string;
|
||||
anio: number;
|
||||
mes: number;
|
||||
was_new: boolean;
|
||||
}>(
|
||||
`
|
||||
INSERT INTO metricas_invalidaciones (contribuyente_id, anio, mes, reason)
|
||||
SELECT DISTINCT contribuyente_id, anio, mes, $1 AS reason
|
||||
FROM metricas_mensuales
|
||||
ON CONFLICT (contribuyente_id, anio, mes) DO UPDATE
|
||||
SET reason = EXCLUDED.reason, marcado_at = now()
|
||||
RETURNING contribuyente_id, anio, mes, (xmax = 0) AS was_new
|
||||
`,
|
||||
[REASON],
|
||||
);
|
||||
|
||||
result.marcadasNuevas = inserted.filter(r => r.was_new).length;
|
||||
result.marcadasUpdate = inserted.length - result.marcadasNuevas;
|
||||
|
||||
if (DRY_RUN) {
|
||||
await client.query('ROLLBACK');
|
||||
} else {
|
||||
await client.query('COMMIT');
|
||||
}
|
||||
} catch (err: any) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
result.error = err?.message || String(err);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`=== Invalidate metricas_mensuales ${DRY_RUN ? '(DRY RUN — no writes)' : ''} ===`);
|
||||
console.log(`Reason: ${REASON}\n`);
|
||||
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
where: { active: true },
|
||||
select: { id: true, rfc: true, databaseName: true },
|
||||
orderBy: { rfc: 'asc' },
|
||||
});
|
||||
|
||||
console.log(`Tenants activos: ${tenants.length}\n`);
|
||||
|
||||
const results: PerTenantResult[] = [];
|
||||
for (const t of tenants) {
|
||||
process.stdout.write(`[${t.rfc}] (${t.databaseName}) ... `);
|
||||
try {
|
||||
const r = await invalidateTenant(t.id, t.rfc, t.databaseName);
|
||||
results.push(r);
|
||||
if (r.error) {
|
||||
console.log(`ERROR: ${r.error}`);
|
||||
} else if (r.metricasRows === 0) {
|
||||
console.log(`sin cache (skip)`);
|
||||
} else {
|
||||
console.log(
|
||||
`cache=${r.metricasRows} (contrib,año,mes), marcadas=${r.marcadasNuevas + r.marcadasUpdate} (nuevas=${r.marcadasNuevas}, re-marcadas=${r.marcadasUpdate})`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(`FATAL: ${err?.message || err}`);
|
||||
results.push({
|
||||
tenantId: t.id,
|
||||
rfc: t.rfc,
|
||||
databaseName: t.databaseName,
|
||||
metricasRows: 0,
|
||||
marcadasNuevas: 0,
|
||||
marcadasUpdate: 0,
|
||||
error: err?.message || String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const totalMetricas = results.reduce((s, r) => s + r.metricasRows, 0);
|
||||
const totalMarcadas = results.reduce((s, r) => s + r.marcadasNuevas + r.marcadasUpdate, 0);
|
||||
const tenantsTouched = results.filter(r => r.marcadasNuevas + r.marcadasUpdate > 0).length;
|
||||
const tenantsFailed = results.filter(r => r.error).length;
|
||||
|
||||
console.log(`\n=== Resumen ===`);
|
||||
console.log(` Tenants procesados: ${results.length}`);
|
||||
console.log(` Tenants con cache: ${tenantsTouched}`);
|
||||
console.log(` Filas cache total: ${totalMetricas}`);
|
||||
console.log(` Invalidaciones: ${totalMarcadas}${DRY_RUN ? ' (rolled back)' : ''}`);
|
||||
if (tenantsFailed > 0) console.log(` Tenants con error: ${tenantsFailed}`);
|
||||
|
||||
if (!DRY_RUN && totalMarcadas > 0) {
|
||||
console.log(`\nCron metricas-invalidations procesará el backlog en <=15 min.`);
|
||||
console.log(`Para disparar manual: runProcessInvalidations() desde un tsx -e ad-hoc.`);
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
process.exit(tenantsFailed > 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