# 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

```mermaid
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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

```sql
-- 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

```sql
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

```sql
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

```sql
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

```sql
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

```sql
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

```sql
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 |
