-- ============================================================= -- ARCHIVO: 15-whatsapp.sql -- DESCRIPCION: Integracion WhatsApp Business API, mensajes, templates -- VERSION: 1.0.0 -- PROYECTO: ERP-Core V2 -- FECHA: 2026-01-10 -- EPIC: SAAS-WHATSAPP (EPIC-SAAS-008) -- HISTORIAS: US-090, US-091, US-092 -- ============================================================= -- ===================== -- SCHEMA: whatsapp -- ===================== CREATE SCHEMA IF NOT EXISTS whatsapp; -- ===================== -- TABLA: whatsapp.accounts -- Cuentas de WhatsApp Business configuradas -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Identificación de la cuenta name VARCHAR(200) NOT NULL, phone_number VARCHAR(20) NOT NULL, phone_number_id VARCHAR(50) NOT NULL, -- ID en Meta business_account_id VARCHAR(50) NOT NULL, -- WABA ID -- Configuración de API access_token TEXT, -- Encriptado webhook_verify_token VARCHAR(255), webhook_secret VARCHAR(255), -- Perfil de negocio business_name VARCHAR(200), business_description TEXT, business_category VARCHAR(100), business_website TEXT, profile_picture_url TEXT, -- Configuración default_language VARCHAR(10) DEFAULT 'es_MX', auto_reply_enabled BOOLEAN DEFAULT FALSE, auto_reply_message TEXT, business_hours JSONB DEFAULT '{}', -- {monday: {start: "09:00", end: "18:00"}, ...} -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, active, suspended, disconnected verified_at TIMESTAMPTZ, -- Límites daily_message_limit INTEGER DEFAULT 1000, messages_sent_today INTEGER DEFAULT 0, last_limit_reset TIMESTAMPTZ, -- Estadísticas total_messages_sent BIGINT DEFAULT 0, total_messages_received BIGINT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), UNIQUE(tenant_id, phone_number) ); -- ===================== -- TABLA: whatsapp.templates -- Templates aprobados por Meta -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES whatsapp.accounts(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Identificación name VARCHAR(512) NOT NULL, -- Nombre en Meta (snake_case) display_name VARCHAR(200) NOT NULL, -- Nombre legible description TEXT, -- Categoría (requerida por Meta) category VARCHAR(30) NOT NULL, -- MARKETING, UTILITY, AUTHENTICATION -- Idioma language VARCHAR(10) NOT NULL DEFAULT 'es_MX', -- Componentes del template header_type VARCHAR(20), -- TEXT, IMAGE, VIDEO, DOCUMENT header_text TEXT, header_media_url TEXT, body_text TEXT NOT NULL, body_variables TEXT[] DEFAULT '{}', -- {{1}}, {{2}}, etc. footer_text VARCHAR(60), -- Botones buttons JSONB DEFAULT '[]', -- [{type: "QUICK_REPLY", text: "Sí"}, {type: "URL", text: "Ver más", url: "..."}] -- Estado en Meta meta_template_id VARCHAR(50), meta_status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, PAUSED, DISABLED rejection_reason TEXT, -- Uso is_active BOOLEAN DEFAULT TRUE, usage_count INTEGER DEFAULT 0, last_used_at TIMESTAMPTZ, -- Versionamiento version INTEGER DEFAULT 1, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, submitted_at TIMESTAMPTZ, approved_at TIMESTAMPTZ, UNIQUE(account_id, name, language) ); -- ===================== -- TABLA: whatsapp.contacts -- Contactos de WhatsApp -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES whatsapp.accounts(id) ON DELETE CASCADE, -- Número de teléfono phone_number VARCHAR(20) NOT NULL, wa_id VARCHAR(50), -- WhatsApp ID -- Perfil (obtenido de WhatsApp) profile_name VARCHAR(200), profile_picture_url TEXT, -- Asociación con entidades customer_id UUID, user_id UUID REFERENCES auth.users(id), -- Estado de conversación conversation_status VARCHAR(20) DEFAULT 'active', -- active, waiting, resolved, blocked last_message_at TIMESTAMPTZ, last_message_direction VARCHAR(10), -- inbound, outbound -- Ventana de 24 horas conversation_window_expires_at TIMESTAMPTZ, can_send_template_only BOOLEAN DEFAULT TRUE, -- Opt-in/Opt-out opted_in BOOLEAN DEFAULT FALSE, opted_in_at TIMESTAMPTZ, opted_out BOOLEAN DEFAULT FALSE, opted_out_at TIMESTAMPTZ, -- Tags y categorización tags TEXT[] DEFAULT '{}', notes TEXT, -- Estadísticas total_messages_sent INTEGER DEFAULT 0, total_messages_received INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, UNIQUE(account_id, phone_number) ); -- ===================== -- TABLA: whatsapp.conversations -- Conversaciones (hilos de chat) -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES whatsapp.accounts(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES whatsapp.contacts(id) ON DELETE CASCADE, -- Estado status VARCHAR(20) DEFAULT 'open', -- open, pending, resolved, closed priority VARCHAR(20) DEFAULT 'normal', -- low, normal, high, urgent -- Asignación assigned_to UUID REFERENCES auth.users(id), assigned_at TIMESTAMPTZ, team_id UUID, -- Categorización category VARCHAR(50), tags TEXT[] DEFAULT '{}', -- Contexto context_type VARCHAR(50), -- support, sales, order, general context_id UUID, -- Tiempos first_response_at TIMESTAMPTZ, resolved_at TIMESTAMPTZ, -- Estadísticas message_count INTEGER DEFAULT 0, unread_count INTEGER DEFAULT 0, -- Metadata metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- TABLA: whatsapp.messages -- Mensajes enviados y recibidos -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES whatsapp.accounts(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES whatsapp.contacts(id) ON DELETE CASCADE, conversation_id UUID REFERENCES whatsapp.conversations(id), -- Identificadores de Meta wa_message_id VARCHAR(100), -- ID del mensaje en WhatsApp wa_conversation_id VARCHAR(100), -- ID de conversación en Meta -- Dirección direction VARCHAR(10) NOT NULL, -- inbound, outbound -- Tipo de mensaje message_type VARCHAR(20) NOT NULL, -- text, image, video, audio, document, sticker, location, contacts, interactive, template, reaction -- Contenido content TEXT, caption TEXT, -- Media media_id VARCHAR(100), media_url TEXT, media_mime_type VARCHAR(100), media_sha256 VARCHAR(64), media_size_bytes INTEGER, -- Template (si aplica) template_id UUID REFERENCES whatsapp.templates(id), template_name VARCHAR(512), template_variables JSONB DEFAULT '[]', -- Interactivo (si aplica) interactive_type VARCHAR(30), -- button, list, product, product_list interactive_data JSONB DEFAULT '{}', -- Contexto (respuesta a otro mensaje) context_message_id VARCHAR(100), quoted_message_id UUID REFERENCES whatsapp.messages(id), -- Estado del mensaje status VARCHAR(20) DEFAULT 'pending', -- pending, sent, delivered, read, failed status_updated_at TIMESTAMPTZ, -- Error (si falló) error_code VARCHAR(20), error_message TEXT, -- Costos (para mensajes de template) is_billable BOOLEAN DEFAULT FALSE, cost_category VARCHAR(30), -- utility, authentication, marketing -- Metadata metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, sent_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, read_at TIMESTAMPTZ ); -- ===================== -- TABLA: whatsapp.message_status_updates -- Actualizaciones de estado de mensajes -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.message_status_updates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES whatsapp.messages(id) ON DELETE CASCADE, -- Estado status VARCHAR(20) NOT NULL, previous_status VARCHAR(20), -- Error (si aplica) error_code VARCHAR(20), error_title VARCHAR(200), error_message TEXT, -- Timestamp de Meta meta_timestamp TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- TABLA: whatsapp.quick_replies -- Respuestas rápidas predefinidas -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.quick_replies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, account_id UUID REFERENCES whatsapp.accounts(id) ON DELETE CASCADE, -- Identificación shortcut VARCHAR(50) NOT NULL, -- /gracias, /horario title VARCHAR(200) NOT NULL, category VARCHAR(50), -- Contenido message_type VARCHAR(20) DEFAULT 'text', content TEXT NOT NULL, media_url TEXT, -- Uso usage_count INTEGER DEFAULT 0, last_used_at TIMESTAMPTZ, -- Estado is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), UNIQUE(tenant_id, shortcut) ); -- ===================== -- TABLA: whatsapp.automations -- Automatizaciones de WhatsApp -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.automations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES whatsapp.accounts(id) ON DELETE CASCADE, -- Identificación name VARCHAR(200) NOT NULL, description TEXT, -- Trigger trigger_type VARCHAR(30) NOT NULL, -- keyword, first_message, after_hours, no_response, webhook trigger_config JSONB NOT NULL DEFAULT '{}', -- keyword: {keywords: ["hola", "info"]} -- after_hours: {message: "..."} -- no_response: {delay_minutes: 30} -- Acción action_type VARCHAR(30) NOT NULL, -- send_message, send_template, assign_agent, add_tag, create_ticket action_config JSONB NOT NULL DEFAULT '{}', -- Condiciones adicionales conditions JSONB DEFAULT '[]', -- Estado is_active BOOLEAN DEFAULT TRUE, priority INTEGER DEFAULT 0, -- Estadísticas trigger_count INTEGER DEFAULT 0, last_triggered_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id) ); -- ===================== -- TABLA: whatsapp.broadcasts -- Envíos masivos de mensajes -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.broadcasts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES whatsapp.accounts(id) ON DELETE CASCADE, -- Identificación name VARCHAR(200) NOT NULL, description TEXT, -- Template a usar template_id UUID NOT NULL REFERENCES whatsapp.templates(id), -- Audiencia audience_type VARCHAR(30) NOT NULL, -- all, segment, custom, file audience_filter JSONB DEFAULT '{}', recipient_count INTEGER DEFAULT 0, -- Programación scheduled_at TIMESTAMPTZ, timezone VARCHAR(50) DEFAULT 'America/Mexico_City', -- Estado status VARCHAR(20) DEFAULT 'draft', -- draft, scheduled, sending, completed, cancelled, failed -- Progreso sent_count INTEGER DEFAULT 0, delivered_count INTEGER DEFAULT 0, read_count INTEGER DEFAULT 0, failed_count INTEGER DEFAULT 0, reply_count INTEGER DEFAULT 0, -- Tiempos started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, -- Costos estimados estimated_cost DECIMAL(10,2), actual_cost DECIMAL(10,2), created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id) ); -- ===================== -- TABLA: whatsapp.broadcast_recipients -- Destinatarios de broadcasts -- ===================== CREATE TABLE IF NOT EXISTS whatsapp.broadcast_recipients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), broadcast_id UUID NOT NULL REFERENCES whatsapp.broadcasts(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES whatsapp.contacts(id) ON DELETE CASCADE, -- Variables del template template_variables JSONB DEFAULT '[]', -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, sent, delivered, read, failed -- Mensaje enviado message_id UUID REFERENCES whatsapp.messages(id), -- Error error_code VARCHAR(20), error_message TEXT, -- Tiempos sent_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, UNIQUE(broadcast_id, contact_id) ); -- ===================== -- INDICES -- ===================== -- Indices para accounts CREATE INDEX IF NOT EXISTS idx_wa_accounts_tenant ON whatsapp.accounts(tenant_id); CREATE INDEX IF NOT EXISTS idx_wa_accounts_phone ON whatsapp.accounts(phone_number); CREATE INDEX IF NOT EXISTS idx_wa_accounts_status ON whatsapp.accounts(status); -- Indices para templates CREATE INDEX IF NOT EXISTS idx_wa_templates_account ON whatsapp.templates(account_id); CREATE INDEX IF NOT EXISTS idx_wa_templates_tenant ON whatsapp.templates(tenant_id); CREATE INDEX IF NOT EXISTS idx_wa_templates_status ON whatsapp.templates(meta_status); CREATE INDEX IF NOT EXISTS idx_wa_templates_category ON whatsapp.templates(category); -- Indices para contacts CREATE INDEX IF NOT EXISTS idx_wa_contacts_tenant ON whatsapp.contacts(tenant_id); CREATE INDEX IF NOT EXISTS idx_wa_contacts_account ON whatsapp.contacts(account_id); CREATE INDEX IF NOT EXISTS idx_wa_contacts_phone ON whatsapp.contacts(phone_number); CREATE INDEX IF NOT EXISTS idx_wa_contacts_tags ON whatsapp.contacts USING GIN(tags); CREATE INDEX IF NOT EXISTS idx_wa_contacts_opted_in ON whatsapp.contacts(opted_in) WHERE opted_in = TRUE; -- Indices para conversations CREATE INDEX IF NOT EXISTS idx_wa_convos_tenant ON whatsapp.conversations(tenant_id); CREATE INDEX IF NOT EXISTS idx_wa_convos_contact ON whatsapp.conversations(contact_id); CREATE INDEX IF NOT EXISTS idx_wa_convos_status ON whatsapp.conversations(status); CREATE INDEX IF NOT EXISTS idx_wa_convos_assigned ON whatsapp.conversations(assigned_to); CREATE INDEX IF NOT EXISTS idx_wa_convos_created ON whatsapp.conversations(created_at DESC); -- Indices para messages CREATE INDEX IF NOT EXISTS idx_wa_messages_tenant ON whatsapp.messages(tenant_id); CREATE INDEX IF NOT EXISTS idx_wa_messages_account ON whatsapp.messages(account_id); CREATE INDEX IF NOT EXISTS idx_wa_messages_contact ON whatsapp.messages(contact_id); CREATE INDEX IF NOT EXISTS idx_wa_messages_conversation ON whatsapp.messages(conversation_id); CREATE INDEX IF NOT EXISTS idx_wa_messages_wa_id ON whatsapp.messages(wa_message_id); CREATE INDEX IF NOT EXISTS idx_wa_messages_status ON whatsapp.messages(status); CREATE INDEX IF NOT EXISTS idx_wa_messages_created ON whatsapp.messages(created_at DESC); CREATE INDEX IF NOT EXISTS idx_wa_messages_direction ON whatsapp.messages(direction); -- Indices para status updates CREATE INDEX IF NOT EXISTS idx_wa_status_message ON whatsapp.message_status_updates(message_id); -- Indices para quick_replies CREATE INDEX IF NOT EXISTS idx_wa_quick_replies_tenant ON whatsapp.quick_replies(tenant_id); CREATE INDEX IF NOT EXISTS idx_wa_quick_replies_shortcut ON whatsapp.quick_replies(shortcut); -- Indices para automations CREATE INDEX IF NOT EXISTS idx_wa_automations_account ON whatsapp.automations(account_id); CREATE INDEX IF NOT EXISTS idx_wa_automations_active ON whatsapp.automations(is_active) WHERE is_active = TRUE; -- Indices para broadcasts CREATE INDEX IF NOT EXISTS idx_wa_broadcasts_account ON whatsapp.broadcasts(account_id); CREATE INDEX IF NOT EXISTS idx_wa_broadcasts_status ON whatsapp.broadcasts(status); CREATE INDEX IF NOT EXISTS idx_wa_broadcasts_scheduled ON whatsapp.broadcasts(scheduled_at) WHERE status = 'scheduled'; -- Indices para broadcast_recipients CREATE INDEX IF NOT EXISTS idx_wa_bcast_recip_broadcast ON whatsapp.broadcast_recipients(broadcast_id); CREATE INDEX IF NOT EXISTS idx_wa_bcast_recip_status ON whatsapp.broadcast_recipients(status); -- ===================== -- RLS POLICIES -- ===================== ALTER TABLE whatsapp.accounts ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_accounts ON whatsapp.accounts USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE whatsapp.templates ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_templates ON whatsapp.templates USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE whatsapp.contacts ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_contacts ON whatsapp.contacts USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE whatsapp.conversations ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_convos ON whatsapp.conversations USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE whatsapp.messages ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_messages ON whatsapp.messages USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE whatsapp.quick_replies ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_quick ON whatsapp.quick_replies USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE whatsapp.automations ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_auto ON whatsapp.automations USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE whatsapp.broadcasts ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_wa_broadcasts ON whatsapp.broadcasts USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- ===================== -- FUNCIONES -- ===================== -- Función para obtener o crear contacto CREATE OR REPLACE FUNCTION whatsapp.get_or_create_contact( p_account_id UUID, p_phone_number VARCHAR(20), p_profile_name VARCHAR(200) DEFAULT NULL, p_wa_id VARCHAR(50) DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_contact_id UUID; v_tenant_id UUID; BEGIN -- Obtener tenant de la cuenta SELECT tenant_id INTO v_tenant_id FROM whatsapp.accounts WHERE id = p_account_id; -- Buscar contacto existente SELECT id INTO v_contact_id FROM whatsapp.contacts WHERE account_id = p_account_id AND phone_number = p_phone_number; IF FOUND THEN -- Actualizar perfil si hay nuevos datos IF p_profile_name IS NOT NULL OR p_wa_id IS NOT NULL THEN UPDATE whatsapp.contacts SET profile_name = COALESCE(p_profile_name, profile_name), wa_id = COALESCE(p_wa_id, wa_id), updated_at = CURRENT_TIMESTAMP WHERE id = v_contact_id; END IF; RETURN v_contact_id; END IF; -- Crear nuevo contacto INSERT INTO whatsapp.contacts ( tenant_id, account_id, phone_number, profile_name, wa_id ) VALUES ( v_tenant_id, p_account_id, p_phone_number, p_profile_name, p_wa_id ) RETURNING id INTO v_contact_id; RETURN v_contact_id; END; $$ LANGUAGE plpgsql; -- Función para crear o obtener conversación activa CREATE OR REPLACE FUNCTION whatsapp.get_active_conversation( p_contact_id UUID ) RETURNS UUID AS $$ DECLARE v_conversation_id UUID; v_contact RECORD; BEGIN -- Obtener datos del contacto SELECT * INTO v_contact FROM whatsapp.contacts WHERE id = p_contact_id; -- Buscar conversación abierta SELECT id INTO v_conversation_id FROM whatsapp.conversations WHERE contact_id = p_contact_id AND status IN ('open', 'pending') ORDER BY created_at DESC LIMIT 1; IF FOUND THEN RETURN v_conversation_id; END IF; -- Crear nueva conversación INSERT INTO whatsapp.conversations ( tenant_id, account_id, contact_id ) VALUES ( v_contact.tenant_id, v_contact.account_id, p_contact_id ) RETURNING id INTO v_conversation_id; RETURN v_conversation_id; END; $$ LANGUAGE plpgsql; -- Función para registrar mensaje entrante CREATE OR REPLACE FUNCTION whatsapp.receive_message( p_account_id UUID, p_phone_number VARCHAR(20), p_wa_message_id VARCHAR(100), p_message_type VARCHAR(20), p_content TEXT, p_media_id VARCHAR(100) DEFAULT NULL, p_media_url TEXT DEFAULT NULL, p_context_message_id VARCHAR(100) DEFAULT NULL, p_profile_name VARCHAR(200) DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_message_id UUID; v_contact_id UUID; v_conversation_id UUID; v_tenant_id UUID; BEGIN -- Obtener tenant SELECT tenant_id INTO v_tenant_id FROM whatsapp.accounts WHERE id = p_account_id; -- Obtener o crear contacto v_contact_id := whatsapp.get_or_create_contact( p_account_id, p_phone_number, p_profile_name ); -- Actualizar ventana de conversación UPDATE whatsapp.contacts SET conversation_window_expires_at = CURRENT_TIMESTAMP + INTERVAL '24 hours', can_send_template_only = FALSE, last_message_at = CURRENT_TIMESTAMP, last_message_direction = 'inbound', total_messages_received = total_messages_received + 1, updated_at = CURRENT_TIMESTAMP WHERE id = v_contact_id; -- Obtener conversación activa v_conversation_id := whatsapp.get_active_conversation(v_contact_id); -- Crear mensaje INSERT INTO whatsapp.messages ( tenant_id, account_id, contact_id, conversation_id, wa_message_id, direction, message_type, content, media_id, media_url, context_message_id, status ) VALUES ( v_tenant_id, p_account_id, v_contact_id, v_conversation_id, p_wa_message_id, 'inbound', p_message_type, p_content, p_media_id, p_media_url, p_context_message_id, 'received' ) RETURNING id INTO v_message_id; -- Actualizar conversación UPDATE whatsapp.conversations SET message_count = message_count + 1, unread_count = unread_count + 1, updated_at = CURRENT_TIMESTAMP WHERE id = v_conversation_id; -- Actualizar estadísticas de la cuenta UPDATE whatsapp.accounts SET total_messages_received = total_messages_received + 1, updated_at = CURRENT_TIMESTAMP WHERE id = p_account_id; RETURN v_message_id; END; $$ LANGUAGE plpgsql; -- Función para enviar mensaje de texto CREATE OR REPLACE FUNCTION whatsapp.send_text_message( p_account_id UUID, p_contact_id UUID, p_content TEXT, p_sent_by UUID DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_message_id UUID; v_conversation_id UUID; v_contact RECORD; v_tenant_id UUID; BEGIN -- Obtener contacto SELECT * INTO v_contact FROM whatsapp.contacts WHERE id = p_contact_id; v_tenant_id := v_contact.tenant_id; -- Verificar ventana de conversación IF v_contact.can_send_template_only AND (v_contact.conversation_window_expires_at IS NULL OR v_contact.conversation_window_expires_at < CURRENT_TIMESTAMP) THEN RAISE EXCEPTION 'Cannot send text message outside conversation window. Use template.'; END IF; -- Obtener conversación v_conversation_id := whatsapp.get_active_conversation(p_contact_id); -- Crear mensaje INSERT INTO whatsapp.messages ( tenant_id, account_id, contact_id, conversation_id, direction, message_type, content, status ) VALUES ( v_tenant_id, p_account_id, p_contact_id, v_conversation_id, 'outbound', 'text', p_content, 'pending' ) RETURNING id INTO v_message_id; -- Actualizar contacto UPDATE whatsapp.contacts SET last_message_at = CURRENT_TIMESTAMP, last_message_direction = 'outbound', total_messages_sent = total_messages_sent + 1, updated_at = CURRENT_TIMESTAMP WHERE id = p_contact_id; -- Actualizar conversación UPDATE whatsapp.conversations SET message_count = message_count + 1, updated_at = CURRENT_TIMESTAMP WHERE id = v_conversation_id; -- Actualizar cuenta UPDATE whatsapp.accounts SET total_messages_sent = total_messages_sent + 1, messages_sent_today = messages_sent_today + 1, updated_at = CURRENT_TIMESTAMP WHERE id = p_account_id; RETURN v_message_id; END; $$ LANGUAGE plpgsql; -- Función para enviar template CREATE OR REPLACE FUNCTION whatsapp.send_template_message( p_account_id UUID, p_contact_id UUID, p_template_id UUID, p_variables JSONB DEFAULT '[]' ) RETURNS UUID AS $$ DECLARE v_message_id UUID; v_conversation_id UUID; v_template RECORD; v_contact RECORD; v_tenant_id UUID; BEGIN -- Obtener template SELECT * INTO v_template FROM whatsapp.templates WHERE id = p_template_id; IF NOT FOUND OR v_template.meta_status != 'APPROVED' THEN RAISE EXCEPTION 'Template not found or not approved'; END IF; -- Obtener contacto SELECT * INTO v_contact FROM whatsapp.contacts WHERE id = p_contact_id; v_tenant_id := v_contact.tenant_id; -- Obtener conversación v_conversation_id := whatsapp.get_active_conversation(p_contact_id); -- Crear mensaje INSERT INTO whatsapp.messages ( tenant_id, account_id, contact_id, conversation_id, direction, message_type, content, template_id, template_name, template_variables, is_billable, cost_category, status ) VALUES ( v_tenant_id, p_account_id, p_contact_id, v_conversation_id, 'outbound', 'template', v_template.body_text, p_template_id, v_template.name, p_variables, TRUE, v_template.category, 'pending' ) RETURNING id INTO v_message_id; -- Actualizar uso del template UPDATE whatsapp.templates SET usage_count = usage_count + 1, last_used_at = CURRENT_TIMESTAMP WHERE id = p_template_id; -- Actualizar contacto UPDATE whatsapp.contacts SET last_message_at = CURRENT_TIMESTAMP, last_message_direction = 'outbound', total_messages_sent = total_messages_sent + 1, updated_at = CURRENT_TIMESTAMP WHERE id = p_contact_id; -- Actualizar cuenta UPDATE whatsapp.accounts SET total_messages_sent = total_messages_sent + 1, messages_sent_today = messages_sent_today + 1, updated_at = CURRENT_TIMESTAMP WHERE id = p_account_id; RETURN v_message_id; END; $$ LANGUAGE plpgsql; -- Función para actualizar estado de mensaje CREATE OR REPLACE FUNCTION whatsapp.update_message_status( p_wa_message_id VARCHAR(100), p_status VARCHAR(20), p_meta_timestamp TIMESTAMPTZ DEFAULT NULL, p_error_code VARCHAR(20) DEFAULT NULL, p_error_message TEXT DEFAULT NULL ) RETURNS BOOLEAN AS $$ DECLARE v_message RECORD; BEGIN SELECT * INTO v_message FROM whatsapp.messages WHERE wa_message_id = p_wa_message_id; IF NOT FOUND THEN RETURN FALSE; END IF; -- Registrar actualización de estado INSERT INTO whatsapp.message_status_updates ( message_id, status, previous_status, error_code, error_message, meta_timestamp ) VALUES ( v_message.id, p_status, v_message.status, p_error_code, p_error_message, p_meta_timestamp ); -- Actualizar mensaje UPDATE whatsapp.messages SET status = p_status, status_updated_at = CURRENT_TIMESTAMP, sent_at = CASE WHEN p_status = 'sent' AND sent_at IS NULL THEN COALESCE(p_meta_timestamp, CURRENT_TIMESTAMP) ELSE sent_at END, delivered_at = CASE WHEN p_status = 'delivered' THEN COALESCE(p_meta_timestamp, CURRENT_TIMESTAMP) ELSE delivered_at END, read_at = CASE WHEN p_status = 'read' THEN COALESCE(p_meta_timestamp, CURRENT_TIMESTAMP) ELSE read_at END, error_code = COALESCE(p_error_code, error_code), error_message = COALESCE(p_error_message, error_message) WHERE id = v_message.id; RETURN TRUE; END; $$ LANGUAGE plpgsql; -- Función para obtener estadísticas de cuenta CREATE OR REPLACE FUNCTION whatsapp.get_account_stats( p_account_id UUID, p_days INTEGER DEFAULT 30 ) RETURNS TABLE ( total_sent BIGINT, total_received BIGINT, total_delivered BIGINT, total_read BIGINT, total_failed BIGINT, delivery_rate DECIMAL, read_rate DECIMAL, active_contacts BIGINT, messages_by_day JSONB ) AS $$ BEGIN RETURN QUERY SELECT COUNT(*) FILTER (WHERE m.direction = 'outbound')::BIGINT as total_sent, COUNT(*) FILTER (WHERE m.direction = 'inbound')::BIGINT as total_received, COUNT(*) FILTER (WHERE m.status = 'delivered')::BIGINT as total_delivered, COUNT(*) FILTER (WHERE m.status = 'read')::BIGINT as total_read, COUNT(*) FILTER (WHERE m.status = 'failed')::BIGINT as total_failed, ROUND( COUNT(*) FILTER (WHERE m.status IN ('delivered', 'read'))::DECIMAL / NULLIF(COUNT(*) FILTER (WHERE m.direction = 'outbound'), 0) * 100, 2 ) as delivery_rate, ROUND( COUNT(*) FILTER (WHERE m.status = 'read')::DECIMAL / NULLIF(COUNT(*) FILTER (WHERE m.status IN ('delivered', 'read')), 0) * 100, 2 ) as read_rate, COUNT(DISTINCT m.contact_id)::BIGINT as active_contacts, jsonb_object_agg( DATE(m.created_at)::TEXT, day_count ) as messages_by_day FROM whatsapp.messages m LEFT JOIN ( SELECT DATE(created_at) as day, COUNT(*) as day_count FROM whatsapp.messages WHERE account_id = p_account_id AND created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL GROUP BY DATE(created_at) ) days ON TRUE WHERE m.account_id = p_account_id AND m.created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL; END; $$ LANGUAGE plpgsql STABLE; -- Función para resetear límite diario CREATE OR REPLACE FUNCTION whatsapp.reset_daily_limits() RETURNS INTEGER AS $$ DECLARE updated_count INTEGER; BEGIN UPDATE whatsapp.accounts SET messages_sent_today = 0, last_limit_reset = CURRENT_TIMESTAMP WHERE DATE(last_limit_reset) < CURRENT_DATE OR last_limit_reset IS NULL; GET DIAGNOSTICS updated_count = ROW_COUNT; RETURN updated_count; END; $$ LANGUAGE plpgsql; -- ===================== -- TRIGGERS -- ===================== CREATE OR REPLACE FUNCTION whatsapp.update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_wa_accounts_updated_at BEFORE UPDATE ON whatsapp.accounts FOR EACH ROW EXECUTE FUNCTION whatsapp.update_timestamp(); CREATE TRIGGER trg_wa_templates_updated_at BEFORE UPDATE ON whatsapp.templates FOR EACH ROW EXECUTE FUNCTION whatsapp.update_timestamp(); CREATE TRIGGER trg_wa_contacts_updated_at BEFORE UPDATE ON whatsapp.contacts FOR EACH ROW EXECUTE FUNCTION whatsapp.update_timestamp(); CREATE TRIGGER trg_wa_convos_updated_at BEFORE UPDATE ON whatsapp.conversations FOR EACH ROW EXECUTE FUNCTION whatsapp.update_timestamp(); CREATE TRIGGER trg_wa_quick_updated_at BEFORE UPDATE ON whatsapp.quick_replies FOR EACH ROW EXECUTE FUNCTION whatsapp.update_timestamp(); CREATE TRIGGER trg_wa_auto_updated_at BEFORE UPDATE ON whatsapp.automations FOR EACH ROW EXECUTE FUNCTION whatsapp.update_timestamp(); CREATE TRIGGER trg_wa_bcast_updated_at BEFORE UPDATE ON whatsapp.broadcasts FOR EACH ROW EXECUTE FUNCTION whatsapp.update_timestamp(); -- ===================== -- COMENTARIOS -- ===================== COMMENT ON TABLE whatsapp.accounts IS 'Cuentas de WhatsApp Business configuradas por tenant'; COMMENT ON TABLE whatsapp.templates IS 'Templates de mensaje aprobados por Meta'; COMMENT ON TABLE whatsapp.contacts IS 'Contactos de WhatsApp con historial de interacción'; COMMENT ON TABLE whatsapp.conversations IS 'Conversaciones (hilos de chat) con contactos'; COMMENT ON TABLE whatsapp.messages IS 'Mensajes enviados y recibidos'; COMMENT ON TABLE whatsapp.message_status_updates IS 'Historial de cambios de estado de mensajes'; COMMENT ON TABLE whatsapp.quick_replies IS 'Respuestas rápidas predefinidas'; COMMENT ON TABLE whatsapp.automations IS 'Reglas de automatización de respuestas'; COMMENT ON TABLE whatsapp.broadcasts IS 'Envíos masivos de mensajes'; COMMENT ON TABLE whatsapp.broadcast_recipients IS 'Destinatarios de broadcasts'; COMMENT ON FUNCTION whatsapp.receive_message IS 'Registra un mensaje entrante de WhatsApp'; COMMENT ON FUNCTION whatsapp.send_text_message IS 'Envía un mensaje de texto'; COMMENT ON FUNCTION whatsapp.send_template_message IS 'Envía un mensaje de template'; COMMENT ON FUNCTION whatsapp.update_message_status IS 'Actualiza el estado de un mensaje desde webhook';