388 lines
14 KiB
PHP
388 lines
14 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Bot Service Mejorado - Laboratorio Ximena Caicedo
|
|
* Integra todos los flujos y servicios del chatbot
|
|
* Fecha: 13 de enero de 2026
|
|
*/
|
|
|
|
require_once __DIR__ . '/WhatsAppService.php';
|
|
require_once __DIR__ . '/BusinessHoursService.php';
|
|
require_once __DIR__ . '/ConversationStateService.php';
|
|
require_once __DIR__ . '/NLPService.php';
|
|
require_once __DIR__ . '/MenuService.php';
|
|
|
|
class BotServiceLab
|
|
{
|
|
private $db;
|
|
private $whatsappService;
|
|
private $businessHoursService;
|
|
private $stateService;
|
|
private $nlpService;
|
|
private $menuService;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = Database::getInstance();
|
|
$this->whatsappService = new WhatsAppService();
|
|
$this->businessHoursService = new BusinessHoursService();
|
|
$this->stateService = new ConversationStateService();
|
|
$this->nlpService = new NLPService();
|
|
$this->menuService = new MenuService();
|
|
}
|
|
|
|
/**
|
|
* Procesa un mensaje entrante
|
|
* @param array $user
|
|
* @param string $messageText
|
|
* @param string $messageType
|
|
* @param string|null $mediaId
|
|
*/
|
|
public function processMessage(array $user, string $messageText, string $messageType = 'text', ?string $mediaId = null): void
|
|
{
|
|
$phoneNumber = $user['phone_number'];
|
|
|
|
try {
|
|
// 1. Verificar si el usuario está con un asesor
|
|
if ($this->stateService->isWaitingForAdvisor($phoneNumber)) {
|
|
// No procesar automáticamente, está con asesor humano
|
|
$this->logMessage($phoneNumber, "Usuario con asesor, mensaje no procesado automáticamente");
|
|
return;
|
|
}
|
|
|
|
// 2. Verificar horario de atención
|
|
if (!$this->businessHoursService->isBusinessHours()) {
|
|
if ($this->businessHoursService->shouldShowOutOfHoursMessage($phoneNumber)) {
|
|
$message = $this->businessHoursService->getMessage();
|
|
$this->sendMessage($phoneNumber, $message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 3. Verificar si el estado ha expirado
|
|
if ($this->stateService->isStateExpired($phoneNumber)) {
|
|
$this->stateService->clearState($phoneNumber);
|
|
}
|
|
|
|
// 4. Procesar según el tipo de mensaje
|
|
if ($messageType !== 'text' && $mediaId) {
|
|
$this->processMediaMessage($user, $messageType, $mediaId, $messageText);
|
|
return;
|
|
}
|
|
|
|
// 5. Verificar si es el primer mensaje (bienvenida)
|
|
if ($this->isFirstMessage($phoneNumber)) {
|
|
$this->sendWelcomeMessage($phoneNumber);
|
|
return;
|
|
}
|
|
|
|
// 6. Procesar mensaje de texto con NLP
|
|
$this->processTextMessage($user, $messageText);
|
|
|
|
} catch (Exception $e) {
|
|
error_log("Error en BotServiceLab->processMessage: " . $e->getMessage());
|
|
$this->sendMessage($phoneNumber, "Lo siento, ocurrió un error. Por favor intenta de nuevo o escribe *ASESOR* para ayuda.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Procesa un mensaje de texto
|
|
* @param array $user
|
|
* @param string $messageText
|
|
*/
|
|
private function processTextMessage(array $user, string $messageText): void
|
|
{
|
|
$phoneNumber = $user['phone_number'];
|
|
|
|
// Procesar con NLP para encontrar intención
|
|
$response = $this->nlpService->processMessage($messageText, $phoneNumber);
|
|
|
|
if (!$response) {
|
|
// No se encontró respuesta, enviar mensaje por defecto
|
|
$this->sendMessage($phoneNumber, "❓ No he comprendido tu mensaje.\n\nEscribe *MENÚ* para ver las opciones disponibles.");
|
|
return;
|
|
}
|
|
|
|
// Incrementar intentos si no entendió
|
|
if ($response['type'] === 'default') {
|
|
$attempts = $this->stateService->incrementAttempts($phoneNumber);
|
|
|
|
// Si son 3 intentos fallidos, transferir a asesor
|
|
if ($attempts >= 3) {
|
|
$this->transferToAdvisor($phoneNumber, 'No se pudo entender el mensaje después de 3 intentos');
|
|
return;
|
|
}
|
|
} else {
|
|
// Resetear intentos si entendió correctamente
|
|
$this->stateService->resetAttempts($phoneNumber);
|
|
}
|
|
|
|
// Detectar frustración
|
|
if ($this->nlpService->detectsFrustration($messageText)) {
|
|
$this->transferToAdvisor($phoneNumber, 'Frustración detectada');
|
|
return;
|
|
}
|
|
|
|
// Ejecutar acción según el tipo de respuesta
|
|
$this->executeResponse($phoneNumber, $response);
|
|
}
|
|
|
|
/**
|
|
* Ejecuta la acción según el tipo de respuesta
|
|
* @param string $phoneNumber
|
|
* @param array $response
|
|
*/
|
|
private function executeResponse(string $phoneNumber, array $response): void
|
|
{
|
|
switch ($response['type']) {
|
|
case 'text':
|
|
$this->sendMessage($phoneNumber, $response['message']);
|
|
break;
|
|
|
|
case 'menu':
|
|
$menuId = $response['menu_id'] ?? null;
|
|
if ($menuId) {
|
|
$this->sendMenu($phoneNumber, $menuId);
|
|
}
|
|
break;
|
|
|
|
case 'template':
|
|
$templateName = $response['template_name'] ?? null;
|
|
if ($templateName) {
|
|
$this->sendTemplate($phoneNumber, $templateName);
|
|
}
|
|
break;
|
|
|
|
case 'transfer':
|
|
$message = $response['message'] ?? 'Conectando con asesor...';
|
|
$this->transferToAdvisor($phoneNumber, 'Solicitud del usuario', $message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Procesa mensajes multimedia
|
|
* @param array $user
|
|
* @param string $messageType
|
|
* @param string $mediaId
|
|
* @param string $caption
|
|
*/
|
|
private function processMediaMessage(array $user, string $messageType, string $mediaId, string $caption = ''): void
|
|
{
|
|
$phoneNumber = $user['phone_number'];
|
|
$currentState = $this->stateService->getState($phoneNumber);
|
|
|
|
// Si está esperando foto de orden médica o documento
|
|
if ($currentState && in_array($currentState['state'], [
|
|
ConversationStateService::STATE_AWAITING_ORDER_PHOTO,
|
|
ConversationStateService::STATE_AWAITING_ID_PHOTO
|
|
])) {
|
|
if ($messageType === 'image' || $messageType === 'document') {
|
|
$this->handleUploadedDocument($user, $mediaId, $currentState['state']);
|
|
} else {
|
|
$this->sendMessage($phoneNumber, "Por favor envía una imagen o documento PDF.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Si no está esperando media, informar
|
|
$this->sendMessage($phoneNumber, "He recibido tu archivo. ¿En qué puedo ayudarte?\n\nEscribe *MENÚ* para ver las opciones.");
|
|
}
|
|
|
|
/**
|
|
* Maneja documentos subidos (órdenes médicas, ID, etc.)
|
|
* @param array $user
|
|
* @param string $mediaId
|
|
* @param string $expectedType
|
|
*/
|
|
private function handleUploadedDocument(array $user, string $mediaId, string $expectedType): void
|
|
{
|
|
$phoneNumber = $user['phone_number'];
|
|
|
|
try {
|
|
// Descargar el archivo desde WhatsApp
|
|
$mediaUrl = $this->whatsappService->downloadMedia($mediaId);
|
|
|
|
if (!$mediaUrl) {
|
|
$this->sendMessage($phoneNumber, "❌ No pude descargar el archivo. Por favor intenta de nuevo.");
|
|
return;
|
|
}
|
|
|
|
// Guardar información del documento
|
|
if ($expectedType === ConversationStateService::STATE_AWAITING_ORDER_PHOTO) {
|
|
$this->stateService->updateStateData($phoneNumber, [
|
|
'order_photo_url' => $mediaUrl,
|
|
'order_received_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
$this->sendMessage($phoneNumber, "✅ Orden médica recibida.\n\nAhora necesito los datos del paciente:\n\n✓ Nombre completo:\n✓ Número de documento:\n✓ Dirección completa:\n✓ Número de celular:\n✓ Correo electrónico:\n✓ ¿Es particular o por seguro?\n\nEnvíalos en UN SOLO mensaje.");
|
|
|
|
$this->stateService->setState($phoneNumber, ConversationStateService::STATE_AWAITING_PATIENT_DATA);
|
|
|
|
} elseif ($expectedType === ConversationStateService::STATE_AWAITING_ID_PHOTO) {
|
|
$this->stateService->updateStateData($phoneNumber, [
|
|
'id_photo_url' => $mediaUrl,
|
|
'id_received_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
$this->sendMessage($phoneNumber, "✅ Documento de identidad recibido.\n\nProcesando tu solicitud...");
|
|
|
|
// Aquí iría lógica adicional según el flujo
|
|
$this->stateService->setState($phoneNumber, ConversationStateService::STATE_VALIDATING_DATA);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
error_log("Error downloading media: " . $e->getMessage());
|
|
$this->sendMessage($phoneNumber, "❌ Hubo un problema al procesar el archivo. Por favor intenta nuevamente.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Envía el mensaje de bienvenida
|
|
* @param string $phoneNumber
|
|
*/
|
|
private function sendWelcomeMessage(string $phoneNumber): void
|
|
{
|
|
$message = $this->businessHoursService->getMessage();
|
|
$this->sendMessage($phoneNumber, $message);
|
|
|
|
// Si está en horario, enviar menú principal
|
|
if ($this->businessHoursService->isBusinessHours()) {
|
|
$mainMenuId = $this->menuService->getMainMenuId();
|
|
if ($mainMenuId) {
|
|
$this->stateService->setCurrentMenu($phoneNumber, $mainMenuId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Envía un menú
|
|
* @param string $phoneNumber
|
|
* @param int $menuId
|
|
*/
|
|
private function sendMenu(string $phoneNumber, int $menuId): void
|
|
{
|
|
$menu = $this->menuService->getMenu($menuId);
|
|
|
|
if (!$menu) {
|
|
$this->sendMessage($phoneNumber, "❌ Menú no encontrado.");
|
|
return;
|
|
}
|
|
|
|
$this->sendMessage($phoneNumber, $menu['welcome_message']);
|
|
$this->stateService->setCurrentMenu($phoneNumber, $menuId);
|
|
}
|
|
|
|
/**
|
|
* Transfiere la conversación a un asesor
|
|
* @param string $phoneNumber
|
|
* @param string $reason
|
|
* @param string $message
|
|
*/
|
|
private function transferToAdvisor(string $phoneNumber, string $reason, string $message = null): void
|
|
{
|
|
// Enviar mensaje al usuario
|
|
$userMessage = $message ?? '👤 SOLICITUD DE ASESOR\n\nUn momento por favor, estamos conectando tu consulta con uno de nuestros asesores.\n\nEn breve te atenderemos.\n\n*Tu consulta es importante para nosotros* 💙';
|
|
|
|
$this->sendMessage($phoneNumber, $userMessage);
|
|
|
|
// Marcar en el estado
|
|
$this->stateService->markForTransfer($phoneNumber, $reason);
|
|
|
|
// Aquí se podría notificar al equipo de asesores
|
|
// Por ejemplo, enviar notificación, email, etc.
|
|
$this->logMessage($phoneNumber, "Transferido a asesor: $reason");
|
|
}
|
|
|
|
/**
|
|
* Envía una plantilla de WhatsApp
|
|
* @param string $phoneNumber
|
|
* @param string $templateName
|
|
* @param array $parameters
|
|
*/
|
|
private function sendTemplate(string $phoneNumber, string $templateName, array $parameters = []): void
|
|
{
|
|
try {
|
|
$result = $this->whatsappService->sendTemplateMessage($phoneNumber, $templateName, $parameters);
|
|
|
|
if (!$result) {
|
|
$this->sendMessage($phoneNumber, "❌ No pude enviar la plantilla. Por favor contacta a un asesor.");
|
|
}
|
|
} catch (Exception $e) {
|
|
error_log("Error sending template: " . $e->getMessage());
|
|
$this->sendMessage($phoneNumber, "❌ Error al enviar plantilla.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Envía un mensaje de texto
|
|
* @param string $phoneNumber
|
|
* @param string $message
|
|
* @return bool
|
|
*/
|
|
private function sendMessage(string $phoneNumber, string $message): bool
|
|
{
|
|
try {
|
|
$result = $this->whatsappService->sendTextMessage($phoneNumber, $message);
|
|
|
|
if ($result) {
|
|
// Guardar en BD
|
|
$this->saveOutgoingMessage($phoneNumber, $message);
|
|
}
|
|
|
|
return $result;
|
|
} catch (Exception $e) {
|
|
error_log("Error sending message: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Guarda un mensaje saliente en la BD
|
|
* @param string $phoneNumber
|
|
* @param string $message
|
|
*/
|
|
private function saveOutgoingMessage(string $phoneNumber, string $message): void
|
|
{
|
|
try {
|
|
$stmt = $this->db->prepare("
|
|
INSERT INTO conversations
|
|
(phone_number, message_type, message_text, direction, status, created_at)
|
|
VALUES (?, 'text', ?, 'outgoing', 'sent', NOW())
|
|
");
|
|
$stmt->execute([$phoneNumber, $message]);
|
|
} catch (Exception $e) {
|
|
error_log("Error saving outgoing message: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifica si es el primer mensaje del usuario
|
|
* @param string $phoneNumber
|
|
* @return bool
|
|
*/
|
|
private function isFirstMessage(string $phoneNumber): bool
|
|
{
|
|
$stmt = $this->db->prepare("
|
|
SELECT COUNT(*) as count
|
|
FROM conversations
|
|
WHERE phone_number = ?
|
|
AND direction = 'incoming'
|
|
");
|
|
$stmt->execute([$phoneNumber]);
|
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
return $result && $result['count'] == 1; // Solo tiene un mensaje (el actual)
|
|
}
|
|
|
|
/**
|
|
* Registra un log de la conversación
|
|
* @param string $phoneNumber
|
|
* @param string $message
|
|
*/
|
|
private function logMessage(string $phoneNumber, string $message): void
|
|
{
|
|
error_log("[Bot - $phoneNumber] $message");
|
|
}
|
|
}
|