delete test
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
<?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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user