Actualiza server.js
This commit is contained in:
286
server.js
286
server.js
@@ -14,6 +14,11 @@ const app = express();
|
|||||||
// 🛑 SEMÁFORO IA: Guarda los IDs de los servicios que están siendo procesados
|
// 🛑 SEMÁFORO IA: Guarda los IDs de los servicios que están siendo procesados
|
||||||
const candadosIA = new Set();
|
const candadosIA = new Set();
|
||||||
|
|
||||||
|
// 🕒 DEBOUNCE IA: si el cliente manda varios WhatsApps seguidos, esperamos a que pare
|
||||||
|
// de escribir para contestar una sola vez (evita respuestas fragmentadas o duplicadas)
|
||||||
|
const debounceTimersIA = new Map();
|
||||||
|
const IA_DEBOUNCE_MS = 8000;
|
||||||
|
|
||||||
// Configuración de CORS Profesional
|
// Configuración de CORS Profesional
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: [
|
origin: [
|
||||||
@@ -1625,6 +1630,20 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
const calcularHuecosDisponiblesExactos = async () => {
|
const calcularHuecosDisponiblesExactos = async () => {
|
||||||
if (!datosExpediente.worker_id) return [];
|
if (!datosExpediente.worker_id) return [];
|
||||||
|
|
||||||
|
// 🧭 Ciudad del cliente actual (misma lógica de zonas que usa el portal público)
|
||||||
|
const workerZonesQ = await pool.query("SELECT zones FROM users WHERE id = $1", [datosExpediente.worker_id]);
|
||||||
|
const workerZones = workerZonesQ.rows[0]?.zones || [];
|
||||||
|
const targetGuildId = datosExpediente.guild_id || null;
|
||||||
|
|
||||||
|
function getCityForCP(cp, fallbackPop) {
|
||||||
|
const cleanCP = String(cp || "").trim();
|
||||||
|
const zone = Array.isArray(workerZones) ? workerZones.find(z => z.cps === cleanCP) : null;
|
||||||
|
if (zone && zone.city) return zone.city.toUpperCase().trim();
|
||||||
|
return String(fallbackPop || "").toUpperCase().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCity = getCityForCP(datosExpediente.cp, datosExpediente.poblacion);
|
||||||
|
|
||||||
const agendaQ = await pool.query(`
|
const agendaQ = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(
|
COALESCE(
|
||||||
@@ -1641,7 +1660,11 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
ELSE NULL
|
ELSE NULL
|
||||||
END
|
END
|
||||||
) as effective_time,
|
) as effective_time,
|
||||||
COALESCE(NULLIF(raw_data->>'duration_minutes', ''), '60') as effective_duration
|
COALESCE(NULLIF(raw_data->>'duration_minutes', ''), '60') as effective_duration,
|
||||||
|
raw_data->>'Población' as poblacion,
|
||||||
|
COALESCE(raw_data->>'Código Postal', raw_data->>'C.P.') as cp,
|
||||||
|
provider,
|
||||||
|
raw_data->>'blocked_guild_id' as blocked_guild_id
|
||||||
FROM scraped_services
|
FROM scraped_services
|
||||||
WHERE owner_id = $1
|
WHERE owner_id = $1
|
||||||
AND assigned_to = $2
|
AND assigned_to = $2
|
||||||
@@ -1656,14 +1679,33 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
`, [ownerId, datosExpediente.worker_id, datosExpediente.dbId]);
|
`, [ownerId, datosExpediente.worker_id, datosExpediente.dbId]);
|
||||||
|
|
||||||
const ocupacionesPorDia = {};
|
const ocupacionesPorDia = {};
|
||||||
|
const ciudadPorDia = {};
|
||||||
|
const afternoonStartMin = parseTimeToMinutes(horarios.a_start);
|
||||||
|
|
||||||
for (const row of agendaQ.rows) {
|
for (const row of agendaQ.rows) {
|
||||||
const isoDate = row.effective_date;
|
const isoDate = row.effective_date;
|
||||||
const startMin = parseTimeToMinutes(row.effective_time);
|
const startMin = parseTimeToMinutes(row.effective_time);
|
||||||
const duration = Math.max(parseInt(row.effective_duration, 10) || 60, 1);
|
const duration = Math.max(parseInt(row.effective_duration, 10) || 60, 1);
|
||||||
|
|
||||||
if (!isoDate || startMin === null) continue;
|
if (!isoDate) continue;
|
||||||
|
|
||||||
|
// 🔒 Un bloqueo manual reservado para OTRO gremio no cuenta para nada (igual que en el portal)
|
||||||
|
if (row.provider === 'SYSTEM_BLOCK' && row.blocked_guild_id && String(row.blocked_guild_id) !== String(targetGuildId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📍 REGLA CRÍTICA DE TURNOS: anotamos la ciudad de la mañana/tarde de ese día
|
||||||
|
if (row.provider !== 'SYSTEM_BLOCK' && startMin !== null && afternoonStartMin !== null) {
|
||||||
|
const city = getCityForCP(row.cp, row.poblacion);
|
||||||
|
if (!ciudadPorDia[isoDate]) ciudadPorDia[isoDate] = { morningCity: null, afternoonCity: null };
|
||||||
|
if (startMin < afternoonStartMin) {
|
||||||
|
if (!ciudadPorDia[isoDate].morningCity) ciudadPorDia[isoDate].morningCity = city;
|
||||||
|
} else {
|
||||||
|
if (!ciudadPorDia[isoDate].afternoonCity) ciudadPorDia[isoDate].afternoonCity = city;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startMin === null) continue;
|
||||||
if (!ocupacionesPorDia[isoDate]) ocupacionesPorDia[isoDate] = [];
|
if (!ocupacionesPorDia[isoDate]) ocupacionesPorDia[isoDate] = [];
|
||||||
ocupacionesPorDia[isoDate].push([startMin, startMin + duration]);
|
ocupacionesPorDia[isoDate].push([startMin, startMin + duration]);
|
||||||
}
|
}
|
||||||
@@ -1673,24 +1715,29 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) {
|
|||||||
[parseTimeToMinutes(horarios.a_start), parseTimeToMinutes(horarios.a_end)]
|
[parseTimeToMinutes(horarios.a_start), parseTimeToMinutes(horarios.a_end)]
|
||||||
].filter(([ini, fin]) => ini !== null && fin !== null && fin > ini);
|
].filter(([ini, fin]) => ini !== null && fin !== null && fin > ini);
|
||||||
|
|
||||||
const { todayISO, currentMinutes } = getMadridNowInfo();
|
const { todayISO } = getMadridNowInfo();
|
||||||
const diasDisponibles = [];
|
const diasDisponibles = [];
|
||||||
|
|
||||||
for (let offset = 0; offset < 21 && diasDisponibles.length < 10; offset++) {
|
// ⚠️ Igual que el portal público: no se ofrece el mismo día, se empieza a partir de mañana
|
||||||
|
for (let offset = 1; offset < 22 && diasDisponibles.length < 10; offset++) {
|
||||||
const isoDate = addDaysISO(todayISO, offset);
|
const isoDate = addDaysISO(todayISO, offset);
|
||||||
if (isWeekendISO(isoDate)) continue;
|
if (isWeekendISO(isoDate)) continue;
|
||||||
|
|
||||||
const ocupadas = mergeIntervals(ocupacionesPorDia[isoDate] || []);
|
const ocupadas = mergeIntervals(ocupacionesPorDia[isoDate] || []);
|
||||||
|
const dayCity = ciudadPorDia[isoDate];
|
||||||
const ventanas = [];
|
const ventanas = [];
|
||||||
|
|
||||||
for (const [sesionStartBase, sesionEndBase] of sesionesTrabajo) {
|
for (const [sesionStartBase, sesionEndBase] of sesionesTrabajo) {
|
||||||
|
// 📍 Si esa mañana/tarde ya está anclada a otra ciudad, no la ofrecemos (evita saltos de zona)
|
||||||
|
if (dayCity && afternoonStartMin !== null) {
|
||||||
|
const esSesionManana = sesionStartBase < afternoonStartMin;
|
||||||
|
if (esSesionManana && dayCity.morningCity && dayCity.morningCity !== targetCity) continue;
|
||||||
|
if (!esSesionManana && dayCity.afternoonCity && dayCity.afternoonCity !== targetCity) continue;
|
||||||
|
}
|
||||||
|
|
||||||
let sesionStart = sesionStartBase;
|
let sesionStart = sesionStartBase;
|
||||||
const sesionEnd = sesionEndBase;
|
const sesionEnd = sesionEndBase;
|
||||||
|
|
||||||
if (isoDate === todayISO) {
|
|
||||||
sesionStart = Math.max(sesionStart, roundUpTo30(currentMinutes + 30));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((sesionEnd - sesionStart) < 60) continue;
|
if ((sesionEnd - sesionStart) < 60) continue;
|
||||||
|
|
||||||
let cursor = sesionStart;
|
let cursor = sesionStart;
|
||||||
@@ -2610,14 +2657,23 @@ app.post("/agenda/requests/:id/approve", authMiddleware, async (req, res) => {
|
|||||||
// 🛑 NUEVO: Recibimos date y time desde la App
|
// 🛑 NUEVO: Recibimos date y time desde la App
|
||||||
const { duration, date, time } = req.body;
|
const { duration, date, time } = req.body;
|
||||||
|
|
||||||
const current = await pool.query('SELECT raw_data FROM scraped_services WHERE id=$1 AND owner_id=$2', [id, req.user.accountId]);
|
const current = await pool.query('SELECT raw_data, assigned_to 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});
|
if (current.rowCount === 0) return res.status(404).json({ok: false});
|
||||||
|
|
||||||
const raw = current.rows[0].raw_data;
|
const raw = current.rows[0].raw_data;
|
||||||
|
const assignedTo = current.rows[0].assigned_to;
|
||||||
// 🛑 NUEVO: Si el técnico ha cambiado la fecha/hora en la App, usamos esa. Si no, la que pidió el cliente.
|
// 🛑 NUEVO: Si el técnico ha cambiado la fecha/hora en la App, usamos esa. Si no, la que pidió el cliente.
|
||||||
const reqDate = date || raw.requested_date;
|
const reqDate = date || raw.requested_date;
|
||||||
const reqTime = time || raw.requested_time;
|
const reqTime = time || raw.requested_time;
|
||||||
|
|
||||||
|
// 🛡️ ESCUDO: Verificamos que el hueco siga libre antes de confirmar (evita solapamientos)
|
||||||
|
if (assignedTo && reqDate && reqTime) {
|
||||||
|
const solapamiento = await comprobarDisponibilidad(req.user.accountId, assignedTo, reqDate, reqTime, duration || 60, id);
|
||||||
|
if (solapamiento.choca) {
|
||||||
|
return res.status(400).json({ ok: false, error: `Ese hueco ya no está libre (choca con ${solapamiento.ref} a las ${solapamiento.time}). Elige otra hora.` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const statusQ = await pool.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name ILIKE '%citado%' LIMIT 1", [req.user.accountId]);
|
const statusQ = await pool.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name ILIKE '%citado%' LIMIT 1", [req.user.accountId]);
|
||||||
const idCitado = statusQ.rows[0]?.id || raw.status_operativo;
|
const idCitado = statusQ.rows[0]?.id || raw.status_operativo;
|
||||||
|
|
||||||
@@ -4925,6 +4981,111 @@ app.get("/services/:id/chat", authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS
|
// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS
|
||||||
|
// 🧠 Ejecuta realmente el turno de la IA para un expediente (llamado tras el debounce)
|
||||||
|
async function procesarTurnoIA({ ownerId, service, mensajeTexto, telefonoCliente, instanceName }) {
|
||||||
|
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 habló hace menos de 120 min, la IA se 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 >= 0 && diffMinutos < 120) {
|
||||||
|
console.log(`🛡️ [ESCUDO IA] Silenciada porque un humano habló hace ${Math.round(diffMinutos)} minutos.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧠 LLAMADA A LA IA (el historial ya incluye, en orden, todos los mensajes que el cliente
|
||||||
|
// haya mandado seguidos durante la espera del debounce)
|
||||||
|
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 || "",
|
||||||
|
cp: service.cp || "",
|
||||||
|
guild_id: service.guild_id || "",
|
||||||
|
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, "")
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.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 ya no lo veo libre 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.post("/webhook/evolution", async (req, res) => {
|
app.post("/webhook/evolution", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
@@ -4978,6 +5139,8 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
COALESCE(s.raw_data->>'scheduled_date', '') as cita,
|
COALESCE(s.raw_data->>'scheduled_date', '') as cita,
|
||||||
COALESCE(s.raw_data->>'scheduled_time', '') as hora_cita,
|
COALESCE(s.raw_data->>'scheduled_time', '') as hora_cita,
|
||||||
COALESCE(s.raw_data->>'Población', s.raw_data->>'POBLACION-PROVINCIA', '') as poblacion,
|
COALESCE(s.raw_data->>'Población', s.raw_data->>'POBLACION-PROVINCIA', '') as poblacion,
|
||||||
|
COALESCE(s.raw_data->>'Código Postal', s.raw_data->>'C.P.', '') as cp,
|
||||||
|
COALESCE(s.raw_data->>'guild_id', '') as guild_id,
|
||||||
COALESCE(s.raw_data->>'appointment_status', '') as appointment_status,
|
COALESCE(s.raw_data->>'appointment_status', '') as appointment_status,
|
||||||
COALESCE(s.raw_data->>'requested_date', '') as cita_pendiente_fecha,
|
COALESCE(s.raw_data->>'requested_date', '') as cita_pendiente_fecha,
|
||||||
COALESCE(s.raw_data->>'requested_time', '') as cita_pendiente_hora,
|
COALESCE(s.raw_data->>'requested_time', '') as cita_pendiente_hora,
|
||||||
@@ -5103,104 +5266,19 @@ app.post("/webhook/evolution", async (req, res) => {
|
|||||||
console.error("Error guardando mensaje del cliente:", err);
|
console.error("Error guardando mensaje del cliente:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candadosIA.has(service.id)) return;
|
// 🕒 Debounce: si el cliente manda varios mensajes seguidos, esperamos a que haga
|
||||||
candadosIA.add(service.id);
|
// una pausa antes de contestar, para no fragmentar ni duplicar respuestas.
|
||||||
|
if (debounceTimersIA.has(service.id)) {
|
||||||
try {
|
clearTimeout(debounceTimersIA.get(service.id));
|
||||||
// 🛑 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 habló hace menos de 120 min, la IA se calla
|
const timerIA = setTimeout(() => {
|
||||||
const checkHumanQ = await pool.query(`
|
debounceTimersIA.delete(service.id);
|
||||||
SELECT sender_role, created_at
|
procesarTurnoIA({ ownerId, service, mensajeTexto, telefonoCliente, instanceName })
|
||||||
FROM service_communications
|
.catch(err => console.error("❌ [WEBHOOK IA DIFERIDA] Error:", err));
|
||||||
WHERE scraped_id = $1
|
}, IA_DEBOUNCE_MS);
|
||||||
AND sender_role IN ('admin', 'superadmin', 'operario')
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
`, [service.id]);
|
|
||||||
|
|
||||||
if (checkHumanQ.rowCount > 0) {
|
debounceTimersIA.set(service.id, timerIA);
|
||||||
const lastMsg = checkHumanQ.rows[0];
|
|
||||||
const diffMinutos = (Date.now() - new Date(lastMsg.created_at).getTime()) / (1000 * 60);
|
|
||||||
|
|
||||||
if (diffMinutos < 0) {
|
|
||||||
console.log(`🛡️ [ESCUDO IA] Silenciada 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) 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, "")
|
|
||||||
.replace(/\n{3,}/g, "\n\n")
|
|
||||||
.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 ya no lo veo libre 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);
|
console.error("❌ [WEBHOOK ERROR]:", e);
|
||||||
|
|||||||
Reference in New Issue
Block a user