const express = require('express'); const { chromium } = require('playwright'); const admin = require('firebase-admin'); const cors = require('cors'); // --- 1. CONFIGURACIÓN FIREBASE (IGUAL QUE TU ROBOT ORIGINAL) --- if (process.env.FIREBASE_PRIVATE_KEY) { try { if (!admin.apps.length) { // Evita error si ya está inicializado 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'), }), }); } } catch (err) { console.error("❌ Error inicializando Firebase:", err.message); process.exit(1); } } else { // Para pruebas locales sin variables de entorno, puedes descomentar esto: // var serviceAccount = require("./serviceAccountKey.json"); // admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); console.error("⚠️ FALTAN LAS CLAVES DE FIREBASE (ENV)"); } const db = admin.firestore(); const APPOINTMENTS_COL = "appointments"; const PROVIDER_DOC = "homeserve"; // --- 2. SERVIDOR EXPRESS --- const app = express(); app.use(cors()); // Permite peticiones desde tu HTML app.use(express.json()); // --- 3. FUNCIONES AUXILIARES --- async function getProviderCredentials(providerDocId) { const snap = await db.collection("providerCredentials").doc(providerDocId).get(); if (!snap.exists) throw new Error(`No existe providerCredentials/${providerDocId}`); const data = snap.data() || {}; return { user: String(data.user || "").trim(), pass: String(data.pass || "").trim() }; } function normalizeServiceNumber(raw) { // Extrae solo dígitos. Homeserve suele usar números largos. return String(raw || "").trim().replace(/\D/g, ""); } function parseMoney(str) { // Convierte "1.200,50 €" -> 1200.50 if (!str) return 0; let clean = str.replace(/[€\s]/g, ''); // Quitar símbolo y espacios clean = clean.replace(/\./g, ''); // Quitar separador miles (punto) clean = clean.replace(',', '.'); // Cambiar coma decimal por punto return parseFloat(clean) || 0; } // --- 4. ENDPOINT DEL ROBOT --- app.post('/api/robot-cobros', async (req, res) => { console.log("🚀 Recibida orden de rescate de cobros..."); // Respondemos rápido al cliente para que no se quede cargando infinitamente res.json({ success: true, message: "Robot iniciado. Revisa la consola." }); // Ejecutamos la lógica en segundo plano await runCobrosRobot(); }); // --- 5. LÓGICA PRINCIPAL DEL ROBOT --- async function runCobrosRobot() { let browser = null; try { console.log('🤖 Iniciando Robot de Cobros...'); // 1. OBTENER CREDENCIALES const creds = await getProviderCredentials(PROVIDER_DOC); console.log(`🔐 Credenciales cargadas para ${creds.user}`); // 2. LANZAR NAVEGADOR browser = await chromium.launch({ headless: true, // Pon false si quieres ver lo que hace en el servidor local args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const context = await browser.newContext(); const page = await context.newPage(); // 3. LOGIN (Misma lógica que tu robot de servicios) console.log('🌍 Entrando a HomeServe...'); await page.goto('https://www.clientes.homeserve.es/cgi-bin/fccgi.exe?w3exec=PROF_PASS', { timeout: 60000 }); const selectorUsuario = 'input[name="CODIGO"]'; const selectorPass = 'input[type="password"]'; if (await page.isVisible(selectorUsuario)) { console.log('🔑 Logueándose...'); await page.fill(selectorUsuario, ""); await page.fill(selectorPass, ""); await page.type(selectorUsuario, creds.user, { delay: 80 }); await page.type(selectorPass, creds.pass, { delay: 80 }); await page.keyboard.press('Enter'); await page.waitForTimeout(5000); } else { console.log("⚠️ Ya estaba logueado o no veo el login."); } // 4. IR A LIQUIDACIONES console.log('📂 Navegando a Liquidaciones...'); await page.goto('https://www.clientes.homeserve.es/cgi-bin/fccgi.exe?w3exec=CONSULTALIQ_WEB'); await page.waitForTimeout(2000); // 5. SELECCIONAR LA ÚLTIMA LIQUIDACIÓN // Buscamos el primer enlace dentro de la tabla de resultados. // Normalmente las webs viejas usan tablas. Buscamos el primer dentro de un console.log('👆 Pulsando en la última liquidación disponible...'); // Estrategia: Buscar enlaces que parezcan fechas o simplemente el primer enlace de la tabla de datos // Ajusta el selector si la estructura es compleja. // Asumimos que la primera fila es la más reciente. const liquidacionClicked = await page.evaluate(() => { const link = document.querySelector('table tr td a'); if (link) { link.click(); return true; } return false; }); if (!liquidacionClicked) throw new Error("No se encontraron enlaces de liquidación."); await page.waitForTimeout(3000); // 6. PULSAR "DESGLOSE" console.log('🔍 Buscando botón "Desglose"...'); // Buscamos un enlace o botón que contenga el texto "Desglose" const desgloseClicked = await page.evaluate(() => { const links = Array.from(document.querySelectorAll('a, button, input[type="button"]')); const target = links.find(el => el.innerText.toLowerCase().includes('desglose') || el.value?.toLowerCase().includes('desglose')); if (target) { target.click(); return true; } return false; }); if (!desgloseClicked) { console.log("⚠️ No vi botón 'Desglose'. Intentando escanear la tabla actual por si ya estamos ahí."); } else { await page.waitForTimeout(3000); } // 7. EXTRAER DATOS (SCRAPING) console.log('📄 Extrayendo datos de la tabla...'); const cobrosDetectados = await page.evaluate(() => { const datos = []; const filas = Array.from(document.querySelectorAll('table tr')); filas.forEach(tr => { const tds = tr.querySelectorAll('td'); // Necesitamos heurística para saber qué columna es cual. // Generalmente: Columna con formato XXXXX (Expediente) y Columna con € (Dinero) let expediente = null; let importeRaw = null; tds.forEach(td => { const txt = td.innerText.trim(); // Detectar expediente: 5 o más dígitos seguidos if (/^\d{5,}$/.test(txt)) expediente = txt; // Detectar dinero: contiene digitos, coma y simbolo € o solo formato decimal if (txt.includes(',') && (txt.includes('€') || txt.match(/\d/))) importeRaw = txt; }); if (expediente && importeRaw) { datos.push({ serviceNumber: expediente, rawAmount: importeRaw }); } }); return datos; }); console.log(`💰 Se han detectado ${cobrosDetectados.length} líneas de cobro.`); // 8. ACTUALIZAR FIREBASE let actualizados = 0; const nowISO = new Date().toISOString(); for (const cobro of cobrosDetectados) { const sn = normalizeServiceNumber(cobro.serviceNumber); const amount = parseMoney(cobro.rawAmount); if (!sn || amount <= 0) continue; // Buscar en appointments const q = await db.collection(APPOINTMENTS_COL).where('serviceNumber', '==', sn).get(); if (!q.empty) { q.forEach(async doc => { const data = doc.data(); // Solo actualizamos si el monto es diferente o no estaba pagado if (data.paidAmount !== amount) { await doc.ref.update({ paidAmount: amount, status: 'completed', // Forzamos a completado si hay cobro paymentDate: nowISO, lastUpdatedByRobot: nowISO }); console.log(`✅ Pago registrado: Exp ${sn} -> ${amount}€`); actualizados++; } }); } else { console.log(`⚠️ Exp ${sn} (${amount}€) no encontrado en base de datos.`); // Opcional: Crear registro en una colección 'pagos_huérfanos' } } console.log(`🏁 FINALIZADO: ${actualizados} expedientes actualizados.`); } catch (e) { console.error("❌ Error en Robot Cobros:", e); } finally { if (browser) await browser.close(); } } // Iniciar servidor const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`🚀 Servidor Robot escuchando en puerto ${PORT}`); });