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, intente de nuevo o escriba *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 su mensaje.\n\nEscriba *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íe una imagen o documento PDF."); } return; } // Si no está esperando media, informar $this->sendMessage($phoneNumber, "He recibido su archivo. ¿En qué puedo ayudarle?\n\nEscriba *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']; // Validar que mediaId no esté vacío antes de intentar descargar if (empty($mediaId) || trim($mediaId) === '') { error_log("BotServiceLab: mediaId vacío recibido para usuario {$phoneNumber}"); $this->sendMessage($phoneNumber, "❌ No se recibió correctamente el archivo. Por favor, envíelo nuevamente."); return; } try { // Descargar el archivo desde WhatsApp $mediaUrl = $this->whatsappService->downloadMedia($mediaId); if (!$mediaUrl) { $this->sendMessage($phoneNumber, "❌ No pude descargar el archivo. Por favor, intente 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íelos 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 su 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, intente 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, contacte 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"); } }