- Replace old DDL structure with new numbered files (01-24) - Update migrations and seeds for new schema - Clean up deprecated files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1019 lines
34 KiB
PL/PgSQL
1019 lines
34 KiB
PL/PgSQL
-- =============================================================
|
|
-- 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';
|