From 01e31d810d3f0c5c92512f7225ddb66413332f73 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sun, 28 Dec 2025 19:24:39 +0000 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=20robot=5Fcobros.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- robot_cobros.js | 230 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 robot_cobros.js diff --git a/robot_cobros.js b/robot_cobros.js new file mode 100644 index 0000000..e5333b3 --- /dev/null +++ b/robot_cobros.js @@ -0,0 +1,230 @@ +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}`); +}); \ No newline at end of file