diff --git a/server.js b/server.js index 12f55bb..8550ed3 100644 --- a/server.js +++ b/server.js @@ -913,6 +913,8 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { [ownerId] ); + if (userQ.rowCount === 0) return null; + const userData = userQ.rows[0]; const settings = userData?.wa_settings || {}; const instruccionesExtra = settings.ai_custom_prompt || ""; @@ -936,9 +938,8 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { 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. + // Historial REAL del chat (ya incluye el último mensaje del cliente + // si el webhook lo ha guardado antes de llamar a esta función) const historyQ = await pool.query(` SELECT sender_role, message FROM service_communications @@ -955,7 +956,7 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { content: row.message })); - // Si solo existe el mensaje actual del cliente, es primer mensaje + // Si el historial solo tiene el mensaje actual del cliente, es primer contacto const esPrimerMensaje = historialRows.length <= 1; let agendaOcupadaTexto = "✅ El técnico tiene la agenda libre en horario laboral."; @@ -988,11 +989,11 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { if (r.date && r.time && r.time.includes(':')) { if (!ocupaciones[r.date]) ocupaciones[r.date] = []; - let [h, m] = r.time.split(':').map(Number); - let dur = parseInt(r.duration || 60, 10); - let endMin = (h * 60 + m) + dur; - let endH = String(Math.floor(endMin / 60) % 24).padStart(2, '0'); - let endM = String(endMin % 60).padStart(2, '0'); + const [h, m] = r.time.split(':').map(Number); + const dur = parseInt(r.duration || 60, 10); + const endMin = (h * 60 + m) + dur; + const endH = String(Math.floor(endMin / 60) % 24).padStart(2, '0'); + const endM = String(endMin % 60).padStart(2, '0'); const tipo = r.provider === 'SYSTEM_BLOCK' ? 'BLOQUEO/AUSENCIA' : 'CITA'; const lugar = r.pob || 'Otra zona'; @@ -1008,6 +1009,7 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { day: 'numeric', month: 'long' }); + return `- Día ${fechaHumana} (${d}):\n * ${ocupaciones[d].join("\n * ")}`; }); @@ -1021,21 +1023,27 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { } } - const hayCitaPendiente = datosExpediente.appointment_status === 'pending' && datosExpediente.cita_pendiente_fecha; - const tieneCitaConfirmada = datosExpediente.cita && datosExpediente.cita !== 'Ninguna'; + const hayCitaPendiente = + datosExpediente.appointment_status === 'pending' && + datosExpediente.cita_pendiente_fecha; + + const tieneCitaConfirmada = + datosExpediente.cita && + datosExpediente.cita !== 'Ninguna'; + const esUrgencia = datosExpediente.is_urgent; let tramoPendiente = datosExpediente.cita_pendiente_hora || ""; if (tramoPendiente && tramoPendiente.includes(":")) { - let [h, m] = tramoPendiente.split(':'); - let hEnd = String((parseInt(h) + 1) % 24).padStart(2, '0'); + const [h, m] = tramoPendiente.split(':'); + const hEnd = String((parseInt(h, 10) + 1) % 24).padStart(2, '0'); tramoPendiente = `entre las ${h}:${m} y las ${hEnd}:${m} aprox`; } let tramoConfirmado = datosExpediente.hora_cita || ""; if (tramoConfirmado && tramoConfirmado.includes(":")) { - let [h, m] = tramoConfirmado.split(':'); - let hEnd = String((parseInt(h) + 1) % 24).padStart(2, '0'); + const [h, m] = tramoConfirmado.split(':'); + const hEnd = String((parseInt(h, 10) + 1) % 24).padStart(2, '0'); tramoConfirmado = `entre las ${h}:${m} y las ${hEnd}:${m} aprox`; } else { tramoConfirmado = 'una hora por confirmar'; @@ -1049,11 +1057,13 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { if (citaTime < hoyTime) citaYaPaso = true; } - const esEstadoFinal = datosExpediente.estado && ( - datosExpediente.estado.toLowerCase().includes('finalizado') || - datosExpediente.estado.toLowerCase().includes('terminado') || - datosExpediente.estado.toLowerCase().includes('anulado') - ); + const esEstadoFinal = + datosExpediente.estado && + ( + datosExpediente.estado.toLowerCase().includes('finalizado') || + datosExpediente.estado.toLowerCase().includes('terminado') || + datosExpediente.estado.toLowerCase().includes('anulado') + ); const noTieneTecnico = !datosExpediente.worker_id; @@ -1062,17 +1072,25 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { if (esEstadoFinal) { directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Informa al cliente que el servicio por su avería (${datosExpediente.averia}) está finalizado. NO AGENDES NADA.`; } else if (noTieneTecnico) { - directivaEstricta = `🛑 ESTADO ACTUAL: SIN TÉCNICO ASIGNADO.\nTU ÚNICO OBJETIVO: Informar al cliente que hemos recibido el aviso de su avería (${datosExpediente.averia}) y que estamos coordinando para asignarle un técnico en su zona.\n⛔ PROHIBICIÓN ABSOLUTA: NO ofrezcas citas, NO des horas, NO agendes nada hasta que se le asigne un técnico.`; + directivaEstricta = `🛑 ESTADO ACTUAL: SIN TÉCNICO ASIGNADO. +TU ÚNICO OBJETIVO: Informar al cliente que hemos recibido el aviso de su avería (${datosExpediente.averia}) y que estamos coordinando para asignarle un técnico en su zona. +⛔ PROHIBICIÓN ABSOLUTA: NO ofrezcas citas, NO des horas, NO agendes nada hasta que se le asigne un técnico.`; } else if (citaYaPaso) { directivaEstricta = `🛑 ESTADO ACTUAL: LA CITA YA PASÓ (${datosExpediente.cita}). Informa que estamos tramitando su avería (${datosExpediente.averia}). NO AGENDES NADA.`; } else if (esUrgencia) { directivaEstricta = `🛑 ESTADO ACTUAL: URGENCIA. Tranquiliza al cliente sobre su avería (${datosExpediente.averia}) y dile que el técnico está avisado. NO PROPONGAS HORAS.`; } else if (hayCitaPendiente) { - directivaEstricta = `🛑 ESTADO ACTUAL: CITA PENDIENTE DE APROBACIÓN.\n📅 Propuesta actual: El día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}.\nTU OBJETIVO: Informar que esperamos confirmación del técnico para reparar su avería (${datosExpediente.averia}).\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIAR o CANCELAR, ofrécele un hueco nuevo.`; + directivaEstricta = `🛑 ESTADO ACTUAL: CITA PENDIENTE DE APROBACIÓN. +📅 Propuesta actual: El día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}. +TU OBJETIVO: Informar que esperamos confirmación del técnico para reparar su avería (${datosExpediente.averia}). +⚠️ EXCEPCIÓN: Si el cliente pide CAMBIAR o CANCELAR, ofrécele un hueco nuevo.`; } else if (tieneCitaConfirmada) { - directivaEstricta = `🛑 ESTADO ACTUAL: CITA CONFIRMADA para el ${datosExpediente.cita} ${tramoConfirmado}. Recuerda la cita para su avería (${datosExpediente.averia}).\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIARLA o CANCELARLA, ofrécele un hueco nuevo.`; + directivaEstricta = `🛑 ESTADO ACTUAL: CITA CONFIRMADA para el ${datosExpediente.cita} ${tramoConfirmado}. Recuerda la cita para su avería (${datosExpediente.averia}). +⚠️ EXCEPCIÓN: Si el cliente pide CAMBIARLA o CANCELARLA, ofrécele un hueco nuevo.`; } else { - directivaEstricta = `🟢 ESTADO ACTUAL: PENDIENTE DE AGENDAR CITA.\nTU OBJETIVO: Acordar fecha y hora para reparar su avería (${datosExpediente.averia}). NUNCA ofrezcas horas ocupadas. Fines de semana solo URGENCIAS.\n⚠️ MUY IMPORTANTE: Cuando el cliente elija un hueco, NO le digas que la cita está confirmada. Dile que le pasas la nota al técnico para que él lo valide.`; + directivaEstricta = `🟢 ESTADO ACTUAL: PENDIENTE DE AGENDAR CITA. +TU OBJETIVO: Acordar fecha y hora para reparar su avería (${datosExpediente.averia}). NUNCA ofrezcas horas ocupadas. Fines de semana solo URGENCIAS. +⚠️ MUY IMPORTANTE: Cuando el cliente elija un hueco, NO le digas que la cita está confirmada. Dile que le pasas la nota al técnico para que él lo valide.`; } const promptSistema = ` @@ -1124,6 +1142,7 @@ ${instruccionesExtra ? `6. Instrucción extra de la empresa: ${instruccionesExtr } } + // ========================================== // 📱 OTP PARA PORTAL DEL CLIENTE (ACCESO WEB) // ==========================================