diff --git a/worker-homeserve.js b/worker-homeserve.js new file mode 100644 index 0000000..c19b540 --- /dev/null +++ b/worker-homeserve.js @@ -0,0 +1,234 @@ +// worker-homeserve.js (Versión PostgreSQL - MULTI-EMPRESA SAAS) +'use strict'; + +const { chromium } = require('playwright'); +const { Pool } = require('pg'); + +// --- CONFIGURACIÓN --- +const CONFIG = { + DATABASE_URL: process.env.DATABASE_URL, + LOGIN_URL: 'https://www.clientes.homeserve.es/cgi-bin/fccgi.exe?w3exec=PROF_PASS', + BASE_CGI: 'https://www.clientes.homeserve.es/cgi-bin/fccgi.exe', + NAV_TIMEOUT: 60000, + POLL_INTERVAL_MS: 5000 // Cada 5 segundos mira si hay trabajo +}; + +if (!CONFIG.DATABASE_URL) { + console.error("❌ ERROR FATAL: Falta la variable de entorno DATABASE_URL"); + process.exit(1); +} + +// Conexión a tu Base de Datos local/servidor +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 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) { + // 🔴 AHORA FILTRAMOS POR EL OWNER_ID EXACTO DEL SERVICIO + 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(`No hay credenciales activas de HomeServe para la empresa/dueño ID: ${ownerId}.`); + } + + const user = q.rows[0].username; + // Convierte el Base64 (ej: UGFqYXJpdG8xNCQ=) a texto normal + const pass = Buffer.from(q.rows[0].password_hash, 'base64').toString('utf-8'); + + return { user, pass }; +} + +// --- PLAYWRIGHT HELPERS --- +async function withBrowser(fn) { + const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); + const context = await browser.newContext(); + const page = await context.newPage(); + try { return await fn(page); } finally { await browser.close().catch(() => {}); } +} + +async function findLocatorInFrames(page, selector) { + for (const fr of page.frames()) { + const loc = fr.locator(selector); + try { if (await loc.count()) return { frame: fr, locator: loc }; } catch (_) {} + } + return null; +} + +async function clickFirstThatExists(page, selectors) { + for (const sel of selectors) { + const hit = await findLocatorInFrames(page, sel); + if (hit) { await hit.locator.first().click(); return sel; } + } + return null; +} + +async function fillFirstThatExists(page, selectors, value) { + for (const sel of selectors) { + const hit = await findLocatorInFrames(page, sel); + if (hit) { await hit.locator.first().fill(String(value)); return sel; } + } + return null; +} + +async function getDebugInfo(page) { + try { + const title = await page.title(); + const url = page.url(); + return `URL: ${url} | Titulo: ${title}`; + } catch (e) { return "Error extrayendo debug info"; } +} + +// --- INYECCIÓN EN HOMESERVE --- +async function loginAndProcess(page, creds, jobData) { + 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 para 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(3000); + + 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 en IntegraRepara.`); + + 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(1500); + + const changeBtn = await clickFirstThatExists(page, ['input[name="repaso"]']); + if (!changeBtn) { + const debug = await getDebugInfo(page); + throw new Error(`No veo el botón 'repaso'. ¿El siniestro ${jobData.service_number} existe y está abierto? DEBUG: ${debug}`); + } + + console.log('>>> 3. Accediendo al formulario. Rellenando datos...'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1000); + + const statusOk = await page.evaluate((code) => { + const selects = document.querySelectorAll('select'); + for (const s of selects) { + for (const opt of s.options) { + if (opt.value == code || opt.text.includes(code)) { + s.value = opt.value; + return true; + } + } + } + return false; + }, jobData.new_status); + + if (!statusOk) throw new Error(`No encontré el estado '${jobData.new_status}' en los desplegables de HomeServe.`); + + 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, pero continuaré.'); + } + + 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 labels = ['informado al cliente', 'informado al Cliente']; + for (const txt of labels) { + const hit = await findLocatorInFrames(page, `label:has-text("${txt}") >> input[type="checkbox"]`); + if (hit && !(await hit.locator.first().isChecked())) await hit.locator.first().check(); + } + } + + const saveBtn = await clickFirstThatExists(page, ['input[type="submit"]', 'input[value="Guardar"]', 'button:has-text("Guardar")']); + if (!saveBtn) throw new Error('No encuentro el botón para guardar los cambios en HomeServe.'); + + console.log('>>> 4. Guardando cambios en HomeServe...'); + await page.waitForTimeout(3000); + return { success: true }; +} + +// --- EL CEREBRO: LECTURA DE LA COLA EN POSTGRESQL --- +async function pollQueue() { + try { + // Buscamos 1 solo trabajo pendiente y lo bloqueamos para que no lo cojan otros robots si abres varios + 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 + ) + RETURNING *; + `); + + if (res.rowCount > 0) { + const job = res.rows[0]; + 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 { + // 🔴 PASAMOS EL OWNER_ID PARA SACAR LAS CREDENCIALES CORRECTAS + const creds = await getHomeServeCreds(job.owner_id); + + await withBrowser(async (page) => { + await loginAndProcess(page, creds, job); + }); + + // Si va bien, marcamos como DONE en la BD + await pool.query("UPDATE robot_queue SET status = 'DONE', updated_at = NOW() WHERE id = $1", [job.id]); + console.log(`✅ TRABAJO #${job.id} COMPLETADO CON ÉXITO.\n`); + + } catch (err) { + // Si falla, guardamos el error en la BD para que el HTML lo muestre + 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]); + } + + // Buscar el siguiente inmediatamente + setTimeout(pollQueue, 1000); + } else { + // Dormimos y volvemos a mirar + setTimeout(pollQueue, CONFIG.POLL_INTERVAL_MS); + } + } catch (e) { + console.error("Error crítico en el bucle del robot:", e.message); + setTimeout(pollQueue, CONFIG.POLL_INTERVAL_MS); + } +} + +// --- 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