Initial commit - Horux Despachos NL

This commit is contained in:
2026-05-03 16:47:53 -06:00
commit b00b677c54
647 changed files with 133843 additions and 0 deletions

View 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 };
}