/** * Backfill de métricas mensuales pre-calculadas (Tanda A hot/cold). * * Itera todos los tenants activos, sus contribuyentes, y popula la tabla * `metricas_mensuales` con los agregados de años pasados (desde el CFDI más * antiguo hasta el año actual - 1). El año actual queda on-the-fly. * * Idempotente: usa upsert — re-correrlo no duplica filas, recalcula valores. * * Uso: * pnpm --filter @horux/api exec tsx scripts/backfill-metricas.ts # ejecuta * pnpm --filter @horux/api exec tsx scripts/backfill-metricas.ts --dry # dry-run * * Opciones via env: * BACKFILL_DESDE_ANIO=2023 # limita el rango inferior * BACKFILL_HASTA_ANIO=2024 # default: año actual - 1 * BACKFILL_TENANT= # procesa solo un tenant */ import { prisma } from '../src/config/database.js'; import { backfillTenant } from '../src/services/metricas-compute.service.js'; const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run'); const TENANT_FILTER = process.env.BACKFILL_TENANT || null; const DESDE_ANIO = process.env.BACKFILL_DESDE_ANIO ? parseInt(process.env.BACKFILL_DESDE_ANIO, 10) : undefined; const HASTA_ANIO = process.env.BACKFILL_HASTA_ANIO ? parseInt(process.env.BACKFILL_HASTA_ANIO, 10) : undefined; async function main() { console.log(`=== Backfill metricas_mensuales ${DRY_RUN ? '(DRY RUN)' : ''} ===\n`); if (DESDE_ANIO) console.log(`Desde año: ${DESDE_ANIO}`); if (HASTA_ANIO) console.log(`Hasta año: ${HASTA_ANIO}`); if (TENANT_FILTER) console.log(`Tenant filtro: ${TENANT_FILTER}`); console.log(); const tenants = await prisma.tenant.findMany({ where: { active: true, ...(TENANT_FILTER ? { id: TENANT_FILTER } : {}), }, select: { id: true, rfc: true, nombre: true }, orderBy: { rfc: 'asc' }, }); console.log(`Tenants activos: ${tenants.length}\n`); let totalContribs = 0; let totalMeses = 0; let totalFilas = 0; let totalErrores = 0; for (const t of tenants) { process.stdout.write(`[${t.rfc}] ${t.nombre} ... `); try { const r = await backfillTenant(t.id, { dryRun: DRY_RUN, desdeAnio: DESDE_ANIO, hastaAnio: HASTA_ANIO, }); if (r.contribuyentesProcesados === 0) { console.log('sin contribuyentes (skip)'); } else { console.log( `${r.contribuyentesProcesados} contribs, ${r.mesesProcesados} meses, ` + `${r.filasEscritas} filas${r.errores.length > 0 ? `, ${r.errores.length} errores` : ''}`, ); if (r.errores.length > 0 && r.errores.length <= 5) { for (const e of r.errores) { console.log(` ERR (${e.anio}-${String(e.mes).padStart(2, '0')}): ${e.error}`); } } else if (r.errores.length > 5) { console.log(` (${r.errores.length} errores — los primeros 3):`); for (const e of r.errores.slice(0, 3)) { console.log(` ERR (${e.anio}-${String(e.mes).padStart(2, '0')}): ${e.error}`); } } } totalContribs += r.contribuyentesProcesados; totalMeses += r.mesesProcesados; totalFilas += r.filasEscritas; totalErrores += r.errores.length; } catch (err: any) { console.log(`FATAL: ${err?.message || err}`); totalErrores++; } } console.log(`\n=== Resumen ===`); console.log(` Tenants procesados: ${tenants.length}`); console.log(` Contribuyentes: ${totalContribs}`); console.log(` (Contribuyente, mes): ${totalMeses}`); console.log(` Filas metricas_mensuales: ${totalFilas}${DRY_RUN ? ' (NO escritas)' : ''}`); if (totalErrores > 0) console.log(` Errores: ${totalErrores}`); await prisma.$disconnect(); process.exit(totalErrores > 0 ? 1 : 0); } main().catch(async (err) => { console.error('Fatal:', err); await prisma.$disconnect().catch(() => {}); process.exit(1); });