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:
@@ -8,6 +8,7 @@ import {
|
||||
verifySatRequest,
|
||||
downloadSatPackage,
|
||||
type FielData,
|
||||
type ProxyInfo,
|
||||
} from './sat-client.service.js';
|
||||
import { processPackage, processMetadataPackage, extractXmlsFromZip, type CfdiParsed, type CfdiMetadata } from './sat-parser.service.js';
|
||||
import { recomputarSaldoPendiente, uuidsAfectadosPorCfdi } from '../../utils/saldo.js';
|
||||
@@ -91,6 +92,7 @@ function computeNextRetryAt(
|
||||
interface SyncContext {
|
||||
fielData: FielData;
|
||||
service: Service;
|
||||
proxyInfo: ProxyInfo | null;
|
||||
rfc: string;
|
||||
tenantId: string;
|
||||
databaseName: string;
|
||||
@@ -98,6 +100,33 @@ interface SyncContext {
|
||||
getPool: () => Promise<Pool>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra un error de bloqueo/devolución del SAT junto con el proxy usado.
|
||||
* Permite reportes diarios de errores por IP.
|
||||
*/
|
||||
async function recordSatProxyError(
|
||||
jobId: string,
|
||||
ctx: SyncContext,
|
||||
errorCode: string | null,
|
||||
stage: string,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const proxyUsed = ctx.proxyInfo ? `${ctx.proxyInfo.host}:${ctx.proxyInfo.port}` : null;
|
||||
await prisma.satProxyError.create({
|
||||
data: {
|
||||
jobId,
|
||||
proxyUsed,
|
||||
errorCode,
|
||||
stage,
|
||||
message,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('[SAT] Error guardando sat_proxy_error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza el progreso de un job
|
||||
*/
|
||||
@@ -117,6 +146,7 @@ async function updateJobProgress(
|
||||
completedAt: Date;
|
||||
retryCount: number;
|
||||
nextRetryAt: Date;
|
||||
proxyUsed: string | null;
|
||||
}>
|
||||
): Promise<void> {
|
||||
await prisma.satSyncJob.update({
|
||||
@@ -700,6 +730,7 @@ async function requestAndDownload(
|
||||
return { packageContents: [], totalCfdis: 0 };
|
||||
}
|
||||
if (/error no controlado/i.test(queryResult.message || '')) {
|
||||
await recordSatProxyError(jobId, ctx, queryResult.statusCode || '404', stageIdForTimeout(label), queryResult.message || 'Error no controlado');
|
||||
if (isDaily) {
|
||||
// En daily no detenemos el job por un 404 transitorio del SAT; se
|
||||
// registra como no fatal para diagnóstico y se continúa.
|
||||
@@ -709,6 +740,7 @@ async function requestAndDownload(
|
||||
console.warn(`[SAT] Rechazo transitorio del SAT (${label}): ${queryResult.message} — se reintentará`);
|
||||
throw new SatTransientError(stageIdForTimeout(label), queryResult.message);
|
||||
}
|
||||
await recordSatProxyError(jobId, ctx, queryResult.statusCode || null, stageIdForTimeout(label), queryResult.message || 'Error SAT');
|
||||
throw new Error(`Error SAT (${label}): ${queryResult.message}`);
|
||||
}
|
||||
|
||||
@@ -735,6 +767,7 @@ async function requestAndDownload(
|
||||
console.log(`[SAT] Solicitudes agotadas de por vida (${label}); se cancela y se omite este rango.`);
|
||||
return { packageContents: [], totalCfdis: 0 };
|
||||
}
|
||||
await recordSatProxyError(jobId, ctx, verifyResult.status, stageIdForTimeout(label), verifyResult.message || `Solicitud ${verifyResult.status}`);
|
||||
throw new Error(`Solicitud fallida (${label}): ${verifyResult.message}`);
|
||||
}
|
||||
}
|
||||
@@ -1523,7 +1556,7 @@ export async function startSync(
|
||||
password: decryptedFiel.password,
|
||||
};
|
||||
|
||||
const service = createSatService(fielData);
|
||||
const { service, proxyInfo } = createSatService(fielData);
|
||||
|
||||
const tenant = await prisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
@@ -1564,12 +1597,14 @@ export async function startSync(
|
||||
dateTo: dateTo || now,
|
||||
startedAt: now,
|
||||
isCustomRange,
|
||||
proxyUsed: proxyInfo ? `${proxyInfo.host}:${proxyInfo.port}` : null,
|
||||
},
|
||||
});
|
||||
|
||||
const ctx: SyncContext = {
|
||||
fielData,
|
||||
service,
|
||||
proxyInfo,
|
||||
rfc: decryptedFiel.rfc,
|
||||
tenantId,
|
||||
databaseName: tenant.databaseName,
|
||||
@@ -1721,7 +1756,7 @@ export async function retryTimedOutJobs(): Promise<void> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const service = createSatService({
|
||||
const { service, proxyInfo } = createSatService({
|
||||
cerContent: decryptedFiel.cerContent,
|
||||
keyContent: decryptedFiel.keyContent,
|
||||
password: decryptedFiel.password,
|
||||
@@ -1734,6 +1769,7 @@ export async function retryTimedOutJobs(): Promise<void> {
|
||||
password: decryptedFiel.password,
|
||||
},
|
||||
service,
|
||||
proxyInfo,
|
||||
rfc: decryptedFiel.rfc,
|
||||
tenantId: job.tenantId,
|
||||
databaseName: job.tenant.databaseName,
|
||||
@@ -1744,7 +1780,7 @@ export async function retryTimedOutJobs(): Promise<void> {
|
||||
// B: resetear startedAt al inicio de ESTE intento para que el watchdog
|
||||
// mida el intento actual (y no mate un retry legítimo por el startedAt
|
||||
// original del job). La política de retries se ancla a createdAt.
|
||||
await updateJobProgress(job.id, { status: 'running', errorMessage: null as any, startedAt: new Date() });
|
||||
await updateJobProgress(job.id, { status: 'running', errorMessage: null as any, startedAt: new Date(), proxyUsed: proxyInfo ? `${proxyInfo.host}:${proxyInfo.port}` : null });
|
||||
|
||||
// Para jobs daily, intentamos retomar desde la última etapa completada.
|
||||
let resumeFromStage: string | undefined;
|
||||
@@ -1890,7 +1926,7 @@ export async function continuePendingDailyRequests(): Promise<void> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const service = createSatService({
|
||||
const { service, proxyInfo } = createSatService({
|
||||
cerContent: decryptedFiel.cerContent,
|
||||
keyContent: decryptedFiel.keyContent,
|
||||
password: decryptedFiel.password,
|
||||
@@ -1903,6 +1939,7 @@ export async function continuePendingDailyRequests(): Promise<void> {
|
||||
password: decryptedFiel.password,
|
||||
},
|
||||
service,
|
||||
proxyInfo,
|
||||
rfc: decryptedFiel.rfc,
|
||||
tenantId: job.tenantId,
|
||||
databaseName: job.tenant.databaseName,
|
||||
|
||||
Reference in New Issue
Block a user