453 lines
16 KiB
PHP
453 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Servicio Bot - Manejo de conversaciones y menús
|
|
* Fecha: 13 de noviembre de 2025
|
|
*/
|
|
|
|
class BotService {
|
|
private $db;
|
|
private $whatsappService;
|
|
|
|
public function __construct() {
|
|
$this->db = Database::getInstance();
|
|
$this->whatsappService = new WhatsAppService();
|
|
}
|
|
|
|
/**
|
|
* Procesar mensaje entrante
|
|
*/
|
|
public function processMessage($user, $messageText, $messageType = 'text') {
|
|
// Solo procesar mensajes de texto
|
|
if ($messageType !== 'text') {
|
|
return;
|
|
}
|
|
|
|
$messageText = trim($messageText);
|
|
$phoneNumber = $user['phone_number'];
|
|
|
|
// Verificar si es un nuevo usuario (enviar mensaje de bienvenida)
|
|
if ($this->isNewUser($user['id'])) {
|
|
$this->sendWelcomeMessage($phoneNumber);
|
|
return;
|
|
}
|
|
|
|
// Procesar comandos especiales
|
|
if ($this->processSpecialCommands($phoneNumber, $messageText)) {
|
|
return;
|
|
}
|
|
|
|
// Si el usuario está en espera por un asesor, no procesar
|
|
if (!empty($user['on_hold']) && $user['on_hold']) {
|
|
try {
|
|
$this->whatsappService->sendTextMessage($phoneNumber, "⚠️ Has sido puesto en espera por un asesor. En breve te contactarán.");
|
|
} catch (Exception $e) {
|
|
// ignore
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Si el bot está pausado por bot_paused_until, no responder automáticamente
|
|
if (!empty($user['bot_paused_until']) && strtotime($user['bot_paused_until']) > time()) {
|
|
return;
|
|
}
|
|
|
|
// Si el asesor ya respondió después del último mensaje del usuario, frenar al bot
|
|
$lastIncoming = $this->db->fetch("SELECT created_at FROM conversations WHERE user_id = :uid AND direction = 'incoming' ORDER BY created_at DESC LIMIT 1", ['uid' => $user['id']]);
|
|
$lastOutgoing = $this->db->fetch("SELECT created_at FROM conversations WHERE user_id = :uid AND direction = 'outgoing' ORDER BY created_at DESC LIMIT 1", ['uid' => $user['id']]);
|
|
if ($lastOutgoing && $lastIncoming && strtotime($lastOutgoing['created_at']) >= strtotime($lastIncoming['created_at'])) {
|
|
// El asesor ya respondió, no enviar respuestas automáticas
|
|
return;
|
|
}
|
|
// Verificar si el usuario está en un menú
|
|
if ($user['current_menu_id']) {
|
|
$this->processMenuSelection($user, $messageText);
|
|
} else {
|
|
// Procesar respuestas automáticas o comando menu
|
|
$this->processAutoResponses($phoneNumber, $messageText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verificar si es un nuevo usuario
|
|
*/
|
|
private function isNewUser($userId) {
|
|
$messageCount = $this->db->fetch(
|
|
"SELECT COUNT(*) as count FROM conversations WHERE user_id = :user_id",
|
|
['user_id' => $userId]
|
|
);
|
|
// Evitar reenviar welcome si ya se envió
|
|
$user = $this->db->fetch("SELECT welcome_sent_at FROM users WHERE id = :id", ['id' => $userId]);
|
|
$welcomeSent = !empty($user['welcome_sent_at']);
|
|
return ($messageCount['count'] <= 1) && !$welcomeSent; // Solo el mensaje actual y si no se ha enviado welcome
|
|
}
|
|
|
|
/**
|
|
* Enviar mensaje de bienvenida
|
|
*/
|
|
public function sendWelcomeMessage($phoneNumber) {
|
|
$enabled = getConfigFromDB('welcome_enabled', '1');
|
|
$welcomeMessage = getConfigFromDB('welcome_message', '¡Hola! 👋 Bienvenido a nuestro servicio automatizado. Escribe *menu* para ver las opciones disponibles.');
|
|
|
|
if (!$enabled || !$welcomeMessage) return;
|
|
|
|
$this->whatsappService->sendTextMessage($phoneNumber, $welcomeMessage);
|
|
|
|
// Registrar que se envió el welcome
|
|
try {
|
|
$this->db->update('users', ['welcome_sent_at' => date('Y-m-d H:i:s')], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
|
} catch (Exception $e) {
|
|
error_log('Failed to set welcome_sent_at for ' . $phoneNumber . ': ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Procesar comandos especiales
|
|
*/
|
|
private function processSpecialCommands($phoneNumber, $messageText) {
|
|
$command = strtolower($messageText);
|
|
|
|
switch ($command) {
|
|
case 'menu':
|
|
case 'menú':
|
|
case 'inicio':
|
|
$this->showMainMenu($phoneNumber);
|
|
return true;
|
|
|
|
case 'salir':
|
|
case 'exit':
|
|
case 'cancelar':
|
|
$this->exitMenu($phoneNumber);
|
|
return true;
|
|
|
|
case 'asesor':
|
|
case 'ayuda':
|
|
// Poner la conversación en espera y notificar al usuario
|
|
$this->putOnHold($phoneNumber);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Mostrar menú principal
|
|
*/
|
|
private function showMainMenu($phoneNumber) {
|
|
$mainMenu = $this->db->fetch(
|
|
"SELECT * FROM menus WHERE is_root = 1 AND is_active = 1 ORDER BY order_position LIMIT 1"
|
|
);
|
|
|
|
if ($mainMenu) {
|
|
$this->showMenu($phoneNumber, $mainMenu['id']);
|
|
} else {
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"❌ No hay menús configurados. Contacta con soporte."
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mostrar menú específico
|
|
*/
|
|
private function showMenu($phoneNumber, $menuId) {
|
|
// Obtener información del menú
|
|
$menu = $this->db->fetch(
|
|
"SELECT * FROM menus WHERE id = :id AND is_active = 1",
|
|
['id' => $menuId]
|
|
);
|
|
|
|
if (!$menu) {
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"❌ Menú no encontrado."
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Obtener opciones del menú
|
|
$options = $this->db->fetchAll(
|
|
"SELECT * FROM menu_options WHERE menu_id = :menu_id AND is_active = 1 ORDER BY option_number",
|
|
['menu_id' => $menuId]
|
|
);
|
|
|
|
if (empty($options)) {
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"❌ Este menú no tiene opciones configuradas."
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Construir mensaje del menú
|
|
$menuText = "📋 *" . $menu['title'] . "*\n\n";
|
|
|
|
if ($menu['description']) {
|
|
$menuText .= $menu['description'] . "\n\n";
|
|
}
|
|
|
|
foreach ($options as $option) {
|
|
$menuText .= $option['option_number'] . ". " . $option['text'] . "\n";
|
|
}
|
|
|
|
$menuText .= "\n💬 *Responde con el número de la opción que deseas*";
|
|
|
|
// Actualizar estado del usuario
|
|
$this->updateUserMenuState($phoneNumber, $menuId);
|
|
|
|
// Enviar mensaje
|
|
$this->whatsappService->sendTextMessage($phoneNumber, $menuText);
|
|
}
|
|
|
|
/**
|
|
* Procesar selección de menú
|
|
*/
|
|
private function processMenuSelection($user, $messageText) {
|
|
$phoneNumber = $user['phone_number'];
|
|
$currentMenuId = $user['current_menu_id'];
|
|
|
|
// Verificar si es un número
|
|
if (!is_numeric($messageText)) {
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"❌ Por favor, responde solo con el número de la opción deseada."
|
|
);
|
|
return;
|
|
}
|
|
|
|
$optionNumber = (int)$messageText;
|
|
|
|
// Buscar la opción seleccionada
|
|
$option = $this->db->fetch(
|
|
"SELECT * FROM menu_options WHERE menu_id = :menu_id AND option_number = :option_number AND is_active = 1",
|
|
['menu_id' => $currentMenuId, 'option_number' => $optionNumber]
|
|
);
|
|
|
|
if (!$option) {
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"❌ Opción inválida. Por favor, selecciona una opción válida del menú."
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Procesar acción de la opción
|
|
$this->processMenuAction($phoneNumber, $option);
|
|
}
|
|
|
|
/**
|
|
* Procesar acción del menú
|
|
*/
|
|
private function processMenuAction($phoneNumber, $option) {
|
|
switch ($option['action_type']) {
|
|
case 'menu':
|
|
// Navegar a otro menú
|
|
$targetMenu = $this->db->fetch(
|
|
"SELECT * FROM menus WHERE name = :name AND is_active = 1",
|
|
['name' => $option['action_value']]
|
|
);
|
|
|
|
if ($targetMenu) {
|
|
$this->showMenu($phoneNumber, $targetMenu['id']);
|
|
} else {
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"❌ Menú no encontrado: " . $option['action_value']
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 'message':
|
|
// Enviar mensaje de respuesta
|
|
$this->whatsappService->sendTextMessage($phoneNumber, $option['action_value']);
|
|
$this->exitMenu($phoneNumber);
|
|
break;
|
|
|
|
case 'api_call':
|
|
// Llamar API externa (implementar según necesidad)
|
|
$this->processApiCall($phoneNumber, $option['action_value']);
|
|
break;
|
|
|
|
case 'end':
|
|
// Finalizar conversación
|
|
$message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
|
|
$this->whatsappService->sendTextMessage($phoneNumber, $message);
|
|
$this->exitMenu($phoneNumber);
|
|
// Pausar el bot para esta conversación por 4 horas
|
|
$this->pauseConversationForHours($phoneNumber, 4);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Procesar llamada API externa
|
|
*/
|
|
private function processApiCall($phoneNumber, $apiEndpoint) {
|
|
// Aquí puedes implementar llamadas a APIs externas
|
|
// Por ejemplo, consultar saldos, procesar pagos, etc.
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"🔄 Procesando tu solicitud... Un momento por favor."
|
|
);
|
|
|
|
// Simular procesamiento
|
|
sleep(2);
|
|
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"✅ Tu solicitud ha sido procesada exitosamente."
|
|
);
|
|
|
|
$this->exitMenu($phoneNumber);
|
|
}
|
|
|
|
/**
|
|
* Procesar respuestas automáticas
|
|
*/
|
|
private function processAutoResponses($phoneNumber, $messageText) {
|
|
$keyword = strtolower($messageText);
|
|
|
|
// Buscar respuesta automática por keyword
|
|
$autoResponse = $this->db->fetch(
|
|
"SELECT * FROM auto_responses WHERE trigger_type = 'keyword' AND LOWER(trigger_value) = :keyword AND is_active = 1",
|
|
['keyword' => $keyword]
|
|
);
|
|
|
|
if ($autoResponse) {
|
|
$this->whatsappService->sendTextMessage($phoneNumber, $autoResponse['response_text']);
|
|
|
|
// Si la keyword es 'menu', mostrar el menú
|
|
if ($keyword === 'menu' || $keyword === 'menú') {
|
|
$this->showMainMenu($phoneNumber);
|
|
}
|
|
} else {
|
|
// Respuesta por defecto para mensajes no reconocidos
|
|
$defaultMessage = "🤖 No entendí tu mensaje. Escribe *menu* para ver las opciones disponibles o *ayuda* para obtener ayuda.";
|
|
$this->whatsappService->sendTextMessage($phoneNumber, $defaultMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Salir del menú actual
|
|
*/
|
|
private function exitMenu($phoneNumber) {
|
|
$this->db->update(
|
|
'users',
|
|
['current_menu_id' => null, 'current_step' => 0, 'session_data' => null],
|
|
'phone_number = :phone',
|
|
['phone' => $phoneNumber]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Poner la conversación en pausa por 4 horas (llamada desde opciones 'end')
|
|
*/
|
|
private function pauseConversationForHours($phoneNumber, $hours = 4) {
|
|
try {
|
|
$pausedUntil = date('Y-m-d H:i:s', strtotime("+{$hours} hours"));
|
|
$this->db->update('users', ['bot_paused_until' => $pausedUntil], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
|
if (function_exists('writeLog')) writeLog('INFO', "Bot paused for {$hours} hours for {$phoneNumber}");
|
|
} catch (Exception $e) {
|
|
error_log('pauseConversationForHours failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Poner conversación en espera (asesor solicitado)
|
|
*/
|
|
public function putOnHold($phoneNumber) {
|
|
try {
|
|
$this->db->update('users', ['on_hold' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
|
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Se ha solicitado un asesor. Te hemos puesto en espera. Te contactaremos en breve.");
|
|
if (function_exists('writeLog')) writeLog('INFO', "Advisor requested for $phoneNumber");
|
|
} catch (Exception $e) {
|
|
error_log('putOnHold failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function releaseHold($phoneNumber) {
|
|
try {
|
|
$this->db->update('users', ['on_hold' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
|
|
$this->whatsappService->sendTextMessage($phoneNumber, "🔔 Tu conversación ha sido retomada por el equipo.");
|
|
if (function_exists('writeLog')) writeLog('INFO', "Advisor released hold for $phoneNumber");
|
|
} catch (Exception $e) {
|
|
error_log('releaseHold failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualizar estado del menú del usuario
|
|
*/
|
|
private function updateUserMenuState($phoneNumber, $menuId) {
|
|
$this->db->update(
|
|
'users',
|
|
['current_menu_id' => $menuId, 'current_step' => 1],
|
|
'phone_number = :phone',
|
|
['phone' => $phoneNumber]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Mostrar ayuda
|
|
*/
|
|
private function showHelp($phoneNumber) {
|
|
$helpText = "🆘 *Ayuda del Sistema*\n\n";
|
|
$helpText .= "📋 *menu* - Mostrar menú principal\n";
|
|
$helpText .= "❌ *salir* - Salir del menú actual\n";
|
|
$helpText .= "🆘 *ayuda* - Mostrar esta ayuda\n\n";
|
|
$helpText .= "💡 *Consejos:*\n";
|
|
$helpText .= "• Responde solo con números en los menús\n";
|
|
$helpText .= "• Escribe palabras clave para obtener respuestas rápidas\n";
|
|
$helpText .= "• Si tienes problemas, escribe *soporte*";
|
|
|
|
$this->whatsappService->sendTextMessage($phoneNumber, $helpText);
|
|
}
|
|
|
|
/**
|
|
* Procesar mensaje de soporte
|
|
*/
|
|
public function processSupportMessage($phoneNumber, $message) {
|
|
// Notificar a administradores sobre consulta de soporte
|
|
$this->whatsappService->sendTextMessage(
|
|
$phoneNumber,
|
|
"🆘 Tu mensaje ha sido enviado a nuestro equipo de soporte. Te contactaremos pronto.\n\n" .
|
|
"📞 También puedes llamarnos al: +57 1 234 5678\n" .
|
|
"📧 O escribirnos a: soporte@miempresa.com"
|
|
);
|
|
|
|
// Aquí podrías implementar notificaciones a administradores
|
|
// Por ejemplo, enviar email o notificación a Slack
|
|
}
|
|
|
|
/**
|
|
* Obtener estadísticas del bot
|
|
*/
|
|
public function getBotStats() {
|
|
$stats = [];
|
|
|
|
// Total de usuarios
|
|
$stats['total_users'] = $this->db->fetch(
|
|
"SELECT COUNT(*) as count FROM users"
|
|
)['count'];
|
|
|
|
// Usuarios activos (último mes)
|
|
$stats['active_users'] = $this->db->fetch(
|
|
"SELECT COUNT(DISTINCT user_id) as count FROM conversations
|
|
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"
|
|
)['count'];
|
|
|
|
// Total de mensajes
|
|
$stats['total_messages'] = $this->db->fetch(
|
|
"SELECT COUNT(*) as count FROM conversations"
|
|
)['count'];
|
|
|
|
// Mensajes hoy
|
|
$stats['messages_today'] = $this->db->fetch(
|
|
"SELECT COUNT(*) as count FROM conversations
|
|
WHERE DATE(created_at) = CURDATE()"
|
|
)['count'];
|
|
|
|
return $stats;
|
|
}
|
|
}
|
|
?>
|