Files
sirpremiumv2/public/DOCUMENTACION_BOT_WHATSAPP.md
T
2026-04-24 21:40:10 -05:00

39 KiB

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=<password>
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:

{
  "messaging_product": "whatsapp",
  "to": "57300XXXXXXX"
}

3.1.1 Mensaje de texto

{
  "messaging_product": "whatsapp",
  "to": "57300XXXXXXX",
  "type": "text",
  "text": {
    "body": "Hola, ¿en qué te podemos ayudar?"
  }
}

3.1.2 Mensaje de plantilla (template)

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "messaging_product": "whatsapp",
  "status": "read",
  "message_id": "wamid.XXXXX"
}

3.1.10 Enviar reacción a un mensaje

{
  "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:

{ "id": "1234567890123456" }

Una vez obtenido el id, se puede usar en mensajes así:

{
  "messaging_product": "whatsapp",
  "to": "57300XXXXXXX",
  "type": "image",
  "image": { "id": "1234567890123456" }
}

3.3 Obtener URL de un media — GET /{media_id}

Respuesta:

{
  "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)

{
  "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:

{
  "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=<id> → Bot espera número de opción

Atendido por asesor:
  in_service=1, in_service_by=<admin_id>
  → Bot NO responde, asesor humano escribe directamente

En espera (hold activo):
  on_hold=1, bot_paused_until=<datetime>
  → Bot notifica que está en espera, solo permite MENU/ATRÁS/números

Asesor solicitado:
  advisor_requested=1, bot_paused_until=<datetime>
  → Bot espera al asesor, solo permite navegación explícita

Bot pausado:
  bot_paused_until=<datetime futura>
  → 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.