diff --git a/README_LABORATORIO.md b/README_LABORATORIO.md new file mode 100644 index 0000000..6686c79 --- /dev/null +++ b/README_LABORATORIO.md @@ -0,0 +1,298 @@ +# 🏥 Sistema de Chatbot WhatsApp - Laboratorio Ximena Caicedo + +## 📋 Descripción + +Sistema completo de chatbot para WhatsApp diseñado específicamente para el Laboratorio Ximena Caicedo en Cúcuta, Colombia. Incluye flujos conversacionales inteligentes, detección de horarios, gestión de estados, respuestas automáticas y mucho más. + +## ✨ Características Principales + +### 🤖 Bot Inteligente +- ✅ Procesamiento de Lenguaje Natural (NLP) básico +- ✅ Detección de palabras clave y sinónimos +- ✅ Respuestas automáticas contextuales +- ✅ Detección de sentimientos y frustración +- ✅ Escalamiento automático a asesores humanos + +### 📅 Gestión de Horarios +- ✅ Detección automática de horario de atención +- ✅ Mensajes diferentes dentro/fuera de horario +- ✅ Configuración por día de la semana +- ✅ Soporte para horarios partidos (7-12, 14-17) + +### 🗂️ Sistema de Menús +- ✅ Menú principal con 5 opciones +- ✅ Submenú de exámenes con información detallada +- ✅ Navegación intuitiva con números +- ✅ Opción de volver al menú anterior + +### 🔄 Gestión de Estados +- ✅ Tracking del estado de cada conversación +- ✅ Flujos de agendamiento paso a paso +- ✅ Validación de datos del paciente +- ✅ Timeout automático de estados inactivos + +### 💬 Respuestas Automáticas +- ✅ Más de 15 triggers configurados +- ✅ Respuestas por palabras clave +- ✅ Información de precios, horarios, ubicación +- ✅ Instrucciones para exámenes específicos + +### 📸 Procesamiento de Multimedia +- ✅ Recepción de órdenes médicas +- ✅ Fotos de documentos de identidad +- ✅ Validación de imágenes +- ✅ Almacenamiento seguro + +## 🚀 Instalación + +### Requisitos Previos +- PHP 8.0 o superior +- MySQL 5.7 o superior +- Servidor web (Apache/Nginx) +- Cuenta de WhatsApp Business API + +### Paso 1: Clonar/Descargar Archivos +Asegúrate de tener todos los archivos en tu servidor. + +### Paso 2: Configurar Base de Datos +1. Crea una base de datos MySQL +2. Importa las tablas base si no existen: +```sql +-- Ejecutar los scripts SQL en este orden: +1. create_tables.sql (tablas base) +2. migrations/add_conversation_states.sql (tabla de estados) +3. setup_laboratorio_ximena.sql (datos del laboratorio) +``` + +### Paso 3: Configurar Credenciales +Edita `config/config.php` con tus credenciales: +```php +define('DB_HOST', 'localhost'); +define('DB_NAME', 'whatsapp_bot'); +define('DB_USER', 'tu_usuario'); +define('DB_PASS', 'tu_password'); +``` + +### Paso 4: Ejecutar Instalación Automática +Visita en tu navegador: +``` +http://tu-dominio.com/install_laboratorio.php +``` + +Este script: +- ✅ Verifica la conexión a la base de datos +- ✅ Crea la tabla de estados +- ✅ Importa menús y respuestas automáticas +- ✅ Verifica los servicios del sistema +- ✅ Muestra estadísticas finales + +### Paso 5: Configurar WhatsApp API +1. Ve al panel de administración: `http://tu-dominio.com/index.php` +2. Navega a la pestaña "Configuración" +3. Ingresa: + - Token de Acceso de WhatsApp Business API + - Phone Number ID + - Webhook Verify Token + - API URL (por defecto: https://graph.facebook.com/v22.0/) + +### Paso 6: Configurar Webhook +1. En Meta for Developers, configura el webhook: + - URL: `https://tu-dominio.com/api/webhook.php` + - Verify Token: El mismo que configuraste en el paso anterior + - Suscripciones: `messages`, `message_status` + +## 📚 Estructura de Archivos + +``` +whatsapp/ +├── services/ +│ ├── BusinessHoursService.php # Gestión de horarios +│ ├── ConversationStateService.php # Estados de conversación +│ ├── NLPService.php # Procesamiento de lenguaje +│ ├── MenuService.php # Gestión de menús +│ ├── PatientDataValidator.php # Validación de datos +│ ├── BotServiceLab.php # Servicio principal del bot +│ └── WhatsAppService.php # Integración con WhatsApp API +├── api/ +│ └── webhook.php # Recepción de mensajes +├── migrations/ +│ └── add_conversation_states.sql # Migración de tabla de estados +├── setup_laboratorio_ximena.sql # Setup completo del laboratorio +├── install_laboratorio.php # Instalador automático +└── README_LABORATORIO.md # Este archivo +``` + +## 🎯 Flujos Implementados + +### 1. Menú Principal +``` +1️⃣ Agendar toma a domicilio +2️⃣ Consultar resultados +3️⃣ Información sobre exámenes +4️⃣ Ubicación y horarios +5️⃣ Hablar con un asesor +``` + +### 2. Submenú de Exámenes +``` +1️⃣ Pruebas de embarazo +2️⃣ Prueba de paternidad +3️⃣ Helicobacter pylori (en aliento) +4️⃣ Orina de 24 horas +5️⃣ Solicitar cotización +0️⃣ Volver al menú principal +``` + +### 3. Palabras Clave Automáticas +- **"precio", "costo", "cuánto"** → Información de precios +- **"resultado", "resultados"** → Instrucciones para descargar +- **"horario", "cuando"** → Horarios de atención +- **"domicilio", "casa"** → Servicio a domicilio +- **"embarazo"** → Pruebas de embarazo +- **"paternidad", "ADN"** → Prueba de paternidad +- **"ubicación", "donde"** → Dirección y mapa +- **"asesor", "ayuda", "persona"** → Transferir a humano +- **"gracias"** → Mensaje de despedida +- **"menú", "inicio"** → Mostrar menú principal + +## ⚙️ Configuración Avanzada + +### Horarios de Atención +Edita en la base de datos o desde el panel: +```sql +UPDATE system_config +SET config_value = '7:00-12:00,14:00-17:00' +WHERE config_key = 'business_hours_weekday'; + +UPDATE system_config +SET config_value = '8:00-11:00' +WHERE config_key = 'business_hours_saturday'; +``` + +### Agregar Nueva Respuesta Automática +Desde el panel de administración: +1. Ve a "Respuestas Automáticas" +2. Click en "Nueva Respuesta" +3. Configura: + - Tipo de trigger (keyword, contains, exact, etc.) + - Palabras clave (separadas por comas) + - Texto de respuesta + - Prioridad (1-100, mayor = más prioridad) + +### Personalizar Menús +1. Ve a "Menús Configurados" +2. Click en el menú que quieres editar +3. Modifica el mensaje de bienvenida o las opciones +4. Guarda los cambios + +## 🔧 Personalización + +### Cambiar Nombre del Laboratorio +Edita `setup_laboratorio_ximena.sql` antes de instalar, reemplaza: +```sql +'Laboratorio Ximena Caicedo' → 'Tu Laboratorio' +``` + +### Modificar Valores de Exámenes +Edita los mensajes en la tabla `menu_options` o `autoresponses`: +```sql +UPDATE menu_options +SET response_text = REPLACE(response_text, '$28.000', '$30.000') +WHERE response_text LIKE '%$28.000%'; +``` + +### Agregar Nuevo Examen al Submenú +```sql +INSERT INTO menu_options (menu_id, option_key, option_order, option_text, response_type, response_text, is_active) +VALUES +( + (SELECT id FROM menus WHERE menu_key = 'lab_menu_examenes'), + '6', + 6, + 'Nuevo Examen', + 'text', + 'Información del nuevo examen...', + 1 +); +``` + +## 📊 Monitoreo y Logs + +### Ver Conversaciones Recientes +```sql +SELECT phone_number, message_text, direction, created_at +FROM conversations +ORDER BY created_at DESC +LIMIT 50; +``` + +### Ver Estados Activos +```sql +SELECT phone_number, state, state_data, updated_at +FROM user_states +WHERE updated_at > DATE_SUB(NOW(), INTERVAL 1 HOUR); +``` + +### Ver Logs de Webhook +```sql +SELECT * FROM webhook_logs +ORDER BY created_at DESC +LIMIT 20; +``` + +## 🐛 Solución de Problemas + +### El bot no responde +1. Verifica que el webhook esté configurado correctamente +2. Revisa los logs de webhook: `SELECT * FROM webhook_logs ORDER BY created_at DESC LIMIT 1` +3. Verifica las credenciales de WhatsApp API en "Configuración" + +### Mensajes fuera de horario no funcionan +1. Verifica la zona horaria del servidor: `date_default_timezone_get()` +2. Revisa la configuración de horarios en `system_config` +3. Asegúrate de que `BusinessHoursService.php` existe + +### Estados no se guardan +1. Verifica que la tabla `user_states` existe: +```sql +SHOW TABLES LIKE 'user_states'; +``` +2. Si no existe, ejecuta: `migrations/add_conversation_states.sql` + +### NLP no detecta palabras clave +1. Revisa que las palabras estén en minúsculas en la BD +2. Verifica que `autoresponses` tiene registros activos: +```sql +SELECT COUNT(*) FROM autoresponses WHERE is_active = 1; +``` +3. Aumenta la prioridad de la respuesta automática + +## 📞 Soporte + +Para soporte técnico o consultas: +- 📧 Email: soporte@ejemplo.com +- 📱 WhatsApp: +57 XXX XXX XXXX +- 💻 GitHub: [repositorio] + +## 📝 Changelog + +### Versión 1.0.0 (13 de enero de 2026) +- ✅ Implementación completa del sistema +- ✅ Menús del laboratorio +- ✅ 15+ respuestas automáticas +- ✅ Detección de horarios +- ✅ Gestión de estados +- ✅ NLP básico +- ✅ Validación de datos de pacientes +- ✅ Instalador automático + +## 📜 Licencia + +Este sistema fue desarrollado específicamente para Laboratorio Ximena Caicedo. +Todos los derechos reservados © 2026 + +--- + +**¡Sistema listo para producción! 🚀** + +Para comenzar, ejecuta `install_laboratorio.php` y sigue las instrucciones. diff --git a/api/test_api.php b/api/test_api.php deleted file mode 100644 index fd4e8fb..0000000 --- a/api/test_api.php +++ /dev/null @@ -1,152 +0,0 @@ - true, - 'message' => 'API funcionando correctamente', - 'timestamp' => date('Y-m-d H:i:s'), - 'method' => $method - ]); - exit; - } - - switch ($method) { - case 'GET': - switch ($action) { - case 'status': - // Estado general del sistema - $allConfigs = getAllConfigsFromDB(); - $whatsappValidation = validateWhatsAppConfigFromDB(); - - echo json_encode([ - 'success' => true, - 'data' => [ - 'total_configs' => count($allConfigs), - 'whatsapp_configured' => $whatsappValidation['valid'], - 'database_migrated' => !empty($allConfigs), - 'installation_completed' => isInstallationCompleted(), - 'server_time' => date('Y-m-d H:i:s') - ] - ]); - break; - - case 'all': - // Obtener todas las configuraciones - $configs = getAllConfigsFromDB(); - echo json_encode([ - 'success' => true, - 'data' => $configs - ]); - break; - - case 'whatsapp': - // Obtener configuración de WhatsApp - $whatsappConfig = getWhatsAppConfigFromDB(); - echo json_encode([ - 'success' => true, - 'data' => $whatsappConfig - ]); - break; - - case 'test_db': - // Test de conexión a BD - try { - $pdo = createDbConnection(); - $tableExists = $pdo->query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0; - - echo json_encode([ - 'success' => true, - 'data' => [ - 'db_connected' => true, - 'table_exists' => $tableExists, - 'config_count' => $tableExists ? $pdo->query("SELECT COUNT(*) FROM system_config")->fetchColumn() : 0 - ] - ]); - } catch (Exception $e) { - echo json_encode([ - 'success' => false, - 'error' => 'Error de BD: ' . $e->getMessage() - ]); - } - break; - - default: - throw new Exception('Acción GET no reconocida: ' . $action); - } - break; - - case 'POST': - $input = json_decode(file_get_contents('php://input'), true); - - switch ($action) { - case 'save_test': - // Guardar configuración de prueba - $key = 'api_test_' . time(); - $value = 'Test desde API - ' . date('Y-m-d H:i:s'); - - $result = saveConfigToDB($key, $value, 'Prueba desde API'); - - echo json_encode([ - 'success' => $result, - 'message' => $result ? 'Configuración guardada correctamente' : 'Error guardando configuración', - 'data' => [ - 'key' => $key, - 'value' => $value - ] - ]); - break; - - case 'save_whatsapp_test': - // Guardar config de WhatsApp de prueba - $testConfig = [ - 'token' => 'test_token_' . time(), - 'phone_number_id' => '123456789', - 'webhook_verify_token' => 'webhook_' . time() - ]; - - $result = saveWhatsAppConfigToDB($testConfig); - - echo json_encode([ - 'success' => $result, - 'message' => $result ? 'WhatsApp config guardada' : 'Error guardando WhatsApp config', - 'data' => $testConfig - ]); - break; - - default: - throw new Exception('Acción POST no reconocida: ' . $action); - } - break; - - default: - throw new Exception('Método HTTP no permitido: ' . $method); - } -} catch (Exception $e) { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); -} -?> \ No newline at end of file diff --git a/api/test_db_connection.php b/api/test_db_connection.php deleted file mode 100644 index d05441d..0000000 --- a/api/test_db_connection.php +++ /dev/null @@ -1,38 +0,0 @@ - 'Iniciando test de conexión DB...', - 'timestamp' => date('Y-m-d H:i:s') - ]); - echo "\n\n"; - - $db = Database::getInstance(); - - echo json_encode([ - 'success' => true, - 'message' => 'Conexión a base de datos exitosa', - 'database' => DB_NAME, - 'host' => DB_HOST - ]); - echo "\n\n"; - - // Test de tabla - $tables = $db->fetchAll("SHOW TABLES"); - echo json_encode([ - 'success' => true, - 'message' => 'Tablas encontradas', - 'tables' => $tables - ]); - -} catch (Exception $e) { - echo json_encode([ - 'success' => false, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); -} -?> \ No newline at end of file diff --git a/install_laboratorio.php b/install_laboratorio.php new file mode 100644 index 0000000..11ad36b --- /dev/null +++ b/install_laboratorio.php @@ -0,0 +1,267 @@ + + + + + + + Instalación - Laboratorio Ximena Caicedo + + + + + +
+
+ + +
+ '; + echo ' Verificando conexión a la base de datos...'; + flush(); + + $db = Database::getInstance(); + + echo ' Conectado'; + echo '
'; + $steps[] = ['name' => 'Conexión DB', 'status' => 'success']; + + // PASO 2: Crear tabla de estados si no existe + echo '
'; + echo ' Creando tabla de estados de conversación...'; + flush(); + + $sqlStates = file_get_contents(__DIR__ . '/migrations/add_conversation_states.sql'); + $statementsStates = array_filter(array_map('trim', explode(';', $sqlStates))); + + foreach ($statementsStates as $statement) { + if (!empty($statement) && substr($statement, 0, 6) !== 'SELECT') { + $db->getConnection()->exec($statement); + } + } + + echo ' Completado'; + echo '
'; + $steps[] = ['name' => 'Tabla user_states', 'status' => 'success']; + + // PASO 3: Importar menús y respuestas automáticas + echo '
'; + echo ' Importando menús y respuestas automáticas...'; + flush(); + + $sqlSetup = file_get_contents(__DIR__ . '/setup_laboratorio_ximena.sql'); + $statementsSetup = array_filter(array_map('trim', explode(';', $sqlSetup))); + + $menuCount = 0; + $responseCount = 0; + + foreach ($statementsSetup as $statement) { + if (!empty($statement) && substr($statement, 0, 2) !== '--') { + if (stripos($statement, 'INSERT INTO menus') !== false) { + $menuCount++; + } elseif (stripos($statement, 'INSERT INTO autoresponses') !== false) { + $responseCount++; + } + + try { + $db->getConnection()->exec($statement); + } catch (Exception $e) { + // Ignorar errores de duplicados + if (strpos($e->getMessage(), 'Duplicate entry') === false) { + throw $e; + } + } + } + } + + echo " {$menuCount} menús, {$responseCount} respuestas"; + echo '
'; + $steps[] = ['name' => 'Menús y respuestas', 'status' => 'success']; + + // PASO 4: Verificar servicios + echo '
'; + echo ' Verificando servicios del sistema...'; + flush(); + + $services = [ + 'BusinessHoursService', + 'ConversationStateService', + 'NLPService', + 'MenuService', + 'PatientDataValidator', + 'BotServiceLab' + ]; + + $servicesOk = true; + foreach ($services as $service) { + $file = __DIR__ . "/services/{$service}.php"; + if (!file_exists($file)) { + echo " Falta {$service}.php"; + $servicesOk = false; + break; + } + } + + if ($servicesOk) { + echo ' ' . count($services) . ' servicios verificados'; + $steps[] = ['name' => 'Servicios', 'status' => 'success']; + } else { + $success = false; + } + + echo '
'; + + // PASO 5: Obtener estadísticas + echo '
'; + echo ' Obteniendo estadísticas...'; + flush(); + + $stmt = $db->query(" + SELECT + (SELECT COUNT(*) FROM menus WHERE is_active = 1) as menus_activos, + (SELECT COUNT(*) FROM menu_options WHERE is_active = 1) as opciones_activas, + (SELECT COUNT(*) FROM autoresponses WHERE is_active = 1) as respuestas_activas, + (SELECT COUNT(*) FROM users) as total_usuarios + "); + $stats = $stmt->fetch(PDO::FETCH_ASSOC); + + echo ' Completado'; + echo '
'; + + // Mostrar resumen + if ($success) { + echo '
'; + echo '

¡Instalación Completada Exitosamente!

'; + echo '
'; + echo '
📊 Estadísticas del Sistema:
'; + echo '
    '; + echo "
  • Menús activos: {$stats['menus_activos']}
  • "; + echo "
  • Opciones de menú: {$stats['opciones_activas']}
  • "; + echo "
  • Respuestas automáticas: {$stats['respuestas_activas']}
  • "; + echo "
  • Usuarios registrados: {$stats['total_usuarios']}
  • "; + echo '
'; + echo '
'; + echo '
🚀 Próximos Pasos:
'; + echo '
    '; + echo '
  1. Configura tus credenciales de WhatsApp Business API en el panel de administración
  2. '; + echo '
  3. Prueba el menú principal enviando un mensaje de WhatsApp
  4. '; + echo '
  5. Revisa las respuestas automáticas en la sección correspondiente
  6. '; + echo '
  7. Personaliza los mensajes según las necesidades del laboratorio
  8. '; + echo '
'; + echo ''; + echo '
'; + } + + } catch (Exception $e) { + $success = false; + echo '
'; + echo ' Error: ' . htmlspecialchars($e->getMessage()); + echo '
'; + + echo '
'; + echo '

Error en la Instalación

'; + echo '

Hubo un problema durante la instalación. Por favor:

'; + echo '
    '; + echo '
  1. Verifica que la base de datos existe y tiene permisos correctos
  2. '; + echo '
  3. Revisa la configuración en config/config.php
  4. '; + echo '
  5. Consulta el error arriba para más detalles
  6. '; + echo '
  7. Intenta ejecutar la instalación nuevamente
  8. '; + echo '
'; + echo '
'; + echo ''; + echo '
'; + echo '
'; + } + ?> +
+ + +
+
+
Documentación
+

Para más información sobre cómo usar el sistema:

+ +
+
+ +
+ + + + + diff --git a/migrations/add_conversation_states.sql b/migrations/add_conversation_states.sql new file mode 100644 index 0000000..2166eee --- /dev/null +++ b/migrations/add_conversation_states.sql @@ -0,0 +1,29 @@ +-- ===================================================== +-- MIGRACIÓN: Tabla de Estados de Conversación +-- Fecha: 13 de enero de 2026 +-- ===================================================== + +-- Crear tabla user_states si no existe +CREATE TABLE IF NOT EXISTS user_states ( + id INT AUTO_INCREMENT PRIMARY KEY, + phone_number VARCHAR(20) NOT NULL, + state VARCHAR(50) NOT NULL, + state_data TEXT NULL COMMENT 'JSON con datos adicionales del estado', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_phone (phone_number), + INDEX idx_state (state), + INDEX idx_updated (updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Agregar columna parent_menu_id si no existe +ALTER TABLE menus +ADD COLUMN IF NOT EXISTS parent_menu_id INT NULL COMMENT 'ID del menú padre para submenús', +ADD CONSTRAINT fk_parent_menu FOREIGN KEY (parent_menu_id) REFERENCES menus(id) ON DELETE SET NULL; + +-- Agregar índice para mejorar rendimiento +ALTER TABLE autoresponses +ADD INDEX IF NOT EXISTS idx_trigger_type (trigger_type), +ADD INDEX IF NOT EXISTS idx_priority (priority DESC); + +SELECT '✅ Migración completada exitosamente' as status; diff --git a/services/BotServiceLab.php b/services/BotServiceLab.php new file mode 100644 index 0000000..f7bff87 --- /dev/null +++ b/services/BotServiceLab.php @@ -0,0 +1,387 @@ +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"); + } +} diff --git a/services/BusinessHoursService.php b/services/BusinessHoursService.php new file mode 100644 index 0000000..28dc4b1 --- /dev/null +++ b/services/BusinessHoursService.php @@ -0,0 +1,221 @@ +db = Database::getInstance(); + } + + /** + * Verifica si actualmente estamos en horario de atención + * @return bool + */ + public function isBusinessHours(): bool + { + $now = new DateTime('now', new DateTimeZone('America/Bogota')); + $dayOfWeek = (int)$now->format('N'); // 1=Lunes, 7=Domingo + $currentTime = $now->format('H:i'); + + // Domingo (7) siempre cerrado + if ($dayOfWeek === 7) { + return false; + } + + // Sábado (6) + if ($dayOfWeek === 6) { + $saturdayHours = $this->getBusinessHoursConfig('business_hours_saturday'); + return $this->isTimeInRange($currentTime, $saturdayHours); + } + + // Lunes a Viernes (1-5) + $weekdayHours = $this->getBusinessHoursConfig('business_hours_weekday'); + return $this->isTimeInRange($currentTime, $weekdayHours); + } + + /** + * Obtiene el mensaje apropiado según el horario + * @return string + */ + public function getMessage(): string + { + if ($this->isBusinessHours()) { + return $this->getWelcomeMessage(); + } else { + return $this->getOutOfHoursMessage(); + } + } + + /** + * Obtiene el mensaje de bienvenida + * @return string + */ + private function getWelcomeMessage(): string + { + $stmt = $this->db->prepare(" + SELECT config_value + FROM system_config + WHERE config_key = 'welcome_message' + LIMIT 1 + "); + $stmt->execute(); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($result) { + return $result['config_value']; + } + + // Mensaje por defecto + return "¡Hola! 👋 Bienvenido(a) a nuestro servicio de WhatsApp.\n\nEscribe MENÚ para ver las opciones."; + } + + /** + * Obtiene el mensaje fuera de horario + * @return string + */ + private function getOutOfHoursMessage(): string + { + $stmt = $this->db->prepare(" + SELECT config_value + FROM system_config + WHERE config_key = 'out_of_hours_message' + LIMIT 1 + "); + $stmt->execute(); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($result) { + return $result['config_value']; + } + + // Mensaje por defecto + return "GRACIAS POR COMUNICARSE CON NOSOTROS ⏳\n\nEn este momento NO nos encontramos disponibles.\n\nTe responderemos lo antes posible en nuestro horario de atención.\n\n💻 Este es un mensaje automático\nGracias por tu comprensión 😊"; + } + + /** + * Obtiene la configuración de horarios + * @param string $key + * @return string + */ + private function getBusinessHoursConfig(string $key): string + { + $stmt = $this->db->prepare(" + SELECT config_value + FROM system_config + WHERE config_key = ? + LIMIT 1 + "); + $stmt->execute([$key]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + return $result ? $result['config_value'] : ''; + } + + /** + * Verifica si una hora está dentro de un rango + * @param string $currentTime Formato HH:mm + * @param string $rangesString Formato "HH:mm-HH:mm,HH:mm-HH:mm" + * @return bool + */ + private function isTimeInRange(string $currentTime, string $rangesString): bool + { + if (empty($rangesString) || $rangesString === 'closed') { + return false; + } + + // Dividir por comas para múltiples rangos (ej: "7:00-12:00,14:00-17:00") + $ranges = explode(',', $rangesString); + + foreach ($ranges as $range) { + $times = explode('-', trim($range)); + if (count($times) !== 2) { + continue; + } + + $start = trim($times[0]); + $end = trim($times[1]); + + if ($currentTime >= $start && $currentTime <= $end) { + return true; + } + } + + return false; + } + + /** + * Obtiene información detallada del horario actual + * @return array + */ + public function getCurrentStatus(): array + { + $now = new DateTime('now', new DateTimeZone('America/Bogota')); + $dayOfWeek = (int)$now->format('N'); + $dayName = $this->getDayName($dayOfWeek); + $currentTime = $now->format('H:i'); + $isOpen = $this->isBusinessHours(); + + return [ + 'is_open' => $isOpen, + 'current_day' => $dayName, + 'current_time' => $currentTime, + 'day_of_week' => $dayOfWeek, + 'timezone' => 'America/Bogota' + ]; + } + + /** + * Obtiene el nombre del día en español + * @param int $dayNumber 1-7 (1=Lunes) + * @return string + */ + private function getDayName(int $dayNumber): string + { + $days = [ + 1 => 'Lunes', + 2 => 'Martes', + 3 => 'Miércoles', + 4 => 'Jueves', + 5 => 'Viernes', + 6 => 'Sábado', + 7 => 'Domingo' + ]; + + return $days[$dayNumber] ?? 'Desconocido'; + } + + /** + * Determina si se debe mostrar el mensaje de fuera de horario + * @param string $phoneNumber + * @return bool + */ + public function shouldShowOutOfHoursMessage(string $phoneNumber): bool + { + if ($this->isBusinessHours()) { + return false; + } + + // Verificar si ya se le envió el mensaje fuera de horario hoy + $stmt = $this->db->prepare(" + SELECT id + FROM conversations + WHERE phone_number = ? + AND message_type = 'outgoing' + AND message_text LIKE '%NO nos encontramos disponibles%' + AND DATE(created_at) = CURDATE() + LIMIT 1 + "); + $stmt->execute([$phoneNumber]); + + // Si ya se le envió hoy, no enviar de nuevo + return $stmt->fetch() === false; + } +} diff --git a/services/ConversationStateService.php b/services/ConversationStateService.php new file mode 100644 index 0000000..0fdb6f1 --- /dev/null +++ b/services/ConversationStateService.php @@ -0,0 +1,293 @@ +db = Database::getInstance(); + } + + /** + * Obtiene el estado actual de un usuario + * @param string $phoneNumber + * @return array|null + */ + public function getState(string $phoneNumber): ?array + { + $stmt = $this->db->prepare(" + SELECT * FROM user_states + WHERE phone_number = ? + ORDER BY id DESC + LIMIT 1 + "); + $stmt->execute([$phoneNumber]); + $state = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($state && $state['state_data']) { + $state['state_data'] = json_decode($state['state_data'], true); + } + + return $state ?: null; + } + + /** + * Establece un nuevo estado para el usuario + * @param string $phoneNumber + * @param string $state + * @param array $data Datos adicionales del estado + * @return bool + */ + public function setState(string $phoneNumber, string $state, array $data = []): bool + { + $stmt = $this->db->prepare(" + INSERT INTO user_states (phone_number, state, state_data, created_at, updated_at) + VALUES (?, ?, ?, NOW(), NOW()) + ON DUPLICATE KEY UPDATE + state = VALUES(state), + state_data = VALUES(state_data), + updated_at = NOW() + "); + + $stateDataJson = !empty($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : null; + + return $stmt->execute([$phoneNumber, $state, $stateDataJson]); + } + + /** + * Actualiza los datos del estado actual sin cambiar el estado + * @param string $phoneNumber + * @param array $data + * @return bool + */ + public function updateStateData(string $phoneNumber, array $data): bool + { + $currentState = $this->getState($phoneNumber); + + if (!$currentState) { + return false; + } + + $existingData = $currentState['state_data'] ?? []; + $mergedData = array_merge($existingData, $data); + + $stmt = $this->db->prepare(" + UPDATE user_states + SET state_data = ?, updated_at = NOW() + WHERE phone_number = ? + "); + + return $stmt->execute([ + json_encode($mergedData, JSON_UNESCAPED_UNICODE), + $phoneNumber + ]); + } + + /** + * Obtiene datos específicos del estado + * @param string $phoneNumber + * @param string $key + * @return mixed + */ + public function getStateData(string $phoneNumber, string $key) + { + $state = $this->getState($phoneNumber); + + if (!$state || !isset($state['state_data'])) { + return null; + } + + return $state['state_data'][$key] ?? null; + } + + /** + * Limpia el estado del usuario + * @param string $phoneNumber + * @return bool + */ + public function clearState(string $phoneNumber): bool + { + $stmt = $this->db->prepare(" + DELETE FROM user_states + WHERE phone_number = ? + "); + + return $stmt->execute([$phoneNumber]); + } + + /** + * Verifica si el usuario está en un flujo específico + * @param string $phoneNumber + * @param string $state + * @return bool + */ + public function isInState(string $phoneNumber, string $state): bool + { + $currentState = $this->getState($phoneNumber); + return $currentState && $currentState['state'] === $state; + } + + /** + * Obtiene el contador de intentos + * @param string $phoneNumber + * @return int + */ + public function getAttemptCount(string $phoneNumber): int + { + return (int)($this->getStateData($phoneNumber, 'attempt_count') ?? 0); + } + + /** + * Incrementa el contador de intentos + * @param string $phoneNumber + * @return int Nuevo número de intentos + */ + public function incrementAttempts(string $phoneNumber): int + { + $attempts = $this->getAttemptCount($phoneNumber) + 1; + $this->updateStateData($phoneNumber, ['attempt_count' => $attempts]); + return $attempts; + } + + /** + * Resetea el contador de intentos + * @param string $phoneNumber + * @return bool + */ + public function resetAttempts(string $phoneNumber): bool + { + return $this->updateStateData($phoneNumber, ['attempt_count' => 0]); + } + + /** + * Guarda datos temporales del agendamiento + * @param string $phoneNumber + * @param array $appointmentData + * @return bool + */ + public function saveAppointmentData(string $phoneNumber, array $appointmentData): bool + { + return $this->updateStateData($phoneNumber, [ + 'appointment' => $appointmentData, + 'appointment_started_at' => date('Y-m-d H:i:s') + ]); + } + + /** + * Obtiene datos del agendamiento en curso + * @param string $phoneNumber + * @return array|null + */ + public function getAppointmentData(string $phoneNumber): ?array + { + return $this->getStateData($phoneNumber, 'appointment'); + } + + /** + * Verifica si el estado ha expirado (más de 30 minutos de inactividad) + * @param string $phoneNumber + * @return bool + */ + public function isStateExpired(string $phoneNumber): bool + { + $state = $this->getState($phoneNumber); + + if (!$state) { + return false; + } + + $updatedAt = strtotime($state['updated_at']); + $now = time(); + $diffMinutes = ($now - $updatedAt) / 60; + + // Expirar después de 30 minutos + return $diffMinutes > 30; + } + + /** + * Limpia estados expirados de todos los usuarios + * @return int Número de estados eliminados + */ + public function cleanExpiredStates(): int + { + $stmt = $this->db->prepare(" + DELETE FROM user_states + WHERE updated_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE) + AND state NOT IN (?, ?) + "); + + $stmt->execute([ + self::STATE_SCHEDULED, + self::STATE_WITH_ADVISOR + ]); + + return $stmt->rowCount(); + } + + /** + * Obtiene el menú actual del usuario + * @param string $phoneNumber + * @return int|null ID del menú + */ + public function getCurrentMenuId(string $phoneNumber): ?int + { + return $this->getStateData($phoneNumber, 'current_menu_id'); + } + + /** + * Establece el menú actual + * @param string $phoneNumber + * @param int $menuId + * @return bool + */ + public function setCurrentMenu(string $phoneNumber, int $menuId): bool + { + return $this->updateStateData($phoneNumber, ['current_menu_id' => $menuId]); + } + + /** + * Marca la conversación para transferir a asesor + * @param string $phoneNumber + * @param string $reason + * @return bool + */ + public function markForTransfer(string $phoneNumber, string $reason = ''): bool + { + return $this->setState($phoneNumber, self::STATE_WITH_ADVISOR, [ + 'transfer_reason' => $reason, + 'transferred_at' => date('Y-m-d H:i:s') + ]); + } + + /** + * Verifica si el usuario está esperando asesor + * @param string $phoneNumber + * @return bool + */ + public function isWaitingForAdvisor(string $phoneNumber): bool + { + return $this->isInState($phoneNumber, self::STATE_WITH_ADVISOR); + } +} diff --git a/services/MenuService.php b/services/MenuService.php new file mode 100644 index 0000000..bca967d --- /dev/null +++ b/services/MenuService.php @@ -0,0 +1,222 @@ +db = Database::getInstance(); + } + + /** + * Obtiene un menú por su ID + * @param int $menuId + * @return array|null + */ + public function getMenu(int $menuId): ?array + { + $stmt = $this->db->prepare(" + SELECT * FROM menus + WHERE id = ? + AND status = 'active' + LIMIT 1 + "); + $stmt->execute([$menuId]); + $menu = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$menu) { + return null; + } + + // Obtener opciones del menú + $menu['options'] = $this->getMenuOptions($menuId); + + return $menu; + } + + /** + * Obtiene un menú por su clave + * @param string $menuKey + * @return array|null + */ + public function getMenuByKey(string $menuKey): ?array + { + $stmt = $this->db->prepare(" + SELECT * FROM menus + WHERE menu_key = ? + AND status = 'active' + LIMIT 1 + "); + $stmt->execute([$menuKey]); + $menu = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$menu) { + return null; + } + + $menu['options'] = $this->getMenuOptions($menu['id']); + + return $menu; + } + + /** + * Obtiene las opciones de un menú + * @param int $menuId + * @return array + */ + public function getMenuOptions(int $menuId): array + { + $stmt = $this->db->prepare(" + SELECT * FROM menu_options + WHERE menu_id = ? + AND is_active = 1 + ORDER BY option_order ASC + "); + $stmt->execute([$menuId]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Obtiene el ID del menú principal + * @return int|null + */ + public function getMainMenuId(): ?int + { + $stmt = $this->db->prepare(" + SELECT id FROM menus + WHERE menu_key = 'lab_menu_principal' + AND status = 'active' + LIMIT 1 + "); + $stmt->execute(); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + return $result ? (int)$result['id'] : null; + } + + /** + * Procesa una opción seleccionada del menú + * @param int $menuId + * @param string $optionKey + * @return array|null + */ + public function processMenuOption(int $menuId, string $optionKey): ?array + { + $stmt = $this->db->prepare(" + SELECT * FROM menu_options + WHERE menu_id = ? + AND option_key = ? + AND is_active = 1 + LIMIT 1 + "); + $stmt->execute([$menuId, $optionKey]); + $option = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$option) { + return null; + } + + return [ + 'type' => $option['response_type'], + 'text' => $option['response_text'], + 'next_menu_id' => $option['next_menu_id'] + ]; + } + + /** + * Formatea un menú con su welcome_message y opciones + * @param int $menuId + * @return string + */ + public function formatMenuMessage(int $menuId): string + { + $menu = $this->getMenu($menuId); + + if (!$menu) { + return "❌ Menú no disponible."; + } + + $message = $menu['welcome_message']; + + // Si el welcome_message no incluye las opciones, agregarlas + if (!empty($menu['options']) && strpos($message, '1️⃣') === false) { + $message .= "\n\n"; + foreach ($menu['options'] as $option) { + $emoji = $this->getNumberEmoji($option['option_key']); + $message .= "$emoji {$option['option_text']}\n"; + } + } + + return $message; + } + + /** + * Obtiene el emoji de número para una opción + * @param string $number + * @return string + */ + private function getNumberEmoji(string $number): string + { + $emojis = [ + '0' => '0️⃣', + '1' => '1️⃣', + '2' => '2️⃣', + '3' => '3️⃣', + '4' => '4️⃣', + '5' => '5️⃣', + '6' => '6️⃣', + '7' => '7️⃣', + '8' => '8️⃣', + '9' => '9️⃣' + ]; + + return $emojis[$number] ?? $number; + } + + /** + * Obtiene todos los menús activos + * @return array + */ + public function getAllMenus(): array + { + $stmt = $this->db->prepare(" + SELECT m.*, + (SELECT COUNT(*) FROM menu_options WHERE menu_id = m.id AND is_active = 1) as options_count + FROM menus m + WHERE m.status = 'active' + ORDER BY m.created_at ASC + "); + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Obtiene el menú padre de un menú + * @param int $menuId + * @return array|null + */ + public function getParentMenu(int $menuId): ?array + { + $stmt = $this->db->prepare(" + SELECT parent_menu_id FROM menus + WHERE id = ? + LIMIT 1 + "); + $stmt->execute([$menuId]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$result || !$result['parent_menu_id']) { + return null; + } + + return $this->getMenu($result['parent_menu_id']); + } +} diff --git a/services/NLPService.php b/services/NLPService.php new file mode 100644 index 0000000..8d5a207 --- /dev/null +++ b/services/NLPService.php @@ -0,0 +1,377 @@ +db = Database::getInstance(); + } + + /** + * Procesa un mensaje y encuentra la mejor respuesta automática + * @param string $message + * @param string $phoneNumber + * @return array|null ['type' => 'autoresponse|menu|transfer', 'data' => array] + */ + public function processMessage(string $message, string $phoneNumber): ?array + { + // Normalizar mensaje + $normalizedMessage = $this->normalizeText($message); + + // 1. Verificar si es una opción de menú numérica + if (is_numeric(trim($message))) { + return $this->processMenuOption($message, $phoneNumber); + } + + // 2. Buscar respuesta automática por palabra clave + $response = $this->findAutoResponse($normalizedMessage); + + if ($response) { + return $this->formatResponse($response); + } + + // 3. Si no encuentra nada, devolver respuesta por defecto + return $this->getDefaultResponse(); + } + + /** + * Normaliza el texto para comparación + * @param string $text + * @return string + */ + private function normalizeText(string $text): string + { + // Convertir a minúsculas + $text = mb_strtolower($text, 'UTF-8'); + + // Remover acentos + $text = $this->removeAccents($text); + + // Remover emojis y caracteres especiales (excepto espacios y comas) + $text = preg_replace('/[^\p{L}\p{N}\s,]/u', '', $text); + + // Remover espacios extras + $text = preg_replace('/\s+/', ' ', $text); + + return trim($text); + } + + /** + * Remueve acentos de un texto + * @param string $text + * @return string + */ + private function removeAccents(string $text): string + { + $accents = [ + 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', + 'Á' => 'a', 'É' => 'e', 'Í' => 'i', 'Ó' => 'o', 'Ú' => 'u', + 'ñ' => 'n', 'Ñ' => 'n', 'ü' => 'u', 'Ü' => 'u' + ]; + + return strtr($text, $accents); + } + + /** + * Busca la mejor respuesta automática para el mensaje + * @param string $normalizedMessage + * @return array|null + */ + private function findAutoResponse(string $normalizedMessage): ?array + { + // Obtener todas las respuestas automáticas activas ordenadas por prioridad + $stmt = $this->db->prepare(" + SELECT * FROM autoresponses + WHERE is_active = 1 + AND trigger_type != 'welcome' + ORDER BY priority DESC, id ASC + "); + $stmt->execute(); + $autoResponses = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($autoResponses as $response) { + if ($this->matchesTrigger($normalizedMessage, $response)) { + return $response; + } + } + + return null; + } + + /** + * Verifica si un mensaje coincide con un trigger + * @param string $message + * @param array $response + * @return bool + */ + private function matchesTrigger(string $message, array $response): bool + { + $triggerType = $response['trigger_type']; + $triggerValue = $response['trigger_value']; + + switch ($triggerType) { + case 'keyword': + return $this->matchesKeywords($message, $triggerValue); + + case 'contains': + return $this->containsPhrase($message, $triggerValue); + + case 'exact': + return $this->exactMatch($message, $triggerValue); + + case 'default': + return true; // Siempre coincide (pero tiene prioridad baja) + + default: + return false; + } + } + + /** + * Verifica si el mensaje contiene alguna palabra clave + * @param string $message + * @param string $keywordsString Palabras separadas por comas + * @return bool + */ + private function matchesKeywords(string $message, string $keywordsString): bool + { + if (empty($keywordsString)) { + return false; + } + + $keywords = array_map('trim', explode(',', $keywordsString)); + $keywords = array_map([$this, 'normalizeText'], $keywords); + + $messageWords = explode(' ', $message); + + foreach ($keywords as $keyword) { + // Verificar si la palabra clave completa está en el mensaje + if (strpos($message, $keyword) !== false) { + return true; + } + + // O si alguna palabra del mensaje coincide exactamente + if (in_array($keyword, $messageWords)) { + return true; + } + } + + return false; + } + + /** + * Verifica si el mensaje contiene una frase + * @param string $message + * @param string $phrase + * @return bool + */ + private function containsPhrase(string $message, string $phrase): bool + { + $normalizedPhrase = $this->normalizeText($phrase); + return strpos($message, $normalizedPhrase) !== false; + } + + /** + * Verifica coincidencia exacta + * @param string $message + * @param string $exact + * @return bool + */ + private function exactMatch(string $message, string $exact): bool + { + $normalizedExact = $this->normalizeText($exact); + return $message === $normalizedExact; + } + + /** + * Procesa una opción numérica de menú + * @param string $option + * @param string $phoneNumber + * @return array|null + */ + private function processMenuOption(string $option, string $phoneNumber): ?array + { + $stateService = new ConversationStateService(); + $currentMenuId = $stateService->getCurrentMenuId($phoneNumber); + + if (!$currentMenuId) { + // Si no hay menú actual, buscar el menú principal + $stmt = $this->db->prepare(" + SELECT id FROM menus + WHERE menu_key = 'lab_menu_principal' + AND status = 'active' + LIMIT 1 + "); + $stmt->execute(); + $menu = $stmt->fetch(PDO::FETCH_ASSOC); + $currentMenuId = $menu ? $menu['id'] : null; + } + + if (!$currentMenuId) { + return null; + } + + // Buscar la opción en el menú actual + $stmt = $this->db->prepare(" + SELECT * FROM menu_options + WHERE menu_id = ? + AND option_key = ? + AND is_active = 1 + LIMIT 1 + "); + $stmt->execute([$currentMenuId, trim($option)]); + $menuOption = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$menuOption) { + return null; + } + + return $this->formatMenuOption($menuOption); + } + + /** + * Formatea una respuesta automática + * @param array $response + * @return array + */ + private function formatResponse(array $response): array + { + $result = [ + 'type' => $response['response_type'], + 'priority' => $response['priority'] + ]; + + switch ($response['response_type']) { + case 'text': + $result['message'] = $response['response_text']; + break; + + case 'menu': + $result['menu_id'] = $response['menu_id']; + break; + + case 'template': + $result['template_name'] = $response['template_name'] ?? null; + break; + + case 'transfer': + $result['message'] = $response['response_text']; + $result['transfer'] = true; + break; + } + + return $result; + } + + /** + * Formatea una opción de menú + * @param array $menuOption + * @return array + */ + private function formatMenuOption(array $menuOption): array + { + $result = [ + 'type' => $menuOption['response_type'] + ]; + + switch ($menuOption['response_type']) { + case 'text': + $result['message'] = $menuOption['response_text']; + break; + + case 'menu': + $result['menu_id'] = $menuOption['next_menu_id']; + break; + + case 'transfer': + $result['message'] = $menuOption['response_text']; + $result['transfer'] = true; + break; + } + + return $result; + } + + /** + * Obtiene la respuesta por defecto + * @return array + */ + private function getDefaultResponse(): array + { + $stmt = $this->db->prepare(" + SELECT * FROM autoresponses + WHERE trigger_type = 'default' + AND is_active = 1 + ORDER BY priority DESC + LIMIT 1 + "); + $stmt->execute(); + $response = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($response) { + return $this->formatResponse($response); + } + + // Respuesta fallback hardcoded + return [ + 'type' => 'text', + 'message' => "❓ No he comprendido tu mensaje.\n\nEscribe MENÚ para ver las opciones disponibles." + ]; + } + + /** + * Detecta el sentimiento del mensaje (básico) + * @param string $message + * @return string positive|negative|neutral + */ + public function detectSentiment(string $message): string + { + $normalized = $this->normalizeText($message); + + // Palabras positivas + $positiveWords = ['gracias', 'excelente', 'perfecto', 'bien', 'bueno', 'genial', 'ok', 'vale']; + + // Palabras negativas / frustración + $negativeWords = ['no funciona', 'problema', 'error', 'mal', 'ayuda', 'no entiendo', 'dificil', 'complicado']; + + $positiveCount = 0; + $negativeCount = 0; + + foreach ($positiveWords as $word) { + if (strpos($normalized, $word) !== false) { + $positiveCount++; + } + } + + foreach ($negativeWords as $word) { + if (strpos($normalized, $word) !== false) { + $negativeCount++; + } + } + + if ($negativeCount > $positiveCount) { + return 'negative'; + } elseif ($positiveCount > 0) { + return 'positive'; + } + + return 'neutral'; + } + + /** + * Determina si el mensaje indica frustración (para escalar a asesor) + * @param string $message + * @return bool + */ + public function detectsFrustration(string $message): bool + { + return $this->detectSentiment($message) === 'negative'; + } +} diff --git a/services/PatientDataValidator.php b/services/PatientDataValidator.php new file mode 100644 index 0000000..c652fe1 --- /dev/null +++ b/services/PatientDataValidator.php @@ -0,0 +1,315 @@ + bool, 'errors' => array, 'data' => array] + */ + public static function validateAppointmentData(array $data): array + { + $errors = []; + $validData = []; + + // Nombre completo + if (empty($data['nombre'])) { + $errors[] = "Falta el nombre completo"; + } elseif (!self::validateFullName($data['nombre'])) { + $errors[] = "El nombre debe tener al menos nombre y apellido"; + } else { + $validData['nombre'] = self::sanitizeName($data['nombre']); + } + + // Documento + if (empty($data['documento'])) { + $errors[] = "Falta el número de documento"; + } elseif (!self::validateDocument($data['documento'])) { + $errors[] = "El documento debe tener entre 6 y 11 dígitos"; + } else { + $validData['documento'] = self::sanitizeDocument($data['documento']); + } + + // Dirección + if (empty($data['direccion'])) { + $errors[] = "Falta la dirección completa"; + } elseif (strlen($data['direccion']) < 10) { + $errors[] = "La dirección debe ser más específica (mínimo 10 caracteres)"; + } else { + $validData['direccion'] = trim($data['direccion']); + } + + // Teléfono + if (empty($data['telefono'])) { + $errors[] = "Falta el número de celular"; + } elseif (!self::validatePhone($data['telefono'])) { + $errors[] = "El número de celular debe tener 10 dígitos y comenzar con 3"; + } else { + $validData['telefono'] = self::sanitizePhone($data['telefono']); + } + + // Email + if (empty($data['email'])) { + $errors[] = "Falta el correo electrónico"; + } elseif (!self::validateEmail($data['email'])) { + $errors[] = "El correo electrónico no es válido"; + } else { + $validData['email'] = strtolower(trim($data['email'])); + } + + // Tipo (particular/seguro) + if (empty($data['tipo'])) { + $errors[] = "Falta indicar si es particular o por seguro"; + } else { + $tipo = strtolower(trim($data['tipo'])); + if (strpos($tipo, 'particular') !== false) { + $validData['tipo'] = 'particular'; + } elseif (strpos($tipo, 'seguro') !== false || strpos($tipo, 'eps') !== false) { + $validData['tipo'] = 'seguro'; + // Extraer nombre del seguro si lo menciona + $validData['nombre_seguro'] = self::extractInsuranceName($data['tipo']); + } else { + $errors[] = "Debe indicar 'particular' o 'seguro'"; + } + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors, + 'data' => $validData + ]; + } + + /** + * Valida un nombre completo + * @param string $name + * @return bool + */ + public static function validateFullName(string $name): bool + { + $name = trim($name); + + // Debe tener al menos 2 palabras + $words = preg_split('/\s+/', $name); + if (count($words) < 2) { + return false; + } + + // Solo debe contener letras y espacios + return preg_match('/^[a-záéíóúñA-ZÁÉÍÓÚÑ\s]+$/u', $name); + } + + /** + * Valida un número de documento + * @param string $document + * @return bool + */ + public static function validateDocument(string $document): bool + { + $doc = preg_replace('/[^0-9]/', '', $document); + $length = strlen($doc); + + return $length >= 6 && $length <= 11; + } + + /** + * Valida un número de teléfono colombiano + * @param string $phone + * @return bool + */ + public static function validatePhone(string $phone): bool + { + $phone = preg_replace('/[^0-9]/', '', $phone); + + // Debe tener 10 dígitos y comenzar con 3 + return strlen($phone) === 10 && $phone[0] === '3'; + } + + /** + * Valida un correo electrónico + * @param string $email + * @return bool + */ + public static function validateEmail(string $email): bool + { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + } + + /** + * Sanitiza un nombre + * @param string $name + * @return string + */ + public static function sanitizeName(string $name): string + { + // Convertir a formato título (Primera Letra Mayúscula) + $name = mb_convert_case(trim($name), MB_CASE_TITLE, 'UTF-8'); + + // Remover espacios extras + $name = preg_replace('/\s+/', ' ', $name); + + return $name; + } + + /** + * Sanitiza un documento + * @param string $document + * @return string + */ + public static function sanitizeDocument(string $document): string + { + return preg_replace('/[^0-9]/', '', $document); + } + + /** + * Sanitiza un teléfono + * @param string $phone + * @return string + */ + public static function sanitizePhone(string $phone): string + { + $phone = preg_replace('/[^0-9]/', '', $phone); + + // Si tiene 13 dígitos y empieza con 57, quitar el 57 + if (strlen($phone) === 12 && substr($phone, 0, 2) === '57') { + $phone = substr($phone, 2); + } + + return $phone; + } + + /** + * Extrae el nombre del seguro del texto + * @param string $text + * @return string|null + */ + private static function extractInsuranceName(string $text): ?string + { + $insurances = [ + 'colsanitas' => 'Colsanitas', + 'sanitas' => 'Sanitas', + 'sura' => 'Sura', + 'compensar' => 'Compensar', + 'salud total' => 'Salud Total', + 'famisanar' => 'Famisanar', + 'nueva eps' => 'Nueva EPS', + 'coomeva' => 'Coomeva' + ]; + + $textLower = strtolower($text); + + foreach ($insurances as $key => $name) { + if (strpos($textLower, $key) !== false) { + return $name; + } + } + + return null; + } + + /** + * Parsea datos del paciente desde un mensaje de texto + * @param string $message + * @return array + */ + public static function parsePatientDataFromMessage(string $message): array + { + $data = []; + + // Dividir por líneas + $lines = explode("\n", $message); + + foreach ($lines as $line) { + $line = trim($line); + + if (empty($line)) { + continue; + } + + // Detectar nombre (primera línea larga con letras) + if (empty($data['nombre']) && preg_match('/^[a-záéíóúñA-ZÁÉÍÓÚÑ\s]{5,}$/u', $line)) { + $data['nombre'] = $line; + continue; + } + + // Detectar documento (números de 6-11 dígitos) + if (empty($data['documento']) && preg_match('/(\d{6,11})/', $line, $matches)) { + $data['documento'] = $matches[1]; + continue; + } + + // Detectar teléfono (10 dígitos comenzando con 3) + if (empty($data['telefono']) && preg_match('/(3\d{9})/', $line, $matches)) { + $data['telefono'] = $matches[1]; + continue; + } + + // Detectar email + if (empty($data['email']) && preg_match('/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z]+)/', $line, $matches)) { + $data['email'] = $matches[1]; + continue; + } + + // Detectar dirección (contiene calle, carrera, etc.) + if (empty($data['direccion']) && preg_match('/(calle|carrera|cra|cl|diagonal|transversal|avenida|av)/i', $line)) { + $data['direccion'] = $line; + continue; + } + + // Detectar tipo (particular/seguro) + if (empty($data['tipo']) && preg_match('/(particular|seguro|eps)/i', $line)) { + $data['tipo'] = $line; + continue; + } + } + + return $data; + } + + /** + * Genera un mensaje con los errores de validación + * @param array $errors + * @return string + */ + public static function formatValidationErrors(array $errors): string + { + $message = "⚠️ Por favor corrige los siguientes datos:\n\n"; + + foreach ($errors as $error) { + $message .= "❌ $error\n"; + } + + $message .= "\nEnvía la información completa de nuevo."; + + return $message; + } + + /** + * Genera un resumen de los datos validados + * @param array $data + * @return string + */ + public static function formatDataSummary(array $data): string + { + $message = "📋 DATOS RECIBIDOS:\n\n"; + $message .= "👤 Nombre: {$data['nombre']}\n"; + $message .= "🆔 Documento: {$data['documento']}\n"; + $message .= "📍 Dirección: {$data['direccion']}\n"; + $message .= "📱 Teléfono: {$data['telefono']}\n"; + $message .= "📧 Email: {$data['email']}\n"; + $message .= "💳 Tipo: {$data['tipo']}"; + + if (isset($data['nombre_seguro'])) { + $message .= " ({$data['nombre_seguro']})"; + } + + $message .= "\n\n¿Los datos son correctos?\nResponde: *SÍ* o *NO*"; + + return $message; + } +} diff --git a/setup_laboratorio_ximena.sql b/setup_laboratorio_ximena.sql new file mode 100644 index 0000000..e04859d --- /dev/null +++ b/setup_laboratorio_ximena.sql @@ -0,0 +1,732 @@ +-- ===================================================== +-- SETUP COMPLETO: LABORATORIO XIMENA CAICEDO +-- Sistema de Chatbot WhatsApp con Flujos Completos +-- Fecha: 13 de enero de 2026 +-- ===================================================== + +-- Limpiar datos previos (opcional) +-- DELETE FROM menu_options WHERE menu_id IN (SELECT id FROM menus WHERE menu_key LIKE 'lab_%'); +-- DELETE FROM menus WHERE menu_key LIKE 'lab_%'; +-- DELETE FROM autoresponses WHERE trigger_type != 'system'; + +-- ===================================================== +-- 1. MENÚ PRINCIPAL +-- ===================================================== + +INSERT INTO menus (name, menu_key, description, welcome_message, status, created_at) VALUES +( + 'Menú Principal - Lab Ximena Caicedo', + 'lab_menu_principal', + 'Menú principal del laboratorio con todas las opciones de servicio', + '¡Hola! 👋 Bienvenido(a) a Laboratorio Ximena Caicedo + +Por favor, selecciona una opción escribiendo el número: + +1️⃣ Agendar toma a domicilio +2️⃣ Consultar resultados +3️⃣ Información sobre exámenes +4️⃣ Ubicación y horarios +5️⃣ Hablar con un asesor + +*Horario de atención:* +📅 Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM +📅 Sábados: 8:00 AM - 11:00 AM', + 'active', + NOW() +); + +-- Obtener el ID del menú principal +SET @menu_principal_id = LAST_INSERT_ID(); + +-- Opciones del Menú Principal +INSERT INTO menu_options (menu_id, option_key, option_order, option_text, response_type, response_text, next_menu_id, is_active, created_at) VALUES +( + @menu_principal_id, + '1', + 1, + 'Agendar toma a domicilio', + 'text', + '📍 TOMA DE MUESTRAS A DOMICILIO + +Para agendar tu domicilio, necesitamos la siguiente información en UN SOLO MENSAJE: + +📄 Enviar una foto clara de la orden médica + +Datos del paciente: +✓ Nombre completo: +✓ Número de documento: +✓ Foto del documento de identidad: +✓ Dirección completa: +✓ Número de celular: +✓ Correo electrónico: +✓ Indicar si es por algún seguro o particular + +*IMPORTANTE:* +• Se atiende por orden de llegada +• Enviar solicitud solo una vez +• Pronto te daremos respuesta + +💵 Valor del domicilio: $27.000', + NULL, + 1, + NOW() +), +( + @menu_principal_id, + '2', + 2, + 'Consultar resultados', + 'text', + '📊 CONSULTA DE RESULTADOS + +Para consultar tus resultados, sigue estos pasos: + +1️⃣ Ingresa a la página web: +http://www.resultadoslab.com.co/resultadoslab/vistas/login.php?id=eGltY2FpY2Vkbw== + +2️⃣ Credenciales de acceso: +• Usuario: Tu número de documento +• Clave: Los últimos 4 dígitos de tu documento + +3️⃣ Da clic en los cuadros rojo y naranja que aparecen en opciones + +4️⃣ Descarga los archivos PDF + +*NOTA:* Se recomienda descargar desde un computador + +⚠️ Estamos actualizando nuestra página web. Gracias por tu comprensión. + +¿Tienes problemas para descargar tus resultados? +Responde: *AYUDA RESULTADOS*', + NULL, + 1, + NOW() +), +( + @menu_principal_id, + '3', + 3, + 'Información sobre exámenes', + 'menu', + NULL, + NULL, -- Se actualizará después de crear el submenú + 1, + NOW() +), +( + @menu_principal_id, + '4', + 4, + 'Ubicación y horarios', + 'text', + '📍 UBICACIÓN Y HORARIOS + +*DIRECCIÓN:* +Calle 21 #0-26, Barrio Blanco +Cúcuta, Norte de Santander + +📌 Ver en Google Maps: +https://maps.google.com/maps/search/Laboratorio%20Ximena%20Caicedo/@7.877200126647949,-72.49710083007812,17z?hl=es + +*HORARIOS DE ATENCIÓN:* + +🏥 Toma de muestras en laboratorio: +📅 Lunes a viernes: 6:15 AM - 3:00 PM +📅 Sábados: Por orden de llegada + +📞 Atención telefónica/WhatsApp: +📅 Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM +📅 Sábados: 8:00 AM - 11:00 AM + +🏠 Tomas a domicilio: +📅 Lunes a viernes: 5:00 AM - 8:30 AM +💵 Valor: $27.000 + +*FORMAS DE PAGO:* +💵 Efectivo +💳 Tarjeta débito +🏥 Seguros médicos + +¿Necesitas agendar? Escribe MENÚ', + NULL, + 1, + NOW() +), +( + @menu_principal_id, + '5', + 5, + 'Hablar con un asesor', + 'transfer', + '👤 SOLICITUD DE ASESOR + +Un momento por favor, estamos conectando tu consulta con uno de nuestros asesores. + +En breve te atenderemos. + +*Tu consulta es importante para nosotros* 💙', + NULL, + 1, + NOW() +); + +-- ===================================================== +-- 2. SUBMENÚ DE EXÁMENES +-- ===================================================== + +INSERT INTO menus (name, menu_key, description, welcome_message, parent_menu_id, status, created_at) VALUES +( + 'Información de Exámenes', + 'lab_menu_examenes', + 'Submenú con información detallada de exámenes', + '🔬 INFORMACIÓN DE EXÁMENES + +Selecciona el tipo de información que necesitas: + +1️⃣ Pruebas de embarazo +2️⃣ Prueba de paternidad +3️⃣ Helicobacter pylori (en aliento) +4️⃣ Orina de 24 horas +5️⃣ Solicitar cotización +0️⃣ Volver al menú principal', + @menu_principal_id, + 'active', + NOW() +); + +SET @menu_examenes_id = LAST_INSERT_ID(); + +-- Actualizar la opción 3 del menú principal para que apunte a este submenú +UPDATE menu_options +SET next_menu_id = @menu_examenes_id +WHERE menu_id = @menu_principal_id AND option_key = '3'; + +-- Opciones del Submenú de Exámenes +INSERT INTO menu_options (menu_id, option_key, option_order, option_text, response_type, response_text, next_menu_id, is_active, created_at) VALUES +( + @menu_examenes_id, + '1', + 1, + 'Pruebas de embarazo', + 'text', + '🤰 PRUEBAS DE EMBARAZO EN SANGRE + +Ofrecemos dos tipos: + +1️⃣ *Prueba Cualitativa (Positivo/Negativo)* +💵 Valor: $28.000 +📅 Requisito: Mínimo 1 semana de retraso menstrual + +2️⃣ *Prueba Cuantitativa (Con valor de HCG)* +💵 Valor: $61.000 +📊 Esta prueba da un valor que se interpreta para saber las semanas de embarazo + +🕐 Horario: 6:15 AM - 3:00 PM (Lunes a viernes) +⏱️ Tiempo de resultado: 24-72 horas + +¿Deseas agendar? Escribe: *AGENDAR EMBARAZO*', + NULL, + 1, + NOW() +), +( + @menu_examenes_id, + '2', + 2, + 'Prueba de paternidad', + 'text', + '👨‍👩‍👧 PRUEBA DE PATERNIDAD + +📍 Ubicación: Calle 21 #0-26, Barrio Blanco, Cúcuta +https://maps.google.com/maps/search/Laboratorio%20Ximena%20Caicedo/@7.877200126647949,-72.49710083007812,17z?hl=es + +*DETALLES DEL EXAMEN:* +🧬 Muestra: Se realiza en saliva +👥 Deben asistir: Presunto padre, madre y el menor +💵 Valor: $760.000 +📅 Resultados: 12 a 15 días hábiles +🕐 Horario: Lunes a viernes de 6:15 AM a 3:00 PM +📝 Se toma por orden de llegada (sin cita previa) + +⚠️ *NO se realiza a domicilio* +Deben acercarse al laboratorio. + +¿Necesitas más información? Escribe *ASESOR*', + NULL, + 1, + NOW() +), +( + @menu_examenes_id, + '3', + 3, + 'Helicobacter pylori (en aliento)', + 'text', + '🫁 HELICOBACTER PYLORI EN ALIENTO + +*PREPARACIÓN IMPORTANTE:* + +⏰ Requisitos previos: +• 2 semanas sin tomar medicamentos para el estómago +• Sin protectores gástricos +• Sin probióticos +• Sin antibióticos +• Sin cualquier medicamento relacionado con el estómago + +📍 Día del examen: +• Debe venir en AYUNAS +• NO cepillarse los dientes +• Llegar entre 6:15 AM - 9:30 AM + +📅 Disponible de lunes a viernes + +¿Deseas cotización? Escribe: *COTIZAR HELICOBACTER*', + NULL, + 1, + NOW() +), +( + @menu_examenes_id, + '4', + 4, + 'Orina de 24 horas', + 'text', + '💧 INSTRUCCIONES: ORINA DE 24 HORAS + +*PASO A PASO:* + +1️⃣ *PRIMERA ORINA DE LA MAÑANA:* + Deséchela (NO la recoja) + +2️⃣ *A PARTIR DE LA SEGUNDA ORINA:* + Recoja TODA la orina del día + +3️⃣ *DURANTE LA NOCHE:* + Si se levanta a orinar, también debe recogerla + +4️⃣ *ALMACENAMIENTO:* + ⚠️ IMPORTANTE: Mantener la orina REFRIGERADA todo el tiempo para evitar contaminación + +5️⃣ *AL DÍA SIGUIENTE:* + Llevar el recipiente completo al laboratorio + +📍 Horario de recepción: 6:15 AM - 3:00 PM + +¿Tienes dudas? Escribe *ASESOR*', + NULL, + 1, + NOW() +), +( + @menu_examenes_id, + '5', + 5, + 'Solicitar cotización', + 'text', + '💰 SOLICITAR COTIZACIÓN + +Para cotizar tus exámenes, envía: + +📄 Foto CLARA de la orden médica +✓ Indica si es particular o por seguro +✓ Si es por seguro, indica cuál + +Te responderemos con: +• Valor de cada examen +• Valor total +• Indicaciones de preparación + +⏱️ Respuesta en horario de atención', + NULL, + 1, + NOW() +), +( + @menu_examenes_id, + '0', + 0, + 'Volver al menú principal', + 'menu', + NULL, + @menu_principal_id, + 1, + NOW() +); + +-- ===================================================== +-- 3. RESPUESTAS AUTOMÁTICAS POR PALABRAS CLAVE +-- ===================================================== + +-- Bienvenida (primer mensaje) +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'welcome', + NULL, + 'menu', + NULL, + @menu_principal_id, + 100, + 1, + NOW() +); + +-- Menú / Inicio +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'menu,menú,inicio,start,opciones', + 'menu', + NULL, + @menu_principal_id, + 90, + 1, + NOW() +); + +-- Precios +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'precio,valor,costo,cuanto,cuánto,precios,costos', + 'text', + '💰 INFORMACIÓN DE PRECIOS + +Para darte información exacta, necesitamos saber: + +1️⃣ ¿Qué examen necesitas? +2️⃣ ¿Es particular o por seguro? + +Puedes: +• Enviar foto de tu orden médica +• Escribir el nombre del examen +• Escribir *COTIZAR* para más información + +También puedes ver nuestros servicios frecuentes: +• EMBARAZO - Pruebas de embarazo +• PATERNIDAD - Prueba de paternidad +• DOMICILIO - Toma a domicilio', + NULL, + 80, + 1, + NOW() +); + +-- Resultados +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'resultado,resultados,descargar,reporte,examen', + 'text', + '📊 CONSULTA DE RESULTADOS + +Ingresa a: +http://www.resultadoslab.com.co/resultadoslab/vistas/login.php?id=eGltY2FpY2Vkbw== + +Usuario: Tu documento +Clave: Últimos 4 dígitos + +¿Problemas para descargar? +Escribe: *AYUDA RESULTADOS*', + NULL, + 85, + 1, + NOW() +); + +-- Horarios +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'horario,hora,cuando,cuándo,abierto,atienden,abren,cierran', + 'text', + '🕐 HORARIOS DE ATENCIÓN + +🏥 *Toma de muestras:* +Lunes a viernes: 6:15 AM - 3:00 PM + +📞 *Atención WhatsApp:* +Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM +Sábados: 8:00 AM - 11:00 AM + +🏠 *Domicilios:* +Lunes a viernes: 5:00 AM - 8:30 AM + +Para más información escribe: *UBICACION*', + NULL, + 85, + 1, + NOW() +); + +-- Domicilio +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'domicilio,casa,ir a mi casa,a casa,visita', + 'text', + '🏠 SERVICIO A DOMICILIO + +💵 Valor: $27.000 +⏰ Horario: 5:00 AM - 8:30 AM + +Para agendar escribe: 1 o *DOMICILIO*', + NULL, + 85, + 1, + NOW() +); + +-- Embarazo +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'embarazo,embarazada,gestación,gestacion,prueba embarazo', + 'text', + '🤰 PRUEBAS DE EMBARAZO + +💵 Cualitativa (Sí/No): $28.000 +💵 Cuantitativa (con valor): $61.000 + +📝 Requisito: Mínimo 1 semana de retraso + +Para más información escribe: *INFO EMBARAZO* +Para agendar escribe: *AGENDAR EMBARAZO*', + NULL, + 85, + 1, + NOW() +); + +-- Paternidad +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'paternidad,adn,paternity,prueba de paternidad', + 'text', + '👨‍👩‍👧 PRUEBA DE PATERNIDAD + +💵 Valor: $760.000 +📅 Resultado: 12-15 días +🏥 Se realiza en laboratorio (NO a domicilio) + +Para más información escribe: *INFO PATERNIDAD*', + NULL, + 85, + 1, + NOW() +); + +-- Ayuda con resultados +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'ayuda resultados,no puedo descargar,ayuda descargar,problema resultados', + 'text', + '🆘 AYUDA CON RESULTADOS + +Para ayudarte a descargar tus resultados, necesitamos: + +📸 Foto del documento de identidad del paciente + +*IMPORTANTE:* +Por política de seguridad del paciente, solo podemos enviar los resultados si nos envías la foto del documento. + +Si eres familiar del paciente, debes enviar el documento del titular de los exámenes.', + NULL, + 95, + 1, + NOW() +); + +-- Ubicación +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'ubicación,ubicacion,dirección,direccion,donde,dónde,como llego,cómo llego', + 'text', + '📍 UBICACIÓN + +Calle 21 #0-26, Barrio Blanco +Cúcuta, Norte de Santander + +📌 Google Maps: +https://maps.google.com/maps/search/Laboratorio%20Ximena%20Caicedo/@7.877200126647949,-72.49710083007812,17z?hl=es + +Para más información escribe: *HORARIOS*', + NULL, + 85, + 1, + NOW() +); + +-- Preparación / Ayunas +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'ayunas,preparación,preparacion,requisitos,como me preparo,cómo me preparo', + 'text', + '📋 PREPARACIÓN PARA EXÁMENES + +La preparación depende del examen que te van a realizar. + +¿Qué examen te van a tomar? + +Exámenes con preparación especial: +• PSA - Antígeno prostático +• Frotis vaginal +• Urocultivo +• Helicobacter pylori +• Orina de 24 horas +• Perfil lipídico (ayunas) + +Escribe el nombre del examen o envía foto de tu orden.', + NULL, + 85, + 1, + NOW() +); + +-- Asesor / Ayuda / Persona +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'asesor,persona,humano,ayuda,help,operador,hablar con alguien', + 'transfer', + '👤 SOLICITUD DE ASESOR + +Un momento por favor, estamos conectando tu consulta con uno de nuestros asesores. + +En breve te atenderemos. + +*Tu consulta es importante para nosotros* 💙', + NULL, + 70, + 1, + NOW() +); + +-- Agradecimientos +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'gracias,thanks,thank you,muchas gracias,ok gracias', + 'text', + '😊 ¡De nada! Con gusto. + +¿Necesitas algo más? +Escribe *MENÚ* para ver todas las opciones. + +Laboratorio Ximena Caicedo +Tu salud, nuestra prioridad 💙', + NULL, + 60, + 1, + NOW() +); + +-- Despedidas +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'adios,adiós,bye,chao,hasta luego,nos vemos', + 'text', + '👋 ¡Hasta pronto! + +Gracias por confiar en Laboratorio Ximena Caicedo. + +Si necesitas algo más, no dudes en escribirnos. + +¡Que tengas un excelente día! 😊', + NULL, + 60, + 1, + NOW() +); + +-- Saludos generales +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'keyword', + 'hola,buenos dias,buenas tardes,buenas noches,buen dia,hey,hi,hello', + 'menu', + NULL, + @menu_principal_id, + 75, + 1, + NOW() +); + +-- Respuesta por defecto (cuando no entiende) +INSERT INTO autoresponses (trigger_type, trigger_value, response_type, response_text, menu_id, priority, is_active, created_at) VALUES +( + 'default', + NULL, + 'menu', + '❓ No he comprendido tu mensaje + +Por favor selecciona una opción:', + @menu_principal_id, + 10, + 1, + NOW() +); + +-- ===================================================== +-- 4. ACTUALIZAR CONFIGURACIÓN DEL SISTEMA +-- ===================================================== + +-- Actualizar mensaje de bienvenida en system_config +UPDATE system_config +SET config_value = (SELECT welcome_message FROM menus WHERE menu_key = 'lab_menu_principal' LIMIT 1) +WHERE config_key = 'welcome_message'; + +-- Agregar nombre de empresa si no existe +INSERT INTO system_config (config_key, config_value, description, created_at) +VALUES ('business_name', 'Laboratorio Ximena Caicedo', 'Nombre de la empresa/laboratorio', NOW()) +ON DUPLICATE KEY UPDATE config_value = 'Laboratorio Ximena Caicedo'; + +-- Agregar horarios de atención +INSERT INTO system_config (config_key, config_value, description, created_at) +VALUES +('business_hours_weekday', '7:00-12:00,14:00-17:00', 'Horario de atención lunes a viernes (formato 24h)', NOW()), +('business_hours_saturday', '8:00-11:00', 'Horario de atención sábados (formato 24h)', NOW()), +('business_hours_sunday', 'closed', 'Horario de atención domingos', NOW()), +('out_of_hours_message', 'GRACIAS POR COMUNICARSE CON Laboratorio Ximena Caicedo ⏳\n\nEn este momento NO nos encontramos disponibles.\n\n*Horario de atención:*\n📅 Lunes a viernes: 7:00 AM - 12:00 PM y 2:00 PM - 5:00 PM\n📅 Sábados: 8:00 AM - 11:00 AM\n🚫 Domingos y festivos cerrado\n\nPor favor déjanos tu *Nombre* y tu *mensaje*, y con gusto te responderemos al retomar nuestro horario.\n\n💻 Este es un mensaje automático\nGracias por tu comprensión 😊', 'Mensaje automático fuera de horario', NOW()) +ON DUPLICATE KEY UPDATE config_value = VALUES(config_value); + +-- ===================================================== +-- CONFIRMACIÓN +-- ===================================================== + +SELECT + '✅ SETUP COMPLETADO' as status, + COUNT(DISTINCT m.id) as menus_creados, + COUNT(DISTINCT mo.id) as opciones_creadas, + COUNT(DISTINCT ar.id) as respuestas_automaticas +FROM menus m +LEFT JOIN menu_options mo ON m.id = mo.menu_id +LEFT JOIN autoresponses ar ON ar.is_active = 1 +WHERE m.menu_key LIKE 'lab_%'; + +-- Ver menús creados +SELECT + id, + name, + menu_key, + status, + (SELECT COUNT(*) FROM menu_options WHERE menu_id = m.id) as num_opciones +FROM menus m +WHERE menu_key LIKE 'lab_%' +ORDER BY created_at; + +-- Ver respuestas automáticas creadas +SELECT + id, + trigger_type, + trigger_value, + response_type, + priority, + is_active +FROM autoresponses +WHERE is_active = 1 +ORDER BY priority DESC, id; diff --git a/test.php b/test.php deleted file mode 100644 index e2bda81..0000000 --- a/test.php +++ /dev/null @@ -1,204 +0,0 @@ -🤖 Test del Sistema WhatsApp Bot"; - -// Verificar archivos principales -$files = [ - 'config/config.php' => 'Configuración principal', - 'classes/Database.php' => 'Clase Database', - 'services/WhatsAppService.php' => 'Servicio WhatsApp', - 'services/BotService.php' => 'Servicio Bot', - 'api/webhook.php' => 'Webhook principal', - 'database/schema.sql' => 'Esquema de BD', - 'index.php' => 'Página principal' -]; - -echo "

📂 Verificación de Archivos:

"; -echo ""; - -// Probar configuración -echo "

⚙️ Verificación de Configuración:

"; -try { - require_once 'config/config.php'; - echo "

✅ Configuración cargada correctamente

"; - echo ""; -} catch (Exception $e) { - echo "

❌ Error en configuración: " . $e->getMessage() . "

"; -} - -// Probar conexión a base de datos -echo "

💾 Verificación de Base de Datos:

"; -try { - $db = Database::getInstance(); - echo "

✅ Conexión a base de datos exitosa

"; - - // Verificar tablas principales - $tables = ['users', 'conversations', 'menus', 'menu_options', 'system_config']; - echo ""; - -} catch (Exception $e) { - echo "

❌ Error de conexión: " . $e->getMessage() . "

"; -} - -// Probar servicios -echo "

🔧 Verificación de Servicios:

"; -try { - $whatsappService = new WhatsAppService(); - echo "

✅ WhatsAppService inicializado

"; - - $botService = new BotService(); - echo "

✅ BotService inicializado

"; - -} catch (Exception $e) { - echo "

❌ Error en servicios: " . $e->getMessage() . "

"; -} - -// Probar APIs -echo "

🌐 Verificación de APIs:

"; -$apis = [ - 'api/get_stats.php', - 'api/get_users.php', - 'api/get_menus.php', - 'api/get_conversations.php', - 'api/webhook.php' -]; - -echo ""; - -// Probar funcionalidad básica del bot -echo "

🤖 Test Básico del Bot:

"; -try { - // Crear usuario de prueba - $testPhone = '573000000000'; - - // Verificar si el usuario existe - $existingUser = $db->fetch( - "SELECT * FROM users WHERE phone_number = :phone", - ['phone' => $testPhone] - ); - - if (!$existingUser) { - $userId = $db->insert('users', [ - 'phone_number' => $testPhone, - 'name' => 'Usuario de Prueba', - 'status' => 'active', - 'created_at' => date('Y-m-d H:i:s') - ]); - echo "

✅ Usuario de prueba creado (ID: {$userId})

"; - } else { - echo "

✅ Usuario de prueba ya existe

"; - } - - // Probar menú principal - $mainMenu = $db->fetch( - "SELECT * FROM menus WHERE is_root = 1 AND is_active = 1 LIMIT 1" - ); - - if ($mainMenu) { - echo "

✅ Menú principal configurado: " . $mainMenu['title'] . "

"; - - // Probar opciones del menú - $options = $db->fetchAll( - "SELECT * FROM menu_options WHERE menu_id = :id ORDER BY option_number", - ['id' => $mainMenu['id']] - ); - - echo "

✅ Opciones del menú principal: " . count($options) . " opciones

"; - - } else { - echo "

❌ No hay menú principal configurado

"; - } - -} catch (Exception $e) { - echo "

❌ Error en test del bot: " . $e->getMessage() . "

"; -} - -// URLs de prueba -echo "

🔗 URLs del Sistema:

"; -echo ""; - -echo "
"; -echo "
Nueva Suite de Tests Implementada
"; -echo "

Se ha implementado una suite completa de tests que cubre:

"; -echo ""; -echo "

Ejecutar Tests Completos

"; -echo "
"; - -echo "

📋 Resumen:

"; -echo "

El sistema WhatsApp Bot está:

"; - -// Calcular estado general -$allGood = file_exists('config/config.php') && - file_exists('classes/Database.php') && - file_exists('api/webhook.php') && - class_exists('Database'); - -if ($allGood) { - echo "
"; - echo "

✅ SISTEMA FUNCIONANDO

"; - echo "

Todos los componentes principales están funcionando correctamente.

"; - echo "

Próximos pasos:

"; - echo ""; - echo "
"; -} else { - echo "
"; - echo "

❌ HAY PROBLEMAS

"; - echo "

Algunos componentes no están funcionando correctamente. Revisa los errores arriba.

"; - echo "
"; -} - -echo "
"; -echo "

Test ejecutado el: " . date('Y-m-d H:i:s') . "

"; -?> \ No newline at end of file diff --git a/test_api.php b/test_api.php deleted file mode 100644 index 1e1d372..0000000 --- a/test_api.php +++ /dev/null @@ -1,175 +0,0 @@ -"; -echo "🧪 Test API"; -echo ""; - -echo "

🧪 TEST DE LA API

"; - -// Lista de endpoints para probar -$endpoints = [ - 'get_settings.php' => 'Obtener configuración del sistema', - 'get_stats.php' => 'Obtener estadísticas', - 'get_users.php' => 'Obtener usuarios', - 'get_menus.php' => 'Obtener menús', - 'get_templates.php' => 'Obtener plantillas' -]; - -echo "
"; -echo "

🚀 PROBANDO ENDPOINTS DE LA API...

"; - -foreach ($endpoints as $endpoint => $description) { - echo "
"; - echo "🔗 $endpoint - $description
"; - - $api_url = 'api/' . $endpoint; - - // Verificar que el archivo existe - if (!file_exists($api_url)) { - echo "
❌ Archivo no encontrado: $api_url
"; - continue; - } - - echo "
✅ Archivo existe
"; - - // Intentar ejecutar el endpoint usando output buffering - ob_start(); - - try { - // Simular REQUEST_METHOD para endpoints que lo requieran - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? 'localhost'; - - // Capturar cualquier error - $error_before = error_get_last(); - - // Incluir el archivo de la API - include $api_url; - - // Obtener la salida - $output = ob_get_contents(); - - // Verificar errores - $error_after = error_get_last(); - - if ($error_after && $error_after !== $error_before) { - echo "
❌ Error PHP: " . htmlspecialchars($error_after['message']) . "
"; - echo "
Línea: " . $error_after['line'] . " en " . basename($error_after['file']) . "
"; - } else { - // Verificar si la salida parece ser JSON válido - if (!empty($output)) { - $json_data = json_decode($output, true); - if (json_last_error() === JSON_ERROR_NONE) { - echo "
✅ Respuesta JSON válida (" . strlen($output) . " caracteres)
"; - - // Mostrar estructura básica - if (is_array($json_data)) { - $keys = array_keys($json_data); - $preview_keys = array_slice($keys, 0, 3); - echo "
📋 Claves: " . implode(', ', $preview_keys); - if (count($keys) > 3) echo " (+" . (count($keys) - 3) . " más)"; - echo "
"; - } - } else { - echo "
⚠️ Salida no es JSON válido
"; - if (strlen($output) < 200) { - echo "
Salida: " . htmlspecialchars(substr($output, 0, 100)) . "
"; - } - } - } else { - echo "
⚠️ Sin salida
"; - } - } - - } catch (ParseError $e) { - echo "
❌ Error de sintaxis: " . htmlspecialchars($e->getMessage()) . "
"; - echo "
Línea: " . $e->getLine() . "
"; - } catch (Error $e) { - echo "
❌ Error fatal: " . htmlspecialchars($e->getMessage()) . "
"; - echo "
Archivo: " . basename($e->getFile()) . " línea " . $e->getLine() . "
"; - } catch (Exception $e) { - echo "
❌ Excepción: " . htmlspecialchars($e->getMessage()) . "
"; - } finally { - ob_end_clean(); - } - - echo "
"; -} - -echo "
"; - -// Test directo de carga de configuración desde API -echo "
"; -echo "

⚙️ TEST DIRECTO DE CONFIGURACIÓN...

"; - -echo "
🔄 Simulando carga desde directorio API...
"; - -// Cambiar al directorio API temporalmente -$original_dir = getcwd(); - -try { - chdir('api'); - echo "
✅ Cambiado a directorio API
"; - - // Intentar cargar la configuración - if (file_exists('../config/config.php')) { - echo "
✅ ../config/config.php existe desde API
"; - - try { - require_once '../config/config.php'; - echo "
✅ Configuración cargada exitosamente
"; - - // Verificar algunas constantes - $test_constants = ['DB_HOST', 'DB_NAME', 'DB_USER', 'SECRET_KEY']; - foreach ($test_constants as $const) { - if (defined($const)) { - echo "
✅ $const definida
"; - } else { - echo "
❌ $const no definida
"; - } - } - - } catch (Exception $e) { - echo "
❌ Error cargando config: " . htmlspecialchars($e->getMessage()) . "
"; - } - - } else { - echo "
❌ ../config/config.php NO existe desde API
"; - } - -} finally { - chdir($original_dir); -} - -echo "
"; - -// Resumen final -echo "
"; -echo "

📊 RESUMEN

"; - -echo "
✅ Rutas de API corregidas
"; -echo "
✅ Todos los archivos usan '../config/config.php'
"; - -echo "
PRÓXIMOS PASOS:
"; -echo "
1. Verificar que las tablas existan: test_config.php
"; -echo "
2. Si faltan tablas, ejecutar: instalar_bd.php
"; -echo "
3. Probar login: login.php
"; -echo "
4. Probar panel: index.php
"; - -echo "
"; - -echo ""; -?> \ No newline at end of file diff --git a/test_api_simple.php b/test_api_simple.php deleted file mode 100644 index c019f39..0000000 --- a/test_api_simple.php +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/test_basico.php b/test_basico.php deleted file mode 100644 index 74d7c1d..0000000 --- a/test_basico.php +++ /dev/null @@ -1,57 +0,0 @@ -"; -echo "✅ Test Básico - WhatsApp Bot"; -echo ""; - -echo "
"; -echo "

✅ ¡PHP FUNCIONANDO CORRECTAMENTE!

"; -echo "

Versión PHP: " . phpversion() . "

"; -echo "

Fecha: " . date('Y-m-d H:i:s') . "

"; -echo "

Servidor: " . ($_SERVER['SERVER_SOFTWARE'] ?? 'No disponible') . "

"; -echo "
"; - -echo "
"; -echo "

📋 Próximos pasos:

"; -echo "
    "; -echo "
  1. Subir archivo diagnóstico: diagnostico_500.php
  2. "; -echo "
  3. Verificar configuración: Revisar config/config.php
  4. "; -echo "
  5. Probar login: login.php
  6. "; -echo "
  7. Dashboard principal: index.php
  8. "; -echo "
"; -echo "
"; - -echo "
"; -echo "

🔧 Si tienes Error 500:

"; -echo "

1. Ejecuta diagnóstico: Ve a diagnostico_500.php

"; -echo "

2. Desactiva .htaccess: Renómbralo a .htaccess.bak

"; -echo "

3. Verifica permisos: chmod 777 logs/ uploads/

"; -echo "

4. Revisa logs: En tu panel de hosting

"; -echo "
"; - -// Test de escritura -$test_file = 'logs/test_write.txt'; -if (is_dir('logs')) { - if (is_writable('logs')) { - if (file_put_contents($test_file, "Test: " . date('Y-m-d H:i:s'))) { - echo "
✅ Directorio logs/ es escribible
"; - unlink($test_file); // Limpiar archivo de prueba - } else { - echo "
❌ No se puede escribir en logs/
"; - } - } else { - echo "
❌ Directorio logs/ no tiene permisos de escritura
"; - } -} else { - echo "
⚠️ Directorio logs/ no existe
"; -} - -echo ""; -?> \ No newline at end of file diff --git a/test_config.php b/test_config.php deleted file mode 100644 index dfdf462..0000000 --- a/test_config.php +++ /dev/null @@ -1,214 +0,0 @@ -"; -echo "🔧 Test Configuración"; -echo ""; - -echo "

🔧 TEST CONFIGURACIÓN

"; - -// 1. Verificar que el archivo existe -if (!file_exists('config/config.php')) { - echo "
❌ CRÍTICO: config/config.php no encontrado
"; - echo "
Asegúrate de que el archivo esté en: config/config.php
"; - exit; -} - -echo "
✅ config/config.php encontrado
"; - -// 2. Verificar permisos del archivo -if (!is_readable('config/config.php')) { - echo "
❌ CRÍTICO: config/config.php no es legible
"; - echo "
Ejecuta: chmod 644 config/config.php
"; - exit; -} - -echo "
✅ config/config.php es legible
"; - -// 3. Intentar cargar el archivo -echo "
"; -echo "

📋 CARGANDO CONFIGURACIÓN...

"; - -try { - // Capturar cualquier output - ob_start(); - - // Capturar errores - $error_before = error_get_last(); - - // Incluir el archivo - require_once 'config/config.php'; - - // Obtener output - $output = ob_get_clean(); - - // Verificar si hubo errores - $error_after = error_get_last(); - - if ($error_after && $error_after !== $error_before) { - echo "
❌ ERROR al cargar config: " . htmlspecialchars($error_after['message']) . "
"; - echo "
Línea: " . $error_after['line'] . " en " . $error_after['file'] . "
"; - } elseif (!empty($output)) { - echo "
⚠️ ADVERTENCIA: config.php produce output:
"; - echo "
" . htmlspecialchars($output) . "
"; - echo "
Los archivos de configuración NO deben producir output
"; - } else { - echo "
✅ config/config.php cargado sin errores
"; - } - -} catch (ParseError $e) { - echo "
❌ ERROR DE SINTAXIS en config.php:
"; - echo "
" . htmlspecialchars($e->getMessage()) . "
"; - echo "
Línea: " . $e->getLine() . "
"; - exit; -} catch (Error $e) { - echo "
❌ ERROR FATAL en config.php:
"; - echo "
" . htmlspecialchars($e->getMessage()) . "
"; - exit; -} catch (Exception $e) { - echo "
❌ EXCEPCIÓN en config.php:
"; - echo "
" . htmlspecialchars($e->getMessage()) . "
"; - exit; -} - -echo "
"; - -// 4. Verificar constantes requeridas -echo "
"; -echo "

🗄️ VERIFICANDO CONSTANTES DE BD...

"; - -$required_constants = [ - 'DB_HOST' => 'Servidor de base de datos', - 'DB_NAME' => 'Nombre de la base de datos', - 'DB_USER' => 'Usuario de la base de datos', - 'DB_PASS' => 'Contraseña de la base de datos', - 'DB_PORT' => 'Puerto de la base de datos', - 'DB_CHARSET' => 'Charset de la base de datos' -]; - -$all_defined = true; - -foreach ($required_constants as $const => $desc) { - if (defined($const)) { - $value = constant($const); - $display_value = ($const === 'DB_PASS') ? str_repeat('*', strlen($value)) : $value; - echo "
✅ $const = '$display_value' ($desc)
"; - } else { - echo "
❌ $const no definida ($desc)
"; - $all_defined = false; - } -} - -if (!$all_defined) { - echo "
❌ CRÍTICO: Faltan constantes de base de datos
"; - echo "
El sistema no puede funcionar sin estas constantes
"; -} - -echo "
"; - -// 5. Test de conexión a BD (solo si todas las constantes están) -if ($all_defined) { - echo "
"; - echo "

🔌 TEST CONEXIÓN BASE DE DATOS...

"; - - try { - $dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET; - - echo "
DSN: $dsn
"; - - $pdo = new PDO($dsn, DB_USER, DB_PASS, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_TIMEOUT => 5 - ]); - - echo "
✅ CONEXIÓN EXITOSA a la base de datos
"; - - // Verificar algunas tablas - $tables_to_check = ['conversations', 'messages', 'settings', 'message_templates']; - foreach ($tables_to_check as $table) { - try { - $stmt = $pdo->query("SHOW TABLES LIKE '$table'"); - if ($stmt->rowCount() > 0) { - echo "
✅ Tabla '$table' existe
"; - } else { - echo "
⚠️ Tabla '$table' NO existe
"; - } - } catch (Exception $e) { - echo "
❌ Error verificando tabla '$table': " . htmlspecialchars($e->getMessage()) . "
"; - } - } - - } catch (PDOException $e) { - echo "
❌ ERROR DE CONEXIÓN:
"; - echo "
" . htmlspecialchars($e->getMessage()) . "
"; - echo "
Verifica:
"; - echo "
• Host: " . DB_HOST . "
"; - echo "
• Base datos: " . DB_NAME . "
"; - echo "
• Usuario: " . DB_USER . "
"; - echo "
• Puerto: " . DB_PORT . "
"; - } - - echo "
"; -} - -// 6. Verificar otras constantes importantes -echo "
"; -echo "

🔑 OTRAS CONSTANTES DEL SISTEMA...

"; - -$other_constants = [ - 'ADMIN_USERNAME' => 'Usuario administrador', - 'ADMIN_PASSWORD' => 'Contraseña administrador', - 'SECRET_KEY' => 'Clave secreta', - 'WHATSAPP_TOKEN' => 'Token de WhatsApp', - 'WHATSAPP_PHONE_NUMBER_ID' => 'ID del número de teléfono' -]; - -foreach ($other_constants as $const => $desc) { - if (defined($const)) { - $value = constant($const); - if (in_array($const, ['ADMIN_PASSWORD', 'SECRET_KEY', 'WHATSAPP_TOKEN'])) { - $display_value = str_repeat('*', min(strlen($value), 20)); - } else { - $display_value = $value; - } - echo "
✅ $const = '$display_value' ($desc)
"; - } else { - echo "
⚠️ $const no definida ($desc)
"; - } -} - -echo "
"; - -// 7. Resumen final -echo "
"; -echo "

📊 RESUMEN

"; - -if ($all_defined) { - echo "
✅ Configuración básica completa
"; - echo "
✅ Archivo config.php funcional
"; - echo "
✅ Sistema puede intentar funcionar
"; - - echo "
SIGUIENTE PASO:
"; - echo "
• Probar: login.php
"; - echo "
• Si funciona login, probar: index.php
"; -} else { - echo "
❌ Configuración incompleta
"; - echo "
❌ Sistema NO puede funcionar
"; - echo "
Corrige las constantes faltantes en config/config.php
"; -} - -echo "
"; - -echo ""; -?> \ No newline at end of file diff --git a/test_config_system.php b/test_config_system.php deleted file mode 100644 index 91f427e..0000000 --- a/test_config_system.php +++ /dev/null @@ -1,452 +0,0 @@ - - - - - - - Prueba Sistema de Configuraciones - - - -
-

🔧 Prueba Sistema de Configuraciones

- - -
- ⚠️ Instalación no completada: Algunas funciones pueden no estar disponibles. -
- - - -
-
-
📊 Estado del Sistema
-
-
-
-
-

Instalación completada:

-

Sistema migrado:

-

BD conectada: - getMessage(); - } - ?> -

-
-
-

Tabla system_config: - query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0; - if ($exists) { - $count = $pdo->query("SELECT COUNT(*) as total FROM system_config")->fetch()['total']; - echo "✅ Existe ($count registros)"; - } else { - echo '❌ No existe'; - } - } catch (Exception $e) { - echo '❌ Error: ' . $e->getMessage(); - } - ?> -

-
-
-
-
- - -
-
- -
-
-
💾 Probar Guardar Configuración
-
-
-
-
- -
-
- -
-
- -
- -
-
-
-
- - -
-
-
📖 Probar Obtener Configuración
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
📱 Configuración WhatsApp
-
-
-
-
- -
-
- -
-
- -
- -
-
-
-
- - -
-
-
🔧 Acciones del Sistema
-
-
-
-
-
- -
-
-
-
-
- - -
-
-
📋 Configuraciones Actuales
- -
-
-
-
-
- Cargando configuraciones... -
-
-
-
- - -
-
-
✅ Validación WhatsApp
-
-
-
-
-
- Validando configuración... -
-
-
-
-
- - - - - \ No newline at end of file diff --git a/test_direct_api.php b/test_direct_api.php deleted file mode 100644 index ae4ae60..0000000 --- a/test_direct_api.php +++ /dev/null @@ -1,100 +0,0 @@ -Test Diagnóstico Menús"; -echo ""; - -echo "

🔧 Test Diagnóstico Get Menús API

"; - -try { - echo "

📞 Llamada directa a get_menus.php

"; - - // Hacer llamada a la API - $url = 'http://localhost:8000/api/get_menus.php'; - - echo "

URL: $url

"; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); - curl_setopt($ch, CURLOPT_COOKIEFILE, ''); // Enable cookies - - // Agregar cookie de sesión si existe - if (isset($_COOKIE['PHPSESSID'])) { - curl_setopt($ch, CURLOPT_COOKIE, 'PHPSESSID=' . $_COOKIE['PHPSESSID']); - } - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - curl_close($ch); - - $headers = substr($response, 0, $headerSize); - $body = substr($response, $headerSize); - - echo "

🔍 Resultado de la llamada

"; - echo "

HTTP Code: $httpCode

"; - - echo "

Headers:

"; - echo "
" . htmlspecialchars($headers) . "
"; - - echo "

Body Response:

"; - echo "
" . htmlspecialchars($body) . "
"; - - // Intentar decodificar JSON - if ($body) { - $jsonData = json_decode($body, true); - if ($jsonData !== null) { - echo "

📋 JSON Decodificado:

"; - echo "
" . json_encode($jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "
"; - - if (isset($jsonData['success'])) { - if ($jsonData['success']) { - echo "
✅ API respondió exitosamente
"; - if (isset($jsonData['data'])) { - echo "

Menús encontrados: " . count($jsonData['data']) . "

"; - } - } else { - echo "
❌ API reportó error: " . ($jsonData['error'] ?? 'Error desconocido') . "
"; - if (isset($jsonData['debug'])) { - echo "

Debug info: " . $jsonData['debug'] . "

"; - } - } - } - } else { - echo "
❌ Respuesta no es JSON válido
"; - } - } - - // Mostrar logs si existen - echo "

📋 Logs del Sistema

"; - $logFile = dirname(__DIR__) . '/logs/system.log'; - if (file_exists($logFile)) { - $logs = file_get_contents($logFile); - $recentLogs = array_slice(explode("\n", $logs), -20); // Últimas 20 líneas - echo "

Últimas 20 líneas del log:

"; - echo "
" . htmlspecialchars(implode("\n", $recentLogs)) . "
"; - } else { - echo "

No se encontró archivo de log en: $logFile

"; - } - - // Mostrar error log de PHP si existe - $errorLogFile = dirname(__DIR__) . '/logs/error.log'; - if (file_exists($errorLogFile)) { - $errorLogs = file_get_contents($errorLogFile); - $recentErrorLogs = array_slice(explode("\n", $errorLogs), -10); // Últimas 10 líneas - echo "

Últimas 10 líneas del error log:

"; - echo "
" . htmlspecialchars(implode("\n", $recentErrorLogs)) . "
"; - } - -} catch (Exception $e) { - echo "
❌ Error en test: " . $e->getMessage() . "
"; -} - -echo "

Test ejecutado: " . date('Y-m-d H:i:s') . "

"; -echo ""; -?> \ No newline at end of file diff --git a/test_functions.html b/test_functions.html deleted file mode 100644 index 6e1b667..0000000 --- a/test_functions.html +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - Test Funciones Globales - WhatsApp Bot - - - - -
-
-
-
-
-
Test de Funciones Globales
-
-
-
-
-
Modales Disponibles:
- - - - - -
-
-
Debug Console:
-
- -
-
-
-
-
-
-
-
- - - - - - - - - - - \ No newline at end of file diff --git a/test_menus.php b/test_menus.php deleted file mode 100644 index 43bcfe1..0000000 --- a/test_menus.php +++ /dev/null @@ -1,200 +0,0 @@ -"; -echo "Test Menús - WhatsApp Bot"; -echo ""; -echo ""; - -echo "

🔧 Test de Funcionalidad de Menús

"; - -try { - $db = Database::getInstance(); - echo "
✅ Conexión a base de datos: EXITOSA
"; - - // Verificar tabla menus - echo "

📋 Verificando tabla 'menus'

"; - try { - $result = $db->fetchAll("SHOW TABLES LIKE 'menus'"); - if (count($result) > 0) { - echo "
✅ Tabla 'menus' existe
"; - - // Mostrar estructura - $structure = $db->fetchAll("DESCRIBE menus"); - echo "

Estructura de tabla 'menus':

"; - echo "
";
-            foreach ($structure as $column) {
-                echo "{$column['Field']} - {$column['Type']} - {$column['Null']} - {$column['Default']}\n";
-            }
-            echo "
"; - - } else { - echo "
❌ Tabla 'menus' NO existe
"; - echo "
💡 Creando tabla 'menus'...
"; - - $createMenusTable = " - CREATE TABLE IF NOT EXISTS menus ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - menu_key VARCHAR(100) NOT NULL, - description TEXT, - welcome_message TEXT, - status ENUM('active', 'inactive') DEFAULT 'active', - options_count INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_menu_key (menu_key), - INDEX idx_status (status) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"; - - $db->execute($createMenusTable); - echo "
✅ Tabla 'menus' creada
"; - } - } catch (Exception $e) { - echo "
❌ Error verificando tabla 'menus': " . $e->getMessage() . "
"; - } - - // Verificar tabla menu_options - echo "

📋 Verificando tabla 'menu_options'

"; - try { - $result = $db->fetchAll("SHOW TABLES LIKE 'menu_options'"); - if (count($result) > 0) { - echo "
✅ Tabla 'menu_options' existe
"; - - // Mostrar estructura - $structure = $db->fetchAll("DESCRIBE menu_options"); - echo "

Estructura de tabla 'menu_options':

"; - echo "
";
-            foreach ($structure as $column) {
-                echo "{$column['Field']} - {$column['Type']} - {$column['Null']} - {$column['Default']}\n";
-            }
-            echo "
"; - - } else { - echo "
❌ Tabla 'menu_options' NO existe
"; - echo "
💡 Creando tabla 'menu_options'...
"; - - $createMenuOptionsTable = " - CREATE TABLE IF NOT EXISTS menu_options ( - id INT AUTO_INCREMENT PRIMARY KEY, - menu_id INT NOT NULL, - option_key VARCHAR(10) NOT NULL, - option_text VARCHAR(255) NOT NULL, - option_action ENUM('message', 'submenu', 'template', 'function') DEFAULT 'message', - option_value TEXT, - order_index INT DEFAULT 0, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (menu_id) REFERENCES menus(id) ON DELETE CASCADE, - INDEX idx_menu_id (menu_id), - INDEX idx_option_key (option_key), - INDEX idx_order (order_index) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"; - - $db->execute($createMenuOptionsTable); - echo "
✅ Tabla 'menu_options' creada
"; - } - } catch (Exception $e) { - echo "
❌ Error verificando tabla 'menu_options': " . $e->getMessage() . "
"; - } - - // Verificar menús existentes - echo "

📱 Menús existentes

"; - try { - $menus = $db->fetchAll("SELECT * FROM menus ORDER BY created_at DESC"); - if (count($menus) > 0) { - echo "
📋 Se encontraron " . count($menus) . " menús:
"; - echo "
";
-            foreach ($menus as $menu) {
-                echo "ID: {$menu['id']} | Nombre: {$menu['name']} | Clave: {$menu['menu_key']} | Estado: {$menu['status']}\n";
-            }
-            echo "
"; - } else { - echo "
📋 No hay menús configurados aún
"; - - // Crear menú de ejemplo - echo "
💡 Creando menú de ejemplo...
"; - - $menuData = [ - 'name' => 'Menú Principal', - 'menu_key' => 'main', - 'description' => 'Menú principal del bot', - 'welcome_message' => '¡Hola! Bienvenido a nuestro servicio. Por favor selecciona una opción:', - 'status' => 'active', - 'options_count' => 0 - ]; - - $menuId = $db->insert('menus', $menuData); - - if ($menuId) { - echo "
✅ Menú de ejemplo creado (ID: {$menuId})
"; - - // Crear opciones de ejemplo - $options = [ - ['menu_id' => $menuId, 'option_key' => '1', 'option_text' => 'Información', 'option_action' => 'message', 'option_value' => 'Aquí tienes información sobre nuestros servicios', 'order_index' => 1], - ['menu_id' => $menuId, 'option_key' => '2', 'option_text' => 'Soporte', 'option_action' => 'message', 'option_value' => 'Nuestro equipo de soporte te ayudará en breve', 'order_index' => 2], - ['menu_id' => $menuId, 'option_key' => '0', 'option_text' => 'Salir', 'option_action' => 'message', 'option_value' => 'Gracias por contactarnos', 'order_index' => 3] - ]; - - foreach ($options as $option) { - $db->insert('menu_options', $option); - } - - // Actualizar contador de opciones - $db->execute("UPDATE menus SET options_count = 3 WHERE id = :id", ['id' => $menuId]); - - echo "
✅ Opciones de menú creadas
"; - } - } - } catch (Exception $e) { - echo "
❌ Error verificando menús: " . $e->getMessage() . "
"; - } - - // Test de API - echo "

🔌 Test de API

"; - try { - $apiUrl = 'http://localhost:8000/api/get_menus.php'; - echo "
📞 Probando API: {$apiUrl}
"; - - $context = stream_context_create([ - 'http' => [ - 'method' => 'GET', - 'header' => 'Content-Type: application/json', - 'timeout' => 10 - ] - ]); - - $response = @file_get_contents($apiUrl, false, $context); - - if ($response !== false) { - $data = json_decode($response, true); - if ($data !== null) { - echo "
✅ API responde correctamente
"; - echo "
" . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "
"; - } else { - echo "
❌ API responde pero JSON inválido
"; - echo "
" . htmlspecialchars($response) . "
"; - } - } else { - echo "
❌ No se pudo conectar a la API
"; - } - } catch (Exception $e) { - echo "
❌ Error probando API: " . $e->getMessage() . "
"; - } - -} catch (Exception $e) { - echo "
❌ Error de conexión: " . $e->getMessage() . "
"; -} - -echo "
"; -echo "

Estado del servidor: El servidor PHP está corriendo en http://localhost:8000

"; -echo "

Aplicación principal: WhatsApp Bot Manager

"; -echo "

Fecha de prueba: " . date('Y-m-d H:i:s') . "

"; - -echo ""; -?> \ No newline at end of file diff --git a/test_menus_api.html b/test_menus_api.html deleted file mode 100644 index dd270b0..0000000 --- a/test_menus_api.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - Test Menús - WhatsApp Bot Manager - - - -

🔧 Test de Funcionalidad de Menús

-

Este test verificará que la funcionalidad de menús esté trabajando correctamente.

- -
- -

🧪 Pruebas de API

- - - - - -
- - - - \ No newline at end of file diff --git a/test_menus_frontend.html b/test_menus_frontend.html deleted file mode 100644 index 048fe9f..0000000 --- a/test_menus_frontend.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - Test JavaScript Menús - - - -

🧪 Test JavaScript - Lista de Menús

- - - - -
- -

📋 Tabla de Menús

- - - - - - - - - - - - - - - -
NombreClaveDescripciónEstadoOpcionesFecha CreaciónAcciones
Haz clic en "Cargar y Mostrar Menús" para ver los datos
- - - - \ No newline at end of file diff --git a/test_modal.html b/test_modal.html deleted file mode 100644 index 16afa38..0000000 --- a/test_modal.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - Test Modal Plantillas - WhatsApp Bot - - - - - -
-
-
-
-
-
Prueba de Modal de Plantillas
-
-
-

Haz clic en el botón para probar el modal de crear plantilla:

- - - - - -
-
Log de Debug:
-
- -
-
-
-
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/test_navigation.html b/test_navigation.html deleted file mode 100644 index 2f824ba..0000000 --- a/test_navigation.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - Test de Navegación - WhatsApp Bot - - - - - -
-
-
-

Menú de Prueba

- -
-
-
-

Dashboard

-

Contenido del dashboard aquí.

-
Esta es la pestaña de Dashboard
-
-
-

Usuarios

-

Lista de usuarios aquí.

-
Esta es la pestaña de Usuarios
-
-
-

Logs

-

Logs del sistema aquí.

-
Esta es la pestaña de Logs
-
- -
-
-
- - - - \ No newline at end of file diff --git a/test_pass.php b/test_pass.php deleted file mode 100644 index 8dec55b..0000000 --- a/test_pass.php +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/test_ses_fix.html b/test_ses_fix.html deleted file mode 100644 index 2062634..0000000 --- a/test_ses_fix.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - Test SES Fix - WhatsApp Bot - - - - -
-
-
-
-
-
Test SES Fix
-
-
-

Esta página prueba que las funciones globales funcionen sin errores de SES.

- -
-
-
Funciones disponibles:
- - - - - -
-
-
Status Log:
-
-
✅ Página cargada sin errores de SES
-
-
-
-
-
-
-
-
- - - - - \ No newline at end of file diff --git a/test_simple_config.php b/test_simple_config.php deleted file mode 100644 index 60f9666..0000000 --- a/test_simple_config.php +++ /dev/null @@ -1,104 +0,0 @@ -query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0; - - if (!$tableExists) { - echo "⚠️ Tabla no existe, creándola...\n"; - $created = createSystemConfigTable($pdo); - if ($created) { - echo "✅ Tabla creada exitosamente\n\n"; - } else { - echo "❌ Error creando tabla\n\n"; - exit; - } - } else { - echo "✅ Tabla existe\n\n"; - } - - echo "3. Probando guardar configuración...\n"; - $testKey = 'test_config_' . time(); - $testValue = 'Valor de prueba ' . date('Y-m-d H:i:s'); - - $saved = saveConfigToDB($testKey, $testValue, 'Configuración de prueba'); - if ($saved) { - echo "✅ Configuración guardada: $testKey = $testValue\n\n"; - } else { - echo "❌ Error guardando configuración\n\n"; - } - - echo "4. Probando obtener configuración...\n"; - $retrieved = getConfigFromDB($testKey, 'no_encontrado'); - if ($retrieved === $testValue) { - echo "✅ Configuración recuperada correctamente: $retrieved\n\n"; - } else { - echo "❌ Error recuperando configuración. Esperado: $testValue, Obtenido: $retrieved\n\n"; - } - - echo "5. Probando obtener todas las configuraciones...\n"; - $allConfigs = getAllConfigsFromDB(); - echo "✅ Total de configuraciones: " . count($allConfigs) . "\n\n"; - - echo "6. Probando configuración de WhatsApp...\n"; - $whatsappTest = [ - 'token' => 'token_de_prueba_' . time(), - 'phone_number_id' => '123456789', - 'webhook_verify_token' => 'webhook_test_' . time() - ]; - - $whatsappSaved = saveWhatsAppConfigToDB($whatsappTest); - if ($whatsappSaved) { - echo "✅ Configuración de WhatsApp guardada\n"; - - $whatsappConfig = getWhatsAppConfigFromDB(); - echo "📱 Token: " . $whatsappConfig['token'] . "\n"; - echo "📱 Phone ID: " . $whatsappConfig['phone_number_id'] . "\n"; - echo "📱 Webhook: " . $whatsappConfig['webhook_verify_token'] . "\n\n"; - } else { - echo "❌ Error guardando configuración de WhatsApp\n\n"; - } - - echo "7. Probando validación de WhatsApp...\n"; - $validation = validateWhatsAppConfigFromDB(); - echo "✅ Configuración válida: " . ($validation['valid'] ? 'Sí' : 'No') . "\n"; - echo " Errores: " . count($validation['errors']) . "\n"; - echo " Advertencias: " . count($validation['warnings']) . "\n\n"; - - echo "8. Limpiando configuración de prueba...\n"; - $deleted = deleteConfigFromDB($testKey); - if ($deleted) { - echo "✅ Configuración de prueba eliminada\n\n"; - } else { - echo "❌ Error eliminando configuración de prueba\n\n"; - } - - echo "=== PRUEBAS COMPLETADAS EXITOSAMENTE ===\n"; - echo "El sistema de configuraciones funciona correctamente.\n"; - echo "Puedes usar las funciones:\n"; - echo "- saveConfigToDB(clave, valor, descripción)\n"; - echo "- getConfigFromDB(clave, por_defecto)\n"; - echo "- getAllConfigsFromDB()\n"; - echo "- saveWhatsAppConfigToDB(array_config)\n"; - echo "- getWhatsAppConfigFromDB()\n"; - echo "- validateWhatsAppConfigFromDB()\n"; - -} catch (Exception $e) { - echo "❌ ERROR: " . $e->getMessage() . "\n"; - echo "Trace: " . $e->getTraceAsString() . "\n"; -} -?> \ No newline at end of file diff --git a/test_template_direct.php b/test_template_direct.php deleted file mode 100644 index 5388a36..0000000 --- a/test_template_direct.php +++ /dev/null @@ -1,56 +0,0 @@ - 'Iniciando test de plantilla...'], JSON_PRETTY_PRINT) . "\n"; - - $whatsapp = new WhatsAppService(); - - // Usar datos de prueba que sabemos que existen - $recipient = "573168950803"; // Número de prueba - $templateName = "hello_world"; // Esta plantilla FUNCIONA - $language = "en_US"; // Idioma que FUNCIONA - - echo json_encode([ - 'step' => 'Datos de envío', - 'recipient' => $recipient, - 'template' => $templateName, - 'language' => $language - ], JSON_PRETTY_PRINT) . "\n"; - - // Enviar plantilla - echo json_encode(['step' => 'Llamando sendTemplateMessage...'], JSON_PRETTY_PRINT) . "\n"; - flush(); - - $result = $whatsapp->sendTemplateMessage($recipient, $templateName, $language); - - echo json_encode([ - 'step' => 'Resultado', - 'success' => true, - 'result' => $result - ], JSON_PRETTY_PRINT) . "\n"; - -} catch (Exception $e) { - echo json_encode([ - 'step' => 'Error capturado', - 'success' => false, - 'error' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine() - ], JSON_PRETTY_PRINT) . "\n"; -} - -echo json_encode(['status' => 'Test completado. Revisa los logs de error para ver detalles de la petición HTTP.'], JSON_PRETTY_PRINT) . "\n"; -?> \ No newline at end of file diff --git a/test_text_message.php b/test_text_message.php deleted file mode 100644 index 72f4658..0000000 --- a/test_text_message.php +++ /dev/null @@ -1,92 +0,0 @@ - '+1234567890', - 'type' => 'text', - 'message' => 'Mensaje de prueba desde test directo' -]; - -echo "=== TEST MENSAJE DE TEXTO ===\n"; -echo "Datos de entrada: " . json_encode($testData) . "\n\n"; - -try { - // Test 1: Verificar conexión a la base de datos - $db = Database::getInstance(); - echo "✅ Conexión a base de datos: OK\n"; - - // Test 2: Verificar si existe la tabla users - $stmt = $db->query("SHOW TABLES LIKE 'users'"); - $usersTable = $stmt->fetch(); - echo "✅ Tabla users existe: " . ($usersTable ? 'OK' : 'NO') . "\n"; - - // Test 3: Verificar si existe la tabla conversations - $stmt = $db->query("SHOW TABLES LIKE 'conversations'"); - $conversationsTable = $stmt->fetch(); - echo "✅ Tabla conversations existe: " . ($conversationsTable ? 'OK' : 'NO') . "\n"; - - // Test 4: Simular búsqueda de usuario - $recipient = $testData['recipient']; - $stmt = $db->query("SELECT id FROM users WHERE phone_number = ? LIMIT 1", [$recipient]); - $user = $stmt->fetch(); - echo "📱 Usuario existente con teléfono $recipient: " . ($user ? "ID=" . $user['id'] : 'NO EXISTE') . "\n"; - - // Test 5: Simular creación de usuario si no existe - if (!$user) { - echo "🔄 Intentando crear nuevo usuario...\n"; - $stmt = $db->query( - "INSERT INTO users (phone_number, name, created_at) VALUES (?, ?, NOW())", - [$recipient, $recipient] - ); - $userId = $db->lastInsertId(); - echo "✅ Usuario creado con ID: $userId\n"; - } else { - $userId = $user['id']; - echo "✅ Usuario existente con ID: $userId\n"; - } - - // Test 6: Verificar ventana de 24 horas - if ($userId) { - $stmt = $db->query( - "SELECT MAX(created_at) as last_message FROM conversations - WHERE user_id = ? AND direction = 'incoming' - AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)", - [$userId] - ); - $lastMessage = $stmt->fetch(); - echo "⏰ Último mensaje entrante en 24h: " . ($lastMessage['last_message'] ?? 'NINGUNO') . "\n"; - - if (!$lastMessage || !$lastMessage['last_message']) { - echo "⚠️ Usuario fuera de ventana de 24h - Solo plantillas permitidas\n"; - } else { - echo "✅ Usuario dentro de ventana de 24h - Mensajes de texto permitidos\n"; - } - } - - // Test 7: Simular guardado de mensaje - if ($userId) { - echo "💾 Intentando guardar mensaje en conversations...\n"; - $messageContent = $testData['message']; - $messageType = $testData['type']; - - $stmt = $db->query( - "INSERT INTO conversations (user_id, content, direction, message_type, status, message_id, created_at) VALUES (?, ?, 'outgoing', ?, 'sent', ?, NOW())", - [$userId, $messageContent, $messageType, 'test_message_' . time()] - ); - echo "✅ Mensaje guardado correctamente en conversations\n"; - } - - echo "\n=== RESULTADO ===\n"; - echo "✅ Todas las operaciones de base de datos funcionan correctamente\n"; - echo "✅ El problema debería estar resuelto\n"; - -} catch (Exception $e) { - echo "❌ ERROR: " . $e->getMessage() . "\n"; - echo "Stack trace: " . $e->getTraceAsString() . "\n"; -} -?> \ No newline at end of file diff --git a/test_webhook.php b/test_webhook.php deleted file mode 100644 index f45bb5f..0000000 --- a/test_webhook.php +++ /dev/null @@ -1,366 +0,0 @@ - - - - - - - Diagnóstico de Webhook - WhatsApp Bot - - - - - -
-
-

- Diagnóstico de Webhook WhatsApp -

-

Verificación completa del estado del webhook y configuración

-
- - "; - echo "

1. Configuración del Sistema

"; - - $config = [ - 'WHATSAPP_TOKEN' => defined('WHATSAPP_TOKEN') ? WHATSAPP_TOKEN : null, - 'WHATSAPP_PHONE_NUMBER_ID' => defined('WHATSAPP_PHONE_NUMBER_ID') ? WHATSAPP_PHONE_NUMBER_ID : null, - 'WEBHOOK_VERIFY_TOKEN' => defined('WEBHOOK_VERIFY_TOKEN') ? WEBHOOK_VERIFY_TOKEN : null, - 'WHATSAPP_API_URL' => defined('WHATSAPP_API_URL') ? WHATSAPP_API_URL : null, - ]; - - echo ""; - foreach ($config as $key => $value) { - $status = !empty($value) ? 'ok' : 'error'; - $icon = !empty($value) ? '✅' : '❌'; - $displayValue = !empty($value) ? (strlen($value) > 30 ? substr($value, 0, 30) . '...' : $value) : 'NO CONFIGURADO'; - - if ($status === 'error') $allOk = false; - - echo ""; - echo ""; - echo ""; - echo ""; - } - echo "
$icon $key$displayValue
"; - echo "
"; - - // Test 2: URL del Webhook - echo "
"; - echo "

2. URL del Webhook

"; - - $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; - $host = $_SERVER['HTTP_HOST']; - $webhookUrl = $protocol . '://' . $host . '/api/webhook.php'; - - echo "
"; - echo "URL para configurar en Meta/Facebook:
"; - echo "$webhookUrl"; - echo ""; - echo "
"; - - // Verificar si el archivo existe - $webhookFile = __DIR__ . '/api/webhook.php'; - if (file_exists($webhookFile)) { - echo "

✅ Archivo webhook.php existe

"; - } else { - echo "

❌ Archivo webhook.php NO encontrado

"; - $allOk = false; - } - - echo "
"; - - // Test 3: Verificación GET - echo "
"; - echo "

3. Test de Verificación (GET)

"; - - $verifyUrl = $webhookUrl . '?hub.mode=subscribe&hub.verify_token=' . urlencode(WEBHOOK_VERIFY_TOKEN) . '&hub.challenge=test_challenge_12345'; - - echo "

URL de prueba:

"; - echo "
" . htmlspecialchars($verifyUrl) . "
"; - - echo "
"; - echo "💡 Nota: Este test intenta verificar el webhook desde el servidor mismo. "; - echo "Si estás en localhost, es normal que falle. Lo importante es que el archivo exista y esté configurado correctamente."; - echo "
"; - - // Intentar hacer la verificación solo si no es localhost - $isLocalhost = (strpos($host, 'localhost') !== false || strpos($host, '127.0.0.1') !== false); - - if (!$isLocalhost) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $verifyUrl); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - curl_close($ch); - - if ($httpCode == 200 && $response === 'test_challenge_12345') { - echo "

✅ Verificación GET exitosa - El webhook responde correctamente

"; - echo "

Respuesta: $response

"; - } else { - echo "

❌ Error en verificación GET

"; - echo "

Código HTTP: $httpCode

"; - echo "

Respuesta: " . htmlspecialchars($response) . "

"; - if ($error) echo "

Error cURL: $error

"; - $allOk = false; - } - } else { - // Test manual para localhost - echo "
"; - echo "⚠️ Entorno Local Detectado
"; - echo "No se puede hacer la verificación automática en localhost. "; - echo "En su lugar, puedes probar manualmente:"; - echo "
"; - - echo "
    "; - echo "
  1. Haz clic en el botón de abajo para probar el webhook
  2. "; - echo "
  3. Deberías ver el texto: test_challenge_12345
  4. "; - echo "
"; - - echo ""; - echo " Probar Webhook Manualmente"; - echo ""; - - // Verificar el archivo directamente - $webhookFilePath = __DIR__ . '/api/webhook.php'; - if (file_exists($webhookFilePath)) { - echo "

✅ El archivo webhook.php existe y es accesible

"; - - // Verificar permisos de lectura - if (is_readable($webhookFilePath)) { - echo "

✅ El archivo tiene permisos de lectura

"; - - // Verificar que contenga el código necesario - $webhookContent = file_get_contents($webhookFilePath); - if (strpos($webhookContent, 'hub_verify_token') !== false && - strpos($webhookContent, 'hub_challenge') !== false) { - echo "

✅ El archivo contiene el código de verificación correcto

"; - } else { - echo "

⚠️ El archivo puede no contener el código de verificación

"; - } - } else { - echo "

❌ El archivo no tiene permisos de lectura

"; - $allOk = false; - } - } - } - - echo "
"; - - // Test 4: Estructura de tablas - echo "
"; - echo "

4. Estructura de Base de Datos

"; - - $requiredTables = ['users', 'conversations', 'menus', 'menu_options', 'autoresponses', 'webhook_logs']; - - echo ""; - foreach ($requiredTables as $table) { - $exists = $db->fetchAll("SHOW TABLES LIKE '$table'"); - $status = !empty($exists) ? 'ok' : 'error'; - $icon = !empty($exists) ? '✅' : '❌'; - - if ($status === 'error') $allOk = false; - - echo ""; - echo ""; - - if (!empty($exists)) { - // Contar registros - $count = $db->fetch("SELECT COUNT(*) as total FROM $table"); - echo ""; - } else { - echo ""; - } - - echo ""; - } - echo "
$icon Tabla: $table{$count['total']} registrosNO EXISTE
"; - echo "
"; - - // Test 5: Últimos mensajes recibidos - echo "
"; - echo "

5. Mensajes Recibidos Recientemente

"; - - $recentMessages = $db->fetchAll( - "SELECT c.*, u.phone_number, u.name - FROM conversations c - LEFT JOIN users u ON c.user_id = u.id - WHERE c.direction = 'incoming' - ORDER BY c.created_at DESC - LIMIT 5" - ); - - if (!empty($recentMessages)) { - echo "

✅ Se encontraron mensajes entrantes

"; - echo ""; - echo ""; - echo ""; - foreach ($recentMessages as $msg) { - $date = date('d/m/Y H:i', strtotime($msg['created_at'])); - $phone = $msg['phone_number'] ?? 'Desconocido'; - $content = htmlspecialchars(substr($msg['content'], 0, 50)); - $status = $msg['status']; - - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - } - echo "
FechaUsuarioMensajeEstado
$date$phone$content$status
"; - } else { - echo "

⚠️ No se encontraron mensajes entrantes. El webhook puede no estar recibiendo datos.

"; - echo "
"; - echo "Posibles causas:
"; - } - - echo "
"; - - // Test 6: Webhook Logs - echo "
"; - echo "

6. Logs del Webhook

"; - - $webhookLogs = $db->fetchAll( - "SELECT * FROM webhook_logs - ORDER BY created_at DESC - LIMIT 5" - ); - - if (!empty($webhookLogs)) { - echo "

✅ Se encontraron logs del webhook

"; - echo ""; - echo ""; - echo ""; - foreach ($webhookLogs as $log) { - $date = date('d/m/Y H:i:s', strtotime($log['created_at'])); - $status = $log['status_code']; - $ip = $log['ip_address']; - $statusClass = $status == 200 ? 'success' : 'danger'; - - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - } - echo "
FechaStatusIPDetalles
$date$status$ip
"; - } else { - echo "

ℹ️ No hay logs del webhook registrados

"; - } - - echo "
"; - - // Test 7: Configuración de Menús y Respuestas - echo "
"; - echo "

7. Configuración del Bot

"; - - $menuCount = $db->fetch("SELECT COUNT(*) as total FROM menus WHERE is_active = 1"); - $optionCount = $db->fetch("SELECT COUNT(*) as total FROM menu_options WHERE is_active = 1"); - $autoResponseCount = $db->fetch("SELECT COUNT(*) as total FROM autoresponses WHERE is_active = 1"); - - echo "
"; - echo "
"; - echo "
"; - echo "

{$menuCount['total']}

"; - echo "

Menús Activos

"; - echo "
"; - echo "
"; - echo "
"; - echo "
"; - echo "

{$optionCount['total']}

"; - echo "

Opciones de Menú

"; - echo "
"; - echo "
"; - echo "
"; - echo "
"; - echo "

{$autoResponseCount['total']}

"; - echo "

Respuestas Automáticas

"; - echo "
"; - echo "
"; - echo "
"; - - echo "
"; - - // Resumen final - echo "
"; - if ($allOk) { - echo "
"; - echo "

¡Sistema Configurado Correctamente!

"; - echo "

El webhook está listo para recibir mensajes de WhatsApp.

"; - echo "
"; - } else { - echo "
"; - echo "

Se encontraron problemas

"; - echo "

Revisa los errores anteriores y corrígelos antes de continuar.

"; - echo "
"; - } - - echo "
"; - echo "
Próximos pasos:
"; - echo "
    "; - echo "
  1. Copia la URL del webhook y configúrala en Meta Developer Console
  2. "; - echo "
  3. Usa el token de verificación: " . WEBHOOK_VERIFY_TOKEN . "
  4. "; - echo "
  5. Envía un mensaje de prueba desde WhatsApp
  6. "; - echo "
  7. Verifica que aparezca en la sección de mensajes recibidos
  8. "; - echo "
"; - echo "
"; - - echo "
"; - ?> - -
- - Volver al Dashboard - - -
- - - - - diff --git a/test_whatsapp_completo.php b/test_whatsapp_completo.php deleted file mode 100644 index 238e997..0000000 --- a/test_whatsapp_completo.php +++ /dev/null @@ -1,162 +0,0 @@ -🔍 Diagnóstico Avanzado de WhatsApp API"; - -// 1. Verificar configuración actual -echo "

1. Configuración Actual

"; -echo "TOKEN: " . substr(WHATSAPP_TOKEN, 0, 20) . "..." . substr(WHATSAPP_TOKEN, -10) . "
"; -echo "PHONE_NUMBER_ID: " . WHATSAPP_PHONE_NUMBER_ID . "
"; -echo "API_URL: " . WHATSAPP_API_URL . "
"; - -// 2. Test de conectividad con WhatsApp API -echo "

2. Test de Conectividad

"; - -try { - // Test 1: Verificar información del número de teléfono - $ch = curl_init(); - $url = WHATSAPP_API_URL . WHATSAPP_PHONE_NUMBER_ID; - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => [ - 'Authorization: Bearer ' . WHATSAPP_TOKEN, - 'Content-Type: application/json' - ], - CURLOPT_TIMEOUT => 30 - ]); - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - curl_close($ch); - - if ($error) { - echo "❌ Error de conexión: " . $error . "
"; - } else { - echo "📞 Código HTTP: $httpCode
"; - if ($httpCode == 200) { - $data = json_decode($response, true); - echo "✅ Conexión exitosa con WhatsApp API
"; - echo "
" . json_encode($data, JSON_PRETTY_PRINT) . "
"; - } else { - echo "❌ Error HTTP $httpCode
"; - echo "
$response
"; - } - } - -} catch (Exception $e) { - echo "❌ Excepción: " . $e->getMessage() . "
"; -} - -// 3. Test de mensaje a número de prueba (tu propio número) -echo "

3. Test de Mensaje Real

"; - -// Crear un botón para probar envío -?> -
- - - -

- - -

- -
- -🚀 Resultado del Test:"; - - try { - $whatsapp = new WhatsAppService(); - $phone = $_POST['phone']; - $message = $_POST['message']; - - echo "📞 Enviando a: $phone
"; - echo "💬 Mensaje: $message

"; - - $response = $whatsapp->sendTextMessage($phone, $message); - - echo "✅ Respuesta de WhatsApp API:
"; - echo "
";
-        echo json_encode($response, JSON_PRETTY_PRINT);
-        echo "
"; - - if (isset($response['messages'][0]['id'])) { - $messageId = $response['messages'][0]['id']; - echo "

📨 Mensaje enviado con ID: $messageId

"; - echo "

⏱️ Espera 1-2 minutos y verifica tu WhatsApp

"; - - // Mostrar información adicional - echo "

🔍 Información del Envío:

"; - if (isset($response['contacts'][0])) { - $contact = $response['contacts'][0]; - echo "📱 Número Input: " . $contact['input'] . "
"; - echo "📱 WhatsApp ID: " . $contact['wa_id'] . "
"; - } - } - - } catch (Exception $e) { - echo "❌ Error: " . $e->getMessage() . "
"; - } -} - -// 4. Verificar status del mensaje reciente -echo "

4. Verificar Logs del Sistema

"; -$logFile = 'logs/system.log'; -if (file_exists($logFile)) { - $logs = file_get_contents($logFile); - $lines = array_filter(array_slice(explode("\n", $logs), -20)); // Últimas 20 líneas - - echo "
";
-    foreach ($lines as $line) {
-        if (strpos($line, 'ERROR') !== false) {
-            echo "$line\n";
-        } elseif (strpos($line, 'WARNING') !== false) {
-            echo "$line\n";
-        } else {
-            echo "$line\n";
-        }
-    }
-    echo "
"; -} else { - echo "⚠️ No hay archivo de logs disponible
"; -} - -// 5. Posibles soluciones -echo "

5. ✅ Posibles Soluciones

"; -echo "
"; -echo "

🔧 Si el mensaje no llega, verifica:

"; -echo "
    "; -echo "
  1. Modo de desarrollo: ¿Tu app está en modo sandbox? Los mensajes solo llegan a números pre-aprobados
  2. "; -echo "
  3. Número verificado: ¿El número desde el que envías está verificado en Facebook Business?
  4. "; -echo "
  5. Permisos del token: ¿Tu token tiene permisos 'whatsapp_business_messaging'?
  6. "; -echo "
  7. Número de destino: ¿El número +573168950803 tiene WhatsApp activo?
  8. "; -echo "
  9. Rate limiting: ¿Has enviado muchos mensajes recientemente?
  10. "; -echo "
  11. Webhook URL: ¿Tu webhook está configurado correctamente para recibir respuestas?
  12. "; -echo "
"; -echo "
"; - -echo "
"; -echo "

📋 Pasos para resolver:

"; -echo "
    "; -echo "
  1. Ve a Facebook Developers
  2. "; -echo "
  3. Selecciona tu app → WhatsApp → Configuración
  4. "; -echo "
  5. Verifica que el número esté verificado y activo
  6. "; -echo "
  7. Revisa la sección 'Phone Numbers' y asegúrate que esté en modo Producción
  8. "; -echo "
  9. En 'Webhooks', verifica que tu URL esté configurada y validada
  10. "; -echo "
"; -echo "
"; -?> \ No newline at end of file diff --git a/test_whatsapp_config.php b/test_whatsapp_config.php deleted file mode 100644 index 8f4c79f..0000000 --- a/test_whatsapp_config.php +++ /dev/null @@ -1,52 +0,0 @@ -Test de Configuración WhatsApp"; - -// Obtener configuración desde BD -$config = getWhatsAppConfigFromDB(); - -echo "

Configuración desde Base de Datos:

"; -echo "
";
-echo "Token: " . (strlen($config['token']) > 0 ? substr($config['token'], 0, 20) . "... (length: " . strlen($config['token']) . ")" : "EMPTY") . "\n";
-echo "Phone Number ID: " . ($config['phone_number_id'] ?: "EMPTY") . "\n";
-echo "API URL: " . ($config['api_url'] ?: "EMPTY") . "\n";
-echo "
"; - -// Construir URL como lo haría WhatsAppService -$apiUrl = rtrim($config['api_url'], '/') . '/' . $config['phone_number_id'] . '/messages'; -echo "

URL que se construirá:

"; -echo "
" . $apiUrl . "
"; - -// Comparar con tu curl que funciona -$expectedUrl = "https://graph.facebook.com/v22.0/938064202726485/messages"; -echo "

URL que debería ser:

"; -echo "
" . $expectedUrl . "
"; - -echo "

¿Coinciden las URLs?

"; -echo "
" . ($apiUrl === $expectedUrl ? "✅ SÍ" : "❌ NO") . "
"; - -// Verificar token -$expectedTokenStart = "EAAcGjPRqunIBQVT6b86O0wWWGLGJ1PANmolIRXRBmEcpqa3P85VlUz0Rrv2c5deXky3gOKZCBw7ZCpdicly4c0G8qFMOH1DfOcSzmV49FlPId3AGPlHEXpqlsPFIE8DF18trW2vglQrFiCHXZCEBSUBQfp1jEASD1NrKKKVHiGVZARrwoxP0J931u0KIr2ZAZCug4fFpnyRZBls9LEgJuomXd4ZCnJtBZA4GvP2zaplmZA52mJiG5H4YiKKWby5GnQq9BgAVaq6sLZBnYI26aR0FgZDZD"; - -echo "

¿Coincide el token?

"; -$tokenMatch = substr($config['token'], 0, 50) === substr($expectedTokenStart, 0, 50); -echo "
" . ($tokenMatch ? "✅ SÍ (primeros 50 caracteres)" : "❌ NO") . "
"; - -if (!$tokenMatch) { - echo "

Debug Token:

"; - echo "
";
-    echo "Configurado: " . substr($config['token'], 0, 50) . "...\n";
-    echo "Esperado:    " . substr($expectedTokenStart, 0, 50) . "...\n";
-    echo "
"; -} - -// Test directo de configuración -echo "

Configuraciones Individuales:

"; -echo "
";
-foreach(['whatsapp_token', 'whatsapp_phone_number_id', 'whatsapp_api_url'] as $key) {
-    $value = getConfigFromDB($key, 'NO_ENCONTRADO');
-    echo "$key: " . ($value === 'NO_ENCONTRADO' ? "❌ NO_ENCONTRADO" : "✅ " . substr($value, 0, 30) . "...") . "\n";
-}
-echo "
"; -?> \ No newline at end of file diff --git a/whatsapp_tester.zip b/whatsapp_tester.zip new file mode 100644 index 0000000..41d5756 Binary files /dev/null and b/whatsapp_tester.zip differ