trading-platform-database-v2/ddl/schemas/notifications/tables/001_notifications.sql
rckrdmrd b86dfa2e06 [DDL] feat: Sprint 1 - Add 12 tables for users, admin, notifications, market_data
## 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>
2026-01-16 19:41:53 -06:00

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;