Files
HoruxDespachosNuevo/apps/api/scripts/debug-csf-response.ts
Horux Dev 099f34c903 feat(sat): reporte diario de errores SAT por proxy
- Agrega tabla sat_proxy_errors con proxy_usado, error_code, stage, message.
- sat-client.service.ts expone proxyInfo usado en cada conexión SAT.
- sat.service.ts registra errores de bloqueo/devolución del SAT en sat_proxy_errors
  y guarda proxyUsed en sat_sync_jobs.
- Nuevo job sat-proxy-report.job.ts: cron 8 AM CDMX, envía email a ADMIN_EMAIL
  con errores de las últimas 24h agrupados por proxy/error_code.
- Template de email sat-proxy-report.ts y método sendSatProxyReport.
- Registra el cron en src/index.ts.
- Actualiza docs/SAT-SYNC-IMPLEMENTATION.md.
2026-08-03 14:27:19 +00:00

174 lines
6.9 KiB
TypeScript

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