erp-core/docs/02-fase-core-business/MGN-008-notifications/especificaciones/ET-NOTIF-database.md

22 KiB

DDL-SPEC: Schema core_notifications

Identificacion

Campo Valor
Schema core_notifications
Modulo MGN-008
Version 1.0
Estado En Diseno
Autor Requirements-Analyst
Fecha 2025-12-05

Descripcion General

El schema core_notifications gestiona notificaciones multi-canal (in-app, email, push), templates de mensajes y preferencias de usuario. Soporta notificaciones en tiempo real via WebSocket y cola asíncrona para email/push.

RF Cubiertos

RF Titulo Tablas
RF-NOTIF-001 Notificaciones In-App notifications
RF-NOTIF-002 Email email_templates, email_jobs
RF-NOTIF-003 Push push_subscriptions, push_jobs
RF-NOTIF-004 Preferencias notification_preferences

Diagrama ER

erDiagram
    notifications {
        uuid id PK
        uuid tenant_id FK
        uuid user_id FK
        varchar type
        varchar title
        text body
        jsonb data
        varchar[] channels
        boolean is_read
        timestamptz read_at
        timestamptz created_at
    }

    email_templates {
        uuid id PK
        uuid tenant_id
        varchar key UK
        varchar name
        varchar subject
        text body_html
        text body_text
        jsonb variables
        boolean is_active
    }

    email_jobs {
        uuid id PK
        uuid tenant_id
        uuid template_id FK
        varchar to_email
        varchar subject
        text body_html
        jsonb variables
        varchar status
        int attempts
        timestamptz scheduled_at
        timestamptz sent_at
    }

    push_subscriptions {
        uuid id PK
        uuid user_id FK
        varchar endpoint UK
        jsonb keys
        varchar device_type
        boolean is_active
        timestamptz last_used_at
    }

    push_jobs {
        uuid id PK
        uuid subscription_id FK
        varchar title
        text body
        jsonb data
        varchar status
        int attempts
        timestamptz sent_at
    }

    notification_preferences {
        uuid id PK
        uuid user_id FK
        varchar notification_type
        boolean in_app
        boolean email
        boolean push
        varchar email_frequency
    }

    email_templates ||--o{ email_jobs : "usa"
    push_subscriptions ||--o{ push_jobs : "recibe"

Tablas

1. notifications

Notificaciones in-app para usuarios.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
user_id UUID NOT NULL - FK a users
type VARCHAR(50) NOT NULL - Tipo de notificacion
title VARCHAR(255) NOT NULL - Titulo
body TEXT NOT NULL - Contenido
data JSONB NULL '{}' Datos adicionales
action_url VARCHAR(500) NULL - URL de accion
action_label VARCHAR(100) NULL - Texto del boton
icon VARCHAR(50) NULL - Icono
channels VARCHAR(20)[] NOT NULL '{in_app}' Canales enviados
is_read BOOLEAN NOT NULL false Leida
read_at TIMESTAMPTZ NULL - Fecha lectura
is_archived BOOLEAN NOT NULL false Archivada
expires_at TIMESTAMPTZ NULL - Expiracion
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_notifications.notifications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    user_id UUID NOT NULL,
    type VARCHAR(50) NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL,
    data JSONB DEFAULT '{}',
    action_url VARCHAR(500),
    action_label VARCHAR(100),
    icon VARCHAR(50),
    channels VARCHAR(20)[] NOT NULL DEFAULT '{in_app}',
    is_read BOOLEAN NOT NULL DEFAULT false,
    read_at TIMESTAMPTZ,
    is_archived BOOLEAN NOT NULL DEFAULT false,
    expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT fk_notifications_tenant
        FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
    CONSTRAINT fk_notifications_user
        FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE
);

-- Indices
CREATE INDEX idx_notifications_user ON core_notifications.notifications(user_id, created_at DESC);
CREATE INDEX idx_notifications_user_unread ON core_notifications.notifications(user_id, is_read)
    WHERE is_read = false;
CREATE INDEX idx_notifications_type ON core_notifications.notifications(type, created_at DESC);
CREATE INDEX idx_notifications_expires ON core_notifications.notifications(expires_at)
    WHERE expires_at IS NOT NULL;

-- RLS
ALTER TABLE core_notifications.notifications ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_notifications.notifications
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
CREATE POLICY user_own ON core_notifications.notifications
    FOR SELECT USING (user_id = current_setting('app.current_user_id')::uuid);

2. email_templates

Templates de email reutilizables.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NULL - NULL = global
key VARCHAR(100) NOT NULL - Clave unica
name VARCHAR(255) NOT NULL - Nombre descriptivo
subject VARCHAR(255) NOT NULL - Asunto (con variables)
body_html TEXT NOT NULL - Cuerpo HTML
body_text TEXT NULL - Cuerpo texto plano
variables JSONB NOT NULL '[]' Variables disponibles
category VARCHAR(50) NOT NULL 'transactional' Categoria
is_active BOOLEAN NOT NULL true Activo
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE TABLE core_notifications.email_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID,
    key VARCHAR(100) NOT NULL,
    name VARCHAR(255) NOT NULL,
    subject VARCHAR(255) NOT NULL,
    body_html TEXT NOT NULL,
    body_text TEXT,
    variables JSONB NOT NULL DEFAULT '[]',
    category VARCHAR(50) NOT NULL DEFAULT 'transactional',
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_email_templates_key UNIQUE (tenant_id, key),
    CONSTRAINT chk_category CHECK (category IN (
        'transactional', 'marketing', 'digest', 'alert', 'welcome'
    ))
);

CREATE INDEX idx_email_templates_tenant ON core_notifications.email_templates(tenant_id);
CREATE INDEX idx_email_templates_category ON core_notifications.email_templates(category);

3. email_jobs

Cola de trabajos de email.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
template_id UUID NULL - FK a email_templates
to_email VARCHAR(255) NOT NULL - Destinatario
to_name VARCHAR(255) NULL - Nombre destinatario
cc VARCHAR(255)[] NULL - Copia
bcc VARCHAR(255)[] NULL - Copia oculta
subject VARCHAR(255) NOT NULL - Asunto renderizado
body_html TEXT NOT NULL - Cuerpo HTML renderizado
body_text TEXT NULL - Cuerpo texto
variables JSONB NULL - Variables usadas
attachments JSONB NULL '[]' Adjuntos
status VARCHAR(20) NOT NULL 'pending' Estado
priority INTEGER NOT NULL 5 Prioridad (1-10)
attempts INTEGER NOT NULL 0 Intentos realizados
max_attempts INTEGER NOT NULL 3 Intentos maximos
error TEXT NULL - Ultimo error
scheduled_at TIMESTAMPTZ NOT NULL NOW() Programado para
sent_at TIMESTAMPTZ NULL - Fecha envio
opened_at TIMESTAMPTZ NULL - Fecha apertura
clicked_at TIMESTAMPTZ NULL - Fecha click
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_notifications.email_jobs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    template_id UUID,
    to_email VARCHAR(255) NOT NULL,
    to_name VARCHAR(255),
    cc VARCHAR(255)[],
    bcc VARCHAR(255)[],
    subject VARCHAR(255) NOT NULL,
    body_html TEXT NOT NULL,
    body_text TEXT,
    variables JSONB,
    attachments JSONB DEFAULT '[]',
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    priority INTEGER NOT NULL DEFAULT 5,
    attempts INTEGER NOT NULL DEFAULT 0,
    max_attempts INTEGER NOT NULL DEFAULT 3,
    error TEXT,
    scheduled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    sent_at TIMESTAMPTZ,
    opened_at TIMESTAMPTZ,
    clicked_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT fk_email_jobs_template
        FOREIGN KEY (template_id) REFERENCES core_notifications.email_templates(id),
    CONSTRAINT chk_status CHECK (status IN (
        'pending', 'processing', 'sent', 'failed', 'cancelled'
    )),
    CONSTRAINT chk_priority CHECK (priority BETWEEN 1 AND 10)
);

-- Indices para procesamiento de cola
CREATE INDEX idx_email_jobs_pending ON core_notifications.email_jobs(scheduled_at, priority DESC)
    WHERE status = 'pending';
CREATE INDEX idx_email_jobs_status ON core_notifications.email_jobs(status, created_at);
CREATE INDEX idx_email_jobs_tenant ON core_notifications.email_jobs(tenant_id, created_at DESC);

4. push_subscriptions

Dispositivos registrados para push notifications.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
user_id UUID NOT NULL - FK a users
endpoint VARCHAR(500) NOT NULL - URL endpoint
p256dh VARCHAR(255) NOT NULL - Public key
auth VARCHAR(255) NOT NULL - Auth secret
device_type VARCHAR(20) NOT NULL - web/android/ios
device_name VARCHAR(100) NULL - Nombre dispositivo
browser VARCHAR(50) NULL - Browser (para web)
is_active BOOLEAN NOT NULL true Activa
last_used_at TIMESTAMPTZ NULL - Ultimo uso
created_at TIMESTAMPTZ NOT NULL NOW() Fecha registro
CREATE TABLE core_notifications.push_subscriptions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,
    endpoint VARCHAR(500) NOT NULL,
    p256dh VARCHAR(255) NOT NULL,
    auth VARCHAR(255) NOT NULL,
    device_type VARCHAR(20) NOT NULL,
    device_name VARCHAR(100),
    browser VARCHAR(50),
    is_active BOOLEAN NOT NULL DEFAULT true,
    last_used_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_push_subscriptions_endpoint UNIQUE (endpoint),
    CONSTRAINT fk_push_subscriptions_user
        FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE,
    CONSTRAINT chk_device_type CHECK (device_type IN ('web', 'android', 'ios'))
);

CREATE INDEX idx_push_subscriptions_user ON core_notifications.push_subscriptions(user_id)
    WHERE is_active = true;

5. push_jobs

Cola de trabajos de push notification.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
subscription_id UUID NOT NULL - FK a push_subscriptions
title VARCHAR(255) NOT NULL - Titulo
body TEXT NOT NULL - Mensaje
icon VARCHAR(500) NULL - URL icono
badge VARCHAR(500) NULL - URL badge
image VARCHAR(500) NULL - URL imagen
data JSONB NULL '{}' Datos payload
action_url VARCHAR(500) NULL - URL al hacer click
status VARCHAR(20) NOT NULL 'pending' Estado
attempts INTEGER NOT NULL 0 Intentos
error TEXT NULL - Error
sent_at TIMESTAMPTZ NULL - Fecha envio
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_notifications.push_jobs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    subscription_id UUID NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL,
    icon VARCHAR(500),
    badge VARCHAR(500),
    image VARCHAR(500),
    data JSONB DEFAULT '{}',
    action_url VARCHAR(500),
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    attempts INTEGER NOT NULL DEFAULT 0,
    error TEXT,
    sent_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT fk_push_jobs_subscription
        FOREIGN KEY (subscription_id) REFERENCES core_notifications.push_subscriptions(id) ON DELETE CASCADE,
    CONSTRAINT chk_status CHECK (status IN ('pending', 'processing', 'sent', 'failed'))
);

CREATE INDEX idx_push_jobs_pending ON core_notifications.push_jobs(created_at)
    WHERE status = 'pending';

6. notification_preferences

Preferencias de notificacion por usuario.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
user_id UUID NOT NULL - FK a users
notification_type VARCHAR(50) NOT NULL - Tipo de notificacion
in_app BOOLEAN NOT NULL true Recibir in-app
email BOOLEAN NOT NULL true Recibir email
push BOOLEAN NOT NULL true Recibir push
email_frequency VARCHAR(20) NOT NULL 'instant' Frecuencia email
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE TABLE core_notifications.notification_preferences (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,
    notification_type VARCHAR(50) NOT NULL,
    in_app BOOLEAN NOT NULL DEFAULT true,
    email BOOLEAN NOT NULL DEFAULT true,
    push BOOLEAN NOT NULL DEFAULT true,
    email_frequency VARCHAR(20) NOT NULL DEFAULT 'instant',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_preferences_user_type UNIQUE (user_id, notification_type),
    CONSTRAINT fk_preferences_user
        FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE,
    CONSTRAINT chk_email_frequency CHECK (email_frequency IN (
        'instant', 'hourly', 'daily', 'weekly', 'never'
    ))
);

CREATE INDEX idx_preferences_user ON core_notifications.notification_preferences(user_id);

Tipos de Notificacion

-- Tipos de notificacion predefinidos
CREATE TYPE core_notifications.notification_type AS ENUM (
    -- Seguridad
    'security.login_new_device',
    'security.password_changed',
    'security.session_expired',

    -- Sistema
    'system.maintenance',
    'system.update',
    'system.announcement',

    -- Usuarios
    'user.mention',
    'user.assignment',
    'user.invitation',

    -- Tareas
    'task.assigned',
    'task.completed',
    'task.due_soon',
    'task.overdue',

    -- Documentos
    'document.shared',
    'document.comment',
    'document.approved',
    'document.rejected',

    -- Pagos
    'payment.received',
    'payment.failed',
    'payment.due',

    -- Reportes
    'report.ready',
    'report.scheduled'
);

Funciones de Utilidad

Enviar Notificacion

CREATE OR REPLACE FUNCTION core_notifications.send_notification(
    p_tenant_id UUID,
    p_user_id UUID,
    p_type VARCHAR,
    p_title VARCHAR,
    p_body TEXT,
    p_data JSONB DEFAULT '{}',
    p_action_url VARCHAR DEFAULT NULL,
    p_channels VARCHAR[] DEFAULT '{in_app}'
) RETURNS UUID AS $$
DECLARE
    v_notification_id UUID;
    v_preferences RECORD;
    v_effective_channels VARCHAR[];
BEGIN
    -- Obtener preferencias del usuario
    SELECT * INTO v_preferences
    FROM core_notifications.notification_preferences
    WHERE user_id = p_user_id AND notification_type = p_type;

    -- Aplicar preferencias a canales
    v_effective_channels := ARRAY[]::VARCHAR[];

    IF 'in_app' = ANY(p_channels) AND (v_preferences IS NULL OR v_preferences.in_app) THEN
        v_effective_channels := array_append(v_effective_channels, 'in_app');
    END IF;

    IF 'email' = ANY(p_channels) AND (v_preferences IS NULL OR v_preferences.email) THEN
        v_effective_channels := array_append(v_effective_channels, 'email');
    END IF;

    IF 'push' = ANY(p_channels) AND (v_preferences IS NULL OR v_preferences.push) THEN
        v_effective_channels := array_append(v_effective_channels, 'push');
    END IF;

    -- Crear notificacion in-app si aplica
    IF 'in_app' = ANY(v_effective_channels) THEN
        INSERT INTO core_notifications.notifications (
            tenant_id, user_id, type, title, body, data, action_url, channels
        ) VALUES (
            p_tenant_id, p_user_id, p_type, p_title, p_body, p_data, p_action_url, v_effective_channels
        ) RETURNING id INTO v_notification_id;
    END IF;

    RETURN v_notification_id;
END;
$$ LANGUAGE plpgsql;

Marcar Como Leidas

CREATE OR REPLACE FUNCTION core_notifications.mark_as_read(
    p_user_id UUID,
    p_notification_ids UUID[] DEFAULT NULL
) RETURNS INTEGER AS $$
DECLARE
    v_count INTEGER;
BEGIN
    IF p_notification_ids IS NULL THEN
        -- Marcar todas
        UPDATE core_notifications.notifications
        SET is_read = true, read_at = NOW()
        WHERE user_id = p_user_id AND is_read = false;
    ELSE
        -- Marcar especificas
        UPDATE core_notifications.notifications
        SET is_read = true, read_at = NOW()
        WHERE user_id = p_user_id AND id = ANY(p_notification_ids) AND is_read = false;
    END IF;

    GET DIAGNOSTICS v_count = ROW_COUNT;
    RETURN v_count;
END;
$$ LANGUAGE plpgsql;

Obtener Conteo de No Leidas

CREATE OR REPLACE FUNCTION core_notifications.get_unread_count(
    p_user_id UUID
) RETURNS JSONB AS $$
BEGIN
    RETURN (
        SELECT jsonb_build_object(
            'total', COUNT(*),
            'by_type', jsonb_object_agg(type, count)
        )
        FROM (
            SELECT type, COUNT(*) as count
            FROM core_notifications.notifications
            WHERE user_id = p_user_id AND is_read = false
            GROUP BY type
        ) counts
    );
END;
$$ LANGUAGE plpgsql STABLE;

Seed Data

Email Templates

INSERT INTO core_notifications.email_templates (tenant_id, key, name, subject, body_html, variables, category) VALUES
(NULL, 'welcome', 'Bienvenida',
 'Bienvenido a {{app_name}}, {{user_name}}!',
 '<h1>Hola {{user_name}}</h1><p>Gracias por registrarte en {{app_name}}.</p>',
 '[{"name": "user_name", "required": true}, {"name": "app_name", "required": true}]',
 'welcome'),

(NULL, 'password_reset', 'Restablecer Contrasena',
 'Solicitud de restablecimiento de contrasena',
 '<p>Haz click en el siguiente enlace para restablecer tu contrasena:</p><a href="{{reset_url}}">Restablecer</a>',
 '[{"name": "reset_url", "required": true}]',
 'transactional'),

(NULL, 'email_verification', 'Verificar Email',
 'Verifica tu direccion de email',
 '<p>Haz click para verificar tu email:</p><a href="{{verify_url}}">Verificar</a>',
 '[{"name": "verify_url", "required": true}]',
 'transactional'),

(NULL, 'invoice', 'Factura',
 'Factura #{{invoice_number}}',
 '<p>Adjuntamos la factura #{{invoice_number}} por {{amount}}.</p>',
 '[{"name": "invoice_number", "required": true}, {"name": "amount", "required": true}]',
 'transactional'),

(NULL, 'security_alert', 'Alerta de Seguridad',
 'Alerta de seguridad: {{alert_title}}',
 '<p>Se ha detectado una actividad sospechosa:</p><p>{{alert_description}}</p>',
 '[{"name": "alert_title", "required": true}, {"name": "alert_description", "required": true}]',
 'alert');

Mantenimiento

Limpiar Notificaciones Antiguas

CREATE OR REPLACE FUNCTION core_notifications.cleanup_old_notifications(
    p_days_read INTEGER DEFAULT 30,
    p_days_unread INTEGER DEFAULT 90
) RETURNS INTEGER AS $$
DECLARE
    v_deleted INTEGER := 0;
BEGIN
    -- Eliminar leidas antiguas
    DELETE FROM core_notifications.notifications
    WHERE is_read = true
    AND read_at < NOW() - (p_days_read || ' days')::INTERVAL;
    GET DIAGNOSTICS v_deleted = ROW_COUNT;

    -- Eliminar no leidas muy antiguas
    DELETE FROM core_notifications.notifications
    WHERE is_read = false
    AND created_at < NOW() - (p_days_unread || ' days')::INTERVAL;
    v_deleted := v_deleted + ROW_COUNT;

    -- Eliminar expiradas
    DELETE FROM core_notifications.notifications
    WHERE expires_at IS NOT NULL AND expires_at < NOW();
    v_deleted := v_deleted + ROW_COUNT;

    RETURN v_deleted;
END;
$$ LANGUAGE plpgsql;

Limpiar Jobs Completados

CREATE OR REPLACE FUNCTION core_notifications.cleanup_completed_jobs(
    p_days INTEGER DEFAULT 7
) RETURNS JSONB AS $$
DECLARE
    v_email_deleted INTEGER;
    v_push_deleted INTEGER;
BEGIN
    DELETE FROM core_notifications.email_jobs
    WHERE status IN ('sent', 'cancelled')
    AND sent_at < NOW() - (p_days || ' days')::INTERVAL;
    GET DIAGNOSTICS v_email_deleted = ROW_COUNT;

    DELETE FROM core_notifications.push_jobs
    WHERE status = 'sent'
    AND sent_at < NOW() - (p_days || ' days')::INTERVAL;
    GET DIAGNOSTICS v_push_deleted = ROW_COUNT;

    RETURN jsonb_build_object(
        'email_jobs_deleted', v_email_deleted,
        'push_jobs_deleted', v_push_deleted
    );
END;
$$ LANGUAGE plpgsql;

Historial

Version Fecha Autor Cambios
1.0 2025-12-05 Requirements-Analyst Creacion inicial