From 8049f71c1031e39489f421a05745585f91ee50d1 Mon Sep 17 00:00:00 2001 From: marsalva Date: Mon, 9 Mar 2026 08:34:03 +0000 Subject: [PATCH] Actualizar worker-homeserve.js --- worker-homeserve.js | 168 ++++++++++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 70 deletions(-) diff --git a/worker-homeserve.js b/worker-homeserve.js index e0ed558..ae413eb 100644 --- a/worker-homeserve.js +++ b/worker-homeserve.js @@ -1,4 +1,4 @@ -// worker-homeserve.js (Versión SaaS - Ultra Estable con Reintentos y Logs) +// worker-homeserve.js (Versión Definitiva PostgreSQL - MULTI-EMPRESA SAAS) import { chromium } from 'playwright'; import pg from 'pg'; @@ -13,29 +13,46 @@ const CONFIG = { POLL_INTERVAL_MS: 5000 }; +if (!CONFIG.DATABASE_URL) { + console.error("❌ ERROR FATAL: Falta la variable de entorno DATABASE_URL"); + process.exit(1); +} + +// Conexión a la Base de Datos const pool = new Pool({ connectionString: CONFIG.DATABASE_URL, ssl: false }); // --- UTILS --- +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + function checkWeekend(dateStr) { if (!dateStr) return; const parts = dateStr.split('/'); if (parts.length !== 3) return; - const d = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0])); - if (d.getDay() === 0 || d.getDay() === 6) { - throw new Error(`⛔ ERROR: La fecha ${dateStr} es fin de semana.`); + const day = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10) - 1; + const year = parseInt(parts[2], 10); + const d = new Date(year, month, day); + const dayOfWeek = d.getDay(); + if (dayOfWeek === 0 || dayOfWeek === 6) { + throw new Error(`⛔ ERROR: La fecha ${dateStr} es fin de semana (Sáb/Dom). No permitido por HomeServe.`); } } +// --- DESENCRIPTAR CREDENCIALES (MULTI-EMPRESA) --- async function getHomeServeCreds(ownerId) { const q = await pool.query( "SELECT username, password_hash FROM provider_credentials WHERE provider = 'homeserve' AND status = 'active' AND owner_id = $1 LIMIT 1", [ownerId] ); - if (q.rowCount === 0) throw new Error(`Sin credenciales para ID: ${ownerId}`); - return { - user: q.rows[0].username, - pass: Buffer.from(q.rows[0].password_hash, 'base64').toString('utf-8') - }; + + if (q.rowCount === 0) { + throw new Error(`No hay credenciales activas de HomeServe para la empresa/dueño ID: ${ownerId}.`); + } + + const user = q.rows[0].username; + const pass = Buffer.from(q.rows[0].password_hash, 'base64').toString('utf-8'); + + return { user, pass }; } // --- PLAYWRIGHT HELPERS --- @@ -72,66 +89,61 @@ async function fillFirstThatExists(page, selectors, value) { // --- INYECCIÓN EN HOMESERVE --- async function loginAndProcess(page, creds, jobData) { - console.log(`>>> 1. Navegando a HomeServe (${creds.user})...`); + console.log(`>>> 1. Login en HomeServe con usuario: ${creds.user}`); if (jobData.appointment_date) checkWeekend(jobData.appointment_date); - if (!jobData.observation || jobData.observation.trim().length === 0) { - throw new Error('⛔ ERROR: El campo Observaciones es obligatorio.'); - } - - // Aceptar diálogos molestos - page.on('dialog', async dialog => { - console.log(` [DIALOG] Mensaje detectado: ${dialog.message()}`); - await dialog.accept(); - }); - - // Login a prueba de fallos (Espera a que cargue todo) - await page.goto(CONFIG.LOGIN_URL, { waitUntil: 'load', timeout: CONFIG.NAV_TIMEOUT }); - console.log(" [DEBUG] Buscando campos de login..."); - let loginExitoso = false; - for (let i = 0; i < 10; i++) { - const u = await findLocatorInFrames(page, 'input[name="w3user"]'); - const p = await findLocatorInFrames(page, 'input[name="w3clau"]'); - - if (u && p) { - await u.locator.first().fill(creds.user); - await p.locator.first().fill(creds.pass); - loginExitoso = true; - break; - } - await page.waitForTimeout(1000); // Reintento si los frames tardan en cargar + if (!jobData.observation || jobData.observation.trim().length === 0) { + throw new Error('⛔ ERROR: El campo Observaciones es obligatorio para HomeServe.'); } - if (!loginExitoso) throw new Error("No se cargaron los campos de login de HomeServe."); + await page.goto(CONFIG.LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: CONFIG.NAV_TIMEOUT }); + await page.waitForTimeout(1000); + await fillFirstThatExists(page, ['input[name="w3user"]', 'input[type="text"]'], creds.user); + await fillFirstThatExists(page, ['input[name="w3clau"]', 'input[type="password"]'], creds.pass); + await page.keyboard.press('Enter'); - await page.waitForTimeout(4000); // Esperar que procese el login + await page.waitForTimeout(3000); - const isStillAtLogin = await findLocatorInFrames(page, 'input[type="password"]'); - if (isStillAtLogin) throw new Error(`Login fallido en HomeServe.`); + const loginFail = await findLocatorInFrames(page, 'input[type="password"]'); + if (loginFail) throw new Error(`Login fallido en HomeServe para el usuario ${creds.user}. Revise las credenciales.`); console.log(`>>> 2. Login OK. Navegando al expediente ${jobData.service_number}...`); const serviceUrl = `${CONFIG.BASE_CGI}?w3exec=ver_servicioencurso&Servicio=${jobData.service_number}&Pag=1`; await page.goto(serviceUrl, { waitUntil: 'domcontentloaded', timeout: CONFIG.NAV_TIMEOUT }); - await page.waitForTimeout(2000); + await page.waitForTimeout(1500); const changeBtn = await clickFirstThatExists(page, ['input[name="repaso"]']); - if (!changeBtn) throw new Error(`No veo el botón 'repaso'.`); + if (!changeBtn) { + throw new Error(`No veo el botón 'repaso'. ¿El siniestro ${jobData.service_number} existe y está abierto?`); + } console.log('>>> 3. Accediendo al formulario. Rellenando datos...'); - await page.waitForTimeout(2000); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1000); - // Traducción y selección de estado (CON REFRESCO FORZADO PARA LA WEB) - let targetCode = jobData.new_status; + const HOMESERVE_MAP = { + 'CITADO': '307', + 'ESPERA': '303', + 'TERMINADO': '345', + 'ANULADO': '352' + }; + + let targetCode = jobData.new_status.toUpperCase(); + if (HOMESERVE_MAP[targetCode]) { + targetCode = HOMESERVE_MAP[targetCode]; + } + + // 👇 AQUÍ ESTÁ EL ARREGLO 1: FORZAR QUE LA WEB RECONOZCA EL DESPLEGABLE const statusOk = await page.evaluate((code) => { const select = document.querySelector('select[name="ESTADO"]'); if (!select) return false; for (const opt of select.options) { if (opt.value == code || opt.text.toUpperCase().includes(code.toUpperCase())) { select.value = opt.value; - // Disparamos los eventos para engañar a HomeServe y que crea que fue un humano + // Disparar eventos para que el servidor de HS registre el cambio select.dispatchEvent(new Event('change', { bubbles: true })); select.dispatchEvent(new Event('blur', { bubbles: true })); return true; @@ -140,10 +152,15 @@ async function loginAndProcess(page, creds, jobData) { return false; }, targetCode); - if (!statusOk) throw new Error(`Estado '${jobData.new_status}' no encontrado.`); + if (!statusOk) throw new Error(`No encontré el estado '${jobData.new_status}' (Buscando código interno: ${targetCode}) en el desplegable de HomeServe.`); - if (jobData.appointment_date) await fillFirstThatExists(page, ['input[name="FECSIG"]'], jobData.appointment_date); - await fillFirstThatExists(page, ['textarea[name="Observaciones"]'], jobData.observation); + if (jobData.appointment_date) { + const dateFilled = await fillFirstThatExists(page, ['input[name="FECSIG"]'], jobData.appointment_date); + if (!dateFilled) console.warn('⚠️ No encontré el recuadro para la fecha.'); + } + + const obsFilled = await fillFirstThatExists(page, ['textarea[name="Observaciones"]'], jobData.observation); + if (!obsFilled) throw new Error('No encontré el recuadro de Observaciones en la web de HomeServe.'); if (jobData.inform_client) { const informCheck = await findLocatorInFrames(page, 'input[name="INFORMO"]'); @@ -152,33 +169,28 @@ async function loginAndProcess(page, creds, jobData) { } } - // Retraso clave para que los scripts de HomeServe asienten los datos - await page.waitForTimeout(1000); + // 👇 AQUÍ ESTÁ EL ARREGLO 2: UN RESPIRO ANTES DEL CLIC + await page.waitForTimeout(1500); + console.log('>>> 4. Guardando cambios en HomeServe...'); const saveBtnHit = await findLocatorInFrames(page, 'input[name="BTNCAMBIAESTADO"]'); - if (!saveBtnHit) throw new Error('No encuentro el botón para guardar los cambios.'); + if (!saveBtnHit) throw new Error('No encuentro el botón para guardar los cambios en HomeServe.'); await saveBtnHit.locator.first().click(); - console.log(' -> Clic realizado, esperando confirmación del servidor (4s)...'); + console.log(' -> Clic realizado, esperando confirmación...'); + await page.waitForTimeout(4000); - // Verificación del texto en pantalla (Buscando en todos los frames) const resultText = await page.evaluate(() => { - // HomeServe suele meter el resultado en rojo o negrita - const frameConTexto = Array.from(window.frames).map(f => { - try { return f.document.querySelector('font[color="#FF0000"], .Estilo4, b, .mensaje'); } catch(e){ return null;} - }).find(el => el != null); - - const elPrincipal = document.querySelector('font[color="#FF0000"], .Estilo4, b, .mensaje'); - const nodoFinal = elPrincipal || frameConTexto; - return nodoFinal ? nodoFinal.innerText : ""; + const el = document.querySelector('font[color="#FF0000"], .Estilo4, b'); + return el ? el.innerText : ""; }); if (resultText && resultText.trim().length > 0) { console.log(`>>> Web dice: ${resultText.trim()}`); const textUpper = resultText.toUpperCase(); - if (!textUpper.includes('EXITO') && !textUpper.includes('ÉXITO') && !textUpper.includes('MODIFICADO') && !textUpper.includes('ACTUALIZADO')) { + if (!textUpper.includes('EXITO') && !textUpper.includes('ÉXITO') && !textUpper.includes('MODIFICADO')) { throw new Error(`Error en HomeServe: ${resultText.trim()}`); } } @@ -186,39 +198,55 @@ async function loginAndProcess(page, creds, jobData) { return { success: true }; } -// --- BUCLE DE COLA --- +// --- EL CEREBRO: LECTURA DE LA COLA EN POSTGRESQL --- async function pollQueue() { try { const res = await pool.query(` - UPDATE robot_queue SET status = 'RUNNING', updated_at = NOW() - WHERE id = (SELECT id FROM robot_queue WHERE status = 'PENDING' AND provider = 'homeserve' ORDER BY created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1) + UPDATE robot_queue + SET status = 'RUNNING', updated_at = NOW() + WHERE id = ( + SELECT id FROM robot_queue + WHERE status = 'PENDING' AND provider = 'homeserve' + ORDER BY created_at ASC + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) RETURNING *; `); if (res.rowCount > 0) { const job = res.rows[0]; - console.log(`\n🤖 TRABAJO #${job.id} | Empresa ID: ${job.owner_id} | Siniestro: ${job.service_number}`); + console.log(`\n========================================`); + console.log(`🤖 TRABAJO #${job.id} | Empresa ID: ${job.owner_id}`); + console.log(`📋 Siniestro: ${job.service_number} -> Cambiar a: ${job.new_status}`); + console.log(`========================================`); try { const creds = await getHomeServeCreds(job.owner_id); + await withBrowser(async (page) => { await loginAndProcess(page, creds, job); }); + await pool.query("UPDATE robot_queue SET status = 'DONE', updated_at = NOW() WHERE id = $1", [job.id]); - console.log(`✅ COMPLETADO #${job.id}\n`); + console.log(`✅ TRABAJO #${job.id} COMPLETADO CON ÉXITO.\n`); + } catch (err) { - console.error(`❌ ERROR #${job.id}:`, err.message); + console.error(`❌ ERROR EN TRABAJO #${job.id}:`, err.message); await pool.query("UPDATE robot_queue SET status = 'FAILED', error_msg = $1, updated_at = NOW() WHERE id = $2", [err.message, job.id]); } + setTimeout(pollQueue, 1000); } else { setTimeout(pollQueue, CONFIG.POLL_INTERVAL_MS); } } catch (e) { - console.error("Error crítico:", e.message); + console.error("Error crítico en el bucle del robot:", e.message); setTimeout(pollQueue, CONFIG.POLL_INTERVAL_MS); } } -console.log("🚀 Robot HomeServe SaaS Online."); +// --- INICIO --- +console.log("🚀 Robot HomeServe (Multi-Empresa SaaS) Iniciado."); +console.log("📡 Conectado a PostgreSQL. Esperando peticiones en la cola..."); pollQueue(); \ No newline at end of file