From 9d5fa18b69207cceb4f8e8d895b108ca55636a70 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sun, 28 Dec 2025 20:11:57 +0000 Subject: [PATCH] Actualizar robot_cobros.js --- robot_cobros.js | 218 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 150 insertions(+), 68 deletions(-) diff --git a/robot_cobros.js b/robot_cobros.js index ceeb40e..e92bba3 100644 --- a/robot_cobros.js +++ b/robot_cobros.js @@ -1,89 +1,171 @@ const express = require('express'); -const cors = require('cors'); const { chromium } = require('playwright'); const admin = require('firebase-admin'); +const cors = require('cors'); -// --- FIREBASE --- +// --- 1. CONFIGURACIÓN FIREBASE --- try { - if (process.env.FIREBASE_PRIVATE_KEY) { - if (!admin.apps.length) { - admin.initializeApp({ - credential: admin.credential.cert({ - projectId: process.env.FIREBASE_PROJECT_ID, - clientEmail: process.env.FIREBASE_CLIENT_EMAIL, - privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), - }), - }); + if (process.env.FIREBASE_PRIVATE_KEY) { + if (!admin.apps.length) { + admin.initializeApp({ + credential: admin.credential.cert({ + projectId: process.env.FIREBASE_PROJECT_ID, + clientEmail: process.env.FIREBASE_CLIENT_EMAIL, + privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), + }), + }); + } + console.log("✅ Firebase inicializado."); + } else { + console.warn("⚠️ SIN FIREBASE: Modo simulación."); } - console.log("✅ Firebase inicializado."); - } else { - console.log("⚠️ FIREBASE_PRIVATE_KEY no está seteada. Firebase OFF."); - } -} catch (e) { - console.error("❌ Error Firebase:", e.message); -} +} catch (e) { console.error("❌ Error Firebase:", e.message); } const db = admin.apps.length ? admin.firestore() : null; -const COLLECTION_NAME = "homeserve_pendientes"; +const APPOINTMENTS_COL = "appointments"; const app = express(); -// --- CORS (robusto) --- -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], -})); - -// IMPORTANTÍSIMO: responder preflight -app.options('*', (req, res) => { - res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization"); - res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - return res.sendStatus(204); -}); - +// --- 2. CORS PERMISIVO (Para evitar bloqueos) --- +app.use(cors({ origin: '*' })); app.use(express.json()); -// TEST -app.get('/', (req, res) => { - res.status(200).send('🤖 El Robot de Cobros está ONLINE y esperando órdenes.'); -}); - -app.get('/health', (req, res) => { - res.status(200).json({ ok: true, firebase: !!db, ts: new Date().toISOString() }); -}); - -// ROBOT +// --- 3. ENDPOINT PRINCIPAL --- app.post('/api/robot-cobros', async (req, res) => { - console.log("🔔 POST /api/robot-cobros recibido."); - - // Respuesta inmediata para que el navegador no espere - res.status(200).json({ success: true, message: "Orden recibida. Iniciando..." }); - - // Ejecutar en segundo plano - try { - await runRobotLogic(); - } catch (err) { - console.error("❌ Error robot:", err?.message || err); - } + console.log("🔔 Orden recibida: Rescatar cobros."); + res.json({ success: true, message: "Robot iniciado. Procesando liquidaciones..." }); + + // Ejecutar en segundo plano + runLiquidationRobot().catch(err => console.error("❌ Error robot:", err)); }); -async function runRobotLogic() { - console.log("🚀 runRobotLogic() arrancando..."); +app.get('/', (req, res) => res.send('🤖 Robot Liquidaciones ONLINE (Usa POST para activar)')); - // Si aquí tu lógica usa Firebase, asegúrate: - // if (!db) throw new Error("Firebase no inicializado (faltan env vars)"); +// --- 4. LÓGICA DE NAVEGACIÓN (Liquidaciones) --- +async function runLiquidationRobot() { + let browser = null; + try { + console.log("🚀 Lanzando navegador..."); + + // 1. Obtener credenciales + let user = ""; + let pass = ""; + + if (db) { + const doc = await db.collection("providerCredentials").doc("homeserve").get(); + if (doc.exists) { + user = doc.data().user; + pass = doc.data().pass; + } + } - // Ejemplo mínimo - // const browser = await chromium.launch({ headless: true }); - // ... - // await browser.close(); + if (!user || !pass) throw new Error("No hay credenciales en Firebase"); - console.log("✅ runRobotLogic() terminado (simulación)."); + browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); + const page = await browser.newPage(); + + // 2. Login + console.log("🌍 Entrando en HomeServe (PROF_PASS)..."); + await page.goto('https://www.clientes.homeserve.es/cgi-bin/fccgi.exe?w3exec=PROF_PASS', { timeout: 60000 }); + + const selUser = 'input[name="CODIGO"]'; + if (await page.isVisible(selUser)) { + console.log("🔑 Logueándose..."); + await page.fill(selUser, user); + await page.fill('input[type="password"]', pass); + await page.keyboard.press('Enter'); + await page.waitForTimeout(4000); + } + + // 3. Ir a Liquidaciones + console.log("📂 Navegando a CONSULTALIQ_WEB..."); + await page.goto('https://www.clientes.homeserve.es/cgi-bin/fccgi.exe?w3exec=CONSULTALIQ_WEB'); + await page.waitForTimeout(2000); + + // 4. Buscar primera fecha (enlace) + console.log("🔍 Buscando última liquidación..."); + // Buscamos el primer enlace dentro de la tabla principal + const liquidacionClicked = await page.evaluate(() => { + const link = document.querySelector('table a'); // Clic en el primer enlace que encuentre en una tabla + if (link) { link.click(); return true; } + return false; + }); + + if (!liquidacionClicked) throw new Error("No se encontraron liquidaciones."); + await page.waitForTimeout(3000); + + // 5. Buscar botón "Desglose" + console.log("📄 Buscando desglose..."); + const desgloseClicked = await page.evaluate(() => { + // Buscar input tipo button o enlace que diga "Desglose" + const inputs = Array.from(document.querySelectorAll('input[type="button"], a, button')); + const target = inputs.find(el => (el.value || el.innerText || "").toLowerCase().includes('desglose')); + if (target) { target.click(); return true; } + return false; + }); + + if (!desgloseClicked) console.warn("⚠️ No vi botón 'Desglose', intentando leer tabla actual..."); + await page.waitForTimeout(3000); + + // 6. Leer Tabla de Cobros + console.log("💰 Leyendo datos..."); + const cobros = await page.evaluate(() => { + const filas = Array.from(document.querySelectorAll('tr')); + const datos = []; + filas.forEach(tr => { + const text = tr.innerText; + // Buscamos patrones de expediente (ej: 15xxxxxx) y dinero + // Esta es una heurística básica, ajustar según HTML real + const tds = tr.querySelectorAll('td'); + let exp = ""; + let money = ""; + + tds.forEach(td => { + const t = td.innerText.trim(); + if (/^\d{6,}$/.test(t)) exp = t; // Numeros largos = expediente + if (t.includes(',') && (t.includes('€') || /\d/.test(t))) money = t; + }); + + if (exp && money) datos.push({ exp, money }); + }); + return datos; + }); + + console.log(`✅ Detectados ${cobros.length} posibles cobros.`); + + // 7. Guardar en Firebase + if (db && cobros.length > 0) { + const batch = db.batch(); + let count = 0; + const nowISO = new Date().toISOString(); + + for (const item of cobros) { + // Limpiar importe + const importe = parseFloat(item.money.replace(/[^\d,]/g, '').replace(',', '.')); + if (!importe) continue; + + // Buscar servicio + const q = await db.collection(APPOINTMENTS_COL).where("serviceNumber", "==", item.exp).get(); + q.forEach(doc => { + batch.update(doc.ref, { + paidAmount: importe, + status: 'completed', // Asumimos completado si pagan + paymentDate: nowISO, + lastUpdatedByRobot: nowISO + }); + count++; + }); + } + await batch.commit(); + console.log(`💾 Base de datos actualizada: ${count} registros.`); + } + + } catch (e) { + console.error("❌ Error en proceso:", e); + } finally { + if (browser) await browser.close(); + } } -const PORT = process.env.PORT || process.env.CAPROVER_PORT || 3000; -app.listen(PORT, () => { - console.log(`🚀 Servidor escuchando en puerto ${PORT}`); -}); \ No newline at end of file +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => console.log(`🚀 Server on port ${PORT}`)); \ No newline at end of file