Actualizar server.js

This commit is contained in:
2026-02-20 20:21:50 +00:00
parent d832aff33d
commit b2a31e9333

126
server.js
View File

@@ -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);