# 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 ```mermaid 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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 ```sql 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 ```sql 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 ```sql 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 ```sql 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) |