Revert "feat(sat): reporte diario de errores SAT por proxy"
This reverts commit 099f34c903.
This commit is contained in:
@@ -7,7 +7,6 @@ import { startWeeklyUpdateJob } from './jobs/weekly-update.job.js';
|
||||
import { startMetricasInvalidationsJob } from './jobs/metricas-invalidations.job.js';
|
||||
import { startNotificationsJob } from './jobs/notifications.job.js';
|
||||
import { startSatSyncMonitorJob } from './jobs/sat-sync-monitor.job.js';
|
||||
import { startSatProxyReportJob } from './jobs/sat-proxy-report.job.js';
|
||||
import { startRecordatoriosPeriodicosJob } from './jobs/recordatorios-periodicos.job.js';
|
||||
|
||||
const PORT = parseInt(env.PORT, 10);
|
||||
@@ -27,7 +26,6 @@ const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
startSatSyncJob();
|
||||
startMetricasInvalidationsJob();
|
||||
startSatSyncMonitorJob();
|
||||
startSatProxyReportJob();
|
||||
startRecordatoriosPeriodicosJob();
|
||||
if (sendRealEmails) {
|
||||
startWeeklyUpdateJob();
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import cron from 'node-cron';
|
||||
import { prisma } from '../config/database.js';
|
||||
import { env } from '../config/env.js';
|
||||
import { emailService } from '../services/email/email.service.js';
|
||||
import type { SatProxyReportData } from '../services/email/templates/sat-proxy-report.js';
|
||||
|
||||
const PROXY_REPORT_CRON_SCHEDULE = '0 8 * * *'; // 8:00 AM CDMX diario
|
||||
|
||||
let reportTask: ReturnType<typeof cron.schedule> | null = null;
|
||||
|
||||
function cdmxDateString(d: Date): string {
|
||||
return d.toLocaleDateString('es-MX', { timeZone: 'America/Mexico_City' });
|
||||
}
|
||||
|
||||
function hoursAgo(hours: number): Date {
|
||||
return new Date(Date.now() - hours * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
export async function runSatProxyReport(): Promise<void> {
|
||||
console.log('[SAT Proxy Report] Generando reporte diario de errores por proxy');
|
||||
|
||||
try {
|
||||
const cutoff = hoursAgo(24);
|
||||
const rows = await prisma.$queryRaw<Array<{ proxy: string | null; errorCode: string | null; count: bigint }>>`
|
||||
SELECT proxy_used AS proxy, error_code AS "errorCode", COUNT(*) AS count
|
||||
FROM sat_proxy_errors
|
||||
WHERE created_at >= ${cutoff}
|
||||
GROUP BY proxy_used, error_code
|
||||
ORDER BY count DESC, proxy_used ASC, error_code ASC
|
||||
`;
|
||||
|
||||
const byProxy = rows.map(r => ({
|
||||
proxy: r.proxy || 'IP directa del servidor',
|
||||
errorCode: r.errorCode || '—',
|
||||
count: Number(r.count),
|
||||
}));
|
||||
|
||||
const totalErrors = byProxy.reduce((sum, r) => sum + r.count, 0);
|
||||
|
||||
const now = new Date();
|
||||
const data: SatProxyReportData = {
|
||||
generatedAt: now.toLocaleString('es-MX', { timeZone: 'America/Mexico_City' }),
|
||||
recipient: env.ADMIN_EMAIL,
|
||||
dateFrom: cdmxDateString(hoursAgo(24)),
|
||||
dateTo: cdmxDateString(now),
|
||||
totalErrors,
|
||||
byProxy,
|
||||
};
|
||||
|
||||
const recipient = env.ADMIN_EMAIL;
|
||||
if (!recipient) {
|
||||
console.warn('[SAT Proxy Report] ADMIN_EMAIL no configurado, no se envía reporte');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[SAT Proxy Report] Enviando reporte a ${recipient}: ${totalErrors} errores en ${byProxy.length} grupos`);
|
||||
await emailService.sendSatProxyReport(recipient, data);
|
||||
console.log('[SAT Proxy Report] Reporte enviado');
|
||||
} catch (error: any) {
|
||||
console.error('[SAT Proxy Report] Error generando reporte:', error.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
export function startSatProxyReportJob(): void {
|
||||
if (reportTask) {
|
||||
console.log('[SAT Proxy Report] Job ya está programado');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cron.validate(PROXY_REPORT_CRON_SCHEDULE)) {
|
||||
console.error('[SAT Proxy Report] Expresión cron inválida:', PROXY_REPORT_CRON_SCHEDULE);
|
||||
return;
|
||||
}
|
||||
|
||||
reportTask = cron.schedule(PROXY_REPORT_CRON_SCHEDULE, async () => {
|
||||
try {
|
||||
await runSatProxyReport();
|
||||
} catch (error: any) {
|
||||
console.error('[SAT Proxy Report Cron] Error:', error.message || error);
|
||||
}
|
||||
}, {
|
||||
timezone: 'America/Mexico_City',
|
||||
});
|
||||
|
||||
console.log(`[SAT Proxy Report] Programado: ${PROXY_REPORT_CRON_SCHEDULE} (America/Mexico_City)`);
|
||||
}
|
||||
|
||||
export function stopSatProxyReportJob(): void {
|
||||
if (reportTask) {
|
||||
reportTask.stop();
|
||||
reportTask = null;
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,6 @@ export const emailService = {
|
||||
await sendEmail(to, `🚨 Alerta SAT: ${total} anomalía${total === 1 ? '' : 's'} detectada${total === 1 ? '' : 's'}`, satSyncAlertEmail(data));
|
||||
},
|
||||
|
||||
sendSatProxyReport: async (to: string, data: import('./templates/sat-proxy-report.js').SatProxyReportData) => {
|
||||
const { satProxyReportEmail } = await import('./templates/sat-proxy-report.js');
|
||||
await sendEmail(to, `📊 Reporte diario SAT: ${data.totalErrors} errores por proxy`, satProxyReportEmail(data));
|
||||
},
|
||||
|
||||
sendSubscriptionExpiring: async (to: string, data: { nombre: string; plan: string; expiresAt: string }) => {
|
||||
const { subscriptionExpiringEmail } = await import('./templates/subscription-expiring.js');
|
||||
await sendEmail(to, 'Tu suscripción vence en 5 días', subscriptionExpiringEmail(data));
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { baseTemplate, heading, infoBox, BRAND_COLORS as C } from './base.js';
|
||||
|
||||
export interface ProxyErrorRow {
|
||||
proxy: string;
|
||||
errorCode: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface SatProxyReportData {
|
||||
generatedAt: string;
|
||||
recipient: string;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
totalErrors: number;
|
||||
byProxy: ProxyErrorRow[];
|
||||
}
|
||||
|
||||
function tableHeader(cells: string[]): string {
|
||||
return `<tr>
|
||||
${cells.map(c => `<th align="left" style="padding:8px 12px;background-color:${C.bgLight};color:${C.textMuted};font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;border-bottom:1px solid ${C.border};">${c}</th>`).join('')}
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function tableRow(cells: string[]): string {
|
||||
return `<tr>
|
||||
${cells.map(c => `<td style="padding:10px 12px;border-bottom:1px solid ${C.border};color:${C.textPrimary};font-size:13px;vertical-align:top;">${c}</td>`).join('')}
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
export function satProxyReportEmail(data: SatProxyReportData): string {
|
||||
const rowsHtml = data.byProxy.length > 0
|
||||
? data.byProxy.map(r => tableRow([
|
||||
r.proxy ? `<code style="font-size:12px;">${r.proxy}</code>` : '<span style="color:#dc2626;">IP directa del servidor</span>',
|
||||
r.errorCode || '—',
|
||||
`<strong>${r.count}</strong>`,
|
||||
])).join('')
|
||||
: tableRow(['—', '—', '<span style="color:#16a34a;">Sin errores de bloqueo</span>']);
|
||||
|
||||
return baseTemplate(`
|
||||
${heading('📊 Reporte diario de errores SAT por proxy')}
|
||||
<p style="color:${C.textPrimary};margin:0 0 16px;">
|
||||
Resumen de errores de bloqueo/devolución del SAT en las últimas 24 horas,
|
||||
agrupados por proxy/IP usada.
|
||||
</p>
|
||||
${infoBox(`
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td style="padding:6px 0;color:${C.textMuted};">Período</td><td style="padding:6px 0;color:${C.textPrimary};font-weight:600;text-align:right;">${data.dateFrom} → ${data.dateTo}</td></tr>
|
||||
<tr><td style="padding:6px 0;color:${C.textMuted};">Errores totales</td><td style="padding:6px 0;color:${data.totalErrors > 0 ? '#dc2626' : '#16a34a'};font-weight:600;text-align:right;">${data.totalErrors}</td></tr>
|
||||
</table>
|
||||
`)}
|
||||
|
||||
<h3 style="font-family:'Inter', sans-serif;font-weight:600;color:${C.textPrimary};margin:28px 0 12px;font-size:16px;">Errores por proxy (${data.byProxy.length})</h3>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
||||
<thead>${tableHeader(['Proxy / IP', 'Código', 'Cantidad'])}</thead>
|
||||
<tbody>${rowsHtml}</tbody>
|
||||
</table>
|
||||
|
||||
<p style="color:${C.textMuted};margin:24px 0 0;font-size:12px;">
|
||||
Reporte generado el ${data.generatedAt} para ${data.recipient}.<br/>
|
||||
Configura la lista de proxies con SAT_PROXY_LIST y la concurrencia con SAT_CONCURRENT_CONTRIBUYENTES.
|
||||
</p>
|
||||
`);
|
||||
}
|
||||
@@ -18,12 +18,6 @@ export interface FielData {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ProxyInfo {
|
||||
host: string;
|
||||
port: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeout explícito para el cliente HTTP del SAT (ms).
|
||||
*
|
||||
@@ -44,7 +38,7 @@ const SAT_WEB_CLIENT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutos
|
||||
/**
|
||||
* Crea el servicio de descarga masiva del SAT usando los datos de la FIEL
|
||||
*/
|
||||
export function createSatService(fielData: FielData): { service: Service; proxyInfo: ProxyInfo | null } {
|
||||
export function createSatService(fielData: FielData): Service {
|
||||
// Crear FIEL usando el método estático create
|
||||
const fiel = Fiel.create(fielData.cerContent, fielData.keyContent, fielData.password);
|
||||
|
||||
@@ -56,8 +50,7 @@ export function createSatService(fielData: FielData): { service: Service; proxyI
|
||||
// Crear cliente HTTP con timeout explícito para evitar el bug de la librería
|
||||
// cuando ocurre un timeout de red. Si hay proxies configurados, se usa uno
|
||||
// del pool para reducir el riesgo de bloqueo por IP del SAT.
|
||||
const proxy = proxyManager.getNextProxy();
|
||||
const proxyAgent = proxy ? proxyManager.createAgent(proxy) : null;
|
||||
const proxyAgent = proxyManager.createNextAgent();
|
||||
if (proxyAgent) {
|
||||
console.log('[SAT] Usando proxy para la conexión con el SAT');
|
||||
} else {
|
||||
@@ -75,11 +68,7 @@ export function createSatService(fielData: FielData): { service: Service; proxyI
|
||||
const requestBuilder = new FielRequestBuilder(fiel);
|
||||
|
||||
// Crear y retornar el servicio
|
||||
const service = new Service(requestBuilder, webClient, undefined, ServiceEndpoints.cfdi());
|
||||
return {
|
||||
service,
|
||||
proxyInfo: proxy ? { host: proxy.host, port: proxy.port, url: proxy.url } : null,
|
||||
};
|
||||
return new Service(requestBuilder, webClient, undefined, ServiceEndpoints.cfdi());
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
|
||||
@@ -8,7 +8,6 @@ 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';
|
||||
@@ -92,7 +91,6 @@ function computeNextRetryAt(
|
||||
interface SyncContext {
|
||||
fielData: FielData;
|
||||
service: Service;
|
||||
proxyInfo: ProxyInfo | null;
|
||||
rfc: string;
|
||||
tenantId: string;
|
||||
databaseName: string;
|
||||
@@ -100,33 +98,6 @@ 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
|
||||
*/
|
||||
@@ -146,7 +117,6 @@ async function updateJobProgress(
|
||||
completedAt: Date;
|
||||
retryCount: number;
|
||||
nextRetryAt: Date;
|
||||
proxyUsed: string | null;
|
||||
}>
|
||||
): Promise<void> {
|
||||
await prisma.satSyncJob.update({
|
||||
@@ -730,7 +700,6 @@ 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.
|
||||
@@ -740,7 +709,6 @@ 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}`);
|
||||
}
|
||||
|
||||
@@ -767,7 +735,6 @@ 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}`);
|
||||
}
|
||||
}
|
||||
@@ -1556,7 +1523,7 @@ export async function startSync(
|
||||
password: decryptedFiel.password,
|
||||
};
|
||||
|
||||
const { service, proxyInfo } = createSatService(fielData);
|
||||
const service = createSatService(fielData);
|
||||
|
||||
const tenant = await prisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
@@ -1597,14 +1564,12 @@ 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,
|
||||
@@ -1756,7 +1721,7 @@ export async function retryTimedOutJobs(): Promise<void> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { service, proxyInfo } = createSatService({
|
||||
const service = createSatService({
|
||||
cerContent: decryptedFiel.cerContent,
|
||||
keyContent: decryptedFiel.keyContent,
|
||||
password: decryptedFiel.password,
|
||||
@@ -1769,7 +1734,6 @@ export async function retryTimedOutJobs(): Promise<void> {
|
||||
password: decryptedFiel.password,
|
||||
},
|
||||
service,
|
||||
proxyInfo,
|
||||
rfc: decryptedFiel.rfc,
|
||||
tenantId: job.tenantId,
|
||||
databaseName: job.tenant.databaseName,
|
||||
@@ -1780,7 +1744,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(), proxyUsed: proxyInfo ? `${proxyInfo.host}:${proxyInfo.port}` : null });
|
||||
await updateJobProgress(job.id, { status: 'running', errorMessage: null as any, startedAt: new Date() });
|
||||
|
||||
// Para jobs daily, intentamos retomar desde la última etapa completada.
|
||||
let resumeFromStage: string | undefined;
|
||||
@@ -1926,7 +1890,7 @@ export async function continuePendingDailyRequests(): Promise<void> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { service, proxyInfo } = createSatService({
|
||||
const service = createSatService({
|
||||
cerContent: decryptedFiel.cerContent,
|
||||
keyContent: decryptedFiel.keyContent,
|
||||
password: decryptedFiel.password,
|
||||
@@ -1939,7 +1903,6 @@ 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