Actualizar server.js
This commit is contained in:
608
server.js
608
server.js
@@ -908,68 +908,96 @@ async function registrarMovimiento(serviceId, userId, action, details) {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
||||||
try {
|
try {
|
||||||
const userQ = await pool.query("SELECT wa_settings, full_name, portal_settings FROM users WHERE id=$1", [ownerId]);
|
const userQ = await pool.query(
|
||||||
|
"SELECT wa_settings, full_name, portal_settings FROM users WHERE id=$1",
|
||||||
|
[ownerId]
|
||||||
|
);
|
||||||
|
|
||||||
const userData = userQ.rows[0];
|
const userData = userQ.rows[0];
|
||||||
const settings = userData?.wa_settings || {};
|
const settings = userData?.wa_settings || {};
|
||||||
const instruccionesExtra = settings.ai_custom_prompt || "";
|
const instruccionesExtra = settings.ai_custom_prompt || "";
|
||||||
const empresaNombre = userData?.full_name || "nuestra empresa";
|
const empresaNombre = userData?.full_name || "nuestra empresa";
|
||||||
|
|
||||||
console.log("🕵️ CHIVATO PROMPT EXTRA:", instruccionesExtra);
|
|
||||||
|
|
||||||
const pSettings = userData?.portal_settings || {};
|
|
||||||
const horarios = { m_start: pSettings.m_start || "09:00", m_end: pSettings.m_end || "14:00", a_start: pSettings.a_start || "16:00", a_end: pSettings.a_end || "19:00" };
|
|
||||||
|
|
||||||
if (!settings.wa_ai_enabled) return null;
|
if (!settings.wa_ai_enabled) return null;
|
||||||
|
|
||||||
const ahora = new Date();
|
const pSettings = userData?.portal_settings || {};
|
||||||
const fechaHoyTexto = ahora.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
const horarios = {
|
||||||
|
m_start: pSettings.m_start || "09:00",
|
||||||
|
m_end: pSettings.m_end || "14:00",
|
||||||
|
a_start: pSettings.a_start || "16:00",
|
||||||
|
a_end: pSettings.a_end || "19:00"
|
||||||
|
};
|
||||||
|
|
||||||
const historyQ = await pool.query("SELECT sender_role, message FROM service_communications WHERE scraped_id = $1 ORDER BY created_at DESC LIMIT 8", [datosExpediente.dbId]);
|
const fechaHoyTexto = new Intl.DateTimeFormat("es-ES", {
|
||||||
const historialChat = historyQ.rows.reverse().map(row => ({
|
timeZone: "Europe/Madrid",
|
||||||
role: (row.sender_role === 'ia' || row.sender_role === 'admin' || row.sender_role === 'operario') ? 'assistant' : 'user',
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
}).format(new Date());
|
||||||
|
|
||||||
|
// 👇 IMPORTANTE:
|
||||||
|
// Aquí leemos el historial REAL, ya incluyendo el mensaje actual del cliente
|
||||||
|
// que el webhook guardará antes de llamar a OpenAI.
|
||||||
|
const historyQ = await pool.query(`
|
||||||
|
SELECT sender_role, message
|
||||||
|
FROM service_communications
|
||||||
|
WHERE scraped_id = $1
|
||||||
|
AND sender_role <> 'system'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 12
|
||||||
|
`, [datosExpediente.dbId]);
|
||||||
|
|
||||||
|
const historialRows = historyQ.rows.reverse();
|
||||||
|
|
||||||
|
const historialChat = historialRows.map(row => ({
|
||||||
|
role: row.sender_role === 'user' ? 'user' : 'assistant',
|
||||||
content: row.message
|
content: row.message
|
||||||
}));
|
}));
|
||||||
const esPrimerMensaje = historialChat.length === 0;
|
|
||||||
|
|
||||||
let agendaOcupadaTexto = "El técnico tiene la agenda libre en horario laboral.";
|
// Si solo existe el mensaje actual del cliente, es primer mensaje
|
||||||
|
const esPrimerMensaje = historialRows.length <= 1;
|
||||||
|
|
||||||
|
let agendaOcupadaTexto = "✅ El técnico tiene la agenda libre en horario laboral.";
|
||||||
|
|
||||||
if (datosExpediente.worker_id) {
|
if (datosExpediente.worker_id) {
|
||||||
// 🛑 AÑADIDO: Consulta avanzada que lee Duraciones, Bloqueos y Citas Pendientes
|
|
||||||
const agendaQ = await pool.query(`
|
const agendaQ = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(NULLIF(raw_data->>'scheduled_date', ''), raw_data->>'requested_date') as date,
|
COALESCE(NULLIF(raw_data->>'scheduled_date', ''), raw_data->>'requested_date') as date,
|
||||||
COALESCE(NULLIF(raw_data->>'scheduled_time', ''), raw_data->>'requested_time') as time,
|
COALESCE(NULLIF(raw_data->>'scheduled_time', ''), raw_data->>'requested_time') as time,
|
||||||
raw_data->>'duration_minutes' as duration,
|
raw_data->>'duration_minutes' as duration,
|
||||||
raw_data->>'Población' as pob,
|
COALESCE(raw_data->>'Población', raw_data->>'POBLACION-PROVINCIA') as pob,
|
||||||
provider
|
provider
|
||||||
FROM scraped_services
|
FROM scraped_services
|
||||||
WHERE assigned_to = $1
|
WHERE owner_id = $1
|
||||||
AND status != 'archived'
|
AND assigned_to = $2
|
||||||
AND id != $2
|
AND status != 'archived'
|
||||||
AND (
|
AND id != $3
|
||||||
|
AND (
|
||||||
(raw_data->>'scheduled_date' IS NOT NULL AND raw_data->>'scheduled_date' >= CURRENT_DATE::text)
|
(raw_data->>'scheduled_date' IS NOT NULL AND raw_data->>'scheduled_date' >= CURRENT_DATE::text)
|
||||||
OR
|
OR
|
||||||
(raw_data->>'appointment_status' = 'pending' AND raw_data->>'requested_date' >= CURRENT_DATE::text)
|
(raw_data->>'appointment_status' = 'pending' AND raw_data->>'requested_date' >= CURRENT_DATE::text)
|
||||||
)
|
)
|
||||||
ORDER BY date ASC, time ASC
|
ORDER BY date ASC, time ASC
|
||||||
`, [datosExpediente.worker_id, datosExpediente.dbId]);
|
`, [ownerId, datosExpediente.worker_id, datosExpediente.dbId]);
|
||||||
|
|
||||||
if (agendaQ.rowCount > 0) {
|
if (agendaQ.rowCount > 0) {
|
||||||
const ocupaciones = {};
|
const ocupaciones = {};
|
||||||
|
|
||||||
agendaQ.rows.forEach(r => {
|
agendaQ.rows.forEach(r => {
|
||||||
if (r.date && r.time && r.time.includes(':')) {
|
if (r.date && r.time && r.time.includes(':')) {
|
||||||
if (!ocupaciones[r.date]) ocupaciones[r.date] = [];
|
if (!ocupaciones[r.date]) ocupaciones[r.date] = [];
|
||||||
|
|
||||||
// Calculamos la hora de fin exacta sumando la duración (Ej: 60, 120, 180 min)
|
|
||||||
let [h, m] = r.time.split(':').map(Number);
|
let [h, m] = r.time.split(':').map(Number);
|
||||||
let dur = parseInt(r.duration || 60, 10);
|
let dur = parseInt(r.duration || 60, 10);
|
||||||
let endMin = (h * 60 + m) + dur;
|
let endMin = (h * 60 + m) + dur;
|
||||||
let endH = String(Math.floor(endMin / 60) % 24).padStart(2, '0');
|
let endH = String(Math.floor(endMin / 60) % 24).padStart(2, '0');
|
||||||
let endM = String(endMin % 60).padStart(2, '0');
|
let endM = String(endMin % 60).padStart(2, '0');
|
||||||
|
|
||||||
let tipo = r.provider === 'SYSTEM_BLOCK' ? 'BLOQUEO/AUSENCIA' : 'CITA';
|
const tipo = r.provider === 'SYSTEM_BLOCK' ? 'BLOQUEO/AUSENCIA' : 'CITA';
|
||||||
let lugar = r.pob || 'Otra zona';
|
const lugar = r.pob || 'Otra zona';
|
||||||
|
|
||||||
ocupaciones[r.date].push(`De ${r.time} a ${endH}:${endM} (${tipo} en ${lugar})`);
|
ocupaciones[r.date].push(`❌ OCUPADO de ${r.time} a ${endH}:${endM} (${tipo} en ${lugar})`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -984,9 +1012,11 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (lineas.length > 0) {
|
if (lineas.length > 0) {
|
||||||
agendaOcupadaTexto = "Ocupaciones actuales del técnico (Citas confirmadas, pendientes y bloqueos):\n" + lineas.join("\n") +
|
agendaOcupadaTexto =
|
||||||
"\n\n👉 IMPORTANTE: Todas las horas que NO se solapen con esos tramos exactos ESTÁN LIBRES." +
|
"⚠️ ¡ATENCIÓN! LA SIGUIENTE LISTA SON LOS HORARIOS QUE YA ESTÁN OCUPADOS Y NO PUEDES OFRECER:\n" +
|
||||||
"\n🚨 REGLA LOGÍSTICA ESTRICTA: El técnico necesita tiempo para viajar. Si el cliente actual es de una localidad distinta a la cita anterior o posterior (ej: Algeciras vs La Línea), ES OBLIGATORIO dejar un margen de al menos 45-60 minutos de viaje entre el final de una cita y el inicio de la siguiente. NUNCA ofrezcas horas pegadas si hay desplazamiento.";
|
lineas.join("\n") +
|
||||||
|
"\n\n👉 INSTRUCCIÓN VITAL: Revisa bien la lista de arriba. Tienes que proponerle al cliente CUALQUIER OTRA HORA que esté totalmente libre y no pise esos tramos." +
|
||||||
|
"\n🚨 REGLA LOGÍSTICA: Si la zona de la avería actual no es la misma que la de la cita anterior o posterior, DEBES dejar al menos 45-60 minutos libres para el viaje.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -995,7 +1025,6 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
const tieneCitaConfirmada = datosExpediente.cita && datosExpediente.cita !== 'Ninguna';
|
const tieneCitaConfirmada = datosExpediente.cita && datosExpediente.cita !== 'Ninguna';
|
||||||
const esUrgencia = datosExpediente.is_urgent;
|
const esUrgencia = datosExpediente.is_urgent;
|
||||||
|
|
||||||
// 🕐 CONVERTIMOS LA HORA PENDIENTE EN TRAMO DE 1 HORA
|
|
||||||
let tramoPendiente = datosExpediente.cita_pendiente_hora || "";
|
let tramoPendiente = datosExpediente.cita_pendiente_hora || "";
|
||||||
if (tramoPendiente && tramoPendiente.includes(":")) {
|
if (tramoPendiente && tramoPendiente.includes(":")) {
|
||||||
let [h, m] = tramoPendiente.split(':');
|
let [h, m] = tramoPendiente.split(':');
|
||||||
@@ -1003,7 +1032,6 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
tramoPendiente = `entre las ${h}:${m} y las ${hEnd}:${m} aprox`;
|
tramoPendiente = `entre las ${h}:${m} y las ${hEnd}:${m} aprox`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🕐 CONVERTIMOS LA HORA CONFIRMADA EN TRAMO DE 1 HORA
|
|
||||||
let tramoConfirmado = datosExpediente.hora_cita || "";
|
let tramoConfirmado = datosExpediente.hora_cita || "";
|
||||||
if (tramoConfirmado && tramoConfirmado.includes(":")) {
|
if (tramoConfirmado && tramoConfirmado.includes(":")) {
|
||||||
let [h, m] = tramoConfirmado.split(':');
|
let [h, m] = tramoConfirmado.split(':');
|
||||||
@@ -1013,19 +1041,20 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
tramoConfirmado = 'una hora por confirmar';
|
tramoConfirmado = 'una hora por confirmar';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🛑 AÑADIDO: MÁQUINA DEL TIEMPO (Saber si la cita ya pasó)
|
|
||||||
let citaYaPaso = false;
|
let citaYaPaso = false;
|
||||||
if (tieneCitaConfirmada && datosExpediente.cita) {
|
if (tieneCitaConfirmada && datosExpediente.cita) {
|
||||||
const hoyTime = new Date().setHours(0,0,0,0);
|
const hoyTime = new Date().setHours(0, 0, 0, 0);
|
||||||
const [y, m, d] = datosExpediente.cita.split('-');
|
const [y, m, d] = datosExpediente.cita.split('-');
|
||||||
const citaTime = new Date(y, m - 1, d).setHours(0,0,0,0);
|
const citaTime = new Date(y, m - 1, d).setHours(0, 0, 0, 0);
|
||||||
if (citaTime < hoyTime) citaYaPaso = true;
|
if (citaTime < hoyTime) citaYaPaso = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🛑 DETECTORES DE ESTADO
|
const esEstadoFinal = datosExpediente.estado && (
|
||||||
const esEstadoFinal = datosExpediente.estado && (datosExpediente.estado.toLowerCase().includes('finalizado') || datosExpediente.estado.toLowerCase().includes('terminado') || datosExpediente.estado.toLowerCase().includes('anulado'));
|
datosExpediente.estado.toLowerCase().includes('finalizado') ||
|
||||||
const nombreCia = datosExpediente.compania || "su Aseguradora";
|
datosExpediente.estado.toLowerCase().includes('terminado') ||
|
||||||
const esSeguro = !nombreCia.toLowerCase().includes('particular');
|
datosExpediente.estado.toLowerCase().includes('anulado')
|
||||||
|
);
|
||||||
|
|
||||||
const noTieneTecnico = !datosExpediente.worker_id;
|
const noTieneTecnico = !datosExpediente.worker_id;
|
||||||
|
|
||||||
let directivaEstricta = "";
|
let directivaEstricta = "";
|
||||||
@@ -1047,42 +1076,48 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const promptSistema = `
|
const promptSistema = `
|
||||||
Eres el coordinador humano de "${empresaNombre}". Hablas de tú, de forma muy natural, empática y al con un buen sentido de humor por WhatsApp.
|
Eres el coordinador humano de "${empresaNombre}". Hablas de tú, de forma muy natural, empática y con buen tono por WhatsApp.
|
||||||
|
|
||||||
--- 📋 CONTEXTO BÁSICO ---
|
--- 📋 CONTEXTO BÁSICO ---
|
||||||
- Hoy es: ${fechaHoyTexto}. (Año 2026).
|
- Hoy es: ${fechaHoyTexto}.
|
||||||
- Horario de la empresa: L-V de ${horarios.m_start} a ${horarios.m_end} y de ${horarios.a_start} a ${horarios.a_end}. Fines de semana solo URGENCIAS.
|
- Horario de la empresa: L-V de ${horarios.m_start} a ${horarios.m_end} y de ${horarios.a_start} a ${horarios.a_end}.
|
||||||
- ⛔ REGLA DE ORO DEL HORARIO: Está ABSOLUTAMENTE PROHIBIDO agendar o proponer citas fuera del horario de la empresa. El tramo entre las ${horarios.m_end} y las ${horarios.a_start} es para COMER y DESCANSAR. NUNCA propongas horas que pisen ese tramo.
|
- ⛔ REGLA DE ORO DEL HORARIO: NUNCA propongas horas fuera de ese horario ni pises el tramo de comer. Fines de semana prohibidos salvo urgencias.
|
||||||
- Localidad del cliente actual: ${datosExpediente.poblacion || 'Localidad no especificada'}.
|
- Localidad del cliente actual: ${datosExpediente.poblacion || 'Localidad no especificada'}.
|
||||||
|
|
||||||
--- 📅 AGENDA DEL TÉCNICO ASIGNADO ---
|
--- 📅 REVISIÓN DE AGENDA (EVITAR SOLAPES) ---
|
||||||
${agendaOcupadaTexto}
|
${agendaOcupadaTexto}
|
||||||
|
|
||||||
--- 🎯 DIRECTIVA ESTRICTA PARA ESTE MENSAJE ---
|
--- 🎯 DIRECTIVA ESTRICTA PARA ESTE MENSAJE ---
|
||||||
${directivaEstricta}
|
${directivaEstricta}
|
||||||
|
|
||||||
--- ⚡ REGLA CRÍTICA DE AGENDA (COMANDO SECRETO) ---
|
--- ⚡ REGLA CRÍTICA DE AGENDA (COMANDO SECRETO) ---
|
||||||
Si (y solo si) has propuesto un hueco y el cliente ACEPTA FIRMEMENTE, DEBES añadir AL FINAL ABSOLUTO de tu respuesta este texto literal:
|
Si (y solo si) has propuesto un hueco y el cliente ACEPTA FIRMEMENTE, DEBES añadir AL FINAL ABSOLUTO de tu respuesta este texto literal:
|
||||||
[PROPUESTA:YYYY-MM-DD HH:mm]
|
[PROPUESTA:YYYY-MM-DD HH:mm]
|
||||||
Ejemplo de respuesta tuya: "Perfecto, le paso la nota al técnico para que te confirme el miércoles entre las 10:00 y las 11:00 aprox. ¡Te decimos algo pronto! [PROPUESTA:2026-03-25 10:00]"
|
|
||||||
⛔ PROHIBICIÓN: NUNCA le digas "te agendo" ni "cita confirmada". El cliente debe saber que dependemos del técnico. NUNCA menciones las palabras "código" o "etiqueta".
|
|
||||||
|
|
||||||
--- ⚙️ REGLAS DE COMUNICACIÓN ---
|
Ejemplo:
|
||||||
1. MÁXIMO 2 FRASES. Mensajes cortos y directos.
|
"Perfecto, le paso la nota al técnico para que te confirme el miércoles entre las 10:00 y las 11:00 aprox. ¡Te decimos algo pronto! [PROPUESTA:2026-03-25 10:00]"
|
||||||
2. NUNCA uses fechas frías (Usa "el martes"). NUNCA des una hora exacta (Usa "entre las 10:00 y las 11:00 aprox").
|
|
||||||
3. NO TE PRESENTES si ya habéis intercambiado mensajes.
|
⛔ PROHIBICIÓN: NUNCA le digas "te agendo" ni "cita confirmada". El cliente debe saber que dependemos del técnico. NUNCA menciones las palabras "código" o "etiqueta".
|
||||||
4. ⛔ MULETILLAS PROHIBIDAS: NUNCA digas "¿En qué más te puedo ayudar?". Da la información y pon un punto.
|
|
||||||
${esPrimerMensaje ? '5. Primer mensaje: preséntate y menciona el aviso (#' + datosExpediente.ref + ').' : ''}
|
--- ⚙️ REGLAS DE COMUNICACIÓN ---
|
||||||
${instruccionesExtra ? '6. Instrucción extra de la empresa: ' + instruccionesExtra : ''}
|
1. MÁXIMO 2 FRASES. Mensajes cortos y directos.
|
||||||
|
2. NUNCA uses fechas frías si puedes decir "el martes". NUNCA des una hora exacta si puedes decir "entre las 10:00 y las 11:00 aprox".
|
||||||
|
3. NO TE PRESENTES si ya habéis intercambiado mensajes.
|
||||||
|
4. ⛔ MULETILLAS PROHIBIDAS: NUNCA digas "¿En qué más te puedo ayudar?".
|
||||||
|
${esPrimerMensaje ? `5. Primer mensaje: preséntate y menciona el aviso (#${datosExpediente.ref}).` : ''}
|
||||||
|
${instruccionesExtra ? `6. Instrucción extra de la empresa: ${instruccionesExtra}` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4o-mini",
|
model: OPENAI_MODEL || "gpt-4o-mini",
|
||||||
messages: [{ role: "system", content: promptSistema }, ...historialChat, { role: "user", content: mensajeCliente }],
|
messages: [
|
||||||
temperature: 0.1,
|
{ role: "system", content: promptSistema },
|
||||||
|
...historialChat
|
||||||
|
],
|
||||||
|
temperature: 0.1
|
||||||
});
|
});
|
||||||
|
|
||||||
return completion.choices[0].message.content;
|
return completion.choices?.[0]?.message?.content || null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ Error OpenAI:", e.message);
|
console.error("❌ Error OpenAI:", e.message);
|
||||||
return null;
|
return null;
|
||||||
@@ -4046,245 +4081,278 @@ app.get("/services/:id/chat", authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Enviar un nuevo mensaje (Oficina u Operario) CON AVISO WHATSAPP Y TRAZABILIDAD
|
|
||||||
app.post("/services/:id/chat", authMiddleware, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { message, is_internal } = req.body;
|
|
||||||
|
|
||||||
if (!message || message.trim() === "") return res.status(400).json({ ok: false, error: "Vacío" });
|
|
||||||
|
|
||||||
const isOperario = req.user.role === 'operario';
|
|
||||||
const finalIsInternal = isOperario ? false : (is_internal || false);
|
|
||||||
|
|
||||||
const userQ = await pool.query("SELECT full_name, role FROM users WHERE id=$1", [req.user.sub]);
|
|
||||||
const senderName = userQ.rows[0]?.full_name || "Usuario";
|
|
||||||
const senderRole = userQ.rows[0]?.role || "operario";
|
|
||||||
|
|
||||||
// 1. Guardar el mensaje en la base de datos (Chat)
|
|
||||||
await pool.query(`
|
|
||||||
INSERT INTO service_communications
|
|
||||||
(scraped_id, owner_id, sender_id, sender_name, sender_role, message, is_internal)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
||||||
`, [id, req.user.accountId, req.user.sub, senderName, senderRole, message.trim(), finalIsInternal]);
|
|
||||||
|
|
||||||
res.json({ ok: true });
|
|
||||||
|
|
||||||
// 2. Lógica de Notificación y Trazabilidad
|
|
||||||
if (!isOperario && !finalIsInternal) {
|
|
||||||
const svcQ = await pool.query("SELECT assigned_to, service_ref FROM scraped_services WHERE id=$1", [id]);
|
|
||||||
|
|
||||||
if (svcQ.rowCount > 0 && svcQ.rows[0].assigned_to) {
|
|
||||||
const workerId = svcQ.rows[0].assigned_to;
|
|
||||||
const ref = svcQ.rows[0].service_ref || id;
|
|
||||||
|
|
||||||
const wQ = await pool.query("SELECT phone, full_name FROM users WHERE id=$1", [workerId]);
|
|
||||||
|
|
||||||
if (wQ.rowCount > 0 && wQ.rows[0].phone) {
|
|
||||||
const workerPhone = wQ.rows[0].phone;
|
|
||||||
const workerName = wQ.rows[0].full_name;
|
|
||||||
|
|
||||||
const msgWa = `💬 *NUEVO MENSAJE DE LA OFICINA*\nExpediente: *#${ref}*\n\n"${message.trim()}"\n\n_Entra en tu App para contestar._`;
|
|
||||||
|
|
||||||
// A) Disparar el WhatsApp
|
|
||||||
const waExito = await sendWhatsAppAuto(workerPhone, msgWa, `cliente_${req.user.accountId}`, false);
|
|
||||||
|
|
||||||
// B) 🟢 DEJAR HUELLA EN EL LOG (Trazabilidad)
|
|
||||||
const logDetalle = waExito
|
|
||||||
? `Aviso enviado por WhatsApp a ${workerName} (${workerPhone}).`
|
|
||||||
: `Intento de aviso por WhatsApp a ${workerName} fallido (revisar conexión Evolution).`;
|
|
||||||
|
|
||||||
await pool.query(
|
|
||||||
"INSERT INTO scraped_service_logs (scraped_id, user_name, action, details) VALUES ($1, $2, $3, $4)",
|
|
||||||
[id, "Sistema (Chat)", "Notificación Enviada", logDetalle]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error enviando mensaje y log:", e);
|
|
||||||
if (!res.headersSent) res.status(500).json({ ok: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS
|
// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS
|
||||||
app.post("/webhook/evolution", async (req, res) => {
|
app.post("/webhook/evolution", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
// 🚨 CAMBIO 1: Quitamos el 'fromMe' de aquí arriba para que el servidor escuche TUS mensajes
|
|
||||||
if (data.event !== "messages.upsert") return res.sendStatus(200);
|
|
||||||
|
|
||||||
const remoteJid = data.data.key.remoteJid;
|
if (data.event !== "messages.upsert") {
|
||||||
const telefonoCliente = remoteJid.split("@")[0];
|
|
||||||
const mensajeTexto = data.data.message?.conversation || data.data.message?.extendedTextMessage?.text;
|
|
||||||
const instanceName = data.instance;
|
|
||||||
|
|
||||||
if (!mensajeTexto || !instanceName || !instanceName.startsWith("cliente_")) {
|
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 CRÍTICO: Responder a Evolution rápido para que no reintente
|
const remoteJid = data?.data?.key?.remoteJid || "";
|
||||||
|
const fromMe = !!data?.data?.key?.fromMe;
|
||||||
|
const instanceName = data?.instance || "";
|
||||||
|
const mensajeTexto = (
|
||||||
|
data?.data?.message?.conversation ||
|
||||||
|
data?.data?.message?.extendedTextMessage?.text ||
|
||||||
|
""
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
if (!mensajeTexto || !remoteJid || !instanceName) {
|
||||||
|
return res.sendStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respondemos rápido a Evolution
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
|
|
||||||
const ownerId = instanceName.split("_")[1];
|
// Tu arquitectura actual depende de cliente_ID
|
||||||
const cleanPhone = telefonoCliente.slice(-9);
|
if (!/^cliente_\d+$/.test(instanceName)) {
|
||||||
|
console.log(`⚠️ [WEBHOOK IA] Instancia ignorada: ${instanceName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 🔍 BUSCAMOS EL EXPEDIENTE ACTIVO MÁS RECIENTE
|
const ownerId = parseInt(instanceName.replace("cliente_", ""), 10);
|
||||||
|
if (!ownerId) return;
|
||||||
|
|
||||||
|
const telefonoCliente = remoteJid.split("@")[0];
|
||||||
|
const cleanPhone = telefonoCliente.replace(/\D/g, "").slice(-9);
|
||||||
|
if (!cleanPhone || cleanPhone.length < 9) return;
|
||||||
|
|
||||||
|
// 🔍 BUSCAMOS EL EXPEDIENTE ACTIVO MÁS RECIENTE POR TELÉFONO REAL
|
||||||
const svcQ = await pool.query(`
|
const svcQ = await pool.query(`
|
||||||
SELECT s.id, s.service_ref, s.assigned_to, u.full_name as worker_name, s.is_urgent,
|
SELECT
|
||||||
st.name as status_name,
|
s.id,
|
||||||
s.raw_data->>'scheduled_date' as cita,
|
s.service_ref,
|
||||||
s.raw_data->>'scheduled_time' as hora_cita,
|
s.assigned_to,
|
||||||
s.raw_data->>'Población' as poblacion,
|
s.is_urgent,
|
||||||
s.raw_data->>'appointment_status' as appointment_status,
|
u.full_name as worker_name,
|
||||||
s.raw_data->>'requested_date' as cita_pendiente_fecha,
|
st.name as status_name,
|
||||||
s.raw_data->>'requested_time' as cita_pendiente_hora,
|
COALESCE(s.raw_data->>'scheduled_date', '') as cita,
|
||||||
s.raw_data->>'Compañía' as compania,
|
COALESCE(s.raw_data->>'scheduled_time', '') as hora_cita,
|
||||||
COALESCE(s.raw_data->>'Descripción', s.raw_data->>'DESCRIPCION') as averia,
|
COALESCE(s.raw_data->>'Población', s.raw_data->>'POBLACION-PROVINCIA', '') as poblacion,
|
||||||
s.raw_data->>'ia_paused' as ia_paused -- 🚨 BÚSQUEDA DEL SEMÁFORO ROJO
|
COALESCE(s.raw_data->>'appointment_status', '') as appointment_status,
|
||||||
|
COALESCE(s.raw_data->>'requested_date', '') as cita_pendiente_fecha,
|
||||||
|
COALESCE(s.raw_data->>'requested_time', '') as cita_pendiente_hora,
|
||||||
|
COALESCE(s.raw_data->>'Compañía', s.raw_data->>'COMPAÑIA', '') as compania,
|
||||||
|
COALESCE(s.raw_data->>'Descripción', s.raw_data->>'DESCRIPCION', '') as averia,
|
||||||
|
COALESCE((s.raw_data->>'ia_paused')::boolean, false) as ia_paused
|
||||||
FROM scraped_services s
|
FROM scraped_services s
|
||||||
LEFT JOIN users u ON s.assigned_to = u.id
|
LEFT JOIN users u ON s.assigned_to = u.id
|
||||||
LEFT JOIN service_statuses st ON (s.raw_data->>'status_operativo')::text = st.id::text
|
LEFT JOIN service_statuses st ON (s.raw_data->>'status_operativo')::text = st.id::text
|
||||||
WHERE s.owner_id = $1
|
WHERE s.owner_id = $1
|
||||||
AND s.status != 'archived'
|
AND s.status != 'archived'
|
||||||
AND s.raw_data::text ILIKE $2
|
AND RIGHT(
|
||||||
AND (st.name IS NULL OR (st.name NOT ILIKE '%finalizado%' AND st.name NOT ILIKE '%anulado%' AND st.name NOT ILIKE '%desasignado%'))
|
REGEXP_REPLACE(
|
||||||
ORDER BY s.created_at DESC LIMIT 1
|
COALESCE(
|
||||||
`, [ownerId, `%${cleanPhone}%`]);
|
s.raw_data->>'Teléfono',
|
||||||
|
s.raw_data->>'TELEFONO',
|
||||||
|
s.raw_data->>'TELEFONOS',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
'\\D',
|
||||||
|
'',
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
9
|
||||||
|
) = $2
|
||||||
|
AND (
|
||||||
|
st.name IS NULL OR (
|
||||||
|
st.name NOT ILIKE '%finalizado%' AND
|
||||||
|
st.name NOT ILIKE '%anulado%' AND
|
||||||
|
st.name NOT ILIKE '%desasignado%'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
(SELECT MAX(sc.created_at) FROM service_communications sc WHERE sc.scraped_id = s.id) DESC NULLS LAST,
|
||||||
|
s.created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, [ownerId, cleanPhone]);
|
||||||
|
|
||||||
if (svcQ.rowCount > 0) {
|
if (svcQ.rowCount === 0) {
|
||||||
const service = svcQ.rows[0];
|
console.log(`⚠️ [WEBHOOK IA] No se encontró expediente activo para ${cleanPhone}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.data.key.fromMe) {
|
const service = svcQ.rows[0];
|
||||||
const msgCmd = mensajeTexto.trim();
|
|
||||||
|
|
||||||
// 🔴 COMANDO MÁGICO 1: PAUSAR LA IA (Resistente a emojis de iPhone)
|
// ==========================================
|
||||||
if (msgCmd.includes('🔴')) {
|
// 🧑🔧 MENSAJES TUYOS / ADMIN / OFICINA
|
||||||
try {
|
// ==========================================
|
||||||
await pool.query(`UPDATE scraped_services SET raw_data = COALESCE(raw_data, '{}'::jsonb) || '{"ia_paused": true}'::jsonb WHERE id = $1`, [service.id]);
|
if (fromMe) {
|
||||||
// 🛑 Guardamos como 'system' para que NO active el escudo humano de 2 horas
|
const msgCmd = mensajeTexto.trim();
|
||||||
await pool.query(`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message, is_internal) VALUES ($1, $2, $3, $4, $5, true)`, [service.id, ownerId, "Sistema", "system", "🔴 IA Pausada manualmente con Emoji."]);
|
|
||||||
console.log(`🔴 [IA PAUSADA] Semáforo rojo activado para exp ${service.service_ref}`);
|
|
||||||
} catch(err) { console.error("Error pausando IA:", err); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🟢 COMANDO MÁGICO 2: ACTIVAR LA IA
|
// 🔴 PAUSAR IA
|
||||||
if (msgCmd.includes('🟢')) {
|
if (msgCmd.includes("🔴")) {
|
||||||
try {
|
|
||||||
await pool.query(`UPDATE scraped_services SET raw_data = raw_data - 'ia_paused' WHERE id = $1`, [service.id]);
|
|
||||||
// 🟢 Guardamos como 'system' para EVITAR que la IA se autobloquee al despertarla
|
|
||||||
await pool.query(`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message, is_internal) VALUES ($1, $2, $3, $4, $5, true)`, [service.id, ownerId, "Sistema", "system", "🟢 IA Reactivada manualmente con Emoji."]);
|
|
||||||
console.log(`🟢 [IA ACTIVADA] Semáforo verde activado para exp ${service.service_ref}`);
|
|
||||||
} catch(err) { console.error("Error activando IA:", err); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guardado normal si es un texto tuyo hablando con el cliente
|
|
||||||
try {
|
try {
|
||||||
await pool.query(`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message) VALUES ($1, $2, $3, $4, $5)`,
|
await pool.query(`
|
||||||
[service.id, ownerId, "Técnico (WhatsApp)", "operario", mensajeTexto]);
|
UPDATE scraped_services
|
||||||
} catch(err) { console.error("Error guardando mensaje de admin:", err); }
|
SET raw_data = COALESCE(raw_data, '{}'::jsonb) || '{"ia_paused": true}'::jsonb
|
||||||
|
WHERE id = $1
|
||||||
|
`, [service.id]);
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO service_communications
|
||||||
|
(scraped_id, owner_id, sender_name, sender_role, message, is_internal)
|
||||||
|
VALUES ($1, $2, 'Sistema', 'system', $3, true)
|
||||||
|
`, [service.id, ownerId, "🔴 IA Pausada manualmente con Emoji."]);
|
||||||
|
|
||||||
|
console.log(`🔴 [IA PAUSADA] ${service.service_ref}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error pausando IA:", err);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candadosIA.has(service.id)) return;
|
// 🟢 REACTIVAR IA
|
||||||
candadosIA.add(service.id);
|
if (msgCmd.includes("🟢")) {
|
||||||
|
try {
|
||||||
|
await pool.query(`
|
||||||
|
UPDATE scraped_services
|
||||||
|
SET raw_data = raw_data - 'ia_paused'
|
||||||
|
WHERE id = $1
|
||||||
|
`, [service.id]);
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO service_communications
|
||||||
|
(scraped_id, owner_id, sender_name, sender_role, message, is_internal)
|
||||||
|
VALUES ($1, $2, 'Sistema', 'system', $3, true)
|
||||||
|
`, [service.id, ownerId, "🟢 IA Reactivada manualmente con Emoji."]);
|
||||||
|
|
||||||
|
console.log(`🟢 [IA ACTIVADA] ${service.service_ref}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error activando IA:", err);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardamos mensajes humanos salientes
|
||||||
try {
|
try {
|
||||||
// 🛑 COMPROBAR SI LA HEMOS PAUSADO CON EL SEMÁFORO ROJO
|
await pool.query(`
|
||||||
if (service.ia_paused === 'true' || service.ia_paused === true) {
|
INSERT INTO service_communications
|
||||||
console.log(`🤫 [IA MUTEADA] El cliente ha hablado, pero la IA está en semáforo rojo para ${service.service_ref}`);
|
(scraped_id, owner_id, sender_name, sender_role, message, is_internal)
|
||||||
|
VALUES ($1, $2, 'Técnico (WhatsApp)', 'operario', $3, false)
|
||||||
|
`, [service.id, ownerId, mensajeTexto]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error guardando mensaje humano saliente:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 👤 MENSAJE ENTRANTE DEL CLIENTE
|
||||||
|
// ==========================================
|
||||||
|
try {
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO service_communications
|
||||||
|
(scraped_id, owner_id, sender_name, sender_role, message, is_internal)
|
||||||
|
VALUES ($1, $2, 'Cliente', 'user', $3, false)
|
||||||
|
`, [service.id, ownerId, mensajeTexto]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error guardando mensaje del cliente:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Candado para que no responda dos veces a la vez
|
||||||
|
if (candadosIA.has(service.id)) return;
|
||||||
|
candadosIA.add(service.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 🛑 Si está pausada, no contestamos
|
||||||
|
if (service.ia_paused === true || service.ia_paused === 'true') {
|
||||||
|
console.log(`🤫 [IA MUTEADA] ${service.service_ref}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🛡️ Escudo humano: si alguien de oficina/operario habló hace menos de 120 min, la IA calla
|
||||||
|
const checkHumanQ = await pool.query(`
|
||||||
|
SELECT sender_role, created_at
|
||||||
|
FROM service_communications
|
||||||
|
WHERE scraped_id = $1
|
||||||
|
AND sender_role IN ('admin', 'superadmin', 'operario')
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, [service.id]);
|
||||||
|
|
||||||
|
if (checkHumanQ.rowCount > 0) {
|
||||||
|
const lastMsg = checkHumanQ.rows[0];
|
||||||
|
const diffMinutos = (Date.now() - new Date(lastMsg.created_at).getTime()) / (1000 * 60);
|
||||||
|
|
||||||
|
if (diffMinutos < 120) {
|
||||||
|
console.log(`🛡️ [ESCUDO IA] Silenciada porque un humano habló hace ${Math.round(diffMinutos)} minutos.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🛡️ VERIFICAR INTERVENCIÓN HUMANA (ESCUDO INTELIGENTE)
|
|
||||||
// Buscamos el último mensaje, PERO ignoramos al 'system' y al 'user'
|
|
||||||
const checkHumanQ = await pool.query(`
|
|
||||||
SELECT sender_role, created_at FROM service_communications
|
|
||||||
WHERE scraped_id = $1 AND sender_role NOT IN ('user', 'system')
|
|
||||||
ORDER BY created_at DESC LIMIT 1
|
|
||||||
`, [service.id]);
|
|
||||||
|
|
||||||
if (checkHumanQ.rowCount > 0) {
|
|
||||||
const lastMsg = checkHumanQ.rows[0];
|
|
||||||
const diffMinutos = (new Date() - new Date(lastMsg.created_at)) / (1000 * 60);
|
|
||||||
|
|
||||||
// 🛑 ESCUDO ACTIVADO: 120 minutos de silencio desde tu último mensaje real
|
|
||||||
if (['admin', 'superadmin', 'operario'].includes(lastMsg.sender_role) && diffMinutos < 120) {
|
|
||||||
console.log(`🛡️ [ESCUDO IA] Silenciando a la IA porque un humano habló hace ${Math.round(diffMinutos)} minutos.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧠 LLAMADA A LA IA
|
|
||||||
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
|
|
||||||
dbId: service.id,
|
|
||||||
ref: service.service_ref,
|
|
||||||
estado: service.status_name || "En proceso",
|
|
||||||
operario: service.worker_name,
|
|
||||||
worker_id: service.assigned_to,
|
|
||||||
cita: service.cita,
|
|
||||||
hora_cita: service.hora_cita,
|
|
||||||
poblacion: service.poblacion || "",
|
|
||||||
is_urgent: service.is_urgent,
|
|
||||||
appointment_status: service.appointment_status,
|
|
||||||
cita_pendiente_fecha: service.cita_pendiente_fecha,
|
|
||||||
cita_pendiente_hora: service.cita_pendiente_hora,
|
|
||||||
compania: service.compania,
|
|
||||||
averia: service.averia
|
|
||||||
});
|
|
||||||
|
|
||||||
if (respuestaIA) {
|
|
||||||
// 🛡️ REGEX BLINDADO: Pilla la etiqueta aunque la IA meta espacios raros
|
|
||||||
const matchPropuesta = respuestaIA.match(/\[PROPUESTA:\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]/i);
|
|
||||||
|
|
||||||
// 🧹 BORRAMOS EL TEXTO DEL CÓDIGO PARA QUE EL CLIENTE NO LO VEA NUNCA
|
|
||||||
let textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/gi, "").replace(/código:/gi, "").trim();
|
|
||||||
|
|
||||||
if (matchPropuesta) {
|
|
||||||
const fechaSugerida = matchPropuesta[1];
|
|
||||||
const horaSugerida = matchPropuesta[2];
|
|
||||||
|
|
||||||
// 🛡️ ESCUDO ANTI-SOLAPE: comprobamos antes de guardar la propuesta
|
|
||||||
const disponibilidad = await comprobarDisponibilidad(
|
|
||||||
ownerId,
|
|
||||||
service.assigned_to,
|
|
||||||
fechaSugerida,
|
|
||||||
horaSugerida,
|
|
||||||
60,
|
|
||||||
service.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (disponibilidad.choca) {
|
|
||||||
console.log(`⛔ [DOBLE-BOOKING EVITADO] Exp ${service.service_ref} chocaba con ${disponibilidad.ref} a las ${disponibilidad.time}`);
|
|
||||||
textoLimpio = "Uy, perdona, se me acaban de cruzar los cables y justo me han bloqueado ese hueco por el sistema interno. 😅 ¿Me dices otra hora que te venga bien?";
|
|
||||||
} else {
|
|
||||||
// 🚀 GUARDADO COMO PENDIENTE (Espera a que el Técnico la apruebe en la App o en la Oficina)
|
|
||||||
await pool.query(`
|
|
||||||
UPDATE scraped_services
|
|
||||||
SET raw_data = raw_data || jsonb_build_object(
|
|
||||||
'requested_date', $1::text,
|
|
||||||
'requested_time', $2::text,
|
|
||||||
'appointment_status', 'pending'
|
|
||||||
) WHERE id = $3
|
|
||||||
`, [fechaSugerida, horaSugerida, service.id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true);
|
|
||||||
await pool.query(
|
|
||||||
`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message) VALUES ($1, $2, $3, $4, $5)`,
|
|
||||||
[service.id, ownerId, "Asistente IA", "ia", textoLimpio]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
candadosIA.delete(service.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🧠 Llamada a la IA
|
||||||
|
const respuestaIA = await procesarConIA(ownerId, mensajeTexto, {
|
||||||
|
dbId: service.id,
|
||||||
|
ref: service.service_ref,
|
||||||
|
estado: service.status_name || "En proceso",
|
||||||
|
operario: service.worker_name,
|
||||||
|
worker_id: service.assigned_to,
|
||||||
|
cita: service.cita,
|
||||||
|
hora_cita: service.hora_cita,
|
||||||
|
poblacion: service.poblacion || "",
|
||||||
|
is_urgent: service.is_urgent,
|
||||||
|
appointment_status: service.appointment_status,
|
||||||
|
cita_pendiente_fecha: service.cita_pendiente_fecha,
|
||||||
|
cita_pendiente_hora: service.cita_pendiente_hora,
|
||||||
|
compania: service.compania,
|
||||||
|
averia: service.averia
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!respuestaIA) return;
|
||||||
|
|
||||||
|
const matchPropuesta = respuestaIA.match(/\[PROPUESTA:\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]/i);
|
||||||
|
let textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/gi, "").trim();
|
||||||
|
|
||||||
|
if (matchPropuesta && service.assigned_to) {
|
||||||
|
const fechaSugerida = matchPropuesta[1];
|
||||||
|
const horaSugerida = matchPropuesta[2];
|
||||||
|
|
||||||
|
const disponibilidad = await comprobarDisponibilidad(
|
||||||
|
ownerId,
|
||||||
|
service.assigned_to,
|
||||||
|
fechaSugerida,
|
||||||
|
horaSugerida,
|
||||||
|
60,
|
||||||
|
service.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (disponibilidad.choca) {
|
||||||
|
console.log(`⛔ [DOBLE-BOOKING EVITADO] ${service.service_ref} chocaba con ${disponibilidad.ref}`);
|
||||||
|
textoLimpio = "Uy, justo ese hueco acaba de quedar ocupado en el sistema. Dime otra hora y lo reviso.";
|
||||||
|
} else {
|
||||||
|
await pool.query(`
|
||||||
|
UPDATE scraped_services
|
||||||
|
SET raw_data = raw_data || jsonb_build_object(
|
||||||
|
'requested_date', $1::text,
|
||||||
|
'requested_time', $2::text,
|
||||||
|
'appointment_status', 'pending'
|
||||||
|
)
|
||||||
|
WHERE id = $3
|
||||||
|
`, [fechaSugerida, horaSugerida, service.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!textoLimpio) return;
|
||||||
|
|
||||||
|
await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true);
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO service_communications
|
||||||
|
(scraped_id, owner_id, sender_name, sender_role, message, is_internal)
|
||||||
|
VALUES ($1, $2, 'Asistente IA', 'ia', $3, false)
|
||||||
|
`, [service.id, ownerId, textoLimpio]);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
candadosIA.delete(service.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ [WEBHOOK ERROR]:", e.message);
|
console.error("❌ [WEBHOOK ERROR]:", e);
|
||||||
|
if (!res.headersSent) return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user