Initial commit - Horux Despachos NL
This commit is contained in:
84
apps/api/src/services/sat/sat-csf-login.ts
Normal file
84
apps/api/src/services/sat/sat-csf-login.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { Browser, BrowserContext, Page } from 'playwright';
|
||||
|
||||
const PUBLIC_URL = 'https://www.sat.gob.mx/portal/public/tramites/constancia-de-situacion-fiscal';
|
||||
|
||||
export interface CsfLoginSession {
|
||||
context: BrowserContext;
|
||||
appPage: Page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates from the public CSF page → "SERVICIO" popup → FIEL login →
|
||||
* returns the post-login app page (popup that became the SPA).
|
||||
* Ver referencia_sat_portal_csf: el botón "Generar" vive en un iframe JSF
|
||||
* dentro de esta appPage, por eso la retornamos tal cual.
|
||||
*/
|
||||
export async function loginSatCsf(
|
||||
browser: Browser,
|
||||
cerPath: string,
|
||||
keyPath: string,
|
||||
password: string,
|
||||
): Promise<CsfLoginSession> {
|
||||
const context = await browser.newContext({ acceptDownloads: true });
|
||||
const publicPage = await context.newPage();
|
||||
publicPage.setDefaultTimeout(60_000);
|
||||
|
||||
await publicPage.goto(PUBLIC_URL, { waitUntil: 'networkidle' });
|
||||
await publicPage.waitForTimeout(2000);
|
||||
|
||||
// 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.scrollIntoViewIfNeeded();
|
||||
await obtenerLocator.click();
|
||||
await publicPage.waitForTimeout(1500);
|
||||
|
||||
// Click "SERVICIO" → popup
|
||||
const popupPromise = context.waitForEvent('page', { timeout: 60_000 });
|
||||
await publicPage.locator('text=/^\\s*SERVICIO\\s*$/i').first().click();
|
||||
const loginPage = await popupPromise;
|
||||
await loginPage.waitForLoadState('domcontentloaded');
|
||||
loginPage.setDefaultTimeout(60_000);
|
||||
|
||||
// Click "e.firma" (NO "e.firma portable"). El SAT a veces aterriza en la
|
||||
// pestaña de contraseña: el botón cambia a la vista FIEL. El click sintético
|
||||
// de Playwright a veces no dispara el handler — afirmamos el efecto (aparece
|
||||
// el file input) y reintentamos con dispatchEvent si hace falta.
|
||||
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.scrollIntoViewIfNeeded();
|
||||
await efirmaBtn.click();
|
||||
|
||||
const fileInputs = loginPage.locator('input[type="file"]');
|
||||
try {
|
||||
await fileInputs.first().waitFor({ state: 'attached', timeout: 10_000 });
|
||||
} catch {
|
||||
// Retry: el click sintético no disparó el handler — forzamos dispatchEvent
|
||||
await efirmaBtn.dispatchEvent('click');
|
||||
await fileInputs.first().waitFor({ state: 'attached', timeout: 30_000 });
|
||||
}
|
||||
|
||||
// Upload .cer (primer input) y .key (segundo)
|
||||
await fileInputs.nth(0).setInputFiles(cerPath);
|
||||
await fileInputs.nth(1).setInputFiles(keyPath);
|
||||
|
||||
// Password + Enviar
|
||||
await loginPage.locator('input[type="password"]').first().fill(password);
|
||||
await loginPage.locator('button:has-text("Enviar"), input[value="Enviar"]').first().click();
|
||||
|
||||
// Esperar a que salga del dominio de login
|
||||
await loginPage.waitForURL(url => !url.toString().includes('loginda.siat.sat.gob.mx'), { timeout: 60_000 });
|
||||
await loginPage.waitForLoadState('networkidle').catch(() => undefined);
|
||||
await loginPage.waitForTimeout(2000);
|
||||
|
||||
const bodyText = await loginPage.locator('body').innerText().catch(() => '');
|
||||
if (/contrase[nñ]a\s+incorrecta|usuario.*no.*v[aá]lido|firma\s+inv[aá]lida/i.test(bodyText)) {
|
||||
throw new Error('FIEL inválida o contraseña incorrecta');
|
||||
}
|
||||
|
||||
return { context, appPage: loginPage };
|
||||
}
|
||||
Reference in New Issue
Block a user