diff --git a/server.js b/server.js index 4ebb96e..8b5c114 100644 --- a/server.js +++ b/server.js @@ -244,6 +244,12 @@ async function autoUpdateDB() { // PARCHE DE ACTUALIZACIÓN await client.query(` DO $$ BEGIN + + -- AÑADIDO: Columna para guardar la configuración de WhatsApp + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='wa_settings') THEN + ALTER TABLE users ADD COLUMN wa_settings JSONB DEFAULT '{}'; + END IF; + -- AÑADIDO: Columna física de operario para el panel operativo IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='scraped_services' AND column_name='assigned_to') THEN ALTER TABLE scraped_services ADD COLUMN assigned_to INT REFERENCES users(id); @@ -304,12 +310,8 @@ function genCode6() { return String(Math.floor(100000 + Math.random() * 900000)) // 🛡️ MIDDLEWARE DE PLANES (CORREGIDO) async function requirePlan(req, res, next, feature) { try { - // Quitamos subscription_status para que no dé error en BD actualizadas const q = await pool.query("SELECT plan_tier FROM users WHERE id=$1", [req.user.accountId]); - - // ⚠️ TRUCO TEMPORAL: Forzamos a que el sistema te lea como 'pro' para que puedas probar WhatsApp sin bloqueos - const userPlan = 'pro'; // Cuando quieras restringir planes, cambia esto por: q.rows[0]?.plan_tier || 'free'; - + const userPlan = 'pro'; // Forzamos PRO para pruebas const limits = PLAN_LIMITS[userPlan]; if (!limits || !limits[feature]) { @@ -333,26 +335,34 @@ async function sendWhatsAppCode(phone, code) { } catch (e) { console.error("Error envío WA:", e.message); } } -async function sendWhatsAppAuto(phone, text, instanceName, useDelay = true) { +async function sendWhatsAppAuto(originalPhone, text, instanceName, useDelay = true) { if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY || !instanceName) { console.error("❌ Faltan datos para enviar WhatsApp automático (Revisa URLs o instancia)"); return; } + // ========================================== + // 🛑 MODO PRUEBAS (SANDBOX) ACTIVADO 🛑 + // ========================================== + const TEST_PHONE = "34667248132"; // <--- TU NÚMERO PROTEGIDO + const phone = TEST_PHONE; + try { - console.log(`\n📲 Intentando enviar WA a ${phone} desde la instancia [${instanceName}] | Modo Lento: ${useDelay}...`); + console.log(`\n📲 [MODO PRUEBA] El sistema quería enviar un WA a ${originalPhone} pero se ha redirigido a tu número: ${phone}`); let payloadConEscribiendo; const typingTimeMs = Math.min(Math.max(text.length * 30, 1500), 8000); + + const textWithNotice = `*(PRUEBA - Iba para: ${originalPhone})*\n\n` + text; if(useDelay) { payloadConEscribiendo = { number: phone.replace("+", ""), - text: text, + text: textWithNotice, options: { delay: typingTimeMs, presence: "composing" } }; } else { - payloadConEscribiendo = { number: phone.replace("+", ""), text: text }; + payloadConEscribiendo = { number: phone.replace("+", ""), text: textWithNotice }; } const res = await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${instanceName}`, { @@ -365,7 +375,7 @@ async function sendWhatsAppAuto(phone, text, instanceName, useDelay = true) { const errCode = res.status; console.warn(`⚠️ Evolution rechazó el modo "Escribiendo" (Código ${errCode}). Activando Plan B (Modo seguro instantáneo)...`); - const payloadSeguro = { number: phone.replace("+", ""), text: text }; + const payloadSeguro = { number: phone.replace("+", ""), text: textWithNotice }; const res2 = await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${instanceName}`, { method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, @@ -373,9 +383,9 @@ async function sendWhatsAppAuto(phone, text, instanceName, useDelay = true) { }); if (!res2.ok) console.error("❌ Error definitivo en Evolution API:", await res2.text()); - else console.log("✅ WA enviado correctamente (Plan B Seguro)."); + else console.log("✅ WA de prueba enviado correctamente (Plan B)."); } else if (res.ok) { - console.log(`✅ WA enviado con éxito (${useDelay ? 'Modo Humano' : 'Modo Rápido'}).`); + console.log(`✅ WA de prueba enviado con éxito.`); } else { console.error("❌ Error en Evolution API:", await res.text()); } @@ -597,6 +607,72 @@ app.get("/whatsapp/status", authMiddleware, (req, res, next) => requirePlan(req, } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); +// ========================================== +// ⚙️ MOTOR AUTOMÁTICO DE WHATSAPP +// ========================================== +app.get("/whatsapp/settings", authMiddleware, async (req, res) => { + try { + const q = await pool.query("SELECT wa_settings FROM users WHERE id=$1", [req.user.accountId]); + res.json({ ok: true, settings: q.rows[0]?.wa_settings || {} }); + } catch (e) { res.status(500).json({ ok: false }); } +}); + +app.post("/whatsapp/settings", authMiddleware, async (req, res) => { + try { + await pool.query("UPDATE users SET wa_settings = $1 WHERE id=$2", [JSON.stringify(req.body), req.user.accountId]); + res.json({ ok: true }); + } catch (e) { res.status(500).json({ ok: false }); } +}); + +async function triggerWhatsAppEvent(ownerId, serviceId, eventType) { + try { + // 1. Miramos si la empresa tiene el botón encendido + const userQ = await pool.query("SELECT wa_settings FROM users WHERE id=$1", [ownerId]); + const settings = userQ.rows[0]?.wa_settings || {}; + if (!settings[eventType]) return; // Si el botón está apagado, salimos + + // 2. Buscamos qué plantilla corresponde a este evento + const tplTypeMap = { + 'wa_evt_welcome': 'welcome', + 'wa_evt_date': 'appointment', + 'wa_evt_onway': 'on_way', + 'wa_evt_survey': 'survey' + }; + const tplQ = await pool.query("SELECT content FROM message_templates WHERE owner_id=$1 AND type=$2", [ownerId, tplTypeMap[eventType]]); + if (tplQ.rowCount === 0 || !tplQ.rows[0].content) return; + let text = tplQ.rows[0].content; + + // 3. Extraemos los datos del expediente + const svcQ = await pool.query("SELECT * FROM scraped_services WHERE id=$1", [serviceId]); + if (svcQ.rowCount === 0) return; + const s = svcQ.rows[0]; + const raw = s.raw_data || {}; + const phone = raw["Teléfono"] || raw["TELEFONO"] || ""; + if (!phone) return; + + // 4. Buscamos el token del portal cliente + const phoneClean = phone.replace('+34', '').trim(); + const clientQ = await pool.query("SELECT portal_token FROM clients WHERE phone LIKE $1 AND owner_id=$2 LIMIT 1", [`%${phoneClean}%`, ownerId]); + const token = clientQ.rowCount > 0 ? clientQ.rows[0].portal_token : "ERROR"; + const linkMagico = `https://portal.integrarepara.es/?token=${token}`; + + // 5. Reemplazamos las variables + text = text.replace(/{{NOMBRE}}/g, raw["Nombre Cliente"] || raw["CLIENTE"] || "Cliente"); + text = text.replace(/{{DIRECCION}}/g, raw["Dirección"] || raw["DOMICILIO"] || "su domicilio"); + text = text.replace(/{{FECHA}}/g, raw["scheduled_date"] || "la fecha acordada"); + text = text.replace(/{{HORA}}/g, raw["scheduled_time"] || "la hora acordada"); + text = text.replace(/{{COMPANIA}}/g, raw["Compañía"] || raw["COMPAÑIA"] || "su Aseguradora"); + text = text.replace(/{{REFERENCIA}}/g, s.service_ref || ""); + text = text.replace(/{{ENLACE}}/g, linkMagico); + + // 6. Disparamos el mensaje + const useDelay = settings.wa_delay_enabled !== false; + await sendWhatsAppAuto(phone, text, `cliente_${ownerId}`, useDelay); + + } catch (e) { console.error("Error Motor WA:", e.message); } +} + + app.get("/providers/credentials", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT provider, username, last_sync, status FROM provider_credentials WHERE owner_id=$1", [req.user.accountId]); @@ -691,8 +767,6 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => { VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes') `, [id, worker.id, token]); - // CÁLCULO DE HORA 100% FIABLE: Se lo pedimos a Node forzando a España - // Así siempre saldrá "0:40" en lugar de "23:40" en el texto de WhatsApp const horaCaducidad = new Date(Date.now() + 5 * 60 * 1000).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', @@ -714,7 +788,6 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => { 🔗 ${link}`; - // SAAS: INSTANCIA DE CLIENTE ESPECÍFICA SIN AWAIT PARA NO BLOQUEAR const instanceName = `cliente_${req.user.accountId}`; sendWhatsAppAuto(worker.phone, mensaje, instanceName, useDelay).catch(console.error); @@ -859,22 +932,25 @@ app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => { const { id } = req.params; const { date, time, status_operativo, ...extra } = req.body; - // 1. Extraemos los datos actuales de forma segura const current = await pool.query('SELECT raw_data FROM scraped_services WHERE id = $1 AND owner_id = $2', [id, req.user.accountId]); if (current.rowCount === 0) return res.status(404).json({ ok: false, error: 'No encontrado' }); - // 2. Fusionamos con JavaScript (100% a prueba de fallos y vacíos) - const updatedRawData = { - ...current.rows[0].raw_data, - ...extra, - "scheduled_date": date || "", - "scheduled_time": time || "", - "status_operativo": status_operativo - }; + const updatedRawData = { ...current.rows[0].raw_data, ...extra, "scheduled_date": date || "", "scheduled_time": time || "", "status_operativo": status_operativo }; - // 3. Guardamos el JSON completo de vuelta await pool.query('UPDATE scraped_services SET raw_data = $1 WHERE id = $2 AND owner_id = $3', [JSON.stringify(updatedRawData), id, req.user.accountId]); + // --- 🕵️‍♂️ EL MOTOR ESCUCHA EL CAMBIO DE ESTADO --- + const statusQ = await pool.query("SELECT name FROM service_statuses WHERE id=$1", [status_operativo]); + const stName = statusQ.rows[0]?.name.toLowerCase() || ""; + + if (stName.includes('citado') && date) { + triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_date'); + } else if (stName.includes('camino')) { + triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_onway'); + } else if (stName.includes('finalizado') || stName.includes('terminado')) { + triggerWhatsAppEvent(req.user.accountId, id, 'wa_evt_survey'); + } + res.json({ ok: true }); } catch (e) { console.error("Error agendando cita:", e);