## New Tables Created (Sprint 1 - DDL Roadmap Q1-2026) ### users schema (4 tables): - profiles: Extended user profile information - user_settings: User preferences and configurations - kyc_verifications: KYC/AML verification records - risk_profiles: Trading risk assessment profiles ### admin schema (3 tables): - admin_roles: Platform administrative roles - platform_analytics: Aggregated platform metrics - api_keys: Programmatic API access keys ### notifications schema (1 table): - notifications: Multi-channel notification system ### market_data schema (4 tables): - tickers: Financial instruments catalog - ohlcv_5m: 5-minute OHLCV price data - technical_indicators: Pre-calculated TA indicators - ohlcv_5m_staging: Staging table for data ingestion ## Features: - Multi-tenancy with RLS policies - Comprehensive indexes for query optimization - Triggers for computed fields and timestamps - Helper functions for common operations - Views for dashboard and reporting - Full GRANTS configuration Roadmap: orchestration/planes/ROADMAP-IMPLEMENTACION-DDL-2026-Q1.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
356 lines
12 KiB
PL/PgSQL
356 lines
12 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: notifications
|
|
-- TABLE: notifications
|
|
-- DESCRIPTION: Sistema de notificaciones del sistema
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 1 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Crear schema si no existe
|
|
CREATE SCHEMA IF NOT EXISTS notifications;
|
|
|
|
-- Grant usage
|
|
GRANT USAGE ON SCHEMA notifications TO trading_app;
|
|
GRANT USAGE ON SCHEMA notifications TO trading_readonly;
|
|
|
|
-- Enum para tipo de notificacion
|
|
DO $$ BEGIN
|
|
CREATE TYPE notifications.notification_type AS ENUM (
|
|
'info', -- Informativa general
|
|
'success', -- Accion exitosa
|
|
'warning', -- Advertencia
|
|
'error', -- Error
|
|
'alert', -- Alerta importante
|
|
'trade', -- Relacionada a trading
|
|
'signal', -- Senal de trading
|
|
'payment', -- Pago/transaccion
|
|
'account', -- Cuenta de usuario
|
|
'security', -- Seguridad
|
|
'system', -- Sistema/mantenimiento
|
|
'promotion' -- Promocional
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Enum para prioridad
|
|
DO $$ BEGIN
|
|
CREATE TYPE notifications.notification_priority AS ENUM (
|
|
'low',
|
|
'normal',
|
|
'high',
|
|
'critical'
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Enum para canal de entrega
|
|
DO $$ BEGIN
|
|
CREATE TYPE notifications.delivery_channel AS ENUM (
|
|
'in_app', -- Notificacion en app
|
|
'email', -- Email
|
|
'push', -- Push notification
|
|
'sms', -- SMS
|
|
'webhook' -- Webhook externo
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Tabla principal de Notificaciones
|
|
CREATE TABLE IF NOT EXISTS notifications.notifications (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
|
|
|
-- Clasificacion
|
|
type notifications.notification_type NOT NULL DEFAULT 'info',
|
|
priority notifications.notification_priority NOT NULL DEFAULT 'normal',
|
|
category VARCHAR(50), -- Subcategoria libre
|
|
|
|
-- Contenido
|
|
title VARCHAR(255) NOT NULL,
|
|
message TEXT NOT NULL,
|
|
message_html TEXT, -- Version HTML del mensaje
|
|
|
|
-- Datos estructurados
|
|
data JSONB DEFAULT '{}'::JSONB, -- Datos adicionales para la UI
|
|
action_url TEXT, -- URL de accion principal
|
|
action_label VARCHAR(100), -- Texto del boton de accion
|
|
actions JSONB, -- Array de acciones multiples
|
|
|
|
-- Estado
|
|
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
read_at TIMESTAMPTZ,
|
|
is_archived BOOLEAN NOT NULL DEFAULT FALSE,
|
|
archived_at TIMESTAMPTZ,
|
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
-- Entrega multicanal
|
|
channels notifications.delivery_channel[] NOT NULL DEFAULT ARRAY['in_app']::notifications.delivery_channel[],
|
|
delivery_status JSONB DEFAULT '{}'::JSONB, -- Estado por canal
|
|
|
|
-- Tracking de emails
|
|
email_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
|
email_sent_at TIMESTAMPTZ,
|
|
email_opened BOOLEAN NOT NULL DEFAULT FALSE,
|
|
email_opened_at TIMESTAMPTZ,
|
|
email_clicked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
email_clicked_at TIMESTAMPTZ,
|
|
|
|
-- Push notifications
|
|
push_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
|
push_sent_at TIMESTAMPTZ,
|
|
push_delivered BOOLEAN NOT NULL DEFAULT FALSE,
|
|
push_delivered_at TIMESTAMPTZ,
|
|
|
|
-- Agrupacion
|
|
group_key VARCHAR(100), -- Agrupar notificaciones similares
|
|
group_count INTEGER DEFAULT 1, -- Contador de agrupacion
|
|
|
|
-- Programacion
|
|
scheduled_for TIMESTAMPTZ, -- Envio programado
|
|
expires_at TIMESTAMPTZ, -- Expiracion automatica
|
|
|
|
-- Origen
|
|
source VARCHAR(50) NOT NULL DEFAULT 'system', -- 'system', 'user', 'bot', 'webhook'
|
|
source_id VARCHAR(255), -- ID del recurso origen
|
|
source_type VARCHAR(50), -- Tipo de recurso origen
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::JSONB,
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
COMMENT ON TABLE notifications.notifications IS
|
|
'Notificaciones del sistema para usuarios, soporta multicanal';
|
|
|
|
COMMENT ON COLUMN notifications.notifications.channels IS
|
|
'Canales de entrega: in_app, email, push, sms, webhook';
|
|
|
|
COMMENT ON COLUMN notifications.notifications.group_key IS
|
|
'Clave para agrupar notificaciones similares y evitar spam';
|
|
|
|
-- Indices para queries comunes
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread
|
|
ON notifications.notifications(user_id, created_at DESC)
|
|
WHERE is_read = FALSE AND is_deleted = FALSE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_user_all
|
|
ON notifications.notifications(user_id, created_at DESC)
|
|
WHERE is_deleted = FALSE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_tenant
|
|
ON notifications.notifications(tenant_id, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_type
|
|
ON notifications.notifications(type);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_priority
|
|
ON notifications.notifications(priority)
|
|
WHERE priority IN ('high', 'critical');
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_scheduled
|
|
ON notifications.notifications(scheduled_for)
|
|
WHERE scheduled_for IS NOT NULL AND scheduled_for > NOW();
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_expires
|
|
ON notifications.notifications(expires_at)
|
|
WHERE expires_at IS NOT NULL AND is_deleted = FALSE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_group
|
|
ON notifications.notifications(user_id, group_key, created_at DESC)
|
|
WHERE group_key IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_source
|
|
ON notifications.notifications(source, source_type, source_id);
|
|
|
|
-- GIN index para busqueda en datos JSONB
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_data_gin
|
|
ON notifications.notifications USING GIN (data);
|
|
|
|
-- Trigger para updated_at
|
|
CREATE OR REPLACE FUNCTION notifications.update_notification_timestamp()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS notification_updated_at ON notifications.notifications;
|
|
CREATE TRIGGER notification_updated_at
|
|
BEFORE UPDATE ON notifications.notifications
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION notifications.update_notification_timestamp();
|
|
|
|
-- Trigger para marcar read_at cuando is_read cambia a true
|
|
CREATE OR REPLACE FUNCTION notifications.set_read_timestamp()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.is_read = TRUE AND OLD.is_read = FALSE THEN
|
|
NEW.read_at = NOW();
|
|
END IF;
|
|
IF NEW.is_archived = TRUE AND OLD.is_archived = FALSE THEN
|
|
NEW.archived_at = NOW();
|
|
END IF;
|
|
IF NEW.is_deleted = TRUE AND OLD.is_deleted = FALSE THEN
|
|
NEW.deleted_at = NOW();
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS notification_status_timestamps ON notifications.notifications;
|
|
CREATE TRIGGER notification_status_timestamps
|
|
BEFORE UPDATE ON notifications.notifications
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION notifications.set_read_timestamp();
|
|
|
|
-- Funcion para crear notificacion
|
|
CREATE OR REPLACE FUNCTION notifications.create_notification(
|
|
p_tenant_id UUID,
|
|
p_user_id UUID,
|
|
p_type notifications.notification_type,
|
|
p_title VARCHAR(255),
|
|
p_message TEXT,
|
|
p_priority notifications.notification_priority DEFAULT 'normal',
|
|
p_channels notifications.delivery_channel[] DEFAULT ARRAY['in_app']::notifications.delivery_channel[],
|
|
p_data JSONB DEFAULT '{}'::JSONB,
|
|
p_action_url TEXT DEFAULT NULL,
|
|
p_source VARCHAR(50) DEFAULT 'system',
|
|
p_source_type VARCHAR(50) DEFAULT NULL,
|
|
p_source_id VARCHAR(255) DEFAULT NULL,
|
|
p_group_key VARCHAR(100) DEFAULT NULL,
|
|
p_scheduled_for TIMESTAMPTZ DEFAULT NULL,
|
|
p_expires_at TIMESTAMPTZ DEFAULT NULL
|
|
)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_notification_id UUID;
|
|
BEGIN
|
|
INSERT INTO notifications.notifications (
|
|
tenant_id, user_id, type, title, message, priority,
|
|
channels, data, action_url, source, source_type, source_id,
|
|
group_key, scheduled_for, expires_at
|
|
) VALUES (
|
|
p_tenant_id, p_user_id, p_type, p_title, p_message, p_priority,
|
|
p_channels, p_data, p_action_url, p_source, p_source_type, p_source_id,
|
|
p_group_key, p_scheduled_for, p_expires_at
|
|
)
|
|
RETURNING id INTO v_notification_id;
|
|
|
|
RETURN v_notification_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para marcar como leidas por lotes
|
|
CREATE OR REPLACE FUNCTION notifications.mark_all_read(
|
|
p_user_id UUID,
|
|
p_before TIMESTAMPTZ DEFAULT NULL
|
|
)
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_count INTEGER;
|
|
BEGIN
|
|
UPDATE notifications.notifications
|
|
SET is_read = TRUE,
|
|
read_at = NOW()
|
|
WHERE user_id = p_user_id
|
|
AND is_read = FALSE
|
|
AND is_deleted = FALSE
|
|
AND (p_before IS NULL OR created_at <= p_before);
|
|
|
|
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
RETURN v_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para limpiar notificaciones expiradas
|
|
CREATE OR REPLACE FUNCTION notifications.cleanup_expired()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_count INTEGER;
|
|
BEGIN
|
|
UPDATE notifications.notifications
|
|
SET is_deleted = TRUE,
|
|
deleted_at = NOW()
|
|
WHERE expires_at IS NOT NULL
|
|
AND expires_at < NOW()
|
|
AND is_deleted = FALSE;
|
|
|
|
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
RETURN v_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Vista de notificaciones no leidas
|
|
CREATE OR REPLACE VIEW notifications.v_unread_notifications AS
|
|
SELECT
|
|
n.id,
|
|
n.tenant_id,
|
|
n.user_id,
|
|
n.type,
|
|
n.priority,
|
|
n.title,
|
|
n.message,
|
|
n.data,
|
|
n.action_url,
|
|
n.source,
|
|
n.created_at
|
|
FROM notifications.notifications n
|
|
WHERE n.is_read = FALSE
|
|
AND n.is_deleted = FALSE
|
|
AND (n.expires_at IS NULL OR n.expires_at > NOW())
|
|
AND (n.scheduled_for IS NULL OR n.scheduled_for <= NOW())
|
|
ORDER BY
|
|
CASE n.priority
|
|
WHEN 'critical' THEN 1
|
|
WHEN 'high' THEN 2
|
|
WHEN 'normal' THEN 3
|
|
WHEN 'low' THEN 4
|
|
END,
|
|
n.created_at DESC;
|
|
|
|
-- Vista de conteo por tipo para dashboard
|
|
CREATE OR REPLACE VIEW notifications.v_notification_counts AS
|
|
SELECT
|
|
user_id,
|
|
COUNT(*) FILTER (WHERE is_read = FALSE) AS unread_count,
|
|
COUNT(*) FILTER (WHERE priority = 'critical' AND is_read = FALSE) AS critical_count,
|
|
COUNT(*) FILTER (WHERE priority = 'high' AND is_read = FALSE) AS high_priority_count,
|
|
COUNT(*) FILTER (WHERE type = 'trade' AND is_read = FALSE) AS trade_count,
|
|
COUNT(*) FILTER (WHERE type = 'signal' AND is_read = FALSE) AS signal_count
|
|
FROM notifications.notifications
|
|
WHERE is_deleted = FALSE
|
|
AND (expires_at IS NULL OR expires_at > NOW())
|
|
GROUP BY user_id;
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE notifications.notifications ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY notifications_tenant_isolation ON notifications.notifications
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Los usuarios solo pueden ver sus propias notificaciones
|
|
CREATE POLICY notifications_user_isolation ON notifications.notifications
|
|
FOR SELECT
|
|
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE ON notifications.notifications TO trading_app;
|
|
GRANT SELECT ON notifications.notifications TO trading_readonly;
|
|
GRANT SELECT ON notifications.v_unread_notifications TO trading_app;
|
|
GRANT SELECT ON notifications.v_notification_counts TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION notifications.create_notification TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION notifications.mark_all_read TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION notifications.cleanup_expired TO trading_app;
|