fix(csf): retry con backoff, delays entre tenants, timeouts aumentados
This commit is contained in:
@@ -351,6 +351,8 @@ async function runCsfJob(): Promise<void> {
|
||||
console.error(`[CSF Cron] Error para ${tenant.rfc}:`, error.message);
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ function rowToConstancia(r: any): ConstanciaRow {
|
||||
* sincroniza automáticamente domicilio + regímenes activos con lo que reporta
|
||||
* 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.
|
||||
*
|
||||
* 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> {
|
||||
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');
|
||||
|
||||
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 tempDir = join(tmpdir(), `horux-csf-${tempId}`);
|
||||
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(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 browser = await chromium.launch({
|
||||
headless,
|
||||
@@ -76,7 +80,7 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
|
||||
});
|
||||
try {
|
||||
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 () => {
|
||||
@@ -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 => {
|
||||
console.error(`[CSF] Error sincronizando datos fiscales para tenant ${tenantId}:`, err);
|
||||
});
|
||||
@@ -116,6 +117,11 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
|
||||
} finally {
|
||||
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 {
|
||||
try { unlinkSync(cerPath); } catch { /* ok */ }
|
||||
try { unlinkSync(keyPath); } catch { /* ok */ }
|
||||
@@ -123,6 +129,9 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No debería llegar aquí');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte el domicilio del CSF a los campos de `tenants` (calle compuesta
|
||||
* por tipoVialidad + nombreVialidad). Solo actualiza campos cuando el CSF
|
||||
|
||||
@@ -30,20 +30,20 @@ export async function loginSatCsf(
|
||||
const publicPage = await context.newPage();
|
||||
publicPage.setDefaultTimeout(60_000);
|
||||
|
||||
await publicPage.goto(PUBLIC_URL, { waitUntil: 'networkidle' });
|
||||
await publicPage.waitForTimeout(2000);
|
||||
await publicPage.goto(PUBLIC_URL, { waitUntil: 'networkidle', timeout: 120_000 });
|
||||
await publicPage.waitForTimeout(3000);
|
||||
|
||||
// Click acordeón "Obtén tu constancia" / "Obtener constancia"
|
||||
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',
|
||||
).first();
|
||||
await obtenerLocator.waitFor({ state: 'visible', timeout: 60_000 });
|
||||
await obtenerLocator.waitFor({ state: 'visible', timeout: 120_000 });
|
||||
await obtenerLocator.scrollIntoViewIfNeeded();
|
||||
await obtenerLocator.click();
|
||||
await publicPage.waitForTimeout(1500);
|
||||
|
||||
// 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();
|
||||
const loginPage = await popupPromise;
|
||||
await loginPage.waitForLoadState('domcontentloaded');
|
||||
@@ -56,7 +56,7 @@ export async function loginSatCsf(
|
||||
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]')
|
||||
.first();
|
||||
await efirmaBtn.waitFor({ state: 'visible', timeout: 30_000 });
|
||||
await efirmaBtn.waitFor({ state: 'visible', timeout: 60_000 });
|
||||
await efirmaBtn.scrollIntoViewIfNeeded();
|
||||
await efirmaBtn.click();
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function loginSatCsf(
|
||||
return rfc !== null && rfc.value.length >= 12;
|
||||
},
|
||||
null,
|
||||
{ timeout: 60_000 },
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
rfcPopulated = true;
|
||||
} catch {
|
||||
@@ -121,7 +121,7 @@ export async function loginSatCsf(
|
||||
// Esperar a que salga del dominio de login y aterrice en el portal SAT
|
||||
await loginPage.waitForURL(
|
||||
url => url.toString().includes('wwwmat.sat.gob.mx/operacion/'),
|
||||
{ timeout: 60_000 },
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
await loginPage.waitForLoadState('networkidle').catch(() => undefined);
|
||||
await loginPage.waitForTimeout(2000);
|
||||
|
||||
Reference in New Issue
Block a user