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'; function log(msg: string) { console.log(`[DEBUG] ${new Date().toISOString()} — ${msg}`); } async function main() { 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'); 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 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 }); }); // Interceptar responses const pdfUrls: string[] = []; context.on('response', async (response) => { const ct = response.headers()['content-type'] ?? ''; const url = response.url(); if (ct.includes('application/pdf') || url.includes('.pdf') || url.includes('IdcSiat')) { log(`Response: ${url} | CT: ${ct} | status: ${response.status()}`); if (ct.includes('application/pdf')) pdfUrls.push(url); } }); const publicPage = await context.newPage(); await publicPage.goto('https://www.sat.gob.mx/portal/public/tramites/constancia-de-situacion-fiscal', { waitUntil: 'networkidle', timeout: 120_000 }); await publicPage.waitForTimeout(3000); await 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().click(); await publicPage.waitForTimeout(1500); 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'); 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: 60_000 }); await efirmaBtn.click(); const fileInputs = loginPage.locator('input[type="file"]'); try { await fileInputs.first().waitFor({ state: 'attached', timeout: 10_000 }); } catch { await efirmaBtn.dispatchEvent('click'); await fileInputs.first().waitFor({ state: 'attached', timeout: 30_000 }); } await fileInputs.nth(0).setInputFiles(cerPath); await fileInputs.nth(1).setInputFiles(keyPath); await loginPage.waitForFunction(() => { const rfc = document.getElementById('rfc') as HTMLInputElement | null; return rfc !== null && rfc.value.length >= 12; }, null, { timeout: 120_000 }); await loginPage.locator('input[type="password"]').first().fill(fiel.password); await loginPage.locator('button:has-text("Enviar"), input[value="Enviar"]').first().click({ noWaitAfter: true }); await loginPage.waitForURL(url => url.toString().includes('wwwmat.sat.gob.mx/operacion/'), { timeout: 180_000 }); await loginPage.waitForLoadState('networkidle').catch(() => undefined); await loginPage.waitForTimeout(3000); log(`URL post-login: ${loginPage.url()}`); // Buscar botón en frames 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; 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) throw new Error('No botón'); log('Click en Generar Constancia...'); await generarLocator.click(); log('Esperando 20s por responses PDF...'); await loginPage.waitForTimeout(20_000); log(`URLs PDF interceptadas: ${pdfUrls.length}`); for (const u of pdfUrls) log(` - ${u}`); // Explorar frames después del click log('Frames después del click:'); for (let i = 0; i < loginPage.frames().length; i++) { const f = loginPage.frames()[i]; log(` Frame ${i}: ${f.url()}`); try { const embeds = await f.locator('embed[type="application/pdf"], iframe').all(); for (const e of embeds) { const src = await e.getAttribute('src'); const tag = await e.evaluate((el) => el.tagName); log(` ${tag} src=${src}`); } } catch (err: any) { log(` Error leyendo frame: ${err.message}`); } } // Intentar obtener PDF desde iframe si es URL for (const f of loginPage.frames()) { try { const embed = f.locator('embed[type="application/pdf"]').first(); if (await embed.count() > 0) { const src = await embed.getAttribute('src'); log(`Embed PDF src: ${src}`); if (src && src.startsWith('http')) { const resp = await f.page().context().request.get(src); log(`Fetch status: ${resp.status()}, content-type: ${resp.headers()['content-type']}`); if (resp.ok() && resp.headers()['content-type']?.includes('pdf')) { const body = await resp.body(); log(`PDF size: ${body.length}`); } } } } catch (err: any) { log(`Error frame: ${err.message}`); } } } finally { await browser.close(); } } main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); });