fix(csf): retry con backoff, delays entre tenants, timeouts aumentados

This commit is contained in:
Horux Dev
2026-06-01 23:43:43 +00:00
parent 44144ebf9d
commit bd7e499ab7
3 changed files with 73 additions and 62 deletions

View File

@@ -351,6 +351,8 @@ async function runCsfJob(): Promise<void> {
console.error(`[CSF Cron] Error para ${tenant.rfc}:`, error.message); console.error(`[CSF Cron] Error para ${tenant.rfc}:`, error.message);
failed++; failed++;
} }
// Delay entre tenants para no saturar al SAT y reducir bloqueos por IP
await new Promise(r => setTimeout(r, 30_000));
} }
console.log(`[CSF Cron] Completado — éxito: ${success}, fallidos: ${failed}, sin FIEL: ${skipped}`); console.log(`[CSF Cron] Completado — éxito: ${success}, fallidos: ${failed}, sin FIEL: ${skipped}`);
} }

View File

@@ -44,6 +44,9 @@ function rowToConstancia(r: any): ConstanciaRow {
* sincroniza automáticamente domicilio + regímenes activos con lo que reporta * sincroniza automáticamente domicilio + regímenes activos con lo que reporta
* el SAT. El auto-fill NO es destructivo para datos custom del usuario: * el SAT. El auto-fill NO es destructivo para datos custom del usuario:
* solo sobreescribe campos si la CSF tiene un valor no-vacío. * solo sobreescribe campos si la CSF tiene un valor no-vacío.
*
* Incluye retry con backoff (3 intentos) para robustez ante timeouts
* transitorios del portal SAT (mantenimiento nocturno, congestión, etc.).
*/ */
export async function consultarConstancia(tenantId: string): Promise<ConstanciaRow> { export async function consultarConstancia(tenantId: string): Promise<ConstanciaRow> {
const fiel = await getDecryptedFiel(tenantId); const fiel = await getDecryptedFiel(tenantId);
@@ -55,6 +58,10 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
}); });
if (!tenant) throw new Error('Tenant no encontrado'); if (!tenant) throw new Error('Tenant no encontrado');
const MAX_RETRIES = 3;
const RETRY_DELAYS = [5_000, 15_000, 30_000]; // backoff
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const tempId = randomUUID(); const tempId = randomUUID();
const tempDir = join(tmpdir(), `horux-csf-${tempId}`); const tempDir = join(tmpdir(), `horux-csf-${tempId}`);
mkdirSync(tempDir, { recursive: true, mode: 0o700 }); mkdirSync(tempDir, { recursive: true, mode: 0o700 });
@@ -65,9 +72,6 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
writeFileSync(cerPath, Buffer.from(fiel.cerContent, 'binary'), { mode: 0o600 }); writeFileSync(cerPath, Buffer.from(fiel.cerContent, 'binary'), { mode: 0o600 });
writeFileSync(keyPath, Buffer.from(fiel.keyContent, 'binary'), { mode: 0o600 }); writeFileSync(keyPath, Buffer.from(fiel.keyContent, 'binary'), { mode: 0o600 });
// Headless por default. El fix de dispatchEvent en sat-csf-login cubre el
// caso donde el click sintético no dispara el handler del SAT. Si algún
// ambiente necesita ver el browser (debug), setear SAT_HEADLESS=false.
const headless = process.env.SAT_HEADLESS !== 'false'; const headless = process.env.SAT_HEADLESS !== 'false';
const browser = await chromium.launch({ const browser = await chromium.launch({
headless, headless,
@@ -76,7 +80,7 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
}); });
try { try {
const timeoutPromise = new Promise<never>((_, reject) => const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout: proceso de CSF excedió 3 minutos')), PROCESS_TIMEOUT), setTimeout(() => reject(new Error('Timeout: proceso de CSF excedió 5 minutos')), 300_000),
); );
const resultPromise = (async () => { const resultPromise = (async () => {
@@ -102,9 +106,6 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
], ],
); );
// Auto-fill domicilio del tenant + regímenes activos desde el CSF.
// Se hace después del INSERT para que si algo falla en la sincronización
// la CSF ya quedó guardada y el usuario puede verla.
await sincronizarDatosFiscales(tenantId, csf).catch(err => { await sincronizarDatosFiscales(tenantId, csf).catch(err => {
console.error(`[CSF] Error sincronizando datos fiscales para tenant ${tenantId}:`, err); console.error(`[CSF] Error sincronizando datos fiscales para tenant ${tenantId}:`, err);
}); });
@@ -116,11 +117,19 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
} finally { } finally {
await browser.close(); await browser.close();
} }
} catch (err: any) {
const willRetry = attempt < MAX_RETRIES - 1;
console.error(`[CSF] Intento ${attempt + 1}/${MAX_RETRIES} falló para tenant ${tenantId}: ${err.message}${willRetry ? ` — reintentando en ${RETRY_DELAYS[attempt]}ms...` : ''}`);
if (!willRetry) throw err;
await new Promise(r => setTimeout(r, RETRY_DELAYS[attempt]));
} finally { } finally {
try { unlinkSync(cerPath); } catch { /* ok */ } try { unlinkSync(cerPath); } catch { /* ok */ }
try { unlinkSync(keyPath); } catch { /* ok */ } try { unlinkSync(keyPath); } catch { /* ok */ }
try { rmdirSync(tempDir); } catch { /* ok */ } try { rmdirSync(tempDir); } catch { /* ok */ }
} }
}
throw new Error('No debería llegar aquí');
} }
/** /**

View File

@@ -30,20 +30,20 @@ export async function loginSatCsf(
const publicPage = await context.newPage(); const publicPage = await context.newPage();
publicPage.setDefaultTimeout(60_000); publicPage.setDefaultTimeout(60_000);
await publicPage.goto(PUBLIC_URL, { waitUntil: 'networkidle' }); await publicPage.goto(PUBLIC_URL, { waitUntil: 'networkidle', timeout: 120_000 });
await publicPage.waitForTimeout(2000); await publicPage.waitForTimeout(3000);
// Click acordeón "Obtén tu constancia" / "Obtener constancia" // Click acordeón "Obtén tu constancia" / "Obtener constancia"
const obtenerLocator = publicPage.locator( const obtenerLocator = publicPage.locator(
'text=/Obt[eé]n\\s+la\\s+constancia|Obt[eé]n\\s+tu\\s+constancia|Obtener\\s+constancia|Obtener\\s+la\\s+constancia/i', 'text=/Obt[eé]n\\s+la\\s+constancia|Obt[eé]n\\s+tu\\s+constancia|Obtener\\s+constancia|Obtener\\s+la\\s+constancia/i',
).first(); ).first();
await obtenerLocator.waitFor({ state: 'visible', timeout: 60_000 }); await obtenerLocator.waitFor({ state: 'visible', timeout: 120_000 });
await obtenerLocator.scrollIntoViewIfNeeded(); await obtenerLocator.scrollIntoViewIfNeeded();
await obtenerLocator.click(); await obtenerLocator.click();
await publicPage.waitForTimeout(1500); await publicPage.waitForTimeout(1500);
// Click "SERVICIO" → popup // Click "SERVICIO" → popup
const popupPromise = context.waitForEvent('page', { timeout: 60_000 }); const popupPromise = context.waitForEvent('page', { timeout: 120_000 });
await publicPage.locator('text=/^\\s*SERVICIO\\s*$/i').first().click(); await publicPage.locator('text=/^\\s*SERVICIO\\s*$/i').first().click();
const loginPage = await popupPromise; const loginPage = await popupPromise;
await loginPage.waitForLoadState('domcontentloaded'); await loginPage.waitForLoadState('domcontentloaded');
@@ -56,7 +56,7 @@ export async function loginSatCsf(
const efirmaBtn = loginPage const efirmaBtn = loginPage
.locator('button:has-text("e.firma"):not(:has-text("portable")), input[type="button"][value="e.firma" i], input[type="submit"][value="e.firma" i]') .locator('button:has-text("e.firma"):not(:has-text("portable")), input[type="button"][value="e.firma" i], input[type="submit"][value="e.firma" i]')
.first(); .first();
await efirmaBtn.waitFor({ state: 'visible', timeout: 30_000 }); await efirmaBtn.waitFor({ state: 'visible', timeout: 60_000 });
await efirmaBtn.scrollIntoViewIfNeeded(); await efirmaBtn.scrollIntoViewIfNeeded();
await efirmaBtn.click(); await efirmaBtn.click();
@@ -82,7 +82,7 @@ export async function loginSatCsf(
return rfc !== null && rfc.value.length >= 12; return rfc !== null && rfc.value.length >= 12;
}, },
null, null,
{ timeout: 60_000 }, { timeout: 120_000 },
); );
rfcPopulated = true; rfcPopulated = true;
} catch { } catch {
@@ -121,7 +121,7 @@ export async function loginSatCsf(
// Esperar a que salga del dominio de login y aterrice en el portal SAT // Esperar a que salga del dominio de login y aterrice en el portal SAT
await loginPage.waitForURL( await loginPage.waitForURL(
url => url.toString().includes('wwwmat.sat.gob.mx/operacion/'), url => url.toString().includes('wwwmat.sat.gob.mx/operacion/'),
{ timeout: 60_000 }, { timeout: 120_000 },
); );
await loginPage.waitForLoadState('networkidle').catch(() => undefined); await loginPage.waitForLoadState('networkidle').catch(() => undefined);
await loginPage.waitForTimeout(2000); await loginPage.waitForTimeout(2000);