import { chromium } from 'playwright'; import { writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { randomUUID } from 'crypto'; import { tenantDb, prisma } from '../src/config/database.js'; import { getDecryptedFielContribuyente } from '../src/services/contribuyente-fiel.service.js'; const CONTRIBUYENTE_ID = 'cc41d462-4270-4949-aa9b-f0a175784659'; const DEBUG_DIR = join(tmpdir(), `horux-csf-debug-${Date.now()}`); let step = 0; async function snapshot(page: any, label: string) { step++; const prefix = `${String(step).padStart(2, '0')}-${label}`; try { await page.screenshot({ path: join(DEBUG_DIR, `${prefix}.png`), fullPage: true }); const html = await page.content().catch(() => ''); writeFileSync(join(DEBUG_DIR, `${prefix}.html`), html); console.log(`[DEBUG] ${prefix} — screenshot + html guardados`); } catch (err: any) { console.error(`[DEBUG] ${prefix} — error al guardar snapshot:`, err.message); } } function log(msg: string) { console.log(`[DEBUG] ${new Date().toISOString()} — ${msg}`); } async function main() { mkdirSync(DEBUG_DIR, { recursive: true }); log(`Directorio debug: ${DEBUG_DIR}`); const tenant = await prisma.tenant.findUnique({ where: { rfc: 'DESPACHO_MPG95QP7_XZVFF' }, select: { id: true, databaseName: true }, }); if (!tenant) throw new Error('Tenant no encontrado'); const pool = await tenantDb.getPool(tenant.id, tenant.databaseName); const fiel = await getDecryptedFielContribuyente(pool, CONTRIBUYENTE_ID); if (!fiel) throw new Error('No hay FIEL'); log(`FIEL obtenida para RFC ${fiel.rfc}`); 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'); writeFileSync(cerPath, Buffer.from(fiel.cerContent, 'binary'), { mode: 0o600 }); writeFileSync(keyPath, Buffer.from(fiel.keyContent, 'binary'), { mode: 0o600 }); const PUBLIC_URL = 'https://www.sat.gob.mx/portal/public/tramites/constancia-de-situacion-fiscal'; log('Lanzando Chromium...'); const start = Date.now(); const browser = await chromium.launch({ headless: true, args: ['--disable-blink-features=AutomationControlled'], ignoreDefaultArgs: ['--enable-automation'], }); try { const context = await browser.newContext({ acceptDownloads: true, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', }); await context.addInitScript(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); }); const publicPage = await context.newPage(); publicPage.setDefaultTimeout(120_000); log(`Navegando a ${PUBLIC_URL}`); await publicPage.goto(PUBLIC_URL, { waitUntil: 'networkidle', timeout: 120_000 }); await publicPage.waitForTimeout(3000); await snapshot(publicPage, 'public-page'); log('Buscando acordeón "Obtén la 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(); try { await obtenerLocator.waitFor({ state: 'visible', timeout: 120_000 }); log('Acordeón encontrado, haciendo click...'); await obtenerLocator.scrollIntoViewIfNeeded(); await obtenerLocator.click(); await publicPage.waitForTimeout(1500); await snapshot(publicPage, 'after-accordion-click'); } catch (err: any) { log(`ERROR acordeón: ${err.message}`); await snapshot(publicPage, 'accordion-error'); throw err; } log('Buscando botón SERVICIO...'); const servicioLocator = publicPage.locator('text=/^\\s*SERVICIO\\s*$/i').first(); try { await servicioLocator.waitFor({ state: 'visible', timeout: 120_000 }); log('Botón SERVICIO encontrado, haciendo click...'); const popupPromise = context.waitForEvent('page', { timeout: 120_000 }); await servicioLocator.click(); log('Esperando popup de login...'); var loginPage = await popupPromise; log(`Popup abierto: ${loginPage.url()}`); await loginPage.waitForLoadState('domcontentloaded'); await snapshot(loginPage, 'login-popup'); } catch (err: any) { log(`ERROR SERVICIO/popup: ${err.message}`); await snapshot(publicPage, 'servicio-error'); throw err; } loginPage!.setDefaultTimeout(120_000); log('Buscando botón e.firma...'); 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(); try { await efirmaBtn.waitFor({ state: 'visible', timeout: 120_000 }); log('Botón e.firma encontrado, haciendo click...'); await efirmaBtn.scrollIntoViewIfNeeded(); await efirmaBtn.click(); await snapshot(loginPage!, 'after-efirma-click'); } catch (err: any) { log(`ERROR e.firma: ${err.message}`); await snapshot(loginPage!, 'efirma-error'); throw err; } log('Esperando inputs de archivo...'); const fileInputs = loginPage!.locator('input[type="file"]'); try { await fileInputs.first().waitFor({ state: 'attached', timeout: 60_000 }); } catch { log('Click no disparó handler, reintentando con dispatchEvent...'); await efirmaBtn.dispatchEvent('click'); await fileInputs.first().waitFor({ state: 'attached', timeout: 60_000 }); await snapshot(loginPage!, 'after-dispatch-click'); } log('Subiendo .cer y .key...'); await fileInputs.nth(0).setInputFiles(cerPath); await fileInputs.nth(1).setInputFiles(keyPath); await snapshot(loginPage!, 'after-file-upload'); log('Esperando auto-populado de RFC...'); try { await loginPage!.waitForFunction( () => { const rfc = document.getElementById('rfc') as HTMLInputElement | null; return rfc !== null && rfc.value.length >= 12; }, null, { timeout: 120_000 }, ); log('RFC auto-populado'); } catch (err: any) { log(`ERROR RFC auto-populate: ${err.message}`); await snapshot(loginPage!, 'rfc-autopopulate-error'); throw err; } log('Llenando password y enviando...'); await loginPage!.locator('input[type="password"]').first().fill(fiel.password); await snapshot(loginPage!, 'before-submit'); await loginPage!.locator('button:has-text("Enviar"), input[value="Enviar"]').first().click({ noWaitAfter: true }); log('Esperando redirección a portal SAT...'); await loginPage!.waitForURL( url => url.toString().includes('wwwmat.sat.gob.mx/operacion/'), { timeout: 180_000 }, ); await loginPage!.waitForLoadState('networkidle').catch(() => undefined); await loginPage!.waitForTimeout(2000); await snapshot(loginPage!, 'after-login-redirect'); log(`URL post-login: ${loginPage!.url()}`); 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'); } log('Buscando botón Generar Constancia...'); const GENERAR_SELECTORS = [ 'button:has-text("Generar Constancia")', 'button:has-text("Generar constancia")', 'input[type="button"][value*="Generar" i]', 'input[type="submit"][value*="Generar" i]', 'a:has-text("Generar Constancia")', 'a:has-text("Generar constancia")', ].join(', '); let generarLocator: any = null; let foundFrame: any = null; const deadline = Date.now() + 120_000; while (Date.now() < deadline) { for (const frame of loginPage!.frames()) { const loc = frame.locator(GENERAR_SELECTORS).first(); const count = await loc.count().catch(() => 0); if (count > 0 && await loc.isVisible().catch(() => false)) { generarLocator = loc; foundFrame = frame; break; } } if (generarLocator) break; await loginPage!.waitForTimeout(1000); } if (!generarLocator) { log('No se encontró botón Generar Constancia'); await snapshot(loginPage!, 'generar-not-found'); throw new Error('Botón Generar Constancia no encontrado'); } log('Botón Generar Constancia encontrado, haciendo click...'); await generarLocator.scrollIntoViewIfNeeded(); await generarLocator.click(); await loginPage!.waitForTimeout(5000); await snapshot(loginPage!, 'after-generar-click'); log('Proceso completado sin timeout'); } catch (err: any) { log(`ERROR: ${err.message}`); throw err; } finally { log(`Cerrando browser. Tiempo total: ${(Date.now() - start) / 1000}s`); await browser.close(); } } main() .then(() => { log('FIN OK'); process.exit(0); }) .catch((e) => { log(`FIN ERROR: ${e.message}`); process.exit(1); });