26 KiB
26 KiB
DDL Specification: messaging Schema
Identificacion
| Campo | Valor |
|---|---|
| Schema | messaging |
| Modulo | MGN-017 WhatsApp Business Integration |
| Version | 1.0 |
| Fecha | 2025-12-05 |
| Estado | Ready |
Diagrama ER
erDiagram
tenants ||--o{ whatsapp_accounts : has
whatsapp_accounts ||--o{ whatsapp_templates : defines
whatsapp_accounts ||--o{ whatsapp_conversations : hosts
whatsapp_conversations ||--o{ whatsapp_messages : contains
whatsapp_conversations }o--|| contacts : with
whatsapp_conversations }o--|| users : assigned_to
tenants ||--o{ chatbot_flows : creates
chatbot_flows ||--o{ chatbot_nodes : contains
whatsapp_conversations ||--o{ chatbot_sessions : runs
whatsapp_accounts {
uuid id PK
uuid tenant_id FK
string phone_number UK
string display_name
string waba_id
string status
boolean is_active
}
whatsapp_templates {
uuid id PK
uuid account_id FK
string name
string category
string language
string status
jsonb components
}
whatsapp_conversations {
uuid id PK
uuid account_id FK
uuid contact_id FK
uuid assigned_user_id FK
string wa_conversation_id
string status
timestamp window_expires_at
}
whatsapp_messages {
uuid id PK
uuid conversation_id FK
string wa_message_id UK
string direction
string message_type
jsonb content
string status
timestamp sent_at
}
chatbot_flows {
uuid id PK
uuid tenant_id FK
string name
boolean is_active
jsonb trigger_config
}
chatbot_nodes {
uuid id PK
uuid flow_id FK
string node_type
jsonb config
jsonb connections
}
chatbot_sessions {
uuid id PK
uuid conversation_id FK
uuid flow_id FK
uuid current_node_id FK
jsonb context
string status
}
Tablas
1. whatsapp_accounts
Cuentas de WhatsApp Business conectadas.
CREATE TABLE messaging.whatsapp_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- WhatsApp Business Account (WABA)
waba_id VARCHAR(50) NOT NULL,
phone_number_id VARCHAR(50) NOT NULL,
phone_number VARCHAR(20) NOT NULL,
display_name VARCHAR(100) NOT NULL,
verified_name VARCHAR(100),
-- Calidad y limites
quality_rating VARCHAR(20) DEFAULT 'unknown',
messaging_limit VARCHAR(20) DEFAULT 'tier_1',
-- tier_1: 1K/day, tier_2: 10K/day, tier_3: 100K/day, unlimited
-- Configuracion
webhook_url VARCHAR(500),
webhook_verify_token VARCHAR(100),
-- Credenciales (encriptadas via integrations schema)
access_token_encrypted BYTEA,
token_expires_at TIMESTAMPTZ,
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'pending',
is_active BOOLEAN NOT NULL DEFAULT true,
last_synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by UUID,
CONSTRAINT uq_whatsapp_accounts UNIQUE (tenant_id, phone_number),
CONSTRAINT chk_whatsapp_accounts_status CHECK (status IN (
'pending', 'connected', 'disconnected', 'suspended', 'error'
)),
CONSTRAINT chk_whatsapp_accounts_quality CHECK (quality_rating IN (
'unknown', 'green', 'yellow', 'red'
)),
CONSTRAINT chk_whatsapp_accounts_phone CHECK (phone_number ~ '^\+[1-9]\d{6,14}$')
);
-- Indices
CREATE INDEX idx_whatsapp_accounts_tenant ON messaging.whatsapp_accounts(tenant_id);
CREATE INDEX idx_whatsapp_accounts_phone ON messaging.whatsapp_accounts(phone_number);
CREATE INDEX idx_whatsapp_accounts_active ON messaging.whatsapp_accounts(tenant_id, is_active)
WHERE is_active = true;
-- Trigger para updated_at
CREATE TRIGGER trg_whatsapp_accounts_updated_at
BEFORE UPDATE ON messaging.whatsapp_accounts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- RLS
ALTER TABLE messaging.whatsapp_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY whatsapp_accounts_tenant_isolation ON messaging.whatsapp_accounts
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Comentarios
COMMENT ON TABLE messaging.whatsapp_accounts IS 'Cuentas de WhatsApp Business conectadas (MGN-017)';
COMMENT ON COLUMN messaging.whatsapp_accounts.waba_id IS 'WhatsApp Business Account ID de Meta';
COMMENT ON COLUMN messaging.whatsapp_accounts.quality_rating IS 'Calidad de la cuenta: green, yellow, red';
2. whatsapp_templates
Templates de mensajes aprobados por Meta.
CREATE TABLE messaging.whatsapp_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES messaging.whatsapp_accounts(id) ON DELETE CASCADE,
-- Identificacion
external_id VARCHAR(50), -- ID en Meta
name VARCHAR(100) NOT NULL,
language VARCHAR(10) NOT NULL DEFAULT 'es',
-- Categoria
category VARCHAR(30) NOT NULL,
-- MARKETING, UTILITY, AUTHENTICATION
-- Componentes del template
components JSONB NOT NULL,
-- Ejemplo: [
-- {"type": "HEADER", "format": "TEXT", "text": "Hola {{1}}"},
-- {"type": "BODY", "text": "Tu pedido #{{1}} está en camino"},
-- {"type": "FOOTER", "text": "Gracias por tu compra"},
-- {"type": "BUTTONS", "buttons": [{"type": "QUICK_REPLY", "text": "OK"}]}
-- ]
-- Estado de aprobacion
status VARCHAR(20) NOT NULL DEFAULT 'pending',
rejection_reason VARCHAR(500),
-- Metadata
quality_score VARCHAR(20),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
submitted_at TIMESTAMPTZ,
approved_at TIMESTAMPTZ,
created_by UUID,
CONSTRAINT uq_whatsapp_templates UNIQUE (account_id, name, language),
CONSTRAINT chk_whatsapp_templates_category CHECK (category IN (
'MARKETING', 'UTILITY', 'AUTHENTICATION'
)),
CONSTRAINT chk_whatsapp_templates_status CHECK (status IN (
'pending', 'approved', 'rejected', 'disabled', 'paused'
))
);
-- Indices
CREATE INDEX idx_whatsapp_templates_account ON messaging.whatsapp_templates(account_id);
CREATE INDEX idx_whatsapp_templates_status ON messaging.whatsapp_templates(status);
CREATE INDEX idx_whatsapp_templates_category ON messaging.whatsapp_templates(category);
-- Trigger para updated_at
CREATE TRIGGER trg_whatsapp_templates_updated_at
BEFORE UPDATE ON messaging.whatsapp_templates
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Comentarios
COMMENT ON TABLE messaging.whatsapp_templates IS 'Templates de mensajes aprobados por Meta';
COMMENT ON COLUMN messaging.whatsapp_templates.components IS 'Estructura del template en formato Meta';
3. whatsapp_conversations
Conversaciones con contactos.
CREATE TABLE messaging.whatsapp_conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES messaging.whatsapp_accounts(id) ON DELETE CASCADE,
contact_id UUID REFERENCES core_catalogs.contacts(id),
-- WhatsApp IDs
wa_conversation_id VARCHAR(50),
contact_wa_id VARCHAR(50) NOT NULL, -- Phone number o WA ID
-- Asignacion
assigned_user_id UUID REFERENCES core_users.users(id),
assigned_at TIMESTAMPTZ,
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'open',
unread_count INT NOT NULL DEFAULT 0,
-- Ventana de 24 horas
window_expires_at TIMESTAMPTZ,
is_window_open BOOLEAN NOT NULL DEFAULT false,
-- Ultima actividad
last_message_at TIMESTAMPTZ,
last_message_preview VARCHAR(200),
last_message_direction VARCHAR(10),
-- Chatbot
chatbot_active BOOLEAN NOT NULL DEFAULT false,
chatbot_session_id UUID,
-- Tags y categorias
tags TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
closed_at TIMESTAMPTZ,
CONSTRAINT uq_whatsapp_conversations UNIQUE (account_id, contact_wa_id),
CONSTRAINT chk_whatsapp_conversations_status CHECK (status IN (
'open', 'pending', 'resolved', 'closed', 'spam'
))
);
-- Indices
CREATE INDEX idx_whatsapp_conversations_account ON messaging.whatsapp_conversations(account_id);
CREATE INDEX idx_whatsapp_conversations_contact ON messaging.whatsapp_conversations(contact_id);
CREATE INDEX idx_whatsapp_conversations_assigned ON messaging.whatsapp_conversations(assigned_user_id);
CREATE INDEX idx_whatsapp_conversations_status ON messaging.whatsapp_conversations(status);
CREATE INDEX idx_whatsapp_conversations_last_message ON messaging.whatsapp_conversations(last_message_at DESC);
CREATE INDEX idx_whatsapp_conversations_open ON messaging.whatsapp_conversations(account_id, status)
WHERE status IN ('open', 'pending');
-- Trigger para updated_at
CREATE TRIGGER trg_whatsapp_conversations_updated_at
BEFORE UPDATE ON messaging.whatsapp_conversations
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Comentarios
COMMENT ON TABLE messaging.whatsapp_conversations IS 'Conversaciones de WhatsApp con contactos';
COMMENT ON COLUMN messaging.whatsapp_conversations.window_expires_at IS 'Expiracion de ventana de 24h para mensajes libres';
4. whatsapp_messages
Mensajes enviados y recibidos.
CREATE TABLE messaging.whatsapp_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES messaging.whatsapp_conversations(id) ON DELETE CASCADE,
-- WhatsApp IDs
wa_message_id VARCHAR(100) NOT NULL,
wa_context_id VARCHAR(100), -- Mensaje al que responde
-- Direccion
direction VARCHAR(10) NOT NULL, -- inbound, outbound
-- Tipo y contenido
message_type VARCHAR(20) NOT NULL,
-- text, image, document, audio, video, sticker, location, contacts, interactive, template
content JSONB NOT NULL,
-- Ejemplo text: {"body": "Hola!"}
-- Ejemplo image: {"id": "...", "mime_type": "image/jpeg", "caption": "..."}
-- Ejemplo template: {"name": "order_confirmation", "language": "es", "components": [...]}
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'pending',
error_code VARCHAR(50),
error_message VARCHAR(500),
-- Timestamps
sent_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
read_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
-- Metadata
is_from_chatbot BOOLEAN NOT NULL DEFAULT false,
is_from_ai BOOLEAN NOT NULL DEFAULT false,
ai_agent_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_whatsapp_messages UNIQUE (conversation_id, wa_message_id),
CONSTRAINT chk_whatsapp_messages_direction CHECK (direction IN ('inbound', 'outbound')),
CONSTRAINT chk_whatsapp_messages_type CHECK (message_type IN (
'text', 'image', 'document', 'audio', 'video', 'sticker',
'location', 'contacts', 'interactive', 'template', 'reaction', 'unknown'
)),
CONSTRAINT chk_whatsapp_messages_status CHECK (status IN (
'pending', 'sent', 'delivered', 'read', 'failed'
))
);
-- Indices
CREATE INDEX idx_whatsapp_messages_conversation ON messaging.whatsapp_messages(conversation_id);
CREATE INDEX idx_whatsapp_messages_wa_id ON messaging.whatsapp_messages(wa_message_id);
CREATE INDEX idx_whatsapp_messages_direction ON messaging.whatsapp_messages(direction);
CREATE INDEX idx_whatsapp_messages_date ON messaging.whatsapp_messages(created_at DESC);
CREATE INDEX idx_whatsapp_messages_status ON messaging.whatsapp_messages(status)
WHERE status IN ('pending', 'failed');
-- Particionamiento por mes (recomendado para alto volumen)
-- CREATE TABLE messaging.whatsapp_messages_y2025m01 PARTITION OF messaging.whatsapp_messages
-- FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
-- Comentarios
COMMENT ON TABLE messaging.whatsapp_messages IS 'Mensajes de WhatsApp enviados y recibidos';
COMMENT ON COLUMN messaging.whatsapp_messages.content IS 'Contenido del mensaje en formato Meta';
5. chatbot_flows
Flujos de chatbot.
CREATE TABLE messaging.chatbot_flows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
-- Identificacion
name VARCHAR(100) NOT NULL,
description VARCHAR(500),
-- Trigger
trigger_type VARCHAR(30) NOT NULL,
-- keyword, first_message, menu_option, always
trigger_config JSONB NOT NULL DEFAULT '{}'::jsonb,
-- Ejemplo keyword: {"keywords": ["hola", "menu", "ayuda"]}
-- Ejemplo menu: {"option_id": "support"}
-- Cuentas donde aplica
account_ids UUID[], -- NULL = todas las cuentas del tenant
-- Estado
is_active BOOLEAN NOT NULL DEFAULT false,
is_draft BOOLEAN NOT NULL DEFAULT true,
-- AI Integration (MGN-018)
use_ai_fallback BOOLEAN NOT NULL DEFAULT false,
ai_agent_id UUID, -- FK a ai_agents.agents
-- Estadisticas
total_sessions INT NOT NULL DEFAULT 0,
completed_sessions INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
published_at TIMESTAMPTZ,
created_by UUID,
CONSTRAINT uq_chatbot_flows UNIQUE (tenant_id, name),
CONSTRAINT chk_chatbot_flows_trigger CHECK (trigger_type IN (
'keyword', 'first_message', 'menu_option', 'always', 'scheduled', 'manual'
))
);
-- Indices
CREATE INDEX idx_chatbot_flows_tenant ON messaging.chatbot_flows(tenant_id);
CREATE INDEX idx_chatbot_flows_active ON messaging.chatbot_flows(tenant_id, is_active)
WHERE is_active = true;
CREATE INDEX idx_chatbot_flows_trigger ON messaging.chatbot_flows(trigger_type);
-- Trigger para updated_at
CREATE TRIGGER trg_chatbot_flows_updated_at
BEFORE UPDATE ON messaging.chatbot_flows
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- RLS
ALTER TABLE messaging.chatbot_flows ENABLE ROW LEVEL SECURITY;
CREATE POLICY chatbot_flows_tenant_isolation ON messaging.chatbot_flows
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Comentarios
COMMENT ON TABLE messaging.chatbot_flows IS 'Flujos de chatbot automatizado';
COMMENT ON COLUMN messaging.chatbot_flows.trigger_type IS 'Cuando se activa: keyword, first_message, menu_option, always';
6. chatbot_nodes
Nodos de flujos de chatbot.
CREATE TABLE messaging.chatbot_nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flow_id UUID NOT NULL REFERENCES messaging.chatbot_flows(id) ON DELETE CASCADE,
-- Identificacion
node_key VARCHAR(50) NOT NULL, -- ID interno para referencias
name VARCHAR(100),
-- Tipo de nodo
node_type VARCHAR(30) NOT NULL,
-- send_message, ask_question, menu, condition, action, delay, handoff, ai_response
-- Configuracion del nodo
config JSONB NOT NULL,
-- Ejemplo send_message: {"message": "Hola, bienvenido!", "type": "text"}
-- Ejemplo ask_question: {"question": "Cual es tu email?", "variable": "email", "validation": "email"}
-- Ejemplo menu: {"text": "Selecciona:", "options": [{"id": "1", "text": "Soporte"}, ...]}
-- Ejemplo condition: {"variable": "email", "operator": "contains", "value": "@"}
-- Ejemplo action: {"type": "create_lead", "params": {"source": "whatsapp"}}
-- Conexiones a otros nodos
connections JSONB NOT NULL DEFAULT '[]'::jsonb,
-- Ejemplo: [{"from_output": "default", "to_node": "node_2"}, {"from_output": "option_1", "to_node": "node_3"}]
-- Posicion visual (para editor)
position JSONB DEFAULT '{"x": 0, "y": 0}'::jsonb,
-- Flags
is_start BOOLEAN NOT NULL DEFAULT false,
is_end BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_chatbot_nodes UNIQUE (flow_id, node_key),
CONSTRAINT chk_chatbot_nodes_type CHECK (node_type IN (
'send_message', 'ask_question', 'menu', 'condition',
'action', 'delay', 'handoff', 'ai_response', 'webhook'
))
);
-- Indices
CREATE INDEX idx_chatbot_nodes_flow ON messaging.chatbot_nodes(flow_id);
CREATE INDEX idx_chatbot_nodes_type ON messaging.chatbot_nodes(node_type);
CREATE INDEX idx_chatbot_nodes_start ON messaging.chatbot_nodes(flow_id, is_start)
WHERE is_start = true;
-- Trigger para updated_at
CREATE TRIGGER trg_chatbot_nodes_updated_at
BEFORE UPDATE ON messaging.chatbot_nodes
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Comentarios
COMMENT ON TABLE messaging.chatbot_nodes IS 'Nodos de flujos de chatbot';
COMMENT ON COLUMN messaging.chatbot_nodes.node_key IS 'Identificador interno para referencias entre nodos';
7. chatbot_sessions
Sesiones activas de chatbot.
CREATE TABLE messaging.chatbot_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES messaging.whatsapp_conversations(id) ON DELETE CASCADE,
flow_id UUID NOT NULL REFERENCES messaging.chatbot_flows(id),
-- Nodo actual
current_node_id UUID REFERENCES messaging.chatbot_nodes(id),
waiting_for_response BOOLEAN NOT NULL DEFAULT false,
-- Contexto y variables
context JSONB NOT NULL DEFAULT '{}'::jsonb,
-- Ejemplo: {"name": "Juan", "email": "juan@email.com", "order_number": "12345"}
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'active',
completed_nodes TEXT[] DEFAULT '{}',
-- Estadisticas
messages_processed INT NOT NULL DEFAULT 0,
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ,
timeout_at TIMESTAMPTZ,
CONSTRAINT chk_chatbot_sessions_status CHECK (status IN (
'active', 'waiting', 'completed', 'handoff', 'timeout', 'error'
))
);
-- Indices
CREATE INDEX idx_chatbot_sessions_conversation ON messaging.chatbot_sessions(conversation_id);
CREATE INDEX idx_chatbot_sessions_flow ON messaging.chatbot_sessions(flow_id);
CREATE INDEX idx_chatbot_sessions_status ON messaging.chatbot_sessions(status);
CREATE INDEX idx_chatbot_sessions_active ON messaging.chatbot_sessions(conversation_id, status)
WHERE status IN ('active', 'waiting');
-- Comentarios
COMMENT ON TABLE messaging.chatbot_sessions IS 'Sesiones activas de chatbot por conversacion';
COMMENT ON COLUMN messaging.chatbot_sessions.context IS 'Variables recopiladas durante el flujo';
8. whatsapp_campaigns
Campanas de marketing masivo.
CREATE TABLE messaging.whatsapp_campaigns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
account_id UUID NOT NULL REFERENCES messaging.whatsapp_accounts(id),
-- Identificacion
name VARCHAR(100) NOT NULL,
description VARCHAR(500),
-- Template a usar
template_id UUID NOT NULL REFERENCES messaging.whatsapp_templates(id),
template_params JSONB DEFAULT '{}'::jsonb,
-- Mapeo de variables: {"1": "contact.name", "2": "custom.promo_code"}
-- Audiencia
audience_type VARCHAR(30) NOT NULL,
-- all_contacts, tagged, segment, custom_list
audience_config JSONB NOT NULL,
-- Ejemplo tagged: {"tags": ["vip", "newsletter"]}
-- Ejemplo segment: {"filter": {"country": "MX", "last_order_days": 30}}
-- Programacion
scheduled_at TIMESTAMPTZ,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'draft',
-- Estadisticas
total_recipients INT NOT NULL DEFAULT 0,
sent_count INT NOT NULL DEFAULT 0,
delivered_count INT NOT NULL DEFAULT 0,
read_count INT NOT NULL DEFAULT 0,
failed_count INT NOT NULL DEFAULT 0,
opt_out_count INT NOT NULL DEFAULT 0,
-- Costo estimado
estimated_cost DECIMAL(10,2),
currency VARCHAR(3) DEFAULT 'MXN',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by UUID,
CONSTRAINT chk_whatsapp_campaigns_status CHECK (status IN (
'draft', 'scheduled', 'sending', 'completed', 'paused', 'cancelled'
)),
CONSTRAINT chk_whatsapp_campaigns_audience CHECK (audience_type IN (
'all_contacts', 'tagged', 'segment', 'custom_list'
))
);
-- Indices
CREATE INDEX idx_whatsapp_campaigns_tenant ON messaging.whatsapp_campaigns(tenant_id);
CREATE INDEX idx_whatsapp_campaigns_account ON messaging.whatsapp_campaigns(account_id);
CREATE INDEX idx_whatsapp_campaigns_status ON messaging.whatsapp_campaigns(status);
CREATE INDEX idx_whatsapp_campaigns_scheduled ON messaging.whatsapp_campaigns(scheduled_at)
WHERE status = 'scheduled';
-- RLS
ALTER TABLE messaging.whatsapp_campaigns ENABLE ROW LEVEL SECURITY;
CREATE POLICY whatsapp_campaigns_tenant_isolation ON messaging.whatsapp_campaigns
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Comentarios
COMMENT ON TABLE messaging.whatsapp_campaigns IS 'Campanas de marketing masivo por WhatsApp';
Funciones de Utilidad
Procesar Mensaje Entrante
CREATE OR REPLACE FUNCTION messaging.process_incoming_message(
p_account_id UUID,
p_wa_message_id VARCHAR,
p_from_number VARCHAR,
p_message_type VARCHAR,
p_content JSONB
) RETURNS UUID AS $$
DECLARE
v_conversation_id UUID;
v_message_id UUID;
v_contact_id UUID;
BEGIN
-- Buscar o crear conversacion
SELECT id, contact_id INTO v_conversation_id, v_contact_id
FROM messaging.whatsapp_conversations
WHERE account_id = p_account_id AND contact_wa_id = p_from_number;
IF v_conversation_id IS NULL THEN
-- Buscar contacto existente
SELECT c.id INTO v_contact_id
FROM core_catalogs.contacts c
JOIN messaging.whatsapp_accounts wa ON wa.tenant_id = c.tenant_id
WHERE wa.id = p_account_id
AND c.whatsapp_number = p_from_number
AND c.deleted_at IS NULL
LIMIT 1;
-- Crear conversacion
INSERT INTO messaging.whatsapp_conversations (
account_id, contact_id, contact_wa_id, status,
is_window_open, window_expires_at
) VALUES (
p_account_id, v_contact_id, p_from_number, 'open',
true, NOW() + INTERVAL '24 hours'
)
RETURNING id INTO v_conversation_id;
ELSE
-- Actualizar ventana
UPDATE messaging.whatsapp_conversations
SET is_window_open = true,
window_expires_at = NOW() + INTERVAL '24 hours',
updated_at = NOW()
WHERE id = v_conversation_id;
END IF;
-- Insertar mensaje
INSERT INTO messaging.whatsapp_messages (
conversation_id, wa_message_id, direction,
message_type, content, status, sent_at
) VALUES (
v_conversation_id, p_wa_message_id, 'inbound',
p_message_type, p_content, 'delivered', NOW()
)
RETURNING id INTO v_message_id;
-- Actualizar conversacion
UPDATE messaging.whatsapp_conversations
SET last_message_at = NOW(),
last_message_preview = LEFT(p_content->>'body', 200),
last_message_direction = 'inbound',
unread_count = unread_count + 1,
updated_at = NOW()
WHERE id = v_conversation_id;
RETURN v_message_id;
END;
$$ LANGUAGE plpgsql;
Verificar Ventana de Conversacion
CREATE OR REPLACE FUNCTION messaging.is_window_open(
p_conversation_id UUID
) RETURNS BOOLEAN AS $$
DECLARE
v_window_open BOOLEAN;
v_window_expires TIMESTAMPTZ;
BEGIN
SELECT is_window_open, window_expires_at
INTO v_window_open, v_window_expires
FROM messaging.whatsapp_conversations
WHERE id = p_conversation_id;
IF v_window_expires IS NOT NULL AND v_window_expires < NOW() THEN
-- Ventana expirada, actualizar
UPDATE messaging.whatsapp_conversations
SET is_window_open = false
WHERE id = p_conversation_id;
RETURN false;
END IF;
RETURN COALESCE(v_window_open, false);
END;
$$ LANGUAGE plpgsql;
Vistas
Vista: Inbox de Conversaciones
CREATE VIEW messaging.vw_inbox AS
SELECT
c.id,
c.account_id,
wa.phone_number AS account_phone,
c.contact_id,
ct.name AS contact_name,
ct.whatsapp_number AS contact_phone,
c.assigned_user_id,
u.full_name AS assigned_user_name,
c.status,
c.unread_count,
c.is_window_open,
c.window_expires_at,
c.last_message_at,
c.last_message_preview,
c.last_message_direction,
c.chatbot_active,
c.tags,
c.created_at
FROM messaging.whatsapp_conversations c
JOIN messaging.whatsapp_accounts wa ON wa.id = c.account_id
LEFT JOIN core_catalogs.contacts ct ON ct.id = c.contact_id
LEFT JOIN core_users.users u ON u.id = c.assigned_user_id
WHERE c.status NOT IN ('closed', 'spam')
ORDER BY c.last_message_at DESC NULLS LAST;
Vista: Metricas de WhatsApp
CREATE VIEW messaging.vw_whatsapp_metrics AS
SELECT
wa.tenant_id,
wa.id AS account_id,
wa.phone_number,
wa.display_name,
wa.quality_rating,
(SELECT COUNT(*) FROM messaging.whatsapp_conversations c
WHERE c.account_id = wa.id AND c.created_at > CURRENT_DATE - 30) AS conversations_30d,
(SELECT COUNT(*) FROM messaging.whatsapp_messages m
JOIN messaging.whatsapp_conversations c ON c.id = m.conversation_id
WHERE c.account_id = wa.id AND m.direction = 'outbound' AND m.created_at > CURRENT_DATE - 30) AS messages_sent_30d,
(SELECT COUNT(*) FROM messaging.whatsapp_messages m
JOIN messaging.whatsapp_conversations c ON c.id = m.conversation_id
WHERE c.account_id = wa.id AND m.direction = 'inbound' AND m.created_at > CURRENT_DATE - 30) AS messages_received_30d,
(SELECT COUNT(*) FROM messaging.whatsapp_conversations c
WHERE c.account_id = wa.id AND c.status = 'open') AS open_conversations
FROM messaging.whatsapp_accounts wa
WHERE wa.is_active = true;
Resumen de Tablas
| Tabla | Columnas | Descripcion |
|---|---|---|
| whatsapp_accounts | 20 | Cuentas de WhatsApp Business |
| whatsapp_templates | 15 | Templates aprobados por Meta |
| whatsapp_conversations | 20 | Conversaciones con contactos |
| whatsapp_messages | 18 | Mensajes enviados/recibidos |
| chatbot_flows | 17 | Flujos de chatbot |
| chatbot_nodes | 13 | Nodos de flujos |
| chatbot_sessions | 12 | Sesiones activas |
| whatsapp_campaigns | 21 | Campanas de marketing |
Total: 8 tablas, 136 columnas
Historial
| Version | Fecha | Autor | Cambios |
|---|---|---|---|
| 1.0 | 2025-12-05 | System | Creacion inicial (MGN-017) |