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
+
+
+
+
+
+
+
+
+
+
Laboratorio Ximena Caicedo
+
Instalación del Sistema de Chatbot WhatsApp
+
+
+
+ ';
+ 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 'Configura tus credenciales de WhatsApp Business API en el panel de administración ';
+ echo 'Prueba el menú principal enviando un mensaje de WhatsApp ';
+ echo 'Revisa las respuestas automáticas en la sección correspondiente ';
+ echo 'Personaliza los mensajes según las necesidades del laboratorio ';
+ 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 'Verifica que la base de datos existe y tiene permisos correctos ';
+ echo 'Revisa la configuración en config/config.php ';
+ echo 'Consulta el error arriba para más detalles ';
+ echo 'Intenta ejecutar la instalación nuevamente ';
+ echo ' ';
+ echo '
';
+ echo '';
+ echo ' Reintentar Instalación';
+ 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 "";
-foreach ($files as $file => $description) {
- $exists = file_exists($file);
- $status = $exists ? "✅ Existe" : "❌ Faltante";
- echo "{$description} ({$file}): {$status} ";
-}
-echo " ";
-
-// Probar configuración
-echo "⚙️ Verificación de Configuración: ";
-try {
- require_once 'config/config.php';
- echo "✅ Configuración cargada correctamente
";
- echo "";
- echo "DB Host: " . DB_HOST . " ";
- echo "DB Name: " . DB_NAME . " ";
- echo "WhatsApp Phone ID: " . WHATSAPP_PHONE_NUMBER_ID . " ";
- echo "App URL: " . APP_URL . " ";
- 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 "";
- foreach ($tables as $table) {
- try {
- $count = $db->fetch("SELECT COUNT(*) as count FROM {$table}")['count'];
- echo "Tabla {$table} : {$count} registros ";
- } catch (Exception $e) {
- echo "Tabla {$table} : ❌ Error: " . $e->getMessage() . " ";
- }
- }
- 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 "";
-foreach ($apis as $api) {
- $exists = file_exists($api);
- $readable = $exists ? is_readable($api) : false;
- $status = $readable ? "✅ Disponible" : "❌ No disponible";
- echo "{$api} : {$status} ";
-}
-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 "✅ 50+ Tests exhaustivos ";
-echo "🔌 17 APIs validadas ";
-echo "🔒 Seguridad completa ";
-echo "📝 Sistema de plantillas ";
-echo "🔗 Webhook y servicios ";
-echo "💾 Base de datos ";
-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 "1. Configura el webhook en Facebook Developers ";
- echo "2. Ajusta la configuración en el panel ";
- echo "3. Personaliza los menús según tus necesidades ";
- echo "4. ¡Comienza a recibir mensajes! ";
- 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 "
";
-echo "
";
-echo "
";
-echo "
";
-
-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 "Subir archivo diagnóstico: diagnostico_500.php ";
-echo "Verificar configuración: Revisar config/config.php ";
-echo "Probar login: login.php ";
-echo "Dashboard principal: index.php ";
-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 "
";
- echo "
";
-} 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.
-
-
-
-
-
-
-
-
-
-
Instalación completada: = isInstallationCompleted() ? '✅ Sí' : '❌ No' ?>
-
Sistema migrado: = isAuthSystemMigrated() ? '✅ Sí' : '❌ No' ?>
-
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();
- }
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Inicializar Defaults
-
Migrar a BD
-
Limpiar Cache
-
Ver Todas
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cargando configuraciones...
-
-
-
-
-
-
-
-
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
-
Modales Disponibles:
-
- Test Modal Plantilla
-
-
-
- Test Modal Menú
-
-
-
- Test Bootstrap
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Este es el modal de plantillas funcionando correctamente.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
- 📋 Probar get_menus.php
- 💾 Probar save_menu.php
- 🗑️ Probar delete_menu.php
- 🧽 Limpiar Resultados
-
-
-
-
-
-
\ 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
-
- 📋 Cargar y Mostrar Menús
- 🧽 Limpiar Tabla
-
-
-
- 📋 Tabla de Menús
-
-
-
- Nombre
- Clave
- Descripción
- Estado
- Opciones
- Fecha Creación
- Acciones
-
-
-
-
-
-
-
-
\ 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
-
-
-
-
-
-
-
-
-
-
-
-
Haz clic en el botón para probar el modal de crear plantilla:
-
-
- Nueva Plantilla
-
-
-
- Test Bootstrap
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
-
-
-
-
-
-
-
-
-
-
-
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
-
-
-
-
-
-
-
-
-
-
-
Esta página prueba que las funciones globales funcionen sin errores de SES.
-
-
-
-
Funciones disponibles:
-
- Test showCreateTemplateModal
-
-
-
- Test showCreateMenuModal
-
-
-
- Limpiar Log
-
-
-
-
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 "$icon $key ";
- echo "$displayValue ";
- echo " ";
- }
- echo "
";
- 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 "
- Copiar
- ";
- 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 "Haz clic en el botón de abajo para probar el webhook ";
- echo "Deberías ver el texto: test_challenge_12345 ";
- 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 "$icon Tabla: $table ";
-
- if (!empty($exists)) {
- // Contar registros
- $count = $db->fetch("SELECT COUNT(*) as total FROM $table");
- echo "{$count['total']} registros ";
- } else {
- echo "NO EXISTE ";
- }
-
- echo " ";
- }
- echo "
";
- 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 "Fecha Usuario Mensaje Estado ";
- 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 "$date ";
- echo "$phone ";
- echo "$content ";
- echo "$status ";
- echo " ";
- }
- echo "
";
- } else {
- echo "
⚠️ No se encontraron mensajes entrantes. El webhook puede no estar recibiendo datos.
";
- echo "
";
- echo "
Posibles causas: ";
- echo "El webhook no está configurado en Meta/Facebook ";
- echo "La URL del webhook es incorrecta ";
- echo "El servidor no es accesible desde Internet ";
- echo "Aún no se han enviado mensajes de prueba ";
- echo " ";
- }
-
- 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 "Fecha Status IP Detalles ";
- 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 "$date ";
- echo "$status ";
- echo "$ip ";
- echo "Ver ";
- echo " ";
- }
- echo "
";
- } 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 "Copia la URL del webhook y configúrala en Meta Developer Console ";
- echo "Usa el token de verificación: " . WEBHOOK_VERIFY_TOKEN . " ";
- echo "Envía un mensaje de prueba desde WhatsApp ";
- echo "Verifica que aparezca en la sección de mensajes recibidos ";
- echo " ";
- echo "
";
-
- echo "
";
- ?>
-
-
-
- Volver al Dashboard
-
-
- Recargar Test
-
-
-
-
-
-
-
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 "Modo de desarrollo: ¿Tu app está en modo sandbox? Los mensajes solo llegan a números pre-aprobados ";
-echo "Número verificado: ¿El número desde el que envías está verificado en Facebook Business? ";
-echo "Permisos del token: ¿Tu token tiene permisos 'whatsapp_business_messaging'? ";
-echo "Número de destino: ¿El número +573168950803 tiene WhatsApp activo? ";
-echo "Rate limiting: ¿Has enviado muchos mensajes recientemente? ";
-echo "Webhook URL: ¿Tu webhook está configurado correctamente para recibir respuestas? ";
-echo " ";
-echo "
";
-
-echo "";
-echo "
📋 Pasos para resolver: ";
-echo "
";
-echo "Ve a Facebook Developers ";
-echo "Selecciona tu app → WhatsApp → Configuración ";
-echo "Verifica que el número esté verificado y activo ";
-echo "Revisa la sección 'Phone Numbers' y asegúrate que esté en modo Producción ";
-echo "En 'Webhooks', verifica que tu URL esté configurada y validada ";
-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