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

917 lines
39 KiB
Markdown

# 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:
```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=<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.