9.9 KiB
9.9 KiB
RF-MGN-017-001: Conexión WhatsApp Business API
Módulo: MGN-017 - WhatsApp Business Integration Prioridad: P1 Story Points: 13 Estado: Definido Fecha: 2025-12-05
Descripción
El sistema debe permitir a los tenants conectar una o más cuentas de WhatsApp Business para habilitar la mensajería automatizada y manual con sus clientes. La conexión se realiza a través de la WhatsApp Business Cloud API de Meta.
Actores
- Actor Principal: Tenant Admin
- Actores Secundarios:
- Meta Business Suite (autorización)
- WhatsApp Business API (mensajería)
- Sistema (webhooks)
Precondiciones
- Tenant debe tener suscripción con feature
whatsapp_enabled - Tenant debe tener cuenta de Meta Business verificada
- Número de WhatsApp Business disponible (no personal)
- Proceso de verificación de negocio completado con Meta
Flujo Principal - Conectar Cuenta
- Tenant Admin accede a "Configuración > Integraciones > WhatsApp"
- Sistema muestra cuentas conectadas (si existen)
- Admin selecciona "Conectar nueva cuenta"
- Sistema muestra requisitos previos
- Admin confirma que cumple requisitos
- Sistema redirige a Meta Business Login (OAuth)
- Admin autoriza permisos requeridos:
whatsapp_business_managementwhatsapp_business_messaging
- Meta retorna access token y WABA ID
- Sistema solicita selección de número de teléfono
- Admin selecciona número a usar
- Sistema configura webhook URL
- Sistema verifica conexión enviando mensaje de prueba
- Sistema confirma configuración exitosa
Flujo Alternativo - Múltiples Números
- Admin tiene múltiples números en su WABA
- Sistema lista todos los números disponibles
- Admin puede conectar varios números (según límite del plan)
- Cada número se configura independientemente
- Sistema asigna alias a cada número ("Ventas", "Soporte")
Requisitos de Meta Business
Verificación de Negocio
- Documentos legales de la empresa
- Verificación de dominio web
- Proceso toma 2-5 días hábiles
Permisos OAuth Requeridos
whatsapp_business_management - Gestionar cuenta WABA
whatsapp_business_messaging - Enviar/recibir mensajes
business_management - Acceso a Business Suite
Límites Iniciales
| Tier | Conversaciones/día | Cómo alcanzar |
|---|---|---|
| Tier 0 | 250 | Cuenta nueva |
| Tier 1 | 1,000 | Número verificado |
| Tier 2 | 10,000 | Buen quality rating |
| Tier 3 | 100,000 | Alto volumen sostenido |
| Tier 4 | Ilimitado | Enterprise |
Reglas de Negocio
- RN-1: Un número de WhatsApp solo puede estar conectado a un tenant
- RN-2: Access tokens se refrescan automáticamente
- RN-3: Webhook URL debe ser HTTPS con certificado válido
- RN-4: Cada mensaje debe tener opt-in del destinatario
- RN-5: Quality rating debe mantenerse en "Green" o "Yellow"
- RN-6: Números desconectados mantienen historial por 90 días
Criterios de Aceptación
- Admin puede iniciar flujo de conexión OAuth
- Sistema obtiene y almacena tokens de forma segura
- Admin puede seleccionar número de múltiples disponibles
- Webhook se configura automáticamente
- Sistema valida conexión con mensaje de prueba
- Dashboard muestra estado de cada número conectado
- Admin puede desconectar número
- Sistema notifica problemas de quality rating
- Logs de conexión/desconexión disponibles
Entidades Involucradas
messaging.whatsapp_accounts
CREATE TABLE messaging.whatsapp_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id),
-- Meta Business
waba_id VARCHAR(50) NOT NULL, -- WhatsApp Business Account ID
phone_number_id VARCHAR(50) NOT NULL,
phone_number VARCHAR(20) NOT NULL, -- +521234567890
display_name VARCHAR(100),
-- Tokens (encriptados)
access_token_encrypted BYTEA NOT NULL,
token_expires_at TIMESTAMPTZ,
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'pending',
quality_rating VARCHAR(20), -- GREEN, YELLOW, RED
messaging_limit INT, -- Tier actual
-- Webhook
webhook_verify_token VARCHAR(100),
-- Configuración
config JSONB DEFAULT '{}',
-- {
-- "alias": "Ventas",
-- "auto_reply_enabled": true,
-- "business_hours": {...},
-- "welcome_message_template": "welcome_v1"
-- }
-- Timestamps
connected_at TIMESTAMPTZ,
last_message_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_phone_number UNIQUE (phone_number),
CONSTRAINT chk_status CHECK (status IN ('pending', 'active', 'suspended', 'disconnected'))
);
CREATE INDEX idx_wa_accounts_tenant ON messaging.whatsapp_accounts(tenant_id);
CREATE INDEX idx_wa_accounts_phone ON messaging.whatsapp_accounts(phone_number);
messaging.whatsapp_webhook_logs
CREATE TABLE messaging.whatsapp_webhook_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID REFERENCES messaging.whatsapp_accounts(id),
event_type VARCHAR(50) NOT NULL,
payload JSONB NOT NULL,
processed BOOLEAN DEFAULT false,
processed_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_wa_webhooks_account ON messaging.whatsapp_webhook_logs(account_id);
CREATE INDEX idx_wa_webhooks_type ON messaging.whatsapp_webhook_logs(event_type);
API WhatsApp Business Cloud
Endpoint Base
https://graph.facebook.com/v18.0/
Verificar Webhook (GET)
// GET /webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=xxx&hub.challenge=yyy
app.get('/webhooks/whatsapp', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
res.status(200).send(challenge);
} else {
res.sendStatus(403);
}
});
Recibir Webhook (POST)
// POST /webhooks/whatsapp
interface WhatsAppWebhook {
object: 'whatsapp_business_account';
entry: [{
id: string; // WABA ID
changes: [{
value: {
messaging_product: 'whatsapp';
metadata: {
display_phone_number: string;
phone_number_id: string;
};
contacts?: [{
profile: { name: string };
wa_id: string;
}];
messages?: [{
from: string;
id: string;
timestamp: string;
type: 'text' | 'image' | 'document' | 'button' | 'interactive';
text?: { body: string };
}];
statuses?: [{
id: string;
status: 'sent' | 'delivered' | 'read' | 'failed';
timestamp: string;
recipient_id: string;
}];
};
field: 'messages';
}];
}];
}
Enviar Mensaje de Texto
// POST /{phone_number_id}/messages
const response = await fetch(
`https://graph.facebook.com/v18.0/${phoneNumberId}/messages`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: '521234567890',
type: 'text',
text: {
preview_url: false,
body: 'Hola, este es un mensaje de prueba'
}
})
}
);
Enviar Template
// POST /{phone_number_id}/messages
{
"messaging_product": "whatsapp",
"to": "521234567890",
"type": "template",
"template": {
"name": "order_confirmation",
"language": { "code": "es_MX" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Juan" },
{ "type": "text", "text": "ORD-12345" },
{ "type": "text", "text": "$1,500.00" }
]
}
]
}
}
Dashboard de Cuenta
interface WhatsAppAccountDashboard {
account: {
phone_number: string;
display_name: string;
status: string;
quality_rating: 'GREEN' | 'YELLOW' | 'RED';
messaging_limit: number;
};
stats_today: {
messages_sent: number;
messages_received: number;
conversations_opened: number;
cost_estimate: number;
};
stats_month: {
total_conversations: number;
by_category: Record<string, number>;
total_cost: number;
};
}
Manejo de Quality Rating
| Rating | Significado | Acción |
|---|---|---|
| GREEN | Buena calidad | Mantener |
| YELLOW | Calidad media | Revisar contenido |
| RED | Mala calidad | Riesgo de suspensión |
Factores que Afectan Quality
- Tasa de bloqueos por usuarios
- Tasa de reportes de spam
- Tasa de respuesta a mensajes
- Contenido de templates rechazados
Seguridad
- Tokens: Encriptados con AES-256
- Webhook: Validar firma X-Hub-Signature-256
- Rate Limiting: Respetar límites de Meta
- Logs: No almacenar contenido sensible en logs
- Opt-in: Verificar consentimiento antes de enviar
Validación de Firma Webhook
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
appSecret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', appSecret)
.update(payload)
.digest('hex');
return `sha256=${expectedSignature}` === signature;
}
Referencias
Dependencias
- RF Requeridos: Ninguno (módulo base)
- Bloqueante para: RF-002 (Templates), RF-003 (Notificaciones), RF-004 (Inbox)