fix(csf): retry con backoff, delays entre tenants, timeouts aumentados
This commit is contained in:
@@ -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,72 +58,78 @@ export async function consultarConstancia(tenantId: string): Promise<ConstanciaR
|
||||
});
|
||||
if (!tenant) throw new Error('Tenant no encontrado');
|
||||
|
||||
const tempId = randomUUID();
|
||||
const tempDir = join(tmpdir(), `horux-csf-${tempId}`);
|
||||
mkdirSync(tempDir, { recursive: true, mode: 0o700 });
|
||||
const cerPath = join(tempDir, 'cert.cer');
|
||||
const keyPath = join(tempDir, 'key.key');
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAYS = [5_000, 15_000, 30_000]; // backoff
|
||||
|
||||
try {
|
||||
writeFileSync(cerPath, Buffer.from(fiel.cerContent, 'binary'), { mode: 0o600 });
|
||||
writeFileSync(keyPath, Buffer.from(fiel.keyContent, 'binary'), { mode: 0o600 });
|
||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
const tempId = randomUUID();
|
||||
const tempDir = join(tmpdir(), `horux-csf-${tempId}`);
|
||||
mkdirSync(tempDir, { recursive: true, mode: 0o700 });
|
||||
const cerPath = join(tempDir, 'cert.cer');
|
||||
const keyPath = join(tempDir, 'key.key');
|
||||
|
||||
// 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,
|
||||
args: ['--disable-blink-features=AutomationControlled'],
|
||||
ignoreDefaultArgs: ['--enable-automation'],
|
||||
});
|
||||
try {
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout: proceso de CSF excedió 3 minutos')), PROCESS_TIMEOUT),
|
||||
);
|
||||
writeFileSync(cerPath, Buffer.from(fiel.cerContent, 'binary'), { mode: 0o600 });
|
||||
writeFileSync(keyPath, Buffer.from(fiel.keyContent, 'binary'), { mode: 0o600 });
|
||||
|
||||
const resultPromise = (async () => {
|
||||
const session = await loginSatCsf(browser, cerPath, keyPath, fiel.password, fiel.rfc);
|
||||
const pdfBuffer = await extractCsfPdf(session);
|
||||
const csf = await parseCsfPdf(pdfBuffer);
|
||||
|
||||
const pool = await tenantDb.getPool(tenantId, tenant.databaseName);
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO constancias_situacion_fiscal
|
||||
(rfc, id_cif, razon_social, estatus_padron, fecha_emision, datos, pdf)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, rfc, id_cif, razon_social, estatus_padron, fecha_emision,
|
||||
datos, fecha_consulta, created_at`,
|
||||
[
|
||||
csf.rfc,
|
||||
csf.idCIF,
|
||||
csf.razonSocial ?? [csf.nombre, csf.primerApellido, csf.segundoApellido].filter(Boolean).join(' ') ?? null,
|
||||
csf.estatusPadron,
|
||||
csf.lugarFechaEmision,
|
||||
JSON.stringify(csf),
|
||||
pdfBuffer,
|
||||
],
|
||||
const headless = process.env.SAT_HEADLESS !== 'false';
|
||||
const browser = await chromium.launch({
|
||||
headless,
|
||||
args: ['--disable-blink-features=AutomationControlled'],
|
||||
ignoreDefaultArgs: ['--enable-automation'],
|
||||
});
|
||||
try {
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout: proceso de CSF excedió 5 minutos')), 300_000),
|
||||
);
|
||||
|
||||
// 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);
|
||||
});
|
||||
const resultPromise = (async () => {
|
||||
const session = await loginSatCsf(browser, cerPath, keyPath, fiel.password, fiel.rfc);
|
||||
const pdfBuffer = await extractCsfPdf(session);
|
||||
const csf = await parseCsfPdf(pdfBuffer);
|
||||
|
||||
return rowToConstancia(rows[0]);
|
||||
})();
|
||||
const pool = await tenantDb.getPool(tenantId, tenant.databaseName);
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO constancias_situacion_fiscal
|
||||
(rfc, id_cif, razon_social, estatus_padron, fecha_emision, datos, pdf)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, rfc, id_cif, razon_social, estatus_padron, fecha_emision,
|
||||
datos, fecha_consulta, created_at`,
|
||||
[
|
||||
csf.rfc,
|
||||
csf.idCIF,
|
||||
csf.razonSocial ?? [csf.nombre, csf.primerApellido, csf.segundoApellido].filter(Boolean).join(' ') ?? null,
|
||||
csf.estatusPadron,
|
||||
csf.lugarFechaEmision,
|
||||
JSON.stringify(csf),
|
||||
pdfBuffer,
|
||||
],
|
||||
);
|
||||
|
||||
return await Promise.race([resultPromise, timeoutPromise]);
|
||||
await sincronizarDatosFiscales(tenantId, csf).catch(err => {
|
||||
console.error(`[CSF] Error sincronizando datos fiscales para tenant ${tenantId}:`, err);
|
||||
});
|
||||
|
||||
return rowToConstancia(rows[0]);
|
||||
})();
|
||||
|
||||
return await Promise.race([resultPromise, timeoutPromise]);
|
||||
} 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 {
|
||||
await browser.close();
|
||||
try { unlinkSync(cerPath); } catch { /* ok */ }
|
||||
try { unlinkSync(keyPath); } catch { /* ok */ }
|
||||
try { rmdirSync(tempDir); } catch { /* ok */ }
|
||||
}
|
||||
} finally {
|
||||
try { unlinkSync(cerPath); } catch { /* ok */ }
|
||||
try { unlinkSync(keyPath); } catch { /* ok */ }
|
||||
try { rmdirSync(tempDir); } catch { /* ok */ }
|
||||
}
|
||||
|
||||
throw new Error('No debería llegar aquí');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user