feat(sat): reporte diario de errores SAT por proxy
- Agrega tabla sat_proxy_errors con proxy_usado, error_code, stage, message. - sat-client.service.ts expone proxyInfo usado en cada conexión SAT. - sat.service.ts registra errores de bloqueo/devolución del SAT en sat_proxy_errors y guarda proxyUsed en sat_sync_jobs. - Nuevo job sat-proxy-report.job.ts: cron 8 AM CDMX, envía email a ADMIN_EMAIL con errores de las últimas 24h agrupados por proxy/error_code. - Template de email sat-proxy-report.ts y método sendSatProxyReport. - Registra el cron en src/index.ts. - Actualiza docs/SAT-SYNC-IMPLEMENTATION.md.
This commit is contained in:
106
apps/api/scripts/test-daily-group.ts
Normal file
106
apps/api/scripts/test-daily-group.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Prueba del sharding: relanza el daily SOLO para los tenants del grupo 0
|
||||
* (mismo hash que usa sat-sync.job.ts para la ventana de la 1 AM).
|
||||
*
|
||||
* Uso: node --import <tsx-loader> scripts/test-daily-group.ts [GRUPO]
|
||||
*/
|
||||
import { prisma } from '../src/config/database.js';
|
||||
import { tenantDb } from '../src/config/database.js';
|
||||
import { startSync, getSyncStatus } from '../src/services/sat/sat.service.js';
|
||||
|
||||
const DAILY_GROUPS = 5;
|
||||
const GROUP = Number(process.argv[2] ?? 0);
|
||||
|
||||
function tenantGroup(tenantId: string): number {
|
||||
let h = 0;
|
||||
for (let i = 0; i < tenantId.length; i++) h = (h * 31 + tenantId.charCodeAt(i)) >>> 0;
|
||||
return h % DAILY_GROUPS;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Población daily real: tenants con job daily de hoy (fallaron a las 3 AM)
|
||||
const jobs = await prisma.satSyncJob.findMany({
|
||||
where: { type: 'daily', createdAt: { gte: new Date('2026-07-12T06:00:00Z') } },
|
||||
select: { tenantId: true },
|
||||
distinct: ['tenantId'],
|
||||
});
|
||||
const allTenants = jobs.map(j => j.tenantId);
|
||||
const tenantArg = process.argv.find(a => a.startsWith('tenant:'));
|
||||
const groupTenants = tenantArg
|
||||
? [tenantArg.slice('tenant:'.length)]
|
||||
: allTenants.filter(id => tenantGroup(id) === GROUP);
|
||||
|
||||
console.log(`[Test] ${allTenants.length} tenants daily de hoy; ${tenantArg ? 'tenant manual' : `grupo ${GROUP}`}: ${groupTenants.length} tenants`);
|
||||
console.log(`[Test] Tenants: ${groupTenants.join(', ')}`);
|
||||
|
||||
const launchedJobIds: string[] = [];
|
||||
|
||||
for (const tenantId of groupTenants) {
|
||||
try {
|
||||
const tenant = await prisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { databaseName: true },
|
||||
});
|
||||
|
||||
let contribuyenteIds: string[] = [];
|
||||
if (tenant?.databaseName) {
|
||||
const pool = await tenantDb.getPool(tenantId, tenant.databaseName);
|
||||
const { rows } = await pool.query('SELECT entidad_id FROM contribuyentes');
|
||||
contribuyenteIds = rows.map((r: any) => r.entidad_id);
|
||||
}
|
||||
|
||||
if (contribuyenteIds.length === 0) {
|
||||
const status = await getSyncStatus(tenantId);
|
||||
if (status.hasActiveSync) {
|
||||
console.log(`[Test] ${tenantId}: sync activo, omitido`);
|
||||
continue;
|
||||
}
|
||||
const completed = await prisma.satSyncJob.findFirst({
|
||||
where: { tenantId, type: 'initial', status: 'completed' },
|
||||
});
|
||||
const syncType = completed ? 'daily' : 'initial';
|
||||
const jobId = await startSync(tenantId, syncType);
|
||||
launchedJobIds.push(jobId);
|
||||
console.log(`[Test] ${tenantId}: job ${jobId} (${syncType}, sin contribuyentes)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const contribuyenteId of contribuyenteIds) {
|
||||
const status = await getSyncStatus(tenantId, contribuyenteId);
|
||||
if (status.hasActiveSync) {
|
||||
console.log(`[Test] ${tenantId}/${contribuyenteId}: sync activo, omitido`);
|
||||
continue;
|
||||
}
|
||||
const completed = await prisma.satSyncJob.findFirst({
|
||||
where: { tenantId, contribuyenteId, type: 'initial', status: 'completed' },
|
||||
});
|
||||
const syncType = completed ? 'daily' : 'initial';
|
||||
const jobId = await startSync(tenantId, syncType, undefined, undefined, contribuyenteId);
|
||||
launchedJobIds.push(jobId);
|
||||
console.log(`[Test] ${tenantId}/${contribuyenteId}: job ${jobId} (${syncType})`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[Test] Error en ${tenantId}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Test] Lanzamiento del grupo terminado. Esperando a que los jobs terminen...');
|
||||
|
||||
// Esperar a que todos los jobs alcancen estado terminal (el sync corre en
|
||||
// ESTE proceso — si salimos antes, matamos el trabajo en vuelo).
|
||||
const maxWaitMs = 45 * 60 * 1000;
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < maxWaitMs) {
|
||||
const jobsStatus = await prisma.satSyncJob.findMany({
|
||||
where: { id: { in: launchedJobIds } },
|
||||
select: { id: true, status: true, progressPercent: true },
|
||||
});
|
||||
const pending = jobsStatus.filter(j => j.status === 'running' || j.status === 'queued');
|
||||
console.log(`[Test] ${new Date().toISOString()} — ${pending.length}/${jobsStatus.length} en vuelo: ` +
|
||||
jobsStatus.map(j => `${j.id.slice(0, 8)}=${j.status}(${j.progressPercent}%)`).join(', '));
|
||||
if (pending.length === 0) break;
|
||||
await new Promise(r => setTimeout(r, 30000));
|
||||
}
|
||||
}
|
||||
|
||||
main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user