# Documentación Técnica — Bot WhatsApp Cloud API > **Propósito:** Esta documentación está pensada para ser entregada a otra IA o equipo de desarrollo para reimplementar el bot en cualquier lenguaje/plataforma, con toda la información necesaria sobre flujos, APIs, base de datos y contratos de datos. --- ## 1. Resumen del sistema El sistema es un **bot de WhatsApp** basado en la **WhatsApp Cloud API (Meta/Facebook Graph API v22.0)**. Permite: - Recibir y procesar mensajes entrantes mediante un **webhook HTTP**. - Responder automáticamente con textos, menús, botones interactivos, listas y archivos multimedia. - Transferir la conversación a un asesor humano. - Enviar **plantillas de mensaje** aprobadas por Meta. - Enviar **mensajes programados**. - Gestionar **términos y condiciones** (envío y aceptación). **Lenguaje original:** PHP 8.x **Base de datos:** MySQL / MariaDB **Cola / Workers:** Worker PHP con cron (no RabbitMQ ni Redis, aunque se usa Redis como opción para rate-limit). --- ## 2. Credenciales y configuración requerida Todas las credenciales se almacenan en la **tabla `system_config`** de la base de datos (no en código duro). También se cargan desde variables de entorno / archivo `.env`. ### 2.1 Variables de entorno (`.env`) ``` DB_HOST=mysql # Host de la base de datos DB_PORT=3306 DB_NAME=usite_whatsapp_bot DB_USER=usite_whatsapp_user DB_PASS= DB_CHARSET=utf8mb4 ``` ### 2.2 Configuración de WhatsApp (en tabla `system_config`) | `config_key` | Descripción | Ejemplo | |----------------------|-----------------------------------------------------------------|--------------------------------------------------| | `whatsapp_token` | Token de acceso permanente de la App de Meta | `EAABz...` | | `phone_number_id` | ID del número de teléfono de la cuenta de WhatsApp Business | `123456789012345` | | `whatsapp_api_url` | URL base de la Graph API | `https://graph.facebook.com/v22.0/` | | `webhook_verify_token` | Token secreto para verificar el webhook (GET de Meta) | `mi_token_secreto` | | `bot_enabled` | Si el bot responde automáticamente (`1`/`0`) | `1` | | `business_hours_*` | Horarios de atención (ver sección 8) | — | | `terms_message` | Mensaje de términos y condiciones que se envía al usuario | texto libre | --- ## 3. API de WhatsApp consumida **Base URL:** `https://graph.facebook.com/v22.0/{phone_number_id}/` **Autenticación:** Header `Authorization: Bearer {whatsapp_token}` **Formato:** JSON (Content-Type: `application/json`) salvo para subida de archivos (multipart/form-data). ### 3.1 Enviar mensajes — `POST /{phone_number_id}/messages` Todos los tipos de mensaje tienen el campo común: ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX" } ``` --- #### 3.1.1 Mensaje de texto ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "text", "text": { "body": "Hola, ¿en qué te podemos ayudar?" } } ``` --- #### 3.1.2 Mensaje de plantilla (template) ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "template", "template": { "name": "nombre_plantilla", "language": { "code": "es" }, "components": [ { "type": "header", "parameters": [ { "type": "text", "text": "Valor del header" } ] }, { "type": "body", "parameters": [ { "type": "text", "parameter_name": "nombre", "text": "Juan" }, { "type": "text", "parameter_name": "fecha", "text": "20/04/2026" } ] } ] } } ``` > **Nota sobre parámetros nombrados vs posicionales:** > - Si la plantilla usa variables `{{nombre}}` (nombradas): incluir `"parameter_name": "nombre"`. > - Si la plantilla usa variables `{{1}}`, `{{2}}` (posicionales): omitir `parameter_name`, usar solo `"type": "text", "text": "valor"` en orden. --- #### 3.1.3 Mensaje interactivo — Botones ```json { "messaging_product": "whatsapp", "recipient_type": "individual", "to": "57300XXXXXXX", "type": "interactive", "interactive": { "type": "button", "header": { "type": "text", "text": "Encabezado opcional" }, "body": { "text": "Selecciona una opción:" }, "footer": { "text": "Pie de página opcional" }, "action": { "buttons": [ { "type": "reply", "reply": { "id": "btn_1", "title": "Opción 1" } }, { "type": "reply", "reply": { "id": "btn_2", "title": "Opción 2" } } ] } } } ``` > Máximo **3 botones**. El `title` no puede superar 20 caracteres. --- #### 3.1.4 Mensaje interactivo — Lista ```json { "messaging_product": "whatsapp", "recipient_type": "individual", "to": "57300XXXXXXX", "type": "interactive", "interactive": { "type": "list", "body": { "text": "Elige una opción de la lista:" }, "action": { "button": "Ver opciones", "sections": [ { "title": "Sección 1", "rows": [ { "id": "row_1", "title": "Opción 1", "description": "Descripción opcional" }, { "id": "row_2", "title": "Opción 2" } ] } ] } } } ``` --- #### 3.1.5 Mensaje de imagen ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "image", "image": { "link": "https://ejemplo.com/imagen.jpg", "caption": "Pie de foto opcional" } } ``` --- #### 3.1.6 Mensaje de video ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "video", "video": { "link": "https://ejemplo.com/video.mp4", "caption": "Descripción del video" } } ``` --- #### 3.1.7 Mensaje de audio ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "audio", "audio": { "link": "https://ejemplo.com/audio.mp3" } } ``` > Para notas de voz, el archivo debe ser `.ogg` con códec **OPUS** y subirse primero al media endpoint. --- #### 3.1.8 Mensaje de documento ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "document", "document": { "link": "https://ejemplo.com/archivo.pdf", "filename": "resultados.pdf", "caption": "Tus resultados de laboratorio" } } ``` --- #### 3.1.9 Marcar mensaje como leído — `POST /{phone_number_id}/conversations` ```json { "messaging_product": "whatsapp", "status": "read", "message_id": "wamid.XXXXX" } ``` --- #### 3.1.10 Enviar reacción a un mensaje ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "reaction", "reaction": { "message_id": "wamid.XXXXX", "emoji": "👍" } } ``` --- ### 3.2 Subir archivo multimedia — `POST /{phone_number_id}/media` **Content-Type:** `multipart/form-data` Campos del formulario: | Campo | Valor | |---------------------|------------------------------------------| | `messaging_product` | `whatsapp` | | `file` | Archivo binario | | `type` | MIME type (ej: `image/jpeg`, `video/mp4`) | **Respuesta exitosa:** ```json { "id": "1234567890123456" } ``` Una vez obtenido el `id`, se puede usar en mensajes así: ```json { "messaging_product": "whatsapp", "to": "57300XXXXXXX", "type": "image", "image": { "id": "1234567890123456" } } ``` --- ### 3.3 Obtener URL de un media — `GET /{media_id}` **Respuesta:** ```json { "url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...", "mime_type": "image/jpeg", "sha256": "...", "file_size": 12345, "id": "1234567890123456", "messaging_product": "whatsapp" } ``` Luego descargar esa URL con el mismo Bearer token. --- ### 3.4 Gestión de plantillas — `GET /{waba_id}/message_templates` ``` GET https://graph.facebook.com/v22.0/{WABA_ID}/message_templates Authorization: Bearer {token} ``` **Parámetros opcionales:** `?name=nombre_plantilla&status=APPROVED` --- ## 4. Webhook — Recepción de mensajes ### 4.1 Verificación del webhook (GET) Meta envía un GET para verificar el endpoint: ``` GET /api/webhook.php?hub.mode=subscribe&hub.verify_token=TOKEN&hub.challenge=CHALLENGE ``` El sistema debe responder con el valor de `hub.challenge` si `hub.verify_token` coincide con `WEBHOOK_VERIFY_TOKEN`. --- ### 4.2 Payload de mensaje entrante (POST) ```json { "object": "whatsapp_business_account", "entry": [ { "id": "WABA_ID", "changes": [ { "field": "messages", "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "57300XXXXXXX", "phone_number_id": "PHONE_NUMBER_ID" }, "contacts": [ { "profile": { "name": "Nombre del Contacto" }, "wa_id": "57300XXXXXXX" } ], "messages": [ { "from": "57300XXXXXXX", "id": "wamid.XXXXXXXXXXXXX", "timestamp": "1713900000", "type": "text", "text": { "body": "Hola" } } ] } } ] } ] } ``` **Tipos de mensaje posibles en el campo `type`:** | `type` | Estructura del mensaje | |--------------|-------------------------------------------------------------| | `text` | `message.text.body` | | `image` | `message.image.id`, `message.image.mime_type` | | `video` | `message.video.id`, `message.video.mime_type` | | `audio` | `message.audio.id`, `message.audio.voice` (bool) | | `document` | `message.document.id`, `document.filename`, `document.mime_type` | | `sticker` | `message.sticker.id` | | `location` | `message.location.latitude`, `message.location.longitude` | | `interactive`| `message.interactive.type` = `list_reply` o `button_reply` | | `reaction` | `message.reaction.emoji`, `message.reaction.message_id` | **Respuesta de acceso interactivo:** ```json { "interactive": { "type": "list_reply", "list_reply": { "id": "row_1", "title": "Opción 1", "description": "descripción" } } } ``` --- ### 4.3 Normalización interna de mensajes interactivos El webhook normaliza las respuestas de lista/botón a texto plano para que el BotService las procese como si fueran texto: - Si `title` empieza con un número (`"1. Opción"`), se extrae solo el número. - Si no, se usa el `title` completo. --- ## 5. Flujo de procesamiento de mensajes ``` WhatsApp Cloud API │ ▼ POST /api/webhook.php │ ├─ Verificar duplicado (por message_id en conversations) ├─ Obtener/crear usuario en tabla `users` ├─ Guardar nombre del contacto si no tenía │ ▼ WhatsAppWebhook::processConversations() │ ├─ Extraer tipo de mensaje y contenido ├─ Guardar mensaje en tabla `conversations` (direction: incoming) │ ▼ BotService::processMessage($user, $messageText, $messageType) │ ├─ 1. Adquirir advisory lock por usuario (MySQL GET_LOCK) → evita race conditions ├─ 2. Si usuario bloqueado → descartar ├─ 3. Si usuario in_service (atendido por asesor) → descartar ├─ 4. maybeResetConversationAfterIdle() → reinicia si inactividad > 6h ├─ 5. Flujo de Términos: │ ├─ Si terms_pending → processTermsResponse() │ └─ Si needsTermsAcceptance() → sendTermsMessage() ├─ 6. Si 'isNewUser' → sendWelcomeMessage() ├─ 7. Si usuario escribe "enviado/a/os/as" → requestAdvisor() ├─ 8. processSpecialCommands() (MENU, ATRÁS, ASESOR, etc.) ├─ 9. Si on_hold activo y no expiró → notificar y retornar ├─10. Si advisor_requested activo → solo navegación explícita ├─11. Si bot_paused_until activo → solo navegación explícita ├─12. Si bot_enabled=false → solo comandos explícitos ├─13. Si current_menu_id → processMenuSelection() └─14. Fallback → sendDefaultNoMatch() ``` --- ## 6. Comandos especiales del bot Los usuarios pueden escribir estas palabras clave en cualquier momento: | Palabra clave | Acción | |----------------------------------------|--------------------------------------------| | `menu`, `menú`, `inicio` | Mostrar menú principal | | `atras`, `atrás`, `volver`, `back` | Retroceder al menú anterior | | `asesor`, `agente`, `humano` | Solicitar atención humana | | `enviado`, `enviada`, `enviados`, `enviadas` | Indicar que se envió documentación — transfiere a asesor | | Número (`1`, `2`, `3`…) | Seleccionar opción de menú activo | --- ## 7. Base de datos — Tablas principales del bot ### 7.1 `users` — Usuarios (contactos de WhatsApp) | Campo | Tipo | Descripción | |---------------------|-----------------------|----------------------------------------------------| | `id` | INT AUTO_INCREMENT | Clave primaria | | `phone_number` | VARCHAR(20) UNIQUE | Número en formato internacional sin `+` (ej: `573001234567`) | | `name` | VARCHAR(100) | Nombre del contacto (viene del perfil de WhatsApp) | | `status` | ENUM | `active`, `blocked`, `inactive` | | `current_menu_id` | INT NULL | FK → `menus.id` — menú donde está el usuario | | `current_step` | INT DEFAULT 0 | Paso dentro del flujo actual | | `session_data` | TEXT (JSON) | Datos temporales de sesión | | `welcome_sent_at` | DATETIME | Última vez que se envió el mensaje de bienvenida | | `in_service` | TINYINT(1) | `1` = está siendo atendido por un asesor | | `in_service_by` | INT NULL | FK → `admin_users.id` — asesor que lo atiende | | `on_hold` | TINYINT(1) | `1` = el asesor lo puso en espera | | `bot_paused_until` | DATETIME NULL | El bot está pausado hasta esta fecha/hora | | `advisor_requested` | TINYINT(1) | `1` = el usuario solicitó asesor | | `bot_enabled` | TINYINT(1) DEFAULT 1 | Si el bot responde a este usuario | | `terms_pending` | TINYINT(1) | `1` = esperando respuesta de términos | | `terms_accepted_at` | DATETIME | Cuándo aceptó los términos | | `terms_version_id` | INT NULL | FK → `terms_versions.id` | | `created_at` | TIMESTAMP | Fecha de creación | | `updated_at` | TIMESTAMP | Última actualización | --- ### 7.2 `conversations` — Mensajes | Campo | Tipo | Descripción | |--------------------------|-----------------|-----------------------------------------------------------| | `id` | INT | Clave primaria | | `user_id` | INT | FK → `users.id` | | `message_id` | VARCHAR(255) | ID único del mensaje en WhatsApp (`wamid.XXX`) | | `reply_to_message_id` | VARCHAR(255) | ID del mensaje al que responde | | `reaction_to_message_id` | VARCHAR(255) | ID del mensaje al que se reaccionó | | `reaction_emoji` | VARCHAR(64) | Emoji de la reacción | | `direction` | ENUM | `incoming` (usuario→bot) / `outgoing` (bot→usuario) | | `message_type` | VARCHAR(32) | `text`, `image`, `video`, `audio`, `document`, `reaction`| | `content` | TEXT | Contenido del mensaje (texto o JSON para multimedia/sistema) | | `media_url` | TEXT | URL pública del archivo | | `whatsapp_media_id` | VARCHAR(255) | ID del media en WhatsApp | | `local_file` | VARCHAR(255) | Ruta local del archivo descargado | | `local_thumb` | VARCHAR(255) | Ruta local del thumbnail | | `media_storage` | VARCHAR(50) | Dónde se almacena (`local`, `whatsapp`, etc.) | | `status` | VARCHAR(32) | `received`, `sent`, `delivered`, `read`, `failed` | | `is_read` | TINYINT(1) | Si el admin marcó la conversación como leída | | `filename` | VARCHAR(255) | Nombre del archivo adjunto | | `mime_type` | VARCHAR(100) | MIME type del archivo | | `created_at` | TIMESTAMP | Fecha del mensaje | --- ### 7.3 `menus` — Menús del bot | Campo | Tipo | Descripción | |-----------------|---------------|----------------------------------------------------------| | `id` | INT | Clave primaria | | `name` | VARCHAR(255) | Nombre interno del menú | | `title` | VARCHAR(255) | Título que se muestra al usuario | | `message` | TEXT | Texto del menú completo | | `parent_id` | INT NULL | FK → `menus.id` — menú padre (para navegación ATRÁS) | | `is_main` | TINYINT(1) | `1` = es el menú principal | | `is_active` | TINYINT(1) | `1` = activo | | `sort_order` | INT | Orden de presentación | | `created_at` | TIMESTAMP | | --- ### 7.4 `menu_options` — Opciones de menú | Campo | Tipo | Descripción | |-----------------|---------------|----------------------------------------------------------------| | `id` | INT | Clave primaria | | `menu_id` | INT | FK → `menus.id` | | `option_number` | INT | Número que el usuario debe escribir (1, 2, 3…) | | `title` | VARCHAR(255) | Texto de la opción | | `action_type` | ENUM | `submenu`, `message`, `template`, `url`, `advisor`, `flow` | | `action_value` | TEXT | Depends en `action_type`: ID de submenú, texto, nombre de plantilla, URL | | `is_active` | TINYINT(1) | `1` = activa | | `sort_order` | INT | Orden | --- ### 7.5 `message_templates` — Plantillas | Campo | Tipo | Descripción | |----------------------|---------------|----------------------------------------------------------------| | `id` | INT | Clave primaria | | `name` | VARCHAR(255) | Nombre interno | | `template_name` | VARCHAR(255) | Nombre en Meta (slug, sin espacios) | | `language_code` | VARCHAR(10) | Idioma (`es`, `en`, etc.) | | `status` | ENUM | `pending`, `approved`, `rejected`, `paused` | | `body_text` | TEXT | Cuerpo del template con variables `{{1}}` o `{{nombre}}` | | `header_type` | ENUM | `text`, `image`, `video`, `document` | | `header_text` | VARCHAR(255) | Texto del header si aplica | | `footer_text` | VARCHAR(255) | Pie del mensaje | | `components` | LONGTEXT JSON | Componentes completos del template tal como los devuelve Meta | | `example_parameters` | LONGTEXT JSON | Ejemplos de valores para las variables | | `created_at` | TIMESTAMP | | | `updated_at` | TIMESTAMP | | --- ### 7.6 `autoresponses` — Respuestas automáticas por keyword | Campo | Tipo | Descripción | |-----------------|-|-----------------------------------------------------------------| | `id` | INT | Clave primaria | | `trigger_type` | ENUM | `keyword`, `contains`, `exact`, `welcome`, `default` | | `trigger_value` | TEXT | Palabras clave separadas por coma | | `response_text` | TEXT | Texto de la respuesta | | `response_type` | ENUM | `text`, `template`, `menu` | | `template_name` | VARCHAR(255) | Nombre de la plantilla si `response_type = template` | | `menu_id` | INT NULL | ID del menú si `response_type = menu` | | `priority` | INT | Prioridad (mayor número = mayor prioridad) | | `is_active` | TINYINT(1) | `1` = activa | --- ### 7.7 `scheduled_messages` — Mensajes programados | Campo | Tipo | Descripción | |-----------------------|---------------|-----------------------------------------------------------| | `id` | INT | Clave primaria | | `user_id` | INT | FK → `users.id` | | `template_id` | INT NULL | FK → `message_templates.id` | | `template_name` | VARCHAR(100) | Nombre de la plantilla | | `template_language` | VARCHAR(10) | Idioma de la plantilla | | `template_parameters` | LONGTEXT JSON | Parámetros para las variables de la plantilla | | `message_type` | ENUM | `text`, `template` | | `message_content` | TEXT | Texto si `message_type = text` | | `scheduled_date` | DATE | Fecha programada | | `scheduled_time` | TIME | Hora programada | | `status` | ENUM | `pending`, `sent`, `failed`, `cancelled` | | `sent_at` | DATETIME | Cuándo se envió efectivamente | | `error_message` | TEXT | Error si falló el envío | | `created_by` | INT NULL | Admin que creó el recordatorio | | `created_at` | DATETIME | | --- ### 7.8 `system_config` — Configuración del sistema | Campo | Tipo | Descripción | |----------------|---------------|------------------------------------| | `id` | INT | Clave primaria | | `config_key` | VARCHAR(255) | Clave de configuración (UNIQUE) | | `config_value` | TEXT | Valor de la configuración | | `description` | TEXT NULL | Descripción opcional | | `created_at` | DATETIME | | | `updated_at` | DATETIME | | **Claves relevantes para el bot:** | `config_key` | Descripción | |--------------------------|------------------------------------------------------| | `whatsapp_token` | Token de acceso Bearer | | `phone_number_id` | ID del número de teléfono | | `whatsapp_api_url` | URL base del API | | `webhook_verify_token` | Token secreto del webhook | | `bot_enabled` | Activar/desactivar el bot (`1`/`0`) | | `welcome_message` | Texto del mensaje de bienvenida | | `default_no_match` | Mensaje cuando no se entiende la entrada del usuario| | `advisor_message` | Mensaje al solicitar asesor | | `terms_message` | Texto de los términos y condiciones | | `business_hours_enabled` | Si se validan horarios de atención | | `business_hours_start` | Hora inicio (ej: `06:15`) | | `business_hours_end` | Hora fin (ej: `17:00`) | --- ### 7.9 `notifications` — Notificaciones internas para asesores | Campo | Tipo | Descripción | |-------------|--------------|------------------------------------------| | `id` | INT | Clave primaria | | `user_id` | INT | FK → `users.id` | | `type` | VARCHAR(50) | Tipo de notificación (`new_message`, `advisor_requested`, etc.) | | `message` | TEXT | Cuerpo de la notificación | | `is_read` | TINYINT(1) | Si fue leída | | `created_at`| TIMESTAMP | | --- ### 7.10 `terms_versions` — Versiones de términos y condiciones | Campo | Tipo | Descripción | |--------------------|---------------|-------------------------------------------------------| | `id` | INT | Clave primaria | | `version` | VARCHAR(20) | Versión (ej: `1.0`, `2.0`) | | `titulo` | VARCHAR(255) | Título del documento | | `documento_url` | TEXT | URL pública del PDF de términos | | `mensaje_aceptacion` | TEXT | Mensaje que se envía al usuario solicitando aceptación | | `activa` | TINYINT(1) | `1` = versión vigente | | `forzar_reenvio` | TINYINT(1) | `1` = forzar que todos los usuarios re-acepten | | `created_at` | TIMESTAMP | | --- ### 7.11 `terms_acceptance` — Aceptación de términos | Campo | Tipo | Descripción | |--------------------|---------------|---------------------------------------| | `id` | INT | Clave primaria | | `user_id` | INT | FK → `users.id` | | `terms_version_id` | INT | FK → `terms_versions.id` | | `phone_number` | VARCHAR(20) | Teléfono del usuario | | `estado` | ENUM | `pendiente`, `aceptado`, `rechazado` | | `fecha_envio` | DATETIME | Cuándo se envió la solicitud | | `fecha_respuesta` | DATETIME NULL | Cuándo respondió el usuario | --- ### 7.12 `admin_users` — Administradores del sistema | Campo | Tipo | Descripción | |----------------|---------------|----------------------------------| | `id` | INT | Clave primaria | | `username` | VARCHAR(50) | Nombre de usuario (UNIQUE) | | `password_hash`| VARCHAR(255) | Bcrypt hash de la contraseña | | `full_name` | VARCHAR(100) | Nombre completo | | `email` | VARCHAR(100) | Correo electrónico | | `is_active` | TINYINT(1) | `1` = activo | | `role_id` | INT NULL | FK → `roles.id` | | `last_login` | TIMESTAMP | Último login | --- ### 7.13 `webhook_logs` — Logs del webhook | Campo | Tipo | Descripción | |---------------|---------------|---------------------------------------| | `id` | INT | Clave primaria | | `payload` | LONGTEXT | Body JSON recibido de WhatsApp | | `response` | TEXT | Respuesta devuelta | | `status_code` | INT | Código HTTP | | `created_at` | TIMESTAMP | Fecha del evento | --- ## 8. Formato del número de teléfono Los números se almacenan y envían **sin el símbolo `+`**, en formato E.164: - Colombia: `573001234567` (código país 57 + número sin prefijo) - Argentina: `5491123456789` El sistema normaliza automáticamente: - Elimina caracteres no numéricos. - Si el número tiene 10 dígitos y empieza con `3` → agrega prefijo `57` (Colombia). - Si ya tiene 12 dígitos → lo usa tal cual. --- ## 9. Lógica de estados del usuario El bot controla el flujo mediante campos en la tabla `users`: ``` Estado normal: bot_enabled=1, in_service=0, on_hold=0, advisor_requested=0, bot_paused_until=NULL → Bot responde normalmente En menú: current_menu_id= → Bot espera número de opción Atendido por asesor: in_service=1, in_service_by= → Bot NO responde, asesor humano escribe directamente En espera (hold activo): on_hold=1, bot_paused_until= → Bot notifica que está en espera, solo permite MENU/ATRÁS/números Asesor solicitado: advisor_requested=1, bot_paused_until= → Bot espera al asesor, solo permite navegación explícita Bot pausado: bot_paused_until= → Bot no responde mensajes libres, solo menús/comandos Pendiente de términos: terms_pending=1 → Bot solo procesa respuesta "acepto" o "rechazo" Usuario bloqueado: status='blocked' → Todos los mensajes se descartan silenciosamente ``` --- ## 10. Estados de conversación (tabla `user_states`) Estados clave/valor persistentes por usuario (como sesión): | `state_key` | Descripción | |----------------|---------------------------------------------------------------| | `menu_history` | Array JSON con historial de IDs de menús visitados | | `current_menu` | ID del menú actual | | `form_data` | Datos temporales de un formulario en curso | --- ## 11. Endpoints internos del sistema (API interna) Estos endpoints son para el **panel de administración**, no son de WhatsApp. Se implementarían como API REST en el nuevo sistema: | Método | Ruta interna | Descripción | |--------|-------------------------------|--------------------------------------------------| | GET | `/api/get_conversations.php` | Lista de conversaciones con paginación | | GET | `/api/get_conversation_detail.php?user_id=X` | Mensajes de un usuario | | POST | `/api/send_message.php` | Enviar mensaje desde el panel | | POST | `/api/send_reply.php` | Responder a un mensaje | | POST | `/api/attend.php` | Marcar usuario como "en atención" por asesor | | POST | `/api/finish_attend.php` | Liberar usuario de la atención | | POST | `/api/release_hold.php` | Liberar usuario del estado on_hold | | GET | `/api/get_templates.php` | Lista de plantillas aprobadas | | POST | `/api/send_broadcast.php` | Enviar mensaje masivo a múltiples usuarios | | POST | `/api/schedule_message.php` | Programar un mensaje | | GET | `/api/get_menus.php` | Lista de menús configurados | | POST | `/api/save_menu.php` | Crear/editar menú | | GET | `/api/get_settings.php` | Configuración del sistema | | POST | `/api/save_settings.php` | Guardar configuración | | GET | `/api/sse_events.php` | Server-Sent Events para tiempo real | --- ## 12. Flujo de términos y condiciones ``` Usuario envía mensaje │ ▼ ¿needsTermsAcceptance()? │ SÍ ▼ sendTermsMessage() → Envía texto con el mensaje de términos + URL del documento → Guarda users.terms_pending = 1 → Inserta en terms_acceptance (estado: 'pendiente') │ Usuario responde │ processTermsResponse() ├─ Si respuesta contiene "acepto", "si", "sí", "ok", "1" → ACEPTA │ → Actualiza users.terms_accepted_at, terms_version_id │ → Actualiza terms_acceptance.estado = 'aceptado' │ → Envía mensaje de confirmación └─ Si respuesta contiene "no", "rechazo", "2" → RECHAZA → Actualiza terms_acceptance.estado = 'rechazado' → Envía mensaje de rechazo ``` --- ## 13. Worker de mensajes programados Un proceso cron (o worker) ejecuta cada minuto: 1. Consulta `scheduled_messages` donde `status = 'pending'` y `scheduled_date + scheduled_time <= NOW()`. 2. Para cada registro: - Si `message_type = 'template'` → llama a `WhatsAppService::sendTemplateMessage()`. - Si `message_type = 'text'` → llama a `WhatsAppService::sendTextMessage()`. 3. Actualiza `status = 'sent'` y registra `sent_at`. 4. Si falla → `status = 'failed'` + registra `error_message`. --- ## 14. Diagrama de clases del bot ``` WhatsAppWebhook (api/webhook.php) │ ├──uses──▶ WhatsAppService (services/WhatsAppService.php) │ ├── sendTextMessage($to, $message) │ ├── sendTemplateMessage($to, $name, $lang, $bodyParams, $headerParams, $rawComponents) │ ├── sendInteractiveMessage($to, $body, $buttons, $header, $footer) │ ├── sendListMessage($to, $body, $buttonText, $sections, $header, $footer) │ ├── sendImageMessage($to, $url, $caption) │ ├── sendVideoMessage($to, $url, $caption) │ ├── sendAudioMessage($to, $url) │ ├── sendDocumentMessage($to, $url, $filename, $caption) │ ├── sendReactionMessage($to, $messageId, $emoji) │ ├── markAsRead($messageId) │ ├── uploadMedia($filePath, $mimeType) → retorna { id: "..." } │ └── getMediaUrl($mediaId) → retorna URL temporal │ └──uses──▶ BotService (services/BotService.php) │ ├──uses──▶ MenuService (services/MenuService.php) │ └── getMenu($id), getMenuOptions($menuId) │ ├──uses──▶ ConversationStateService (services/ConversationStateService.php) │ ├── getCurrentMenuId($phone) │ ├── setCurrentMenu($phone, $menuId) │ └── getStateData($phone, $key) │ ├──uses──▶ BusinessHoursService (services/BusinessHoursService.php) │ └── isWithinBusinessHours() │ └──uses──▶ NLPService (services/NLPService.php) └── analyze($text) → detección de intenciones básica ``` --- ## 15. Checklist de implementación Para reimplementar el bot en otro sistema/lenguaje: - [ ] Configurar la App en Meta for Developers con permisos `whatsapp_business_messaging`, `whatsapp_business_management`. - [ ] Obtener: `Access Token`, `Phone Number ID`, `WABA ID`, `Webhook Verify Token`. - [ ] Crear endpoint público HTTPS para el webhook (GET para verificación, POST para mensajes). - [ ] Crear las tablas de base de datos: `users`, `conversations`, `menus`, `menu_options`, `message_templates`, `autoresponses`, `system_config`, `notifications`, `terms_versions`, `terms_acceptance`, `scheduled_messages`, `webhook_logs`. - [ ] Implementar `WhatsAppService` con los métodos de envío (ver sección 3). - [ ] Implementar la lógica de estados del usuario (sección 9). - [ ] Implementar el flujo de menús dinámicos (mensajes numerados). - [ ] Implementar el flujo de términos y condiciones (sección 12). - [ ] Implementar sistema de advisory lock por usuario para evitar mensajes duplicados en procesamiento concurrente. - [ ] Implementar cron/worker para mensajes programados (sección 13). - [ ] Implementar reinicio de sesión después de 6h de inactividad. --- ## 16. Notas de seguridad - **Nunca** expongas el `whatsapp_token` en el frontend. - Validar siempre el `hub.verify_token` en el handshake del webhook. - Sanear todos los campos de texto antes de almacenar en BD para evitar XSS/SQLi. - Los mensajes duplicados se detectan por `message_id` único (`wamid.XXX`) — siempre validar antes de procesar. - Usar advisory locks a nivel de BD para evitar race conditions cuando WhatsApp envía el mismo webhook dos veces en paralelo.