feat: Initial commit - Database schemas and scripts
DDL schemas for Trading Platform: - User management - Authentication - Payments - Education - ML predictions - Trading data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
45e77e9a9c
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
*.sql.bak
|
||||||
|
*.dump
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
tmp/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
284
DIRECTIVA-POLITICA-CARGA-LIMPIA.md
Normal file
284
DIRECTIVA-POLITICA-CARGA-LIMPIA.md
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
# DIRECTIVA: Politica de Carga Limpia (DDL-First)
|
||||||
|
|
||||||
|
**ID:** DIR-DB-001
|
||||||
|
**Version:** 1.1.0
|
||||||
|
**Fecha:** 2026-01-04
|
||||||
|
**Estado:** ACTIVA
|
||||||
|
**Aplica a:** Todos los agentes que trabajen con base de datos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OBJETIVO
|
||||||
|
|
||||||
|
Establecer una politica clara y obligatoria para la gestion del esquema de base de datos del proyecto Trading Platform (Trading Platform), garantizando que la base de datos pueda ser creada o recreada completamente desde archivos DDL sin dependencia de migraciones incrementales.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRINCIPIO FUNDAMENTAL
|
||||||
|
|
||||||
|
> **La base de datos SIEMPRE debe poder ser creada desde cero ejecutando unicamente los archivos DDL.**
|
||||||
|
|
||||||
|
Esto significa:
|
||||||
|
- NO migraciones incrementales
|
||||||
|
- NO archivos de "fix" o "patch"
|
||||||
|
- NO scripts de correccion
|
||||||
|
- NO ALTER TABLE en archivos separados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REGLAS OBLIGATORIAS
|
||||||
|
|
||||||
|
### 1. Estructura de Archivos
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/database/
|
||||||
|
├── ddl/
|
||||||
|
│ └── schemas/
|
||||||
|
│ ├── {schema}/
|
||||||
|
│ │ ├── 00-extensions.sql # Extensiones del schema (opcional)
|
||||||
|
│ │ ├── 00-enums.sql # Tipos enumerados (o 01-enums.sql)
|
||||||
|
│ │ ├── tables/
|
||||||
|
│ │ │ ├── 01-{tabla}.sql # Una tabla por archivo
|
||||||
|
│ │ │ ├── 02-{tabla}.sql
|
||||||
|
│ │ │ ├── 99-deferred-constraints.sql # Constraints diferidos (opcional)
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ ├── functions/
|
||||||
|
│ │ │ ├── 01-{funcion}.sql
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ ├── triggers/
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── views/
|
||||||
|
│ │ └── ...
|
||||||
|
├── seeds/
|
||||||
|
│ ├── prod/ # Datos de produccion
|
||||||
|
│ └── dev/ # Datos de desarrollo
|
||||||
|
└── scripts/
|
||||||
|
├── create-database.sh # Crear BD
|
||||||
|
└── drop-and-recreate-database.sh # Recrear BD
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Nomenclatura de Archivos
|
||||||
|
|
||||||
|
| Tipo | Patron | Ejemplo |
|
||||||
|
|------|--------|---------|
|
||||||
|
| Extensiones | `00-extensions.sql` | `00-extensions.sql` (citext, pgcrypto por schema) |
|
||||||
|
| Enums | `00-enums.sql` o `01-enums.sql` | `00-enums.sql`, `01-enums.sql` |
|
||||||
|
| Tablas | `NN-{nombre}.sql` | `01-users.sql`, `02-profiles.sql` |
|
||||||
|
| Constraints diferidos | `99-deferred-constraints.sql` | Documentación de constraints no soportados |
|
||||||
|
| Funciones | `NN-{nombre}.sql` | `01-update_updated_at.sql` |
|
||||||
|
| Triggers | `NN-{nombre}.sql` | `01-audit_trigger.sql` |
|
||||||
|
| Views | `NN-{nombre}.sql` | `01-user_summary.sql` |
|
||||||
|
|
||||||
|
El numero `NN` indica el orden de ejecucion dentro de cada carpeta.
|
||||||
|
|
||||||
|
**Nota:** El script busca enums en `00-enums.sql` primero; si no existe, busca en `01-enums.sql`.
|
||||||
|
|
||||||
|
### 3. Modificaciones al Schema
|
||||||
|
|
||||||
|
**CORRECTO:**
|
||||||
|
```sql
|
||||||
|
-- Editar directamente el archivo DDL original
|
||||||
|
-- apps/database/ddl/schemas/auth/tables/01-users.sql
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
-- Agregar nuevas columnas aqui
|
||||||
|
phone VARCHAR(20), -- <-- Nueva columna
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**INCORRECTO:**
|
||||||
|
```sql
|
||||||
|
-- NO crear archivos de migracion
|
||||||
|
-- migrations/20251206_add_phone_to_users.sql <-- PROHIBIDO
|
||||||
|
|
||||||
|
ALTER TABLE auth.users ADD COLUMN phone VARCHAR(20);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Cuando Necesitas Cambiar el Schema
|
||||||
|
|
||||||
|
1. **Edita el archivo DDL original** de la tabla/funcion/trigger
|
||||||
|
2. **Ejecuta recreacion** en tu ambiente de desarrollo:
|
||||||
|
```bash
|
||||||
|
./scripts/drop-and-recreate-database.sh
|
||||||
|
```
|
||||||
|
3. **Verifica** que todo funcione correctamente
|
||||||
|
4. **Commit** los cambios al DDL
|
||||||
|
|
||||||
|
### 5. Prohibiciones Explicitas
|
||||||
|
|
||||||
|
| Accion | Permitido | Razon |
|
||||||
|
|--------|-----------|-------|
|
||||||
|
| Crear archivo `migrations/*.sql` | NO | Rompe carga limpia |
|
||||||
|
| Crear archivo `fix-*.sql` | NO | Rompe carga limpia |
|
||||||
|
| Crear archivo `patch-*.sql` | NO | Rompe carga limpia |
|
||||||
|
| Crear archivo `alter-*.sql` | NO | Rompe carga limpia |
|
||||||
|
| Usar `ALTER TABLE` en archivo separado | NO | Debe estar en DDL original |
|
||||||
|
| Modificar DDL original directamente | SI | Es la forma correcta |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ESTANDARES TECNICOS
|
||||||
|
|
||||||
|
### Tipos de Datos
|
||||||
|
|
||||||
|
| Uso | Tipo Correcto | Tipo Incorrecto |
|
||||||
|
|-----|---------------|-----------------|
|
||||||
|
| Timestamps | `TIMESTAMPTZ` | `TIMESTAMP` |
|
||||||
|
| UUIDs | `gen_random_uuid()` | `uuid_generate_v4()` |
|
||||||
|
| Moneda | `DECIMAL(20,8)` | `FLOAT`, `DOUBLE` |
|
||||||
|
| Texto variable | `VARCHAR(n)` | `CHAR(n)` para texto variable |
|
||||||
|
|
||||||
|
### Convenciones SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Nombres en snake_case
|
||||||
|
CREATE TABLE auth.user_profiles ( -- Correcto
|
||||||
|
CREATE TABLE auth.UserProfiles ( -- Incorrecto
|
||||||
|
|
||||||
|
-- Siempre incluir schema
|
||||||
|
CREATE TABLE auth.users ( -- Correcto
|
||||||
|
CREATE TABLE users ( -- Incorrecto (usa public)
|
||||||
|
|
||||||
|
-- IF NOT EXISTS en CREATE
|
||||||
|
CREATE TABLE IF NOT EXISTS auth.users (
|
||||||
|
CREATE TYPE IF NOT EXISTS auth.user_status AS ENUM (
|
||||||
|
|
||||||
|
-- Indices con prefijo descriptivo
|
||||||
|
CREATE INDEX idx_users_email ON auth.users(email);
|
||||||
|
CREATE INDEX idx_users_created ON auth.users(created_at DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Foreign Keys
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Siempre referenciar con schema completo
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Nunca asumir schema
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id), -- INCORRECTO
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ORDEN DE CARGA
|
||||||
|
|
||||||
|
El script `create-database.sh` ejecuta en este orden:
|
||||||
|
|
||||||
|
1. **Extensiones Globales**
|
||||||
|
- uuid-ossp
|
||||||
|
- pgcrypto
|
||||||
|
- pg_trgm
|
||||||
|
- btree_gin
|
||||||
|
- vector (si disponible)
|
||||||
|
- citext
|
||||||
|
|
||||||
|
2. **Schemas** (en orden)
|
||||||
|
- auth
|
||||||
|
- education
|
||||||
|
- financial
|
||||||
|
- trading
|
||||||
|
- investment
|
||||||
|
- ml
|
||||||
|
- llm
|
||||||
|
- audit
|
||||||
|
|
||||||
|
3. **Por cada schema:**
|
||||||
|
- 00-extensions.sql (si existe) - extensiones específicas del schema
|
||||||
|
- 00-enums.sql o 01-enums.sql (si existe)
|
||||||
|
- tables/*.sql (orden numérico)
|
||||||
|
- functions/*.sql (orden numérico)
|
||||||
|
- triggers/*.sql (orden numérico)
|
||||||
|
- views/*.sql (orden numérico)
|
||||||
|
|
||||||
|
4. **Seeds**
|
||||||
|
- prod/ o dev/ según ambiente
|
||||||
|
|
||||||
|
### Configuración por Defecto
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DB_NAME=trading_platform
|
||||||
|
DB_USER=trading
|
||||||
|
DB_PASSWORD=trading_dev_2025
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5433
|
||||||
|
```
|
||||||
|
|
||||||
|
Estas variables pueden sobrescribirse via entorno:
|
||||||
|
```bash
|
||||||
|
DB_PORT=5433 ./drop-and-recreate-database.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VALIDACION
|
||||||
|
|
||||||
|
### Pre-commit Checklist
|
||||||
|
|
||||||
|
Antes de hacer commit de cambios a DDL:
|
||||||
|
|
||||||
|
- [ ] No existen archivos `migrations/`, `fix-*`, `patch-*`, `alter-*`
|
||||||
|
- [ ] Todos los cambios estan en archivos DDL originales
|
||||||
|
- [ ] Se puede ejecutar `drop-and-recreate-database.sh` sin errores
|
||||||
|
- [ ] Todas las FKs usan schema completo (`auth.users`, no `users`)
|
||||||
|
- [ ] Todos los timestamps son `TIMESTAMPTZ`
|
||||||
|
- [ ] Todos los UUIDs usan `gen_random_uuid()`
|
||||||
|
|
||||||
|
### Script de Validacion
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verificar que no hay archivos prohibidos
|
||||||
|
find apps/database -name "fix-*.sql" -o -name "patch-*.sql" -o -name "alter-*.sql"
|
||||||
|
# Debe retornar vacio
|
||||||
|
|
||||||
|
# Verificar que no hay carpeta migrations con contenido nuevo
|
||||||
|
ls apps/database/migrations/
|
||||||
|
# Solo debe existir si hay migraciones legacy (a eliminar)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EXCEPCIONES
|
||||||
|
|
||||||
|
### Unica Excepcion: Datos de Produccion
|
||||||
|
|
||||||
|
Si hay datos de produccion que NO pueden perderse:
|
||||||
|
|
||||||
|
1. **Exportar datos** antes de recrear
|
||||||
|
2. **Recrear schema** con DDL limpio
|
||||||
|
3. **Importar datos** desde backup
|
||||||
|
|
||||||
|
Esto NO es una migracion, es un proceso de backup/restore.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CONSECUENCIAS DE VIOLAR ESTA DIRECTIVA
|
||||||
|
|
||||||
|
1. **Build fallara** - CI/CD rechazara archivos prohibidos
|
||||||
|
2. **PR sera rechazado** - Code review detectara violaciones
|
||||||
|
3. **Deuda tecnica** - Se acumularan inconsistencias
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REFERENCIAS
|
||||||
|
|
||||||
|
- [_MAP.md - Database Schemas](../apps/database/schemas/_MAP.md)
|
||||||
|
- [DECISIONES-ARQUITECTONICAS.md](../docs/99-analisis/DECISIONES-ARQUITECTONICAS.md)
|
||||||
|
- [create-database.sh](../apps/database/scripts/create-database.sh)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HISTORIAL DE CAMBIOS
|
||||||
|
|
||||||
|
| Version | Fecha | Cambio |
|
||||||
|
|---------|-------|--------|
|
||||||
|
| 1.0.0 | 2025-12-06 | Versión inicial |
|
||||||
|
| 1.1.0 | 2026-01-04 | Soporte para 00-extensions.sql, búsqueda flexible de enums (00 o 01), constraints diferidos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Directiva establecida por Requirements-Analyst Agent*
|
||||||
|
*Trading Platform*
|
||||||
|
*Actualizada: 2026-01-04*
|
||||||
173
README.md
Normal file
173
README.md
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# Trading Platform Database
|
||||||
|
|
||||||
|
Definiciones DDL y scripts de base de datos para Trading Platform.
|
||||||
|
|
||||||
|
## Stack Tecnologico
|
||||||
|
|
||||||
|
- **DBMS:** PostgreSQL 16
|
||||||
|
- **Extension ML:** pgvector (embeddings LLM)
|
||||||
|
- **Contenedor:** Docker (pgvector/pgvector:pg16)
|
||||||
|
|
||||||
|
## Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
database/
|
||||||
|
├── ddl/
|
||||||
|
│ └── schemas/ # Definiciones DDL por schema
|
||||||
|
│ ├── audit/ # Logs de auditoria
|
||||||
|
│ ├── auth/ # Autenticacion y sesiones
|
||||||
|
│ ├── education/ # Cursos y gamificacion
|
||||||
|
│ ├── financial/ # Transacciones y wallets
|
||||||
|
│ ├── investment/ # Inversiones y portfolios
|
||||||
|
│ ├── llm/ # Embeddings y contextos LLM
|
||||||
|
│ ├── ml/ # Modelos y predicciones ML
|
||||||
|
│ └── trading/ # Ordenes y operaciones
|
||||||
|
├── schemas/ # _MAP.md e indices
|
||||||
|
├── scripts/ # Scripts de gestion
|
||||||
|
│ ├── create-database.sh
|
||||||
|
│ ├── drop-and-recreate-database.sh
|
||||||
|
│ └── validate-ddl.sh
|
||||||
|
└── seeds/ # Datos iniciales (dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schemas
|
||||||
|
|
||||||
|
| Schema | Tablas | Descripcion |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `auth` | 12 | Usuarios, sesiones, tokens, OAuth |
|
||||||
|
| `education` | 15 | Cursos, lecciones, quizzes, progreso |
|
||||||
|
| `trading` | 18 | Ordenes, posiciones, historial |
|
||||||
|
| `investment` | 12 | Portfolios, assets, distribuciones |
|
||||||
|
| `financial` | 10 | Wallets, transacciones, pagos |
|
||||||
|
| `ml` | 8 | Modelos, predicciones, senales |
|
||||||
|
| `llm` | 6 | Embeddings, conversaciones, contextos |
|
||||||
|
| `audit` | 9 | Logs, eventos, trazabilidad |
|
||||||
|
|
||||||
|
**Total:** ~90 tablas, 102+ foreign keys
|
||||||
|
|
||||||
|
## Instalacion
|
||||||
|
|
||||||
|
### Opcion 1: Docker (Recomendado)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Desde raiz del proyecto trading-platform
|
||||||
|
docker-compose up -d postgres
|
||||||
|
|
||||||
|
# Crear base de datos
|
||||||
|
cd apps/database/scripts
|
||||||
|
./create-database.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opcion 2: PostgreSQL Local
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requiere PostgreSQL 16 con pgvector instalado
|
||||||
|
# Ubuntu/Debian:
|
||||||
|
sudo apt install postgresql-16-pgvector
|
||||||
|
|
||||||
|
# Crear base de datos
|
||||||
|
cd apps/database/scripts
|
||||||
|
./create-database.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts Disponibles
|
||||||
|
|
||||||
|
| Script | Descripcion |
|
||||||
|
|--------|-------------|
|
||||||
|
| `create-database.sh` | Crear BD y cargar DDL |
|
||||||
|
| `drop-and-recreate-database.sh` | Recrear BD desde cero |
|
||||||
|
| `validate-ddl.sh` | Validar sintaxis SQL |
|
||||||
|
|
||||||
|
## Variables de Entorno
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database connection - Instancia nativa compartida
|
||||||
|
# Ref: orchestration/inventarios/DEVENV-PORTS-INVENTORY.yml
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432 # Instancia NATIVA (NO Docker)
|
||||||
|
DB_NAME=trading_platform
|
||||||
|
DB_USER=trading_user
|
||||||
|
DB_PASSWORD=trading_dev_2025
|
||||||
|
```
|
||||||
|
|
||||||
|
> **IMPORTANTE:** El workspace usa arquitectura de instancia única compartida. PostgreSQL nativo en puerto 5432, NO Docker. Los proyectos se separan por DATABASE + USER.
|
||||||
|
|
||||||
|
## Uso de Scripts
|
||||||
|
|
||||||
|
### Crear Base de Datos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/database/scripts
|
||||||
|
|
||||||
|
# Con variables de entorno (instancia nativa)
|
||||||
|
export DB_HOST=localhost
|
||||||
|
export DB_PORT=5432
|
||||||
|
export DB_NAME=trading_platform
|
||||||
|
export DB_USER=trading_user
|
||||||
|
export DB_PASSWORD=trading_dev_2025
|
||||||
|
|
||||||
|
./create-database.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recrear Base de Datos (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ADVERTENCIA: Elimina todos los datos
|
||||||
|
./drop-and-recreate-database.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estructura DDL
|
||||||
|
|
||||||
|
Cada schema sigue la estructura:
|
||||||
|
|
||||||
|
```
|
||||||
|
ddl/schemas/{schema}/
|
||||||
|
├── 00-extensions.sql # Extensiones (si aplica)
|
||||||
|
├── tables/
|
||||||
|
│ ├── 01-{tabla}.sql
|
||||||
|
│ ├── 02-{tabla}.sql
|
||||||
|
│ └── ...
|
||||||
|
├── functions/
|
||||||
|
│ ├── 01-{funcion}.sql
|
||||||
|
│ └── ...
|
||||||
|
└── triggers/
|
||||||
|
└── 01-{trigger}.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Convencion de Nombres
|
||||||
|
|
||||||
|
- **Tablas:** snake_case plural (`users`, `trading_orders`)
|
||||||
|
- **Columnas:** snake_case (`created_at`, `user_id`)
|
||||||
|
- **Primary Keys:** `id` (UUID)
|
||||||
|
- **Foreign Keys:** `{tabla_singular}_id`
|
||||||
|
- **Indices:** `idx_{tabla}_{columna}`
|
||||||
|
- **Triggers:** `trg_{tabla}_{accion}`
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
- Row Level Security (RLS) habilitado en tablas multi-tenant
|
||||||
|
- Funciones con `SECURITY DEFINER` donde corresponde
|
||||||
|
- Passwords hasheados con bcrypt (en aplicacion)
|
||||||
|
- Audit logs automaticos via triggers
|
||||||
|
|
||||||
|
## Migraciones
|
||||||
|
|
||||||
|
Actualmente se usa DDL directo sin sistema de migraciones.
|
||||||
|
Para cambios:
|
||||||
|
|
||||||
|
1. Modificar archivo DDL correspondiente
|
||||||
|
2. Documentar en `_MAP.md` del schema
|
||||||
|
3. Ejecutar `drop-and-recreate-database.sh` (dev)
|
||||||
|
4. En produccion: scripts de migracion manuales
|
||||||
|
|
||||||
|
## Documentacion Relacionada
|
||||||
|
|
||||||
|
- [Mapa de Schemas](./schemas/_MAP.md)
|
||||||
|
- [Inventario Database](../../docs/90-transversal/inventarios/DATABASE_INVENTORY.yml)
|
||||||
|
- [Politica de Carga Limpia](./DIRECTIVA-POLITICA-CARGA-LIMPIA.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Proyecto:** Trading Platform
|
||||||
|
**Version:** 0.1.0
|
||||||
|
**Actualizado:** 2026-01-07
|
||||||
26
ddl/00-extensions.sql
Normal file
26
ddl/00-extensions.sql
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- File: 00-extensions.sql
|
||||||
|
-- Description: PostgreSQL extensions required globally
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- UUID generation extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Cryptographic functions for password hashing and token generation
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
-- Network address types and functions
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "citext";
|
||||||
|
|
||||||
|
-- Full text search
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "unaccent";
|
||||||
|
|
||||||
|
-- Trigram similarity for fuzzy text matching
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION "uuid-ossp" IS 'UUID generation functions';
|
||||||
|
COMMENT ON EXTENSION "pgcrypto" IS 'Cryptographic functions for secure password and token handling';
|
||||||
|
COMMENT ON EXTENSION "citext" IS 'Case-insensitive text type for email addresses';
|
||||||
|
COMMENT ON EXTENSION "unaccent" IS 'Text search dictionary that removes accents';
|
||||||
|
COMMENT ON EXTENSION "pg_trgm" IS 'Trigram matching for similarity searches';
|
||||||
37
ddl/01-schemas.sql
Normal file
37
ddl/01-schemas.sql
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- File: 01-schemas.sql
|
||||||
|
-- Description: Database schemas creation
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Authentication and User Management
|
||||||
|
CREATE SCHEMA IF NOT EXISTS auth;
|
||||||
|
COMMENT ON SCHEMA auth IS 'Authentication, authorization, and user management';
|
||||||
|
|
||||||
|
-- Education and Learning
|
||||||
|
CREATE SCHEMA IF NOT EXISTS education;
|
||||||
|
COMMENT ON SCHEMA education IS 'Educational content, courses, and learning progress';
|
||||||
|
|
||||||
|
-- Trading Operations
|
||||||
|
CREATE SCHEMA IF NOT EXISTS trading;
|
||||||
|
COMMENT ON SCHEMA trading IS 'Trading bots, orders, positions, and market data';
|
||||||
|
|
||||||
|
-- Investment Management
|
||||||
|
CREATE SCHEMA IF NOT EXISTS investment;
|
||||||
|
COMMENT ON SCHEMA investment IS 'Investment products, accounts, and transactions';
|
||||||
|
|
||||||
|
-- Financial Operations
|
||||||
|
CREATE SCHEMA IF NOT EXISTS financial;
|
||||||
|
COMMENT ON SCHEMA financial IS 'Wallets, payments, subscriptions, and financial transactions';
|
||||||
|
|
||||||
|
-- Machine Learning
|
||||||
|
CREATE SCHEMA IF NOT EXISTS ml;
|
||||||
|
COMMENT ON SCHEMA ml IS 'ML models, predictions, and feature store';
|
||||||
|
|
||||||
|
-- Large Language Models
|
||||||
|
CREATE SCHEMA IF NOT EXISTS llm;
|
||||||
|
COMMENT ON SCHEMA llm IS 'LLM conversations, messages, and user preferences';
|
||||||
|
|
||||||
|
-- Audit and Compliance
|
||||||
|
CREATE SCHEMA IF NOT EXISTS audit;
|
||||||
|
COMMENT ON SCHEMA audit IS 'Audit logs, security events, and compliance tracking';
|
||||||
63
ddl/schemas/audit/00-enums.sql
Normal file
63
ddl/schemas/audit/00-enums.sql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - ENUMs
|
||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA Trading Platform
|
||||||
|
-- Schema: audit
|
||||||
|
-- Propósito: Tipos enumerados para auditoría y logging
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Tipo de evento de auditoría
|
||||||
|
CREATE TYPE audit.audit_event_type AS ENUM (
|
||||||
|
'create',
|
||||||
|
'read',
|
||||||
|
'update',
|
||||||
|
'delete',
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'permission_change',
|
||||||
|
'config_change',
|
||||||
|
'export',
|
||||||
|
'import'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Severidad del evento
|
||||||
|
CREATE TYPE audit.event_severity AS ENUM (
|
||||||
|
'debug',
|
||||||
|
'info',
|
||||||
|
'warning',
|
||||||
|
'error',
|
||||||
|
'critical'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Categoría del evento de seguridad
|
||||||
|
CREATE TYPE audit.security_event_category AS ENUM (
|
||||||
|
'authentication',
|
||||||
|
'authorization',
|
||||||
|
'data_access',
|
||||||
|
'configuration',
|
||||||
|
'suspicious_activity',
|
||||||
|
'compliance'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado del evento
|
||||||
|
CREATE TYPE audit.event_status AS ENUM (
|
||||||
|
'success',
|
||||||
|
'failure',
|
||||||
|
'blocked',
|
||||||
|
'pending_review'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de recurso auditado
|
||||||
|
CREATE TYPE audit.resource_type AS ENUM (
|
||||||
|
'user',
|
||||||
|
'account',
|
||||||
|
'transaction',
|
||||||
|
'order',
|
||||||
|
'position',
|
||||||
|
'bot',
|
||||||
|
'subscription',
|
||||||
|
'payment',
|
||||||
|
'course',
|
||||||
|
'model',
|
||||||
|
'system_config'
|
||||||
|
);
|
||||||
54
ddl/schemas/audit/tables/01-audit_logs.sql
Normal file
54
ddl/schemas/audit/tables/01-audit_logs.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - Tabla: audit_logs
|
||||||
|
-- ============================================================================
|
||||||
|
-- Tabla principal de auditoría general
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.audit_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Información del evento
|
||||||
|
event_type audit.audit_event_type NOT NULL,
|
||||||
|
event_status audit.event_status NOT NULL DEFAULT 'success',
|
||||||
|
severity audit.event_severity NOT NULL DEFAULT 'info',
|
||||||
|
|
||||||
|
-- Quién realizó la acción
|
||||||
|
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
session_id UUID,
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Qué se modificó
|
||||||
|
resource_type audit.resource_type NOT NULL,
|
||||||
|
resource_id UUID,
|
||||||
|
resource_name VARCHAR(255),
|
||||||
|
|
||||||
|
-- Detalles del cambio
|
||||||
|
action VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
old_values JSONB,
|
||||||
|
new_values JSONB,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
request_id UUID,
|
||||||
|
correlation_id UUID,
|
||||||
|
service_name VARCHAR(50),
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices para consultas frecuentes
|
||||||
|
CREATE INDEX idx_audit_logs_user_id ON audit.audit_logs(user_id);
|
||||||
|
CREATE INDEX idx_audit_logs_resource ON audit.audit_logs(resource_type, resource_id);
|
||||||
|
CREATE INDEX idx_audit_logs_event_type ON audit.audit_logs(event_type);
|
||||||
|
CREATE INDEX idx_audit_logs_created_at ON audit.audit_logs(created_at DESC);
|
||||||
|
CREATE INDEX idx_audit_logs_severity ON audit.audit_logs(severity) WHERE severity IN ('error', 'critical');
|
||||||
|
CREATE INDEX idx_audit_logs_correlation ON audit.audit_logs(correlation_id) WHERE correlation_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Índice GIN para búsqueda en JSONB
|
||||||
|
CREATE INDEX idx_audit_logs_metadata ON audit.audit_logs USING GIN (metadata);
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit.audit_logs IS 'Log general de auditoría para todas las acciones del sistema';
|
||||||
|
COMMENT ON COLUMN audit.audit_logs.correlation_id IS 'ID para correlacionar eventos relacionados en una misma operación';
|
||||||
57
ddl/schemas/audit/tables/02-security_events.sql
Normal file
57
ddl/schemas/audit/tables/02-security_events.sql
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - Tabla: security_events
|
||||||
|
-- ============================================================================
|
||||||
|
-- Eventos de seguridad específicos
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.security_events (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Categorización
|
||||||
|
category audit.security_event_category NOT NULL,
|
||||||
|
severity audit.event_severity NOT NULL,
|
||||||
|
event_status audit.event_status NOT NULL DEFAULT 'success',
|
||||||
|
|
||||||
|
-- Actor
|
||||||
|
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
ip_address INET NOT NULL,
|
||||||
|
user_agent TEXT,
|
||||||
|
geo_location JSONB,
|
||||||
|
|
||||||
|
-- Detalles del evento
|
||||||
|
event_code VARCHAR(50) NOT NULL,
|
||||||
|
event_name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Contexto técnico
|
||||||
|
request_path VARCHAR(500),
|
||||||
|
request_method VARCHAR(10),
|
||||||
|
response_code INTEGER,
|
||||||
|
|
||||||
|
-- Datos adicionales
|
||||||
|
risk_score DECIMAL(3, 2),
|
||||||
|
is_blocked BOOLEAN DEFAULT FALSE,
|
||||||
|
block_reason TEXT,
|
||||||
|
requires_review BOOLEAN DEFAULT FALSE,
|
||||||
|
reviewed_by UUID REFERENCES auth.users(id),
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
review_notes TEXT,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
raw_data JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_security_events_user ON audit.security_events(user_id);
|
||||||
|
CREATE INDEX idx_security_events_category ON audit.security_events(category);
|
||||||
|
CREATE INDEX idx_security_events_severity ON audit.security_events(severity);
|
||||||
|
CREATE INDEX idx_security_events_ip ON audit.security_events(ip_address);
|
||||||
|
CREATE INDEX idx_security_events_created ON audit.security_events(created_at DESC);
|
||||||
|
CREATE INDEX idx_security_events_blocked ON audit.security_events(is_blocked) WHERE is_blocked = TRUE;
|
||||||
|
CREATE INDEX idx_security_events_review ON audit.security_events(requires_review) WHERE requires_review = TRUE;
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit.security_events IS 'Eventos de seguridad para monitoreo y respuesta a incidentes';
|
||||||
|
COMMENT ON COLUMN audit.security_events.risk_score IS 'Puntuación de riesgo calculada (0.00-1.00)';
|
||||||
47
ddl/schemas/audit/tables/03-system_events.sql
Normal file
47
ddl/schemas/audit/tables/03-system_events.sql
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - Tabla: system_events
|
||||||
|
-- ============================================================================
|
||||||
|
-- Eventos del sistema (jobs, tareas programadas, errores)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.system_events (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Categorización
|
||||||
|
event_type VARCHAR(50) NOT NULL,
|
||||||
|
severity audit.event_severity NOT NULL DEFAULT 'info',
|
||||||
|
|
||||||
|
-- Origen
|
||||||
|
service_name VARCHAR(100) NOT NULL,
|
||||||
|
component VARCHAR(100),
|
||||||
|
environment VARCHAR(20) NOT NULL DEFAULT 'production',
|
||||||
|
hostname VARCHAR(255),
|
||||||
|
|
||||||
|
-- Detalles
|
||||||
|
event_name VARCHAR(255) NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
stack_trace TEXT,
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
correlation_id UUID,
|
||||||
|
job_id VARCHAR(100),
|
||||||
|
duration_ms INTEGER,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
tags TEXT[],
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_system_events_service ON audit.system_events(service_name);
|
||||||
|
CREATE INDEX idx_system_events_type ON audit.system_events(event_type);
|
||||||
|
CREATE INDEX idx_system_events_severity ON audit.system_events(severity);
|
||||||
|
CREATE INDEX idx_system_events_created ON audit.system_events(created_at DESC);
|
||||||
|
CREATE INDEX idx_system_events_correlation ON audit.system_events(correlation_id) WHERE correlation_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_system_events_job ON audit.system_events(job_id) WHERE job_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_system_events_tags ON audit.system_events USING GIN (tags);
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit.system_events IS 'Eventos del sistema para monitoreo de infraestructura y jobs';
|
||||||
57
ddl/schemas/audit/tables/04-trading_audit.sql
Normal file
57
ddl/schemas/audit/tables/04-trading_audit.sql
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - Tabla: trading_audit
|
||||||
|
-- ============================================================================
|
||||||
|
-- Auditoría específica de operaciones de trading
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.trading_audit (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Actor
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
bot_id UUID,
|
||||||
|
|
||||||
|
-- Acción
|
||||||
|
action VARCHAR(50) NOT NULL,
|
||||||
|
action_status audit.event_status NOT NULL,
|
||||||
|
|
||||||
|
-- Objeto de la acción
|
||||||
|
order_id UUID,
|
||||||
|
position_id UUID,
|
||||||
|
symbol VARCHAR(20) NOT NULL,
|
||||||
|
|
||||||
|
-- Detalles de la operación
|
||||||
|
side VARCHAR(4) NOT NULL, -- 'buy' o 'sell'
|
||||||
|
order_type VARCHAR(20),
|
||||||
|
quantity DECIMAL(20, 8) NOT NULL,
|
||||||
|
price DECIMAL(20, 8),
|
||||||
|
executed_price DECIMAL(20, 8),
|
||||||
|
|
||||||
|
-- Resultado
|
||||||
|
pnl DECIMAL(20, 8),
|
||||||
|
fees DECIMAL(20, 8),
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
strategy_id UUID,
|
||||||
|
signal_id UUID,
|
||||||
|
is_paper_trading BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
execution_time_ms INTEGER,
|
||||||
|
broker_response JSONB,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_trading_audit_user ON audit.trading_audit(user_id);
|
||||||
|
CREATE INDEX idx_trading_audit_bot ON audit.trading_audit(bot_id) WHERE bot_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_trading_audit_symbol ON audit.trading_audit(symbol);
|
||||||
|
CREATE INDEX idx_trading_audit_action ON audit.trading_audit(action);
|
||||||
|
CREATE INDEX idx_trading_audit_created ON audit.trading_audit(created_at DESC);
|
||||||
|
CREATE INDEX idx_trading_audit_order ON audit.trading_audit(order_id) WHERE order_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_trading_audit_position ON audit.trading_audit(position_id) WHERE position_id IS NOT NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit.trading_audit IS 'Auditoría detallada de todas las operaciones de trading';
|
||||||
49
ddl/schemas/audit/tables/05-api_request_logs.sql
Normal file
49
ddl/schemas/audit/tables/05-api_request_logs.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - Tabla: api_request_logs
|
||||||
|
-- ============================================================================
|
||||||
|
-- Log de requests a la API (para debugging y análisis)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.api_request_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Request
|
||||||
|
request_id UUID NOT NULL,
|
||||||
|
method VARCHAR(10) NOT NULL,
|
||||||
|
path VARCHAR(500) NOT NULL,
|
||||||
|
query_params JSONB,
|
||||||
|
headers JSONB,
|
||||||
|
body_size INTEGER,
|
||||||
|
|
||||||
|
-- Actor
|
||||||
|
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
api_key_id UUID,
|
||||||
|
ip_address INET NOT NULL,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Response
|
||||||
|
status_code INTEGER NOT NULL,
|
||||||
|
response_size INTEGER,
|
||||||
|
response_time_ms INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
service_name VARCHAR(50),
|
||||||
|
version VARCHAR(20),
|
||||||
|
error_code VARCHAR(50),
|
||||||
|
error_message TEXT,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices optimizados para consultas de análisis
|
||||||
|
CREATE INDEX idx_api_logs_user ON audit.api_request_logs(user_id);
|
||||||
|
CREATE INDEX idx_api_logs_path ON audit.api_request_logs(path);
|
||||||
|
CREATE INDEX idx_api_logs_status ON audit.api_request_logs(status_code);
|
||||||
|
CREATE INDEX idx_api_logs_created ON audit.api_request_logs(created_at DESC);
|
||||||
|
CREATE INDEX idx_api_logs_ip ON audit.api_request_logs(ip_address);
|
||||||
|
CREATE INDEX idx_api_logs_slow ON audit.api_request_logs(response_time_ms) WHERE response_time_ms > 1000;
|
||||||
|
CREATE INDEX idx_api_logs_errors ON audit.api_request_logs(status_code) WHERE status_code >= 400;
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit.api_request_logs IS 'Log de requests HTTP para análisis y debugging';
|
||||||
|
COMMENT ON COLUMN audit.api_request_logs.body_size IS 'Tamaño del body en bytes (no se guarda contenido por seguridad)';
|
||||||
45
ddl/schemas/audit/tables/06-data_access_logs.sql
Normal file
45
ddl/schemas/audit/tables/06-data_access_logs.sql
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - Tabla: data_access_logs
|
||||||
|
-- ============================================================================
|
||||||
|
-- Log de acceso a datos sensibles (cumplimiento regulatorio)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.data_access_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Quién accedió
|
||||||
|
accessor_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
accessor_role VARCHAR(50) NOT NULL,
|
||||||
|
|
||||||
|
-- A qué datos se accedió
|
||||||
|
target_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
data_category VARCHAR(50) NOT NULL, -- 'pii', 'financial', 'health', 'credentials'
|
||||||
|
data_fields TEXT[], -- campos específicos accedidos
|
||||||
|
|
||||||
|
-- Cómo se accedió
|
||||||
|
access_type VARCHAR(20) NOT NULL, -- 'view', 'export', 'modify', 'delete'
|
||||||
|
access_reason TEXT,
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
request_id UUID,
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Compliance
|
||||||
|
consent_verified BOOLEAN DEFAULT FALSE,
|
||||||
|
legal_basis VARCHAR(100),
|
||||||
|
retention_days INTEGER,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_data_access_accessor ON audit.data_access_logs(accessor_user_id);
|
||||||
|
CREATE INDEX idx_data_access_target ON audit.data_access_logs(target_user_id);
|
||||||
|
CREATE INDEX idx_data_access_category ON audit.data_access_logs(data_category);
|
||||||
|
CREATE INDEX idx_data_access_type ON audit.data_access_logs(access_type);
|
||||||
|
CREATE INDEX idx_data_access_created ON audit.data_access_logs(created_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit.data_access_logs IS 'Registro de acceso a datos sensibles para cumplimiento GDPR/CCPA';
|
||||||
|
COMMENT ON COLUMN audit.data_access_logs.legal_basis IS 'Base legal para el acceso (consentimiento, contrato, obligación legal, etc.)';
|
||||||
52
ddl/schemas/audit/tables/07-compliance_logs.sql
Normal file
52
ddl/schemas/audit/tables/07-compliance_logs.sql
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- AUDIT SCHEMA - Tabla: compliance_logs
|
||||||
|
-- ============================================================================
|
||||||
|
-- Log de eventos de cumplimiento regulatorio
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.compliance_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Regulación
|
||||||
|
regulation VARCHAR(50) NOT NULL, -- 'GDPR', 'CCPA', 'SOX', 'PCI-DSS', 'MiFID'
|
||||||
|
requirement VARCHAR(100) NOT NULL,
|
||||||
|
|
||||||
|
-- Evento
|
||||||
|
event_type VARCHAR(50) NOT NULL,
|
||||||
|
event_description TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- Actor
|
||||||
|
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
system_initiated BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
compliance_status VARCHAR(20) NOT NULL, -- 'compliant', 'non_compliant', 'remediation'
|
||||||
|
risk_level VARCHAR(20), -- 'low', 'medium', 'high', 'critical'
|
||||||
|
|
||||||
|
-- Detalles
|
||||||
|
evidence JSONB,
|
||||||
|
remediation_required BOOLEAN DEFAULT FALSE,
|
||||||
|
remediation_deadline TIMESTAMPTZ,
|
||||||
|
remediation_notes TEXT,
|
||||||
|
|
||||||
|
-- Revisión
|
||||||
|
reviewed_by UUID REFERENCES auth.users(id),
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_compliance_regulation ON audit.compliance_logs(regulation);
|
||||||
|
CREATE INDEX idx_compliance_status ON audit.compliance_logs(compliance_status);
|
||||||
|
CREATE INDEX idx_compliance_risk ON audit.compliance_logs(risk_level);
|
||||||
|
CREATE INDEX idx_compliance_created ON audit.compliance_logs(created_at DESC);
|
||||||
|
CREATE INDEX idx_compliance_remediation ON audit.compliance_logs(remediation_required)
|
||||||
|
WHERE remediation_required = TRUE;
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit.compliance_logs IS 'Registro de cumplimiento regulatorio para auditorías';
|
||||||
19
ddl/schemas/auth/00-extensions.sql
Normal file
19
ddl/schemas/auth/00-extensions.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: 00-extensions.sql
|
||||||
|
-- Description: PostgreSQL extensions required for authentication schema
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- UUID generation extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Cryptographic functions for password hashing and token generation
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
-- Network address types and functions
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "citext";
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION "uuid-ossp" IS 'UUID generation functions';
|
||||||
|
COMMENT ON EXTENSION "pgcrypto" IS 'Cryptographic functions for secure password and token handling';
|
||||||
|
COMMENT ON EXTENSION "citext" IS 'Case-insensitive text type for email addresses';
|
||||||
80
ddl/schemas/auth/01-enums.sql
Normal file
80
ddl/schemas/auth/01-enums.sql
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: 01-enums.sql
|
||||||
|
-- Description: Enumerated types for authentication and authorization
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- User account status
|
||||||
|
CREATE TYPE auth.user_status AS ENUM (
|
||||||
|
'pending_verification',
|
||||||
|
'active',
|
||||||
|
'suspended',
|
||||||
|
'deactivated',
|
||||||
|
'banned'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE auth.user_status IS 'User account status lifecycle states';
|
||||||
|
|
||||||
|
-- User role for RBAC
|
||||||
|
CREATE TYPE auth.user_role AS ENUM (
|
||||||
|
'user',
|
||||||
|
'trader',
|
||||||
|
'analyst',
|
||||||
|
'admin',
|
||||||
|
'super_admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE auth.user_role IS 'Role-based access control roles';
|
||||||
|
|
||||||
|
-- OAuth provider types
|
||||||
|
CREATE TYPE auth.oauth_provider AS ENUM (
|
||||||
|
'google',
|
||||||
|
'facebook',
|
||||||
|
'apple',
|
||||||
|
'github',
|
||||||
|
'microsoft',
|
||||||
|
'twitter'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE auth.oauth_provider IS 'Supported OAuth 2.0 providers';
|
||||||
|
|
||||||
|
-- Phone verification channel
|
||||||
|
CREATE TYPE auth.phone_channel AS ENUM (
|
||||||
|
'sms',
|
||||||
|
'whatsapp'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE auth.phone_channel IS 'Phone verification delivery channels';
|
||||||
|
|
||||||
|
-- Authentication event types for logging
|
||||||
|
CREATE TYPE auth.auth_event_type AS ENUM (
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'register',
|
||||||
|
'password_change',
|
||||||
|
'password_reset_request',
|
||||||
|
'password_reset_complete',
|
||||||
|
'email_verification',
|
||||||
|
'phone_verification',
|
||||||
|
'mfa_enabled',
|
||||||
|
'mfa_disabled',
|
||||||
|
'session_expired',
|
||||||
|
'account_suspended',
|
||||||
|
'account_reactivated',
|
||||||
|
'failed_login',
|
||||||
|
'oauth_linked',
|
||||||
|
'oauth_unlinked'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE auth.auth_event_type IS 'Types of authentication events for audit logging';
|
||||||
|
|
||||||
|
-- MFA method types
|
||||||
|
CREATE TYPE auth.mfa_method AS ENUM (
|
||||||
|
'none',
|
||||||
|
'totp',
|
||||||
|
'sms',
|
||||||
|
'email'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE auth.mfa_method IS 'Multi-factor authentication methods';
|
||||||
48
ddl/schemas/auth/functions/01-update_updated_at.sql
Normal file
48
ddl/schemas/auth/functions/01-update_updated_at.sql
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: functions/01-update_updated_at.sql
|
||||||
|
-- Description: Trigger function to automatically update updated_at timestamp
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION auth.update_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION auth.update_updated_at() IS 'Trigger function to automatically update updated_at column on row modification';
|
||||||
|
|
||||||
|
-- Apply trigger to all tables with updated_at column
|
||||||
|
|
||||||
|
-- Users table
|
||||||
|
CREATE TRIGGER trigger_update_users_updated_at
|
||||||
|
BEFORE UPDATE ON auth.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION auth.update_updated_at();
|
||||||
|
|
||||||
|
-- User profiles table
|
||||||
|
CREATE TRIGGER trigger_update_user_profiles_updated_at
|
||||||
|
BEFORE UPDATE ON auth.user_profiles
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION auth.update_updated_at();
|
||||||
|
|
||||||
|
-- OAuth accounts table
|
||||||
|
CREATE TRIGGER trigger_update_oauth_accounts_updated_at
|
||||||
|
BEFORE UPDATE ON auth.oauth_accounts
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION auth.update_updated_at();
|
||||||
|
|
||||||
|
-- Sessions table
|
||||||
|
CREATE TRIGGER trigger_update_sessions_updated_at
|
||||||
|
BEFORE UPDATE ON auth.sessions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION auth.update_updated_at();
|
||||||
|
|
||||||
|
-- Rate limiting config table
|
||||||
|
CREATE TRIGGER trigger_update_rate_limiting_config_updated_at
|
||||||
|
BEFORE UPDATE ON auth.rate_limiting_config
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION auth.update_updated_at();
|
||||||
75
ddl/schemas/auth/functions/02-log_auth_event.sql
Normal file
75
ddl/schemas/auth/functions/02-log_auth_event.sql
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: functions/02-log_auth_event.sql
|
||||||
|
-- Description: Function to log authentication events to auth_logs table
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION auth.log_auth_event(
|
||||||
|
p_event_type auth.auth_event_type,
|
||||||
|
p_user_id UUID DEFAULT NULL,
|
||||||
|
p_email CITEXT DEFAULT NULL,
|
||||||
|
p_ip_address INET DEFAULT NULL,
|
||||||
|
p_user_agent TEXT DEFAULT NULL,
|
||||||
|
p_session_id UUID DEFAULT NULL,
|
||||||
|
p_success BOOLEAN DEFAULT true,
|
||||||
|
p_failure_reason VARCHAR(255) DEFAULT NULL,
|
||||||
|
p_metadata JSONB DEFAULT '{}'::jsonb
|
||||||
|
)
|
||||||
|
RETURNS UUID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_log_id UUID;
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO auth.auth_logs (
|
||||||
|
event_type,
|
||||||
|
user_id,
|
||||||
|
email,
|
||||||
|
ip_address,
|
||||||
|
user_agent,
|
||||||
|
session_id,
|
||||||
|
success,
|
||||||
|
failure_reason,
|
||||||
|
metadata,
|
||||||
|
created_at
|
||||||
|
) VALUES (
|
||||||
|
p_event_type,
|
||||||
|
p_user_id,
|
||||||
|
p_email,
|
||||||
|
p_ip_address,
|
||||||
|
p_user_agent,
|
||||||
|
p_session_id,
|
||||||
|
p_success,
|
||||||
|
p_failure_reason,
|
||||||
|
p_metadata,
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
RETURNING id INTO v_log_id;
|
||||||
|
|
||||||
|
RETURN v_log_id;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION auth.log_auth_event(
|
||||||
|
auth.auth_event_type,
|
||||||
|
UUID,
|
||||||
|
CITEXT,
|
||||||
|
INET,
|
||||||
|
TEXT,
|
||||||
|
UUID,
|
||||||
|
BOOLEAN,
|
||||||
|
VARCHAR,
|
||||||
|
JSONB
|
||||||
|
) IS 'Logs authentication events to the auth_logs table with optional metadata';
|
||||||
|
|
||||||
|
-- Example usage:
|
||||||
|
-- SELECT auth.log_auth_event(
|
||||||
|
-- 'login'::auth.auth_event_type,
|
||||||
|
-- '123e4567-e89b-12d3-a456-426614174000'::UUID,
|
||||||
|
-- 'user@example.com'::CITEXT,
|
||||||
|
-- '192.168.1.1'::INET,
|
||||||
|
-- 'Mozilla/5.0...',
|
||||||
|
-- '123e4567-e89b-12d3-a456-426614174001'::UUID,
|
||||||
|
-- true,
|
||||||
|
-- NULL,
|
||||||
|
-- '{"device": "mobile"}'::JSONB
|
||||||
|
-- );
|
||||||
58
ddl/schemas/auth/functions/03-cleanup_expired_sessions.sql
Normal file
58
ddl/schemas/auth/functions/03-cleanup_expired_sessions.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: functions/03-cleanup_expired_sessions.sql
|
||||||
|
-- Description: Function to cleanup expired and inactive sessions
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION auth.cleanup_expired_sessions(
|
||||||
|
p_batch_size INTEGER DEFAULT 1000
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
deleted_count INTEGER,
|
||||||
|
execution_time_ms NUMERIC
|
||||||
|
) AS $$
|
||||||
|
DECLARE
|
||||||
|
v_start_time TIMESTAMPTZ;
|
||||||
|
v_deleted_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
v_start_time := clock_timestamp();
|
||||||
|
|
||||||
|
-- Delete expired sessions
|
||||||
|
WITH deleted AS (
|
||||||
|
DELETE FROM auth.sessions
|
||||||
|
WHERE (
|
||||||
|
-- Expired sessions
|
||||||
|
expires_at < NOW() OR
|
||||||
|
-- Inactive sessions older than 30 days
|
||||||
|
(is_active = false AND invalidated_at < NOW() - INTERVAL '30 days')
|
||||||
|
)
|
||||||
|
AND id IN (
|
||||||
|
SELECT id FROM auth.sessions
|
||||||
|
WHERE (
|
||||||
|
expires_at < NOW() OR
|
||||||
|
(is_active = false AND invalidated_at < NOW() - INTERVAL '30 days')
|
||||||
|
)
|
||||||
|
LIMIT p_batch_size
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT COUNT(*)::INTEGER INTO v_deleted_count FROM deleted;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
v_deleted_count,
|
||||||
|
EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time) * 1000)::NUMERIC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION auth.cleanup_expired_sessions(INTEGER) IS
|
||||||
|
'Deletes expired and old inactive sessions in batches. Returns deleted count and execution time.';
|
||||||
|
|
||||||
|
-- Example usage:
|
||||||
|
-- SELECT * FROM auth.cleanup_expired_sessions(1000);
|
||||||
|
|
||||||
|
-- Recommended: Schedule this function to run periodically via pg_cron or external scheduler
|
||||||
|
-- Example pg_cron schedule (runs daily at 2 AM):
|
||||||
|
-- SELECT cron.schedule('cleanup-expired-sessions', '0 2 * * *',
|
||||||
|
-- 'SELECT auth.cleanup_expired_sessions(1000);');
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: functions/04-create_user_profile_trigger.sql
|
||||||
|
-- Description: Automatically create user profile when new user is created
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION auth.create_user_profile()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Create a new user profile for the newly created user
|
||||||
|
INSERT INTO auth.user_profiles (
|
||||||
|
user_id,
|
||||||
|
language,
|
||||||
|
timezone,
|
||||||
|
newsletter_subscribed,
|
||||||
|
marketing_emails_enabled,
|
||||||
|
notifications_enabled,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
NEW.id,
|
||||||
|
'en',
|
||||||
|
'UTC',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION auth.create_user_profile() IS
|
||||||
|
'Trigger function to automatically create a user profile when a new user is registered';
|
||||||
|
|
||||||
|
-- Create trigger on users table
|
||||||
|
CREATE TRIGGER trigger_create_user_profile
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION auth.create_user_profile();
|
||||||
|
|
||||||
|
COMMENT ON TRIGGER trigger_create_user_profile ON auth.users IS
|
||||||
|
'Automatically creates a user profile entry when a new user is inserted';
|
||||||
106
ddl/schemas/auth/tables/01-users.sql
Normal file
106
ddl/schemas/auth/tables/01-users.sql
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/01-users.sql
|
||||||
|
-- Description: Core users table for authentication and user management
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.users (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Authentication Credentials
|
||||||
|
email CITEXT NOT NULL UNIQUE,
|
||||||
|
email_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
email_verified_at TIMESTAMPTZ,
|
||||||
|
password_hash VARCHAR(255),
|
||||||
|
|
||||||
|
-- User Status and Role
|
||||||
|
status auth.user_status NOT NULL DEFAULT 'pending_verification',
|
||||||
|
role auth.user_role NOT NULL DEFAULT 'user',
|
||||||
|
|
||||||
|
-- Multi-Factor Authentication
|
||||||
|
mfa_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
mfa_method auth.mfa_method NOT NULL DEFAULT 'none',
|
||||||
|
mfa_secret VARCHAR(255),
|
||||||
|
backup_codes JSONB DEFAULT '[]',
|
||||||
|
|
||||||
|
-- Phone Information
|
||||||
|
phone_number VARCHAR(20),
|
||||||
|
phone_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
phone_verified_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Security Settings
|
||||||
|
last_login_at TIMESTAMPTZ,
|
||||||
|
last_login_ip INET,
|
||||||
|
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
|
locked_until TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Account Lifecycle
|
||||||
|
suspended_at TIMESTAMPTZ,
|
||||||
|
suspended_reason TEXT,
|
||||||
|
deactivated_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
created_by_id UUID,
|
||||||
|
updated_by_id UUID,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT valid_email CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
||||||
|
-- NOTE: password_or_oauth constraint moved to 99-deferred-constraints.sql
|
||||||
|
-- to resolve circular dependency with oauth_accounts
|
||||||
|
CONSTRAINT failed_attempts_non_negative CHECK (failed_login_attempts >= 0),
|
||||||
|
CONSTRAINT email_verified_at_consistency CHECK (
|
||||||
|
(email_verified = true AND email_verified_at IS NOT NULL) OR
|
||||||
|
(email_verified = false AND email_verified_at IS NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT phone_verified_at_consistency CHECK (
|
||||||
|
(phone_verified = true AND phone_verified_at IS NOT NULL) OR
|
||||||
|
(phone_verified = false AND phone_verified_at IS NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT mfa_secret_consistency CHECK (
|
||||||
|
(mfa_enabled = true AND mfa_secret IS NOT NULL AND mfa_method != 'none') OR
|
||||||
|
(mfa_enabled = false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_users_email ON auth.users(email);
|
||||||
|
CREATE INDEX idx_users_status ON auth.users(status);
|
||||||
|
CREATE INDEX idx_users_role ON auth.users(role);
|
||||||
|
CREATE INDEX idx_users_last_login ON auth.users(last_login_at DESC);
|
||||||
|
CREATE INDEX idx_users_created_at ON auth.users(created_at DESC);
|
||||||
|
CREATE INDEX idx_users_email_verified ON auth.users(email_verified) WHERE email_verified = false;
|
||||||
|
CREATE INDEX idx_users_locked ON auth.users(locked_until) WHERE locked_until IS NOT NULL;
|
||||||
|
CREATE INDEX idx_users_phone ON auth.users(phone_number) WHERE phone_number IS NOT NULL;
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.users IS 'Core users table for authentication and user management';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.users.id IS 'Unique identifier for the user';
|
||||||
|
COMMENT ON COLUMN auth.users.email IS 'User email address (case-insensitive, unique)';
|
||||||
|
COMMENT ON COLUMN auth.users.email_verified IS 'Whether the email has been verified';
|
||||||
|
COMMENT ON COLUMN auth.users.email_verified_at IS 'Timestamp when email was verified';
|
||||||
|
COMMENT ON COLUMN auth.users.password_hash IS 'Bcrypt hashed password (null for OAuth-only users)';
|
||||||
|
COMMENT ON COLUMN auth.users.status IS 'Current status of the user account';
|
||||||
|
COMMENT ON COLUMN auth.users.role IS 'User role for role-based access control';
|
||||||
|
COMMENT ON COLUMN auth.users.mfa_enabled IS 'Whether multi-factor authentication is enabled';
|
||||||
|
COMMENT ON COLUMN auth.users.mfa_method IS 'MFA method used (totp, sms, email)';
|
||||||
|
COMMENT ON COLUMN auth.users.mfa_secret IS 'Secret key for TOTP MFA';
|
||||||
|
COMMENT ON COLUMN auth.users.phone_number IS 'User phone number for SMS verification';
|
||||||
|
COMMENT ON COLUMN auth.users.phone_verified IS 'Whether the phone number has been verified';
|
||||||
|
COMMENT ON COLUMN auth.users.phone_verified_at IS 'Timestamp when phone was verified';
|
||||||
|
COMMENT ON COLUMN auth.users.last_login_at IS 'Timestamp of last successful login';
|
||||||
|
COMMENT ON COLUMN auth.users.last_login_ip IS 'IP address of last successful login';
|
||||||
|
COMMENT ON COLUMN auth.users.failed_login_attempts IS 'Counter for failed login attempts';
|
||||||
|
COMMENT ON COLUMN auth.users.locked_until IS 'Account locked until this timestamp (null if not locked)';
|
||||||
|
COMMENT ON COLUMN auth.users.suspended_at IS 'Timestamp when account was suspended';
|
||||||
|
COMMENT ON COLUMN auth.users.suspended_reason IS 'Reason for account suspension';
|
||||||
|
COMMENT ON COLUMN auth.users.deactivated_at IS 'Timestamp when account was deactivated';
|
||||||
|
COMMENT ON COLUMN auth.users.created_at IS 'Timestamp when user was created';
|
||||||
|
COMMENT ON COLUMN auth.users.updated_at IS 'Timestamp when user was last updated';
|
||||||
|
COMMENT ON COLUMN auth.users.created_by_id IS 'ID of user who created this record';
|
||||||
|
COMMENT ON COLUMN auth.users.updated_by_id IS 'ID of user who last updated this record';
|
||||||
70
ddl/schemas/auth/tables/02-user_profiles.sql
Normal file
70
ddl/schemas/auth/tables/02-user_profiles.sql
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/02-user_profiles.sql
|
||||||
|
-- Description: Extended user profile information
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.user_profiles (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Foreign Key to Users
|
||||||
|
user_id UUID NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- Personal Information
|
||||||
|
first_name VARCHAR(100),
|
||||||
|
last_name VARCHAR(100),
|
||||||
|
display_name VARCHAR(200),
|
||||||
|
avatar_url TEXT,
|
||||||
|
bio TEXT,
|
||||||
|
|
||||||
|
-- Localization
|
||||||
|
language VARCHAR(10) DEFAULT 'en',
|
||||||
|
timezone VARCHAR(50) DEFAULT 'UTC',
|
||||||
|
country_code VARCHAR(2),
|
||||||
|
|
||||||
|
-- Preferences
|
||||||
|
newsletter_subscribed BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
marketing_emails_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
notifications_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}'::jsonb,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Foreign Key Constraints
|
||||||
|
CONSTRAINT fk_user_profiles_user FOREIGN KEY (user_id)
|
||||||
|
REFERENCES auth.users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_user_profiles_user_id ON auth.user_profiles(user_id);
|
||||||
|
CREATE INDEX idx_user_profiles_display_name ON auth.user_profiles(display_name);
|
||||||
|
CREATE INDEX idx_user_profiles_country ON auth.user_profiles(country_code);
|
||||||
|
CREATE INDEX idx_user_profiles_metadata ON auth.user_profiles USING gin(metadata);
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.user_profiles IS 'Extended user profile information and preferences';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.id IS 'Unique identifier for the profile';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.user_id IS 'Reference to the user account';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.first_name IS 'User first name';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.last_name IS 'User last name';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.display_name IS 'Public display name';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.avatar_url IS 'URL to user avatar image';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.bio IS 'User biography or description';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.language IS 'Preferred language (ISO 639-1 code)';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.timezone IS 'User timezone (IANA timezone database)';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.country_code IS 'Country code (ISO 3166-1 alpha-2)';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.newsletter_subscribed IS 'Newsletter subscription preference';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.marketing_emails_enabled IS 'Marketing emails preference';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.notifications_enabled IS 'In-app notifications preference';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.metadata IS 'Additional profile metadata as JSON';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.created_at IS 'Timestamp when profile was created';
|
||||||
|
COMMENT ON COLUMN auth.user_profiles.updated_at IS 'Timestamp when profile was last updated';
|
||||||
69
ddl/schemas/auth/tables/03-oauth_accounts.sql
Normal file
69
ddl/schemas/auth/tables/03-oauth_accounts.sql
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/03-oauth_accounts.sql
|
||||||
|
-- Description: OAuth provider accounts linked to users
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.oauth_accounts (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Foreign Key to Users
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
|
||||||
|
-- OAuth Provider Information
|
||||||
|
provider auth.oauth_provider NOT NULL,
|
||||||
|
provider_account_id VARCHAR(255) NOT NULL,
|
||||||
|
provider_email CITEXT,
|
||||||
|
|
||||||
|
-- OAuth Tokens
|
||||||
|
access_token TEXT,
|
||||||
|
refresh_token TEXT,
|
||||||
|
token_expires_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Provider Profile Data
|
||||||
|
profile_data JSONB DEFAULT '{}'::jsonb,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_used_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Foreign Key Constraints
|
||||||
|
CONSTRAINT fk_oauth_accounts_user FOREIGN KEY (user_id)
|
||||||
|
REFERENCES auth.users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Unique Constraint: One provider account per user
|
||||||
|
CONSTRAINT unique_user_provider UNIQUE (user_id, provider),
|
||||||
|
|
||||||
|
-- Unique Constraint: Provider account can only link to one user
|
||||||
|
CONSTRAINT unique_provider_account UNIQUE (provider, provider_account_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_oauth_accounts_user_id ON auth.oauth_accounts(user_id);
|
||||||
|
CREATE INDEX idx_oauth_accounts_provider ON auth.oauth_accounts(provider);
|
||||||
|
CREATE INDEX idx_oauth_accounts_provider_email ON auth.oauth_accounts(provider_email);
|
||||||
|
CREATE INDEX idx_oauth_accounts_last_used ON auth.oauth_accounts(last_used_at DESC);
|
||||||
|
CREATE INDEX idx_oauth_accounts_profile_data ON auth.oauth_accounts USING gin(profile_data);
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.oauth_accounts IS 'OAuth provider accounts linked to users for social authentication';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.id IS 'Unique identifier for the OAuth account';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.user_id IS 'Reference to the user account';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.provider IS 'OAuth provider (google, facebook, etc.)';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.provider_account_id IS 'User ID from the OAuth provider';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.provider_email IS 'Email address from OAuth provider';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.access_token IS 'OAuth access token (encrypted)';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.refresh_token IS 'OAuth refresh token (encrypted)';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.token_expires_at IS 'Access token expiration timestamp';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.profile_data IS 'Profile data from OAuth provider as JSON';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.linked_at IS 'Timestamp when account was linked';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.last_used_at IS 'Timestamp when last used for authentication';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.created_at IS 'Timestamp when record was created';
|
||||||
|
COMMENT ON COLUMN auth.oauth_accounts.updated_at IS 'Timestamp when record was last updated';
|
||||||
87
ddl/schemas/auth/tables/04-sessions.sql
Normal file
87
ddl/schemas/auth/tables/04-sessions.sql
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/04-sessions.sql
|
||||||
|
-- Description: User session management for authentication
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.sessions (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Foreign Key to Users
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
|
||||||
|
-- Session Token
|
||||||
|
session_token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- Session Lifecycle
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
-- Session Metadata
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
device_type VARCHAR(50),
|
||||||
|
device_name VARCHAR(100),
|
||||||
|
browser VARCHAR(50),
|
||||||
|
os VARCHAR(50),
|
||||||
|
|
||||||
|
-- Geolocation
|
||||||
|
country_code VARCHAR(2),
|
||||||
|
city VARCHAR(100),
|
||||||
|
|
||||||
|
-- Security
|
||||||
|
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
invalidated_at TIMESTAMPTZ,
|
||||||
|
invalidation_reason VARCHAR(100),
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Foreign Key Constraints
|
||||||
|
CONSTRAINT fk_sessions_user FOREIGN KEY (user_id)
|
||||||
|
REFERENCES auth.users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Check Constraints
|
||||||
|
CONSTRAINT valid_session_dates CHECK (expires_at > created_at),
|
||||||
|
CONSTRAINT invalidated_consistency CHECK (
|
||||||
|
(is_active = false AND invalidated_at IS NOT NULL) OR
|
||||||
|
(is_active = true AND invalidated_at IS NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_sessions_user_id ON auth.sessions(user_id);
|
||||||
|
CREATE INDEX idx_sessions_token ON auth.sessions(session_token);
|
||||||
|
CREATE INDEX idx_sessions_expires_at ON auth.sessions(expires_at);
|
||||||
|
CREATE INDEX idx_sessions_active ON auth.sessions(is_active, expires_at) WHERE is_active = true;
|
||||||
|
CREATE INDEX idx_sessions_last_activity ON auth.sessions(last_activity_at DESC);
|
||||||
|
CREATE INDEX idx_sessions_ip_address ON auth.sessions(ip_address);
|
||||||
|
CREATE INDEX idx_sessions_user_active ON auth.sessions(user_id, is_active, expires_at)
|
||||||
|
WHERE is_active = true;
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.sessions IS 'User session management for authentication and activity tracking';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.sessions.id IS 'Unique identifier for the session';
|
||||||
|
COMMENT ON COLUMN auth.sessions.user_id IS 'Reference to the user account';
|
||||||
|
COMMENT ON COLUMN auth.sessions.session_token IS 'Unique session token for authentication';
|
||||||
|
COMMENT ON COLUMN auth.sessions.expires_at IS 'Session expiration timestamp';
|
||||||
|
COMMENT ON COLUMN auth.sessions.is_active IS 'Whether the session is currently active';
|
||||||
|
COMMENT ON COLUMN auth.sessions.ip_address IS 'IP address of the session';
|
||||||
|
COMMENT ON COLUMN auth.sessions.user_agent IS 'User agent string from the browser';
|
||||||
|
COMMENT ON COLUMN auth.sessions.device_type IS 'Device type (desktop, mobile, tablet)';
|
||||||
|
COMMENT ON COLUMN auth.sessions.device_name IS 'Device name or model';
|
||||||
|
COMMENT ON COLUMN auth.sessions.browser IS 'Browser name and version';
|
||||||
|
COMMENT ON COLUMN auth.sessions.os IS 'Operating system name and version';
|
||||||
|
COMMENT ON COLUMN auth.sessions.country_code IS 'Country code from IP geolocation';
|
||||||
|
COMMENT ON COLUMN auth.sessions.city IS 'City from IP geolocation';
|
||||||
|
COMMENT ON COLUMN auth.sessions.last_activity_at IS 'Timestamp of last session activity';
|
||||||
|
COMMENT ON COLUMN auth.sessions.invalidated_at IS 'Timestamp when session was invalidated';
|
||||||
|
COMMENT ON COLUMN auth.sessions.invalidation_reason IS 'Reason for session invalidation';
|
||||||
|
COMMENT ON COLUMN auth.sessions.created_at IS 'Timestamp when session was created';
|
||||||
|
COMMENT ON COLUMN auth.sessions.updated_at IS 'Timestamp when session was last updated';
|
||||||
65
ddl/schemas/auth/tables/05-email_verifications.sql
Normal file
65
ddl/schemas/auth/tables/05-email_verifications.sql
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/05-email_verifications.sql
|
||||||
|
-- Description: Email verification tokens and tracking
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.email_verifications (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Foreign Key to Users
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
|
||||||
|
-- Email and Token
|
||||||
|
email CITEXT NOT NULL,
|
||||||
|
token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- Token Lifecycle
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
verified_at TIMESTAMPTZ,
|
||||||
|
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Foreign Key Constraints
|
||||||
|
CONSTRAINT fk_email_verifications_user FOREIGN KEY (user_id)
|
||||||
|
REFERENCES auth.users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Check Constraints
|
||||||
|
CONSTRAINT valid_expiration CHECK (expires_at > created_at),
|
||||||
|
CONSTRAINT verified_consistency CHECK (
|
||||||
|
(is_verified = true AND verified_at IS NOT NULL) OR
|
||||||
|
(is_verified = false AND verified_at IS NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_email_verifications_user_id ON auth.email_verifications(user_id);
|
||||||
|
CREATE INDEX idx_email_verifications_token ON auth.email_verifications(token);
|
||||||
|
CREATE INDEX idx_email_verifications_email ON auth.email_verifications(email);
|
||||||
|
CREATE INDEX idx_email_verifications_expires ON auth.email_verifications(expires_at);
|
||||||
|
CREATE INDEX idx_email_verifications_pending ON auth.email_verifications(user_id, is_verified, expires_at)
|
||||||
|
WHERE is_verified = false;
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.email_verifications IS 'Email verification tokens and tracking for user email confirmation';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.id IS 'Unique identifier for the verification record';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.user_id IS 'Reference to the user account';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.email IS 'Email address to be verified';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.token IS 'Unique verification token sent to email';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.expires_at IS 'Token expiration timestamp';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.verified_at IS 'Timestamp when email was verified';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.is_verified IS 'Whether the email has been verified';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.ip_address IS 'IP address when verification was requested';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.user_agent IS 'User agent when verification was requested';
|
||||||
|
COMMENT ON COLUMN auth.email_verifications.created_at IS 'Timestamp when verification was created';
|
||||||
78
ddl/schemas/auth/tables/06-phone_verifications.sql
Normal file
78
ddl/schemas/auth/tables/06-phone_verifications.sql
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/06-phone_verifications.sql
|
||||||
|
-- Description: Phone number verification tokens and tracking
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.phone_verifications (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Foreign Key to Users
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
|
||||||
|
-- Phone and Verification Code
|
||||||
|
phone_number VARCHAR(20) NOT NULL,
|
||||||
|
verification_code VARCHAR(10) NOT NULL,
|
||||||
|
channel auth.phone_channel NOT NULL DEFAULT 'sms',
|
||||||
|
|
||||||
|
-- Token Lifecycle
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
verified_at TIMESTAMPTZ,
|
||||||
|
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
-- Attempt Tracking
|
||||||
|
send_attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
|
verification_attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_attempts INTEGER NOT NULL DEFAULT 3,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Foreign Key Constraints
|
||||||
|
CONSTRAINT fk_phone_verifications_user FOREIGN KEY (user_id)
|
||||||
|
REFERENCES auth.users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Check Constraints
|
||||||
|
CONSTRAINT valid_expiration CHECK (expires_at > created_at),
|
||||||
|
CONSTRAINT verified_consistency CHECK (
|
||||||
|
(is_verified = true AND verified_at IS NOT NULL) OR
|
||||||
|
(is_verified = false AND verified_at IS NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT valid_attempts CHECK (
|
||||||
|
send_attempts >= 0 AND
|
||||||
|
verification_attempts >= 0 AND
|
||||||
|
verification_attempts <= max_attempts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_phone_verifications_user_id ON auth.phone_verifications(user_id);
|
||||||
|
CREATE INDEX idx_phone_verifications_phone ON auth.phone_verifications(phone_number);
|
||||||
|
CREATE INDEX idx_phone_verifications_expires ON auth.phone_verifications(expires_at);
|
||||||
|
CREATE INDEX idx_phone_verifications_pending ON auth.phone_verifications(user_id, is_verified, expires_at)
|
||||||
|
WHERE is_verified = false;
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.phone_verifications IS 'Phone number verification codes and tracking for user phone confirmation';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.id IS 'Unique identifier for the verification record';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.user_id IS 'Reference to the user account';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.phone_number IS 'Phone number to be verified';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.verification_code IS 'Verification code sent via SMS';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.expires_at IS 'Code expiration timestamp';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.verified_at IS 'Timestamp when phone was verified';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.is_verified IS 'Whether the phone has been verified';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.send_attempts IS 'Number of times code was sent';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.verification_attempts IS 'Number of verification attempts';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.max_attempts IS 'Maximum allowed verification attempts';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.ip_address IS 'IP address when verification was requested';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.user_agent IS 'User agent when verification was requested';
|
||||||
|
COMMENT ON COLUMN auth.phone_verifications.created_at IS 'Timestamp when verification was created';
|
||||||
65
ddl/schemas/auth/tables/07-password_reset_tokens.sql
Normal file
65
ddl/schemas/auth/tables/07-password_reset_tokens.sql
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/07-password_reset_tokens.sql
|
||||||
|
-- Description: Password reset tokens and tracking
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.password_reset_tokens (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Foreign Key to Users
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
|
||||||
|
-- Email and Token
|
||||||
|
email CITEXT NOT NULL,
|
||||||
|
token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- Token Lifecycle
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
used_at TIMESTAMPTZ,
|
||||||
|
is_used BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Foreign Key Constraints
|
||||||
|
CONSTRAINT fk_password_reset_tokens_user FOREIGN KEY (user_id)
|
||||||
|
REFERENCES auth.users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Check Constraints
|
||||||
|
CONSTRAINT valid_expiration CHECK (expires_at > created_at),
|
||||||
|
CONSTRAINT used_consistency CHECK (
|
||||||
|
(is_used = true AND used_at IS NOT NULL) OR
|
||||||
|
(is_used = false AND used_at IS NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_password_reset_tokens_user_id ON auth.password_reset_tokens(user_id);
|
||||||
|
CREATE INDEX idx_password_reset_tokens_token ON auth.password_reset_tokens(token);
|
||||||
|
CREATE INDEX idx_password_reset_tokens_email ON auth.password_reset_tokens(email);
|
||||||
|
CREATE INDEX idx_password_reset_tokens_expires ON auth.password_reset_tokens(expires_at);
|
||||||
|
CREATE INDEX idx_password_reset_tokens_active ON auth.password_reset_tokens(user_id, is_used, expires_at)
|
||||||
|
WHERE is_used = false;
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.password_reset_tokens IS 'Password reset tokens for secure password recovery';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.id IS 'Unique identifier for the reset token';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.user_id IS 'Reference to the user account';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.email IS 'Email address for password reset';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.token IS 'Unique reset token sent to email';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.expires_at IS 'Token expiration timestamp';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.used_at IS 'Timestamp when token was used';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.is_used IS 'Whether the token has been used';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.ip_address IS 'IP address when reset was requested';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.user_agent IS 'User agent when reset was requested';
|
||||||
|
COMMENT ON COLUMN auth.password_reset_tokens.created_at IS 'Timestamp when token was created';
|
||||||
74
ddl/schemas/auth/tables/08-auth_logs.sql
Normal file
74
ddl/schemas/auth/tables/08-auth_logs.sql
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/08-auth_logs.sql
|
||||||
|
-- Description: Authentication event audit logging with optional partitioning
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.auth_logs (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Event Information
|
||||||
|
event_type auth.auth_event_type NOT NULL,
|
||||||
|
user_id UUID,
|
||||||
|
email CITEXT,
|
||||||
|
|
||||||
|
-- Request Context
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
session_id UUID,
|
||||||
|
|
||||||
|
-- Event Details
|
||||||
|
success BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
failure_reason VARCHAR(255),
|
||||||
|
|
||||||
|
-- Additional Metadata
|
||||||
|
metadata JSONB DEFAULT '{}'::jsonb,
|
||||||
|
|
||||||
|
-- Timestamp (partition key)
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Primary Key includes partition key for partitioned tables
|
||||||
|
PRIMARY KEY (id, created_at)
|
||||||
|
) PARTITION BY RANGE (created_at);
|
||||||
|
|
||||||
|
-- Create initial partitions for current and next month
|
||||||
|
-- These should be created dynamically by a maintenance job in production
|
||||||
|
|
||||||
|
-- Current month partition
|
||||||
|
CREATE TABLE auth.auth_logs_current PARTITION OF auth.auth_logs
|
||||||
|
FOR VALUES FROM (DATE_TRUNC('month', CURRENT_DATE))
|
||||||
|
TO (DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month'));
|
||||||
|
|
||||||
|
-- Next month partition
|
||||||
|
CREATE TABLE auth.auth_logs_next PARTITION OF auth.auth_logs
|
||||||
|
FOR VALUES FROM (DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month'))
|
||||||
|
TO (DATE_TRUNC('month', CURRENT_DATE + INTERVAL '2 months'));
|
||||||
|
|
||||||
|
-- Indexes for Performance (will be inherited by partitions)
|
||||||
|
CREATE INDEX idx_auth_logs_user_id ON auth.auth_logs(user_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_auth_logs_email ON auth.auth_logs(email, created_at DESC);
|
||||||
|
CREATE INDEX idx_auth_logs_event_type ON auth.auth_logs(event_type, created_at DESC);
|
||||||
|
CREATE INDEX idx_auth_logs_ip_address ON auth.auth_logs(ip_address, created_at DESC);
|
||||||
|
CREATE INDEX idx_auth_logs_session_id ON auth.auth_logs(session_id);
|
||||||
|
CREATE INDEX idx_auth_logs_created_at ON auth.auth_logs(created_at DESC);
|
||||||
|
CREATE INDEX idx_auth_logs_failures ON auth.auth_logs(user_id, created_at DESC)
|
||||||
|
WHERE success = false;
|
||||||
|
CREATE INDEX idx_auth_logs_metadata ON auth.auth_logs USING gin(metadata);
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.auth_logs IS 'Authentication event audit logging with monthly partitioning for performance';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.id IS 'Unique identifier for the log entry';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.event_type IS 'Type of authentication event';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.user_id IS 'Reference to the user (null for failed logins)';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.email IS 'Email address associated with the event';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.ip_address IS 'IP address of the request';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.user_agent IS 'User agent string from the request';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.session_id IS 'Session ID if applicable';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.success IS 'Whether the event was successful';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.failure_reason IS 'Reason for failure if applicable';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.metadata IS 'Additional event metadata as JSON';
|
||||||
|
COMMENT ON COLUMN auth.auth_logs.created_at IS 'Timestamp when event occurred (partition key)';
|
||||||
67
ddl/schemas/auth/tables/09-login_attempts.sql
Normal file
67
ddl/schemas/auth/tables/09-login_attempts.sql
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/09-login_attempts.sql
|
||||||
|
-- Description: Login attempt tracking for rate limiting and security monitoring
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.login_attempts (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Attempt Information
|
||||||
|
email CITEXT,
|
||||||
|
user_id UUID,
|
||||||
|
|
||||||
|
-- Request Context
|
||||||
|
ip_address INET NOT NULL,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Attempt Result
|
||||||
|
success BOOLEAN NOT NULL,
|
||||||
|
failure_reason VARCHAR(100),
|
||||||
|
|
||||||
|
-- Additional Details
|
||||||
|
attempted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}'::jsonb,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Check Constraints
|
||||||
|
CONSTRAINT login_attempt_has_identifier CHECK (
|
||||||
|
email IS NOT NULL OR user_id IS NOT NULL
|
||||||
|
),
|
||||||
|
CONSTRAINT failure_reason_consistency CHECK (
|
||||||
|
(success = false AND failure_reason IS NOT NULL) OR
|
||||||
|
(success = true AND failure_reason IS NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_login_attempts_email ON auth.login_attempts(email, created_at DESC);
|
||||||
|
CREATE INDEX idx_login_attempts_user_id ON auth.login_attempts(user_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_login_attempts_ip ON auth.login_attempts(ip_address, created_at DESC);
|
||||||
|
CREATE INDEX idx_login_attempts_created ON auth.login_attempts(created_at DESC);
|
||||||
|
CREATE INDEX idx_login_attempts_failures ON auth.login_attempts(email, ip_address, created_at DESC)
|
||||||
|
WHERE success = false;
|
||||||
|
CREATE INDEX idx_login_attempts_success ON auth.login_attempts(email, created_at DESC)
|
||||||
|
WHERE success = true;
|
||||||
|
CREATE INDEX idx_login_attempts_metadata ON auth.login_attempts USING gin(metadata);
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.login_attempts IS 'Login attempt tracking for rate limiting, brute force detection, and security monitoring';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.id IS 'Unique identifier for the login attempt';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.email IS 'Email address used in login attempt';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.user_id IS 'User ID if resolved from email';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.ip_address IS 'IP address of the login attempt';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.user_agent IS 'User agent string from the request';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.success IS 'Whether the login attempt was successful';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.failure_reason IS 'Reason for login failure (invalid_password, account_locked, etc.)';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.attempted_at IS 'Timestamp when login was attempted';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.metadata IS 'Additional attempt metadata as JSON';
|
||||||
|
COMMENT ON COLUMN auth.login_attempts.created_at IS 'Timestamp when record was created';
|
||||||
82
ddl/schemas/auth/tables/10-rate_limiting_config.sql
Normal file
82
ddl/schemas/auth/tables/10-rate_limiting_config.sql
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/10-rate_limiting_config.sql
|
||||||
|
-- Description: Rate limiting configuration for API endpoints and auth operations
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE auth.rate_limiting_config (
|
||||||
|
-- Primary Key
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Endpoint Configuration
|
||||||
|
endpoint VARCHAR(200) NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Rate Limiting Parameters
|
||||||
|
max_requests INTEGER NOT NULL DEFAULT 100,
|
||||||
|
window_seconds INTEGER NOT NULL DEFAULT 60,
|
||||||
|
block_duration_seconds INTEGER DEFAULT 300,
|
||||||
|
|
||||||
|
-- Scope Configuration
|
||||||
|
scope VARCHAR(50) NOT NULL DEFAULT 'ip',
|
||||||
|
|
||||||
|
-- Status
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}'::jsonb,
|
||||||
|
|
||||||
|
-- Audit Fields
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
created_by_id UUID,
|
||||||
|
updated_by_id UUID,
|
||||||
|
|
||||||
|
-- Check Constraints
|
||||||
|
CONSTRAINT valid_rate_limits CHECK (
|
||||||
|
max_requests > 0 AND
|
||||||
|
window_seconds > 0 AND
|
||||||
|
(block_duration_seconds IS NULL OR block_duration_seconds > 0)
|
||||||
|
),
|
||||||
|
CONSTRAINT valid_scope CHECK (
|
||||||
|
scope IN ('ip', 'user', 'email', 'global')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for Performance
|
||||||
|
CREATE INDEX idx_rate_limiting_endpoint ON auth.rate_limiting_config(endpoint);
|
||||||
|
CREATE INDEX idx_rate_limiting_active ON auth.rate_limiting_config(is_active) WHERE is_active = true;
|
||||||
|
CREATE INDEX idx_rate_limiting_scope ON auth.rate_limiting_config(scope);
|
||||||
|
CREATE INDEX idx_rate_limiting_metadata ON auth.rate_limiting_config USING gin(metadata);
|
||||||
|
|
||||||
|
-- Insert Default Rate Limiting Rules
|
||||||
|
INSERT INTO auth.rate_limiting_config (endpoint, description, max_requests, window_seconds, block_duration_seconds, scope) VALUES
|
||||||
|
('/auth/login', 'Login endpoint rate limit', 5, 300, 900, 'ip'),
|
||||||
|
('/auth/register', 'Registration endpoint rate limit', 3, 3600, 1800, 'ip'),
|
||||||
|
('/auth/password-reset/request', 'Password reset request limit', 3, 3600, 1800, 'email'),
|
||||||
|
('/auth/password-reset/verify', 'Password reset verification limit', 5, 300, 900, 'ip'),
|
||||||
|
('/auth/verify-email', 'Email verification limit', 10, 3600, 1800, 'user'),
|
||||||
|
('/auth/verify-phone', 'Phone verification limit', 5, 3600, 1800, 'user'),
|
||||||
|
('/auth/refresh-token', 'Token refresh limit', 20, 300, 600, 'user'),
|
||||||
|
('/auth/logout', 'Logout endpoint limit', 10, 60, NULL, 'user'),
|
||||||
|
('/auth/mfa/enable', 'MFA enable limit', 5, 3600, NULL, 'user'),
|
||||||
|
('/auth/mfa/verify', 'MFA verification limit', 5, 300, 900, 'user');
|
||||||
|
|
||||||
|
-- Table Comments
|
||||||
|
COMMENT ON TABLE auth.rate_limiting_config IS 'Rate limiting configuration for API endpoints to prevent abuse and brute force attacks';
|
||||||
|
|
||||||
|
-- Column Comments
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.id IS 'Unique identifier for the configuration';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.endpoint IS 'API endpoint path to rate limit';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.description IS 'Description of the rate limit purpose';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.max_requests IS 'Maximum requests allowed within the time window';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.window_seconds IS 'Time window in seconds for rate limiting';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.block_duration_seconds IS 'Duration to block after exceeding limit (null for no block)';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.scope IS 'Scope of rate limit (ip, user, email, global)';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.is_active IS 'Whether this rate limit is currently active';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.metadata IS 'Additional configuration metadata as JSON';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.created_at IS 'Timestamp when configuration was created';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.updated_at IS 'Timestamp when configuration was last updated';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.created_by_id IS 'ID of user who created this configuration';
|
||||||
|
COMMENT ON COLUMN auth.rate_limiting_config.updated_by_id IS 'ID of user who last updated this configuration';
|
||||||
34
ddl/schemas/auth/tables/99-deferred-constraints.sql
Normal file
34
ddl/schemas/auth/tables/99-deferred-constraints.sql
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: auth
|
||||||
|
-- File: tables/99-deferred-constraints.sql
|
||||||
|
-- Description: Notes on deferred constraints
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- NOTE: The password_or_oauth constraint has been removed because PostgreSQL
|
||||||
|
-- does not support subqueries in CHECK constraints.
|
||||||
|
--
|
||||||
|
-- Original constraint intent:
|
||||||
|
-- Ensure user has either a password OR an OAuth account for authentication
|
||||||
|
--
|
||||||
|
-- This validation should be implemented at the application level:
|
||||||
|
-- - Backend: Validate in auth.service.ts during user creation/update
|
||||||
|
-- - Database: Consider using a TRIGGER if strict enforcement is required
|
||||||
|
--
|
||||||
|
-- Alternative: Create a trigger function
|
||||||
|
-- CREATE OR REPLACE FUNCTION auth.check_password_or_oauth()
|
||||||
|
-- RETURNS TRIGGER AS $$
|
||||||
|
-- BEGIN
|
||||||
|
-- IF NEW.password_hash IS NULL THEN
|
||||||
|
-- IF NOT EXISTS (SELECT 1 FROM auth.oauth_accounts WHERE user_id = NEW.id) THEN
|
||||||
|
-- RAISE EXCEPTION 'User must have either password or OAuth account';
|
||||||
|
-- END IF;
|
||||||
|
-- END IF;
|
||||||
|
-- RETURN NEW;
|
||||||
|
-- END;
|
||||||
|
-- $$ LANGUAGE plpgsql;
|
||||||
|
--
|
||||||
|
-- CREATE TRIGGER trg_check_password_or_oauth
|
||||||
|
-- AFTER INSERT OR UPDATE ON auth.users
|
||||||
|
-- FOR EACH ROW
|
||||||
|
-- EXECUTE FUNCTION auth.check_password_or_oauth();
|
||||||
64
ddl/schemas/education/00-enums.sql
Normal file
64
ddl/schemas/education/00-enums.sql
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ENUMS - Schema Education
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- PostgreSQL: 15+
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Nivel de dificultad
|
||||||
|
CREATE TYPE education.difficulty_level AS ENUM (
|
||||||
|
'beginner',
|
||||||
|
'intermediate',
|
||||||
|
'advanced',
|
||||||
|
'expert'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado de curso
|
||||||
|
CREATE TYPE education.course_status AS ENUM (
|
||||||
|
'draft',
|
||||||
|
'published',
|
||||||
|
'archived'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado de enrollment
|
||||||
|
CREATE TYPE education.enrollment_status AS ENUM (
|
||||||
|
'active',
|
||||||
|
'completed',
|
||||||
|
'expired',
|
||||||
|
'cancelled'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de contenido de lección
|
||||||
|
CREATE TYPE education.lesson_content_type AS ENUM (
|
||||||
|
'video',
|
||||||
|
'article',
|
||||||
|
'interactive',
|
||||||
|
'quiz'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de pregunta de quiz
|
||||||
|
CREATE TYPE education.question_type AS ENUM (
|
||||||
|
'multiple_choice',
|
||||||
|
'true_false',
|
||||||
|
'multiple_select',
|
||||||
|
'fill_blank',
|
||||||
|
'code_challenge'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de logro/badge
|
||||||
|
CREATE TYPE education.achievement_type AS ENUM (
|
||||||
|
'course_completion',
|
||||||
|
'quiz_perfect_score',
|
||||||
|
'streak_milestone',
|
||||||
|
'level_up',
|
||||||
|
'special_event'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE education.difficulty_level IS 'Nivel de dificultad de cursos';
|
||||||
|
COMMENT ON TYPE education.course_status IS 'Estado del curso (draft, published, archived)';
|
||||||
|
COMMENT ON TYPE education.enrollment_status IS 'Estado de la inscripción del usuario';
|
||||||
|
COMMENT ON TYPE education.lesson_content_type IS 'Tipo de contenido de la lección';
|
||||||
|
COMMENT ON TYPE education.question_type IS 'Tipo de pregunta en quizzes';
|
||||||
|
COMMENT ON TYPE education.achievement_type IS 'Tipo de logro/badge';
|
||||||
353
ddl/schemas/education/README.md
Normal file
353
ddl/schemas/education/README.md
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
# Schema: education
|
||||||
|
|
||||||
|
**Proyecto:** Trading Platform (Trading Platform)
|
||||||
|
**Módulo:** OQI-002 - Education
|
||||||
|
**Especificación:** ET-EDU-001-database.md
|
||||||
|
**PostgreSQL:** 15+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Schema completo para el módulo educativo de Trading Platform, implementando:
|
||||||
|
- Gestión de cursos, módulos y lecciones
|
||||||
|
- Sistema de enrollments y progreso de estudiantes
|
||||||
|
- Quizzes y evaluaciones
|
||||||
|
- Certificados de finalización
|
||||||
|
- Sistema de gamificación (XP, niveles, streaks, achievements)
|
||||||
|
- Reviews de cursos
|
||||||
|
- Activity logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estructura de Archivos
|
||||||
|
|
||||||
|
```
|
||||||
|
education/
|
||||||
|
├── 00-enums.sql # Tipos ENUM
|
||||||
|
├── tables/
|
||||||
|
│ ├── 01-categories.sql # Categorías de cursos
|
||||||
|
│ ├── 02-courses.sql # Cursos
|
||||||
|
│ ├── 03-modules.sql # Módulos del curso
|
||||||
|
│ ├── 04-lessons.sql # Lecciones
|
||||||
|
│ ├── 05-enrollments.sql # Inscripciones de usuarios
|
||||||
|
│ ├── 06-progress.sql # Progreso en lecciones
|
||||||
|
│ ├── 07-quizzes.sql # Quizzes/evaluaciones
|
||||||
|
│ ├── 08-quiz_questions.sql # Preguntas de quiz
|
||||||
|
│ ├── 09-quiz_attempts.sql # Intentos de quiz
|
||||||
|
│ ├── 10-certificates.sql # Certificados
|
||||||
|
│ ├── 11-user_achievements.sql # Logros/badges
|
||||||
|
│ ├── 12-user_gamification_profile.sql # Perfil de gamificación
|
||||||
|
│ ├── 13-user_activity_log.sql # Log de actividades
|
||||||
|
│ └── 14-course_reviews.sql # Reviews de cursos
|
||||||
|
└── functions/
|
||||||
|
├── 01-update_updated_at.sql # Trigger updated_at
|
||||||
|
├── 02-update_enrollment_progress.sql # Actualizar progreso
|
||||||
|
├── 03-auto_complete_enrollment.sql # Auto-completar enrollment
|
||||||
|
├── 04-generate_certificate.sql # Generar certificados
|
||||||
|
├── 05-update_course_stats.sql # Actualizar estadísticas
|
||||||
|
├── 06-update_enrollment_count.sql # Contador de enrollments
|
||||||
|
├── 07-update_gamification_profile.sql # Sistema de gamificación
|
||||||
|
└── 08-views.sql # Vistas útiles
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Orden de Ejecución
|
||||||
|
|
||||||
|
Para crear el schema completo, ejecutar en este orden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. ENUMs
|
||||||
|
psql -f 00-enums.sql
|
||||||
|
|
||||||
|
# 2. Tablas (en orden de dependencias)
|
||||||
|
psql -f tables/01-categories.sql
|
||||||
|
psql -f tables/02-courses.sql
|
||||||
|
psql -f tables/03-modules.sql
|
||||||
|
psql -f tables/04-lessons.sql
|
||||||
|
psql -f tables/05-enrollments.sql
|
||||||
|
psql -f tables/06-progress.sql
|
||||||
|
psql -f tables/07-quizzes.sql
|
||||||
|
psql -f tables/08-quiz_questions.sql
|
||||||
|
psql -f tables/09-quiz_attempts.sql
|
||||||
|
psql -f tables/10-certificates.sql
|
||||||
|
psql -f tables/11-user_achievements.sql
|
||||||
|
psql -f tables/12-user_gamification_profile.sql
|
||||||
|
psql -f tables/13-user_activity_log.sql
|
||||||
|
psql -f tables/14-course_reviews.sql
|
||||||
|
|
||||||
|
# 3. Funciones y Triggers
|
||||||
|
psql -f functions/01-update_updated_at.sql
|
||||||
|
psql -f functions/02-update_enrollment_progress.sql
|
||||||
|
psql -f functions/03-auto_complete_enrollment.sql
|
||||||
|
psql -f functions/04-generate_certificate.sql
|
||||||
|
psql -f functions/05-update_course_stats.sql
|
||||||
|
psql -f functions/06-update_enrollment_count.sql
|
||||||
|
psql -f functions/07-update_gamification_profile.sql
|
||||||
|
psql -f functions/08-views.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tablas
|
||||||
|
|
||||||
|
### Principales
|
||||||
|
|
||||||
|
| Tabla | Descripción |
|
||||||
|
|-------|-------------|
|
||||||
|
| `categories` | Categorías de cursos con soporte para jerarquía |
|
||||||
|
| `courses` | Cursos del módulo educativo |
|
||||||
|
| `modules` | Módulos que agrupan lecciones |
|
||||||
|
| `lessons` | Lecciones individuales (video, artículo, interactivo) |
|
||||||
|
| `enrollments` | Inscripciones de usuarios a cursos |
|
||||||
|
| `progress` | Progreso del usuario en cada lección |
|
||||||
|
|
||||||
|
### Evaluación
|
||||||
|
|
||||||
|
| Tabla | Descripción |
|
||||||
|
|-------|-------------|
|
||||||
|
| `quizzes` | Quizzes/evaluaciones |
|
||||||
|
| `quiz_questions` | Preguntas de los quizzes |
|
||||||
|
| `quiz_attempts` | Intentos de usuarios en quizzes |
|
||||||
|
|
||||||
|
### Logros
|
||||||
|
|
||||||
|
| Tabla | Descripción |
|
||||||
|
|-------|-------------|
|
||||||
|
| `certificates` | Certificados de finalización |
|
||||||
|
| `user_achievements` | Logros/badges obtenidos |
|
||||||
|
|
||||||
|
### Gamificación
|
||||||
|
|
||||||
|
| Tabla | Descripción |
|
||||||
|
|-------|-------------|
|
||||||
|
| `user_gamification_profile` | XP, niveles, streaks, estadísticas |
|
||||||
|
| `user_activity_log` | Log de todas las actividades |
|
||||||
|
| `course_reviews` | Reviews y calificaciones de cursos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ENUMs
|
||||||
|
|
||||||
|
- `difficulty_level`: beginner, intermediate, advanced, expert
|
||||||
|
- `course_status`: draft, published, archived
|
||||||
|
- `enrollment_status`: active, completed, expired, cancelled
|
||||||
|
- `lesson_content_type`: video, article, interactive, quiz
|
||||||
|
- `question_type`: multiple_choice, true_false, multiple_select, fill_blank, code_challenge
|
||||||
|
- `achievement_type`: course_completion, quiz_perfect_score, streak_milestone, level_up, special_event
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funciones Principales
|
||||||
|
|
||||||
|
### Gamificación
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Actualizar XP del usuario
|
||||||
|
SELECT education.update_user_xp(
|
||||||
|
'user-uuid', -- user_id
|
||||||
|
100 -- xp_to_add
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Actualizar streak del usuario
|
||||||
|
SELECT education.update_user_streak('user-uuid');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Triggers Automáticos
|
||||||
|
|
||||||
|
- `updated_at`: Se actualiza automáticamente en todas las tablas
|
||||||
|
- `update_enrollment_progress()`: Calcula progreso al completar lecciones
|
||||||
|
- `auto_complete_enrollment()`: Completa enrollment al alcanzar 100%
|
||||||
|
- `generate_certificate_number()`: Genera número único de certificado
|
||||||
|
- `update_course_rating_stats()`: Actualiza rating promedio del curso
|
||||||
|
- `update_enrollment_count()`: Actualiza contador de enrollments
|
||||||
|
- `update_streak_on_activity()`: Actualiza streak en cada actividad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vistas
|
||||||
|
|
||||||
|
| Vista | Descripción |
|
||||||
|
|-------|-------------|
|
||||||
|
| `v_courses_with_stats` | Cursos con estadísticas agregadas |
|
||||||
|
| `v_user_course_progress` | Progreso del usuario por curso |
|
||||||
|
| `v_leaderboard_weekly` | Top 100 usuarios por XP semanal |
|
||||||
|
| `v_leaderboard_monthly` | Top 100 usuarios por XP mensual |
|
||||||
|
| `v_leaderboard_alltime` | Top 100 usuarios por XP total |
|
||||||
|
| `v_user_statistics` | Estadísticas completas del usuario |
|
||||||
|
| `v_popular_courses` | Top 50 cursos más populares |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencias
|
||||||
|
|
||||||
|
### Schemas externos
|
||||||
|
- `auth.users` - Tabla de usuarios (requerida)
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
- `gen_random_uuid()` - Built-in en PostgreSQL 13+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Políticas de Seguridad (RLS)
|
||||||
|
|
||||||
|
Para habilitar Row Level Security (implementar según necesidad):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Habilitar RLS
|
||||||
|
ALTER TABLE education.enrollments ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE education.progress ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE education.quiz_attempts ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE education.certificates ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Política: usuarios solo ven sus propios datos
|
||||||
|
CREATE POLICY user_own_data ON education.enrollments
|
||||||
|
FOR ALL
|
||||||
|
USING (user_id = current_setting('app.user_id')::UUID);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplos de Uso
|
||||||
|
|
||||||
|
### Enrollar usuario a un curso
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO education.enrollments (user_id, course_id)
|
||||||
|
VALUES ('user-uuid', 'course-uuid')
|
||||||
|
RETURNING *;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registrar progreso en lección
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO education.progress (
|
||||||
|
user_id,
|
||||||
|
lesson_id,
|
||||||
|
enrollment_id,
|
||||||
|
is_completed,
|
||||||
|
watch_percentage
|
||||||
|
) VALUES (
|
||||||
|
'user-uuid',
|
||||||
|
'lesson-uuid',
|
||||||
|
'enrollment-uuid',
|
||||||
|
true,
|
||||||
|
100.00
|
||||||
|
);
|
||||||
|
-- Esto automáticamente actualizará el enrollment progress
|
||||||
|
```
|
||||||
|
|
||||||
|
### Completar quiz
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO education.quiz_attempts (
|
||||||
|
user_id,
|
||||||
|
quiz_id,
|
||||||
|
enrollment_id,
|
||||||
|
is_completed,
|
||||||
|
is_passed,
|
||||||
|
user_answers,
|
||||||
|
score_percentage,
|
||||||
|
xp_earned
|
||||||
|
) VALUES (
|
||||||
|
'user-uuid',
|
||||||
|
'quiz-uuid',
|
||||||
|
'enrollment-uuid',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
'[{"questionId": "q1", "answer": "A", "isCorrect": true}]'::jsonb,
|
||||||
|
85.00,
|
||||||
|
50
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Emitir certificado
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO education.certificates (
|
||||||
|
user_id,
|
||||||
|
course_id,
|
||||||
|
enrollment_id,
|
||||||
|
user_name,
|
||||||
|
course_title,
|
||||||
|
completion_date,
|
||||||
|
final_score
|
||||||
|
) VALUES (
|
||||||
|
'user-uuid',
|
||||||
|
'course-uuid',
|
||||||
|
'enrollment-uuid',
|
||||||
|
'John Doe',
|
||||||
|
'Introducción al Trading',
|
||||||
|
CURRENT_DATE,
|
||||||
|
92.50
|
||||||
|
);
|
||||||
|
-- El número de certificado y código de verificación se generan automáticamente
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agregar review a curso
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO education.course_reviews (
|
||||||
|
user_id,
|
||||||
|
course_id,
|
||||||
|
enrollment_id,
|
||||||
|
rating,
|
||||||
|
title,
|
||||||
|
content
|
||||||
|
) VALUES (
|
||||||
|
'user-uuid',
|
||||||
|
'course-uuid',
|
||||||
|
'enrollment-uuid',
|
||||||
|
5,
|
||||||
|
'Excelente curso',
|
||||||
|
'Muy bien explicado y con ejemplos prácticos'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas Importantes
|
||||||
|
|
||||||
|
1. **Referencias**: Todas las FKs a usuarios usan `auth.users(id)`
|
||||||
|
2. **Cascadas**: Las eliminaciones en CASCADE están definidas donde corresponde
|
||||||
|
3. **Índices**: Creados para optimizar queries frecuentes
|
||||||
|
4. **Constraints**: Validaciones de lógica de negocio implementadas
|
||||||
|
5. **JSONB**: Usado para metadata flexible (attachments, user_answers, etc.)
|
||||||
|
6. **Denormalización**: Algunas estadísticas están denormalizadas para performance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mantenimiento
|
||||||
|
|
||||||
|
### Resetear XP semanal/mensual
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Resetear XP semanal (ejecutar cada lunes)
|
||||||
|
UPDATE education.user_gamification_profile SET weekly_xp = 0;
|
||||||
|
|
||||||
|
-- Resetear XP mensual (ejecutar el 1ro de cada mes)
|
||||||
|
UPDATE education.user_gamification_profile SET monthly_xp = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recalcular estadísticas de curso
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recalcular total de módulos y lecciones
|
||||||
|
UPDATE education.courses c
|
||||||
|
SET
|
||||||
|
total_modules = (SELECT COUNT(*) FROM education.modules WHERE course_id = c.id),
|
||||||
|
total_lessons = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM education.lessons l
|
||||||
|
JOIN education.modules m ON l.module_id = m.id
|
||||||
|
WHERE m.course_id = c.id
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versión
|
||||||
|
|
||||||
|
**Versión:** 1.0.0
|
||||||
|
**Última actualización:** 2025-12-06
|
||||||
458
ddl/schemas/education/TECHNICAL.md
Normal file
458
ddl/schemas/education/TECHNICAL.md
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
# Documentación Técnica - Schema Education
|
||||||
|
|
||||||
|
**Proyecto:** Trading Platform (Trading Platform)
|
||||||
|
**Schema:** education
|
||||||
|
**PostgreSQL:** 15+
|
||||||
|
**Versión:** 1.0.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estadísticas del Schema
|
||||||
|
|
||||||
|
- **ENUMs:** 6 tipos
|
||||||
|
- **Tablas:** 14 tablas
|
||||||
|
- **Funciones:** 8 funciones
|
||||||
|
- **Triggers:** 15+ triggers
|
||||||
|
- **Vistas:** 7 vistas
|
||||||
|
- **Índices:** 60+ índices
|
||||||
|
- **Total líneas SQL:** ~1,350 líneas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Índices por Tabla
|
||||||
|
|
||||||
|
### categories
|
||||||
|
- `idx_categories_parent` - parent_id
|
||||||
|
- `idx_categories_slug` - slug
|
||||||
|
- `idx_categories_active` - is_active (WHERE is_active = true)
|
||||||
|
|
||||||
|
### courses
|
||||||
|
- `idx_courses_category` - category_id
|
||||||
|
- `idx_courses_slug` - slug
|
||||||
|
- `idx_courses_status` - status
|
||||||
|
- `idx_courses_difficulty` - difficulty_level
|
||||||
|
- `idx_courses_instructor` - instructor_id
|
||||||
|
- `idx_courses_published` - published_at (WHERE status = 'published')
|
||||||
|
|
||||||
|
### modules
|
||||||
|
- `idx_modules_course` - course_id
|
||||||
|
- `idx_modules_order` - course_id, display_order
|
||||||
|
|
||||||
|
### lessons
|
||||||
|
- `idx_lessons_module` - module_id
|
||||||
|
- `idx_lessons_order` - module_id, display_order
|
||||||
|
- `idx_lessons_type` - content_type
|
||||||
|
- `idx_lessons_preview` - is_preview (WHERE is_preview = true)
|
||||||
|
|
||||||
|
### enrollments
|
||||||
|
- `idx_enrollments_user` - user_id
|
||||||
|
- `idx_enrollments_course` - course_id
|
||||||
|
- `idx_enrollments_status` - status
|
||||||
|
- `idx_enrollments_user_active` - user_id, status (WHERE status = 'active')
|
||||||
|
|
||||||
|
### progress
|
||||||
|
- `idx_progress_user` - user_id
|
||||||
|
- `idx_progress_lesson` - lesson_id
|
||||||
|
- `idx_progress_enrollment` - enrollment_id
|
||||||
|
- `idx_progress_completed` - is_completed (WHERE is_completed = true)
|
||||||
|
- `idx_progress_user_enrollment` - user_id, enrollment_id
|
||||||
|
|
||||||
|
### quizzes
|
||||||
|
- `idx_quizzes_module` - module_id
|
||||||
|
- `idx_quizzes_lesson` - lesson_id
|
||||||
|
- `idx_quizzes_active` - is_active (WHERE is_active = true)
|
||||||
|
|
||||||
|
### quiz_questions
|
||||||
|
- `idx_quiz_questions_quiz` - quiz_id
|
||||||
|
- `idx_quiz_questions_order` - quiz_id, display_order
|
||||||
|
|
||||||
|
### quiz_attempts
|
||||||
|
- `idx_quiz_attempts_user` - user_id
|
||||||
|
- `idx_quiz_attempts_quiz` - quiz_id
|
||||||
|
- `idx_quiz_attempts_enrollment` - enrollment_id
|
||||||
|
- `idx_quiz_attempts_user_quiz` - user_id, quiz_id
|
||||||
|
- `idx_quiz_attempts_completed` - is_completed, completed_at
|
||||||
|
|
||||||
|
### certificates
|
||||||
|
- `idx_certificates_user` - user_id
|
||||||
|
- `idx_certificates_course` - course_id
|
||||||
|
- `idx_certificates_number` - certificate_number
|
||||||
|
- `idx_certificates_verification` - verification_code
|
||||||
|
|
||||||
|
### user_achievements
|
||||||
|
- `idx_user_achievements_user` - user_id
|
||||||
|
- `idx_user_achievements_type` - achievement_type
|
||||||
|
- `idx_user_achievements_earned` - earned_at DESC
|
||||||
|
- `idx_user_achievements_course` - course_id
|
||||||
|
|
||||||
|
### user_gamification_profile
|
||||||
|
- `idx_gamification_user` - user_id
|
||||||
|
- `idx_gamification_level` - current_level DESC
|
||||||
|
- `idx_gamification_xp` - total_xp DESC
|
||||||
|
- `idx_gamification_weekly` - weekly_xp DESC
|
||||||
|
- `idx_gamification_monthly` - monthly_xp DESC
|
||||||
|
|
||||||
|
### user_activity_log
|
||||||
|
- `idx_activity_user` - user_id
|
||||||
|
- `idx_activity_type` - activity_type
|
||||||
|
- `idx_activity_created` - created_at DESC
|
||||||
|
- `idx_activity_user_date` - user_id, created_at DESC
|
||||||
|
- `idx_activity_course` - course_id (WHERE course_id IS NOT NULL)
|
||||||
|
|
||||||
|
### course_reviews
|
||||||
|
- `idx_reviews_course` - course_id
|
||||||
|
- `idx_reviews_user` - user_id
|
||||||
|
- `idx_reviews_rating` - rating
|
||||||
|
- `idx_reviews_approved` - is_approved (WHERE is_approved = true)
|
||||||
|
- `idx_reviews_featured` - is_featured (WHERE is_featured = true)
|
||||||
|
- `idx_reviews_helpful` - helpful_votes DESC
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
### CHECK Constraints
|
||||||
|
|
||||||
|
**categories:**
|
||||||
|
- `valid_color_format` - Color debe ser formato #RRGGBB
|
||||||
|
|
||||||
|
**courses:**
|
||||||
|
- `valid_rating` - avg_rating >= 0 AND <= 5
|
||||||
|
- `valid_price` - price_usd >= 0
|
||||||
|
|
||||||
|
**lessons:**
|
||||||
|
- `video_fields_required` - Si content_type='video', video_url y video_duration_seconds requeridos
|
||||||
|
|
||||||
|
**enrollments:**
|
||||||
|
- `valid_progress` - progress_percentage >= 0 AND <= 100
|
||||||
|
- `valid_completion` - Si status='completed', completed_at y progress=100 requeridos
|
||||||
|
|
||||||
|
**progress:**
|
||||||
|
- `valid_watch_percentage` - watch_percentage >= 0 AND <= 100
|
||||||
|
- `completion_requires_date` - Si is_completed=true, completed_at requerido
|
||||||
|
|
||||||
|
**quizzes:**
|
||||||
|
- `valid_passing_score` - passing_score_percentage > 0 AND <= 100
|
||||||
|
- `quiz_association` - Debe tener module_id O lesson_id (no ambos)
|
||||||
|
|
||||||
|
**quiz_questions:**
|
||||||
|
- `valid_options` - Si question_type requiere options, options no puede ser NULL
|
||||||
|
|
||||||
|
**quiz_attempts:**
|
||||||
|
- `valid_score_percentage` - score_percentage >= 0 AND <= 100
|
||||||
|
|
||||||
|
**user_gamification_profile:**
|
||||||
|
- `valid_level` - current_level >= 1
|
||||||
|
- `valid_xp` - total_xp >= 0
|
||||||
|
- `valid_streak` - current_streak_days >= 0 AND longest_streak_days >= 0
|
||||||
|
- `valid_avg_score` - average_quiz_score >= 0 AND <= 100
|
||||||
|
|
||||||
|
**course_reviews:**
|
||||||
|
- `rating` - rating >= 1 AND <= 5
|
||||||
|
|
||||||
|
### UNIQUE Constraints
|
||||||
|
|
||||||
|
- `categories.slug` - UNIQUE
|
||||||
|
- `courses.slug` - UNIQUE
|
||||||
|
- `modules.unique_course_order` - UNIQUE(course_id, display_order)
|
||||||
|
- `lessons.unique_module_order` - UNIQUE(module_id, display_order)
|
||||||
|
- `enrollments.unique_user_course` - UNIQUE(user_id, course_id)
|
||||||
|
- `progress.unique_user_lesson` - UNIQUE(user_id, lesson_id)
|
||||||
|
- `certificates.certificate_number` - UNIQUE
|
||||||
|
- `certificates.verification_code` - UNIQUE
|
||||||
|
- `certificates.unique_user_course_cert` - UNIQUE(user_id, course_id)
|
||||||
|
- `user_gamification_profile.unique_user_gamification` - UNIQUE(user_id)
|
||||||
|
- `course_reviews.unique_user_course_review` - UNIQUE(user_id, course_id)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Foreign Keys
|
||||||
|
|
||||||
|
### Relaciones con auth.users
|
||||||
|
- `courses.instructor_id` → `auth.users(id)` ON DELETE RESTRICT
|
||||||
|
- `enrollments.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `progress.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `quiz_attempts.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `certificates.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `user_achievements.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `user_gamification_profile.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `user_activity_log.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `course_reviews.user_id` → `auth.users(id)` ON DELETE CASCADE
|
||||||
|
- `course_reviews.approved_by` → `auth.users(id)`
|
||||||
|
|
||||||
|
### Relaciones internas
|
||||||
|
- `categories.parent_id` → `categories(id)` ON DELETE SET NULL
|
||||||
|
- `courses.category_id` → `categories(id)` ON DELETE RESTRICT
|
||||||
|
- `modules.course_id` → `courses(id)` ON DELETE CASCADE
|
||||||
|
- `modules.unlock_after_module_id` → `modules(id)` ON DELETE SET NULL
|
||||||
|
- `lessons.module_id` → `modules(id)` ON DELETE CASCADE
|
||||||
|
- `enrollments.course_id` → `courses(id)` ON DELETE RESTRICT
|
||||||
|
- `progress.lesson_id` → `lessons(id)` ON DELETE CASCADE
|
||||||
|
- `progress.enrollment_id` → `enrollments(id)` ON DELETE CASCADE
|
||||||
|
- `quizzes.module_id` → `modules(id)` ON DELETE CASCADE
|
||||||
|
- `quizzes.lesson_id` → `lessons(id)` ON DELETE CASCADE
|
||||||
|
- `quiz_questions.quiz_id` → `quizzes(id)` ON DELETE CASCADE
|
||||||
|
- `quiz_attempts.quiz_id` → `quizzes(id)` ON DELETE RESTRICT
|
||||||
|
- `quiz_attempts.enrollment_id` → `enrollments(id)` ON DELETE SET NULL
|
||||||
|
- `certificates.course_id` → `courses(id)` ON DELETE RESTRICT
|
||||||
|
- `certificates.enrollment_id` → `enrollments(id)` ON DELETE RESTRICT
|
||||||
|
- `user_achievements.course_id` → `courses(id)` ON DELETE SET NULL
|
||||||
|
- `user_achievements.quiz_id` → `quizzes(id)` ON DELETE SET NULL
|
||||||
|
- `user_activity_log.course_id` → `courses(id)` ON DELETE SET NULL
|
||||||
|
- `user_activity_log.lesson_id` → `lessons(id)` ON DELETE SET NULL
|
||||||
|
- `user_activity_log.quiz_id` → `quizzes(id)` ON DELETE SET NULL
|
||||||
|
- `course_reviews.course_id` → `courses(id)` ON DELETE CASCADE
|
||||||
|
- `course_reviews.enrollment_id` → `enrollments(id)` ON DELETE CASCADE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
### Triggers de updated_at
|
||||||
|
Aplica a: categories, courses, modules, lessons, enrollments, progress, quizzes, quiz_questions, user_gamification_profile, course_reviews
|
||||||
|
|
||||||
|
**Función:** `education.update_updated_at_column()`
|
||||||
|
**Trigger:** `update_{table}_updated_at`
|
||||||
|
**Evento:** BEFORE UPDATE
|
||||||
|
**Acción:** Actualiza `updated_at = NOW()`
|
||||||
|
|
||||||
|
### Triggers de lógica de negocio
|
||||||
|
|
||||||
|
**update_enrollment_on_progress**
|
||||||
|
- Tabla: progress
|
||||||
|
- Función: `education.update_enrollment_progress()`
|
||||||
|
- Evento: AFTER INSERT OR UPDATE
|
||||||
|
- Condición: WHEN (NEW.is_completed = true)
|
||||||
|
- Acción: Recalcula progreso del enrollment
|
||||||
|
|
||||||
|
**auto_complete_enrollment_trigger**
|
||||||
|
- Tabla: enrollments
|
||||||
|
- Función: `education.auto_complete_enrollment()`
|
||||||
|
- Evento: BEFORE UPDATE
|
||||||
|
- Acción: Completa enrollment si progress >= 100%
|
||||||
|
|
||||||
|
**generate_certificate_number_trigger**
|
||||||
|
- Tabla: certificates
|
||||||
|
- Función: `education.generate_certificate_number()`
|
||||||
|
- Evento: BEFORE INSERT
|
||||||
|
- Acción: Genera certificate_number y verification_code
|
||||||
|
|
||||||
|
**update_course_rating_on_review_insert**
|
||||||
|
- Tabla: course_reviews
|
||||||
|
- Función: `education.update_course_rating_stats()`
|
||||||
|
- Evento: AFTER INSERT
|
||||||
|
- Acción: Actualiza avg_rating del curso
|
||||||
|
|
||||||
|
**update_course_rating_on_review_update**
|
||||||
|
- Tabla: course_reviews
|
||||||
|
- Función: `education.update_course_rating_stats()`
|
||||||
|
- Evento: AFTER UPDATE
|
||||||
|
- Condición: rating o is_approved cambió
|
||||||
|
- Acción: Actualiza avg_rating del curso
|
||||||
|
|
||||||
|
**update_course_rating_on_review_delete**
|
||||||
|
- Tabla: course_reviews
|
||||||
|
- Función: `education.update_course_rating_stats()`
|
||||||
|
- Evento: AFTER DELETE
|
||||||
|
- Acción: Actualiza avg_rating del curso
|
||||||
|
|
||||||
|
**update_enrollment_count_on_insert**
|
||||||
|
- Tabla: enrollments
|
||||||
|
- Función: `education.update_enrollment_count()`
|
||||||
|
- Evento: AFTER INSERT
|
||||||
|
- Acción: Incrementa contador en courses
|
||||||
|
|
||||||
|
**update_enrollment_count_on_delete**
|
||||||
|
- Tabla: enrollments
|
||||||
|
- Función: `education.update_enrollment_count()`
|
||||||
|
- Evento: AFTER DELETE
|
||||||
|
- Acción: Decrementa contador en courses
|
||||||
|
|
||||||
|
**update_streak_on_activity**
|
||||||
|
- Tabla: user_activity_log
|
||||||
|
- Función: `education.trigger_update_streak()`
|
||||||
|
- Evento: AFTER INSERT
|
||||||
|
- Acción: Actualiza streak del usuario
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funciones Públicas
|
||||||
|
|
||||||
|
### education.update_user_xp(user_id UUID, xp_to_add INTEGER)
|
||||||
|
Actualiza XP del usuario y recalcula nivel.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `user_id`: UUID del usuario
|
||||||
|
- `xp_to_add`: Cantidad de XP a agregar
|
||||||
|
|
||||||
|
**Lógica:**
|
||||||
|
- Suma XP al total
|
||||||
|
- Calcula nuevo nivel basado en fórmula cuadrática
|
||||||
|
- Actualiza weekly_xp y monthly_xp
|
||||||
|
- Crea achievement si subió de nivel
|
||||||
|
|
||||||
|
**Ejemplo:**
|
||||||
|
```sql
|
||||||
|
SELECT education.update_user_xp(
|
||||||
|
'00000000-0000-0000-0000-000000000001',
|
||||||
|
100
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### education.update_user_streak(user_id UUID)
|
||||||
|
Actualiza streak del usuario basado en actividad diaria.
|
||||||
|
|
||||||
|
**Parámetros:**
|
||||||
|
- `user_id`: UUID del usuario
|
||||||
|
|
||||||
|
**Lógica:**
|
||||||
|
- Verifica última actividad
|
||||||
|
- Incrementa streak si es día consecutivo
|
||||||
|
- Resetea streak si se rompió
|
||||||
|
- Crea achievement en milestones (7, 30, 100 días)
|
||||||
|
|
||||||
|
**Ejemplo:**
|
||||||
|
```sql
|
||||||
|
SELECT education.update_user_streak(
|
||||||
|
'00000000-0000-0000-0000-000000000001'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vistas Materializadas Recomendadas
|
||||||
|
|
||||||
|
Para mejorar performance en queries frecuentes:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Top cursos por enrollments (actualizar diariamente)
|
||||||
|
CREATE MATERIALIZED VIEW education.mv_top_courses AS
|
||||||
|
SELECT * FROM education.v_popular_courses;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX ON education.mv_top_courses(id);
|
||||||
|
|
||||||
|
-- Leaderboards (actualizar cada hora)
|
||||||
|
CREATE MATERIALIZED VIEW education.mv_leaderboard_weekly AS
|
||||||
|
SELECT * FROM education.v_leaderboard_weekly;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX ON education.mv_leaderboard_weekly(user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimizaciones Recomendadas
|
||||||
|
|
||||||
|
### 1. Particionamiento de user_activity_log
|
||||||
|
|
||||||
|
Para logs con alto volumen:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Particionar por mes
|
||||||
|
CREATE TABLE education.user_activity_log_2025_12
|
||||||
|
PARTITION OF education.user_activity_log
|
||||||
|
FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Índices adicionales según uso
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Si hay muchas búsquedas por título de curso
|
||||||
|
CREATE INDEX idx_courses_title_trgm ON education.courses
|
||||||
|
USING gin(title gin_trgm_ops);
|
||||||
|
|
||||||
|
-- Requiere extension pg_trgm
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Vacuum y Analyze automático
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Configurar autovacuum para tablas con alta escritura
|
||||||
|
ALTER TABLE education.user_activity_log
|
||||||
|
SET (autovacuum_vacuum_scale_factor = 0.01);
|
||||||
|
|
||||||
|
ALTER TABLE education.progress
|
||||||
|
SET (autovacuum_vacuum_scale_factor = 0.02);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoreo
|
||||||
|
|
||||||
|
### Queries útiles para monitoreo
|
||||||
|
|
||||||
|
**Tamaño de tablas:**
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
tablename,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'education'
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Índices no usados:**
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
tablename,
|
||||||
|
indexname,
|
||||||
|
idx_scan
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname = 'education' AND idx_scan = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actividad de enrollments hoy:**
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM education.enrollments
|
||||||
|
WHERE enrolled_at::date = CURRENT_DATE;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cursos más populares (últimos 7 días):**
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
c.title,
|
||||||
|
COUNT(e.id) as new_enrollments
|
||||||
|
FROM education.courses c
|
||||||
|
LEFT JOIN education.enrollments e ON c.id = e.course_id
|
||||||
|
AND e.enrolled_at >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY c.id, c.title
|
||||||
|
ORDER BY new_enrollments DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup y Restore
|
||||||
|
|
||||||
|
### Backup solo del schema education
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_dump -h localhost -U postgres -n education trading_platform > education_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
psql -h localhost -U postgres trading_platform < education_backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versión y Changelog
|
||||||
|
|
||||||
|
**v1.0.0** (2025-12-06)
|
||||||
|
- Implementación inicial completa
|
||||||
|
- 14 tablas
|
||||||
|
- 8 funciones
|
||||||
|
- 7 vistas
|
||||||
|
- Sistema de gamificación completo
|
||||||
|
- Reviews de cursos
|
||||||
|
- Activity logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Documentación generada:** 2025-12-06
|
||||||
|
**Última revisión:** 2025-12-06
|
||||||
69
ddl/schemas/education/functions/01-update_updated_at.sql
Normal file
69
ddl/schemas/education/functions/01-update_updated_at.sql
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- FUNCTION: education.update_updated_at_column()
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- Descripción: Actualiza automáticamente el campo updated_at
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION education.update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.update_updated_at_column() IS 'Actualiza automáticamente updated_at en cada UPDATE';
|
||||||
|
|
||||||
|
-- Aplicar trigger a todas las tablas relevantes
|
||||||
|
CREATE TRIGGER update_categories_updated_at
|
||||||
|
BEFORE UPDATE ON education.categories
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_courses_updated_at
|
||||||
|
BEFORE UPDATE ON education.courses
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_modules_updated_at
|
||||||
|
BEFORE UPDATE ON education.modules
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_lessons_updated_at
|
||||||
|
BEFORE UPDATE ON education.lessons
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_enrollments_updated_at
|
||||||
|
BEFORE UPDATE ON education.enrollments
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_progress_updated_at
|
||||||
|
BEFORE UPDATE ON education.progress
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_quizzes_updated_at
|
||||||
|
BEFORE UPDATE ON education.quizzes
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_quiz_questions_updated_at
|
||||||
|
BEFORE UPDATE ON education.quiz_questions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_user_gamification_profile_updated_at
|
||||||
|
BEFORE UPDATE ON education.user_gamification_profile
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_course_reviews_updated_at
|
||||||
|
BEFORE UPDATE ON education.course_reviews
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_updated_at_column();
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- FUNCTION: education.update_enrollment_progress()
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- Descripción: Actualiza el progreso del enrollment cuando se completa una lección
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION education.update_enrollment_progress()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_total_lessons INTEGER;
|
||||||
|
v_completed_lessons INTEGER;
|
||||||
|
v_progress_percentage DECIMAL(5,2);
|
||||||
|
BEGIN
|
||||||
|
-- Obtener total de lecciones obligatorias del curso
|
||||||
|
SELECT COUNT(*)
|
||||||
|
INTO v_total_lessons
|
||||||
|
FROM education.lessons l
|
||||||
|
JOIN education.modules m ON l.module_id = m.id
|
||||||
|
JOIN education.courses c ON m.course_id = c.id
|
||||||
|
WHERE c.id = (
|
||||||
|
SELECT course_id FROM education.enrollments WHERE id = NEW.enrollment_id
|
||||||
|
) AND l.is_mandatory = true;
|
||||||
|
|
||||||
|
-- Obtener lecciones completadas
|
||||||
|
SELECT COUNT(*)
|
||||||
|
INTO v_completed_lessons
|
||||||
|
FROM education.progress
|
||||||
|
WHERE enrollment_id = NEW.enrollment_id
|
||||||
|
AND is_completed = true;
|
||||||
|
|
||||||
|
-- Calcular porcentaje de progreso
|
||||||
|
v_progress_percentage := (v_completed_lessons::DECIMAL / NULLIF(v_total_lessons, 0)::DECIMAL) * 100;
|
||||||
|
|
||||||
|
-- Actualizar enrollment
|
||||||
|
UPDATE education.enrollments
|
||||||
|
SET
|
||||||
|
progress_percentage = COALESCE(v_progress_percentage, 0),
|
||||||
|
completed_lessons = v_completed_lessons,
|
||||||
|
total_lessons = v_total_lessons,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = NEW.enrollment_id;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.update_enrollment_progress() IS 'Actualiza progreso del enrollment al completar lecciones';
|
||||||
|
|
||||||
|
-- Trigger para actualizar el progreso
|
||||||
|
CREATE TRIGGER update_enrollment_on_progress
|
||||||
|
AFTER INSERT OR UPDATE ON education.progress
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.is_completed = true)
|
||||||
|
EXECUTE FUNCTION education.update_enrollment_progress();
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- FUNCTION: education.auto_complete_enrollment()
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- Descripción: Completa automáticamente el enrollment cuando alcanza 100%
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION education.auto_complete_enrollment()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Si el progreso llegó al 100% y está activo, completarlo
|
||||||
|
IF NEW.progress_percentage >= 100 AND NEW.status = 'active' THEN
|
||||||
|
NEW.status := 'completed';
|
||||||
|
NEW.completed_at := NOW();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.auto_complete_enrollment() IS 'Completa automáticamente el enrollment al alcanzar 100%';
|
||||||
|
|
||||||
|
-- Trigger para auto-completar enrollment
|
||||||
|
CREATE TRIGGER auto_complete_enrollment_trigger
|
||||||
|
BEFORE UPDATE ON education.enrollments
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.auto_complete_enrollment();
|
||||||
47
ddl/schemas/education/functions/04-generate_certificate.sql
Normal file
47
ddl/schemas/education/functions/04-generate_certificate.sql
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- FUNCTION: education.generate_certificate_number()
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- Descripción: Genera automáticamente el número de certificado y código de verificación
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION education.generate_certificate_number()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_year INTEGER;
|
||||||
|
v_sequence INTEGER;
|
||||||
|
BEGIN
|
||||||
|
v_year := EXTRACT(YEAR FROM NOW());
|
||||||
|
|
||||||
|
-- Obtener siguiente número de secuencia para el año
|
||||||
|
SELECT COALESCE(MAX(
|
||||||
|
CAST(SUBSTRING(certificate_number FROM 14) AS INTEGER)
|
||||||
|
), 0) + 1
|
||||||
|
INTO v_sequence
|
||||||
|
FROM education.certificates
|
||||||
|
WHERE certificate_number LIKE 'OQI-CERT-' || v_year || '-%';
|
||||||
|
|
||||||
|
-- Generar número de certificado: OQI-CERT-2025-00001
|
||||||
|
NEW.certificate_number := FORMAT('OQI-CERT-%s-%s',
|
||||||
|
v_year,
|
||||||
|
LPAD(v_sequence::TEXT, 5, '0')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Generar código de verificación único
|
||||||
|
NEW.verification_code := UPPER(
|
||||||
|
SUBSTRING(MD5(RANDOM()::TEXT || NOW()::TEXT) FROM 1 FOR 16)
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.generate_certificate_number() IS 'Genera número de certificado y código de verificación automáticamente';
|
||||||
|
|
||||||
|
-- Trigger para generar número de certificado
|
||||||
|
CREATE TRIGGER generate_certificate_number_trigger
|
||||||
|
BEFORE INSERT ON education.certificates
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.generate_certificate_number();
|
||||||
59
ddl/schemas/education/functions/05-update_course_stats.sql
Normal file
59
ddl/schemas/education/functions/05-update_course_stats.sql
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- FUNCTION: education.update_course_stats()
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- Descripción: Actualiza estadísticas denormalizadas del curso
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Función para actualizar estadísticas de reviews
|
||||||
|
CREATE OR REPLACE FUNCTION education.update_course_rating_stats()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_course_id UUID;
|
||||||
|
v_avg_rating DECIMAL(3,2);
|
||||||
|
v_total_reviews INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Obtener course_id del NEW o OLD record
|
||||||
|
v_course_id := COALESCE(NEW.course_id, OLD.course_id);
|
||||||
|
|
||||||
|
-- Calcular promedio solo de reviews aprobadas
|
||||||
|
SELECT
|
||||||
|
COALESCE(AVG(rating), 0),
|
||||||
|
COUNT(*)
|
||||||
|
INTO v_avg_rating, v_total_reviews
|
||||||
|
FROM education.course_reviews
|
||||||
|
WHERE course_id = v_course_id
|
||||||
|
AND is_approved = true;
|
||||||
|
|
||||||
|
-- Actualizar estadísticas en el curso
|
||||||
|
UPDATE education.courses
|
||||||
|
SET
|
||||||
|
avg_rating = v_avg_rating,
|
||||||
|
total_reviews = v_total_reviews,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = v_course_id;
|
||||||
|
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.update_course_rating_stats() IS 'Actualiza avg_rating y total_reviews del curso';
|
||||||
|
|
||||||
|
-- Triggers para actualizar estadísticas
|
||||||
|
CREATE TRIGGER update_course_rating_on_review_insert
|
||||||
|
AFTER INSERT ON education.course_reviews
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_course_rating_stats();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_course_rating_on_review_update
|
||||||
|
AFTER UPDATE ON education.course_reviews
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (OLD.rating IS DISTINCT FROM NEW.rating OR OLD.is_approved IS DISTINCT FROM NEW.is_approved)
|
||||||
|
EXECUTE FUNCTION education.update_course_rating_stats();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_course_rating_on_review_delete
|
||||||
|
AFTER DELETE ON education.course_reviews
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_course_rating_stats();
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- FUNCTION: education.update_enrollment_count()
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- Descripción: Actualiza el contador de enrollments en courses
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION education.update_enrollment_count()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_course_id UUID;
|
||||||
|
BEGIN
|
||||||
|
v_course_id := COALESCE(NEW.course_id, OLD.course_id);
|
||||||
|
|
||||||
|
-- Actualizar contador de enrollments
|
||||||
|
UPDATE education.courses
|
||||||
|
SET
|
||||||
|
total_enrollments = (
|
||||||
|
SELECT COUNT(*) FROM education.enrollments WHERE course_id = v_course_id
|
||||||
|
),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = v_course_id;
|
||||||
|
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.update_enrollment_count() IS 'Actualiza total_enrollments del curso';
|
||||||
|
|
||||||
|
-- Triggers para actualizar contador
|
||||||
|
CREATE TRIGGER update_enrollment_count_on_insert
|
||||||
|
AFTER INSERT ON education.enrollments
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_enrollment_count();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_enrollment_count_on_delete
|
||||||
|
AFTER DELETE ON education.enrollments
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.update_enrollment_count();
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- FUNCTION: education.update_gamification_profile()
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: Función adicional para gamificación
|
||||||
|
-- Descripción: Actualiza el perfil de gamificación del usuario
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Función para actualizar XP y nivel
|
||||||
|
CREATE OR REPLACE FUNCTION education.update_user_xp(
|
||||||
|
p_user_id UUID,
|
||||||
|
p_xp_to_add INTEGER
|
||||||
|
)
|
||||||
|
RETURNS VOID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_profile RECORD;
|
||||||
|
v_new_total_xp INTEGER;
|
||||||
|
v_new_level INTEGER;
|
||||||
|
v_xp_to_next INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Obtener o crear perfil
|
||||||
|
INSERT INTO education.user_gamification_profile (user_id)
|
||||||
|
VALUES (p_user_id)
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Obtener perfil actual
|
||||||
|
SELECT * INTO v_profile
|
||||||
|
FROM education.user_gamification_profile
|
||||||
|
WHERE user_id = p_user_id;
|
||||||
|
|
||||||
|
-- Calcular nuevo XP total
|
||||||
|
v_new_total_xp := v_profile.total_xp + p_xp_to_add;
|
||||||
|
|
||||||
|
-- Calcular nuevo nivel (cada nivel requiere 100 XP más que el anterior)
|
||||||
|
-- Nivel 1: 0-99 XP, Nivel 2: 100-299 XP, Nivel 3: 300-599 XP, etc.
|
||||||
|
v_new_level := FLOOR((-100 + SQRT(10000 + 800 * v_new_total_xp)) / 200) + 1;
|
||||||
|
|
||||||
|
-- XP necesario para siguiente nivel
|
||||||
|
v_xp_to_next := (v_new_level * 100 + (v_new_level * (v_new_level - 1) * 100)) - v_new_total_xp;
|
||||||
|
|
||||||
|
-- Actualizar perfil
|
||||||
|
UPDATE education.user_gamification_profile
|
||||||
|
SET
|
||||||
|
total_xp = v_new_total_xp,
|
||||||
|
current_level = v_new_level,
|
||||||
|
xp_to_next_level = v_xp_to_next,
|
||||||
|
weekly_xp = weekly_xp + p_xp_to_add,
|
||||||
|
monthly_xp = monthly_xp + p_xp_to_add,
|
||||||
|
last_activity_date = CURRENT_DATE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE user_id = p_user_id;
|
||||||
|
|
||||||
|
-- Si subió de nivel, crear achievement
|
||||||
|
IF v_new_level > v_profile.current_level THEN
|
||||||
|
INSERT INTO education.user_achievements (
|
||||||
|
user_id,
|
||||||
|
achievement_type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
xp_bonus
|
||||||
|
) VALUES (
|
||||||
|
p_user_id,
|
||||||
|
'level_up',
|
||||||
|
'Level Up! - Nivel ' || v_new_level,
|
||||||
|
'Has alcanzado el nivel ' || v_new_level,
|
||||||
|
50
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.update_user_xp(UUID, INTEGER) IS 'Actualiza XP del usuario y recalcula nivel';
|
||||||
|
|
||||||
|
-- Función para actualizar streak
|
||||||
|
CREATE OR REPLACE FUNCTION education.update_user_streak(p_user_id UUID)
|
||||||
|
RETURNS VOID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_last_activity DATE;
|
||||||
|
v_current_streak INTEGER;
|
||||||
|
v_longest_streak INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Obtener o crear perfil
|
||||||
|
INSERT INTO education.user_gamification_profile (user_id)
|
||||||
|
VALUES (p_user_id)
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Obtener datos actuales
|
||||||
|
SELECT last_activity_date, current_streak_days, longest_streak_days
|
||||||
|
INTO v_last_activity, v_current_streak, v_longest_streak
|
||||||
|
FROM education.user_gamification_profile
|
||||||
|
WHERE user_id = p_user_id;
|
||||||
|
|
||||||
|
-- Actualizar streak
|
||||||
|
IF v_last_activity IS NULL THEN
|
||||||
|
-- Primera actividad
|
||||||
|
v_current_streak := 1;
|
||||||
|
ELSIF v_last_activity = CURRENT_DATE THEN
|
||||||
|
-- Ya tuvo actividad hoy, no hacer nada
|
||||||
|
RETURN;
|
||||||
|
ELSIF v_last_activity = CURRENT_DATE - INTERVAL '1 day' THEN
|
||||||
|
-- Actividad día consecutivo
|
||||||
|
v_current_streak := v_current_streak + 1;
|
||||||
|
ELSE
|
||||||
|
-- Se rompió el streak
|
||||||
|
v_current_streak := 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Actualizar longest streak si corresponde
|
||||||
|
IF v_current_streak > v_longest_streak THEN
|
||||||
|
v_longest_streak := v_current_streak;
|
||||||
|
|
||||||
|
-- Crear achievement por streak milestones
|
||||||
|
IF v_current_streak IN (7, 30, 100) THEN
|
||||||
|
INSERT INTO education.user_achievements (
|
||||||
|
user_id,
|
||||||
|
achievement_type,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
xp_bonus,
|
||||||
|
metadata
|
||||||
|
) VALUES (
|
||||||
|
p_user_id,
|
||||||
|
'streak_milestone',
|
||||||
|
'Streak de ' || v_current_streak || ' días',
|
||||||
|
'Has mantenido una racha de ' || v_current_streak || ' días consecutivos',
|
||||||
|
v_current_streak * 5,
|
||||||
|
jsonb_build_object('streak_days', v_current_streak)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Actualizar perfil
|
||||||
|
UPDATE education.user_gamification_profile
|
||||||
|
SET
|
||||||
|
current_streak_days = v_current_streak,
|
||||||
|
longest_streak_days = v_longest_streak,
|
||||||
|
last_activity_date = CURRENT_DATE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE user_id = p_user_id;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION education.update_user_streak(UUID) IS 'Actualiza streak del usuario basado en actividad diaria';
|
||||||
|
|
||||||
|
-- Trigger para actualizar streak en actividades
|
||||||
|
CREATE OR REPLACE FUNCTION education.trigger_update_streak()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM education.update_user_streak(NEW.user_id);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER update_streak_on_activity
|
||||||
|
AFTER INSERT ON education.user_activity_log
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION education.trigger_update_streak();
|
||||||
142
ddl/schemas/education/functions/08-views.sql
Normal file
142
ddl/schemas/education/functions/08-views.sql
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- VIEWS - Schema Education
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Vista: Cursos con estadísticas completas
|
||||||
|
CREATE OR REPLACE VIEW education.v_courses_with_stats AS
|
||||||
|
SELECT
|
||||||
|
c.*,
|
||||||
|
cat.name as category_name,
|
||||||
|
cat.slug as category_slug,
|
||||||
|
COUNT(DISTINCT m.id) as modules_count,
|
||||||
|
COUNT(DISTINCT l.id) as lessons_count,
|
||||||
|
SUM(l.video_duration_seconds) as total_duration_seconds,
|
||||||
|
COUNT(DISTINCT e.id) as enrollments_count,
|
||||||
|
COUNT(DISTINCT CASE WHEN e.status = 'completed' THEN e.id END) as completions_count
|
||||||
|
FROM education.courses c
|
||||||
|
LEFT JOIN education.categories cat ON c.category_id = cat.id
|
||||||
|
LEFT JOIN education.modules m ON c.id = m.course_id
|
||||||
|
LEFT JOIN education.lessons l ON m.id = l.module_id
|
||||||
|
LEFT JOIN education.enrollments e ON c.id = e.course_id
|
||||||
|
GROUP BY c.id, cat.name, cat.slug;
|
||||||
|
|
||||||
|
COMMENT ON VIEW education.v_courses_with_stats IS 'Cursos con estadísticas agregadas de módulos, lecciones y enrollments';
|
||||||
|
|
||||||
|
-- Vista: Progreso del usuario por curso
|
||||||
|
CREATE OR REPLACE VIEW education.v_user_course_progress AS
|
||||||
|
SELECT
|
||||||
|
e.user_id,
|
||||||
|
e.course_id,
|
||||||
|
c.title as course_title,
|
||||||
|
c.slug as course_slug,
|
||||||
|
c.thumbnail_url,
|
||||||
|
e.status as enrollment_status,
|
||||||
|
e.progress_percentage,
|
||||||
|
e.enrolled_at,
|
||||||
|
e.completed_at,
|
||||||
|
e.total_xp_earned,
|
||||||
|
COUNT(DISTINCT p.id) as lessons_viewed,
|
||||||
|
COUNT(DISTINCT CASE WHEN p.is_completed THEN p.id END) as lessons_completed
|
||||||
|
FROM education.enrollments e
|
||||||
|
JOIN education.courses c ON e.course_id = c.id
|
||||||
|
LEFT JOIN education.progress p ON e.id = p.enrollment_id
|
||||||
|
GROUP BY e.id, e.user_id, e.course_id, c.title, c.slug, c.thumbnail_url;
|
||||||
|
|
||||||
|
COMMENT ON VIEW education.v_user_course_progress IS 'Progreso detallado del usuario en cada curso enrollado';
|
||||||
|
|
||||||
|
-- Vista: Leaderboard de usuarios
|
||||||
|
CREATE OR REPLACE VIEW education.v_leaderboard_weekly AS
|
||||||
|
SELECT
|
||||||
|
ugp.user_id,
|
||||||
|
ugp.weekly_xp,
|
||||||
|
ugp.current_level,
|
||||||
|
ugp.current_streak_days,
|
||||||
|
RANK() OVER (ORDER BY ugp.weekly_xp DESC) as rank
|
||||||
|
FROM education.user_gamification_profile ugp
|
||||||
|
WHERE ugp.weekly_xp > 0
|
||||||
|
ORDER BY ugp.weekly_xp DESC
|
||||||
|
LIMIT 100;
|
||||||
|
|
||||||
|
COMMENT ON VIEW education.v_leaderboard_weekly IS 'Top 100 usuarios por XP semanal';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW education.v_leaderboard_monthly AS
|
||||||
|
SELECT
|
||||||
|
ugp.user_id,
|
||||||
|
ugp.monthly_xp,
|
||||||
|
ugp.current_level,
|
||||||
|
ugp.current_streak_days,
|
||||||
|
RANK() OVER (ORDER BY ugp.monthly_xp DESC) as rank
|
||||||
|
FROM education.user_gamification_profile ugp
|
||||||
|
WHERE ugp.monthly_xp > 0
|
||||||
|
ORDER BY ugp.monthly_xp DESC
|
||||||
|
LIMIT 100;
|
||||||
|
|
||||||
|
COMMENT ON VIEW education.v_leaderboard_monthly IS 'Top 100 usuarios por XP mensual';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW education.v_leaderboard_alltime AS
|
||||||
|
SELECT
|
||||||
|
ugp.user_id,
|
||||||
|
ugp.total_xp,
|
||||||
|
ugp.current_level,
|
||||||
|
ugp.total_courses_completed,
|
||||||
|
RANK() OVER (ORDER BY ugp.total_xp DESC) as rank
|
||||||
|
FROM education.user_gamification_profile ugp
|
||||||
|
WHERE ugp.total_xp > 0
|
||||||
|
ORDER BY ugp.total_xp DESC
|
||||||
|
LIMIT 100;
|
||||||
|
|
||||||
|
COMMENT ON VIEW education.v_leaderboard_alltime IS 'Top 100 usuarios por XP total histórico';
|
||||||
|
|
||||||
|
-- Vista: Estadísticas del usuario
|
||||||
|
CREATE OR REPLACE VIEW education.v_user_statistics AS
|
||||||
|
SELECT
|
||||||
|
ugp.user_id,
|
||||||
|
ugp.total_xp,
|
||||||
|
ugp.current_level,
|
||||||
|
ugp.xp_to_next_level,
|
||||||
|
ugp.current_streak_days,
|
||||||
|
ugp.longest_streak_days,
|
||||||
|
ugp.total_courses_completed,
|
||||||
|
ugp.total_lessons_completed,
|
||||||
|
ugp.total_quizzes_passed,
|
||||||
|
ugp.total_certificates_earned,
|
||||||
|
ugp.average_quiz_score,
|
||||||
|
COUNT(DISTINCT e.id) as total_enrollments,
|
||||||
|
COUNT(DISTINCT CASE WHEN e.status = 'active' THEN e.id END) as active_enrollments,
|
||||||
|
COUNT(DISTINCT ua.id) as total_achievements
|
||||||
|
FROM education.user_gamification_profile ugp
|
||||||
|
LEFT JOIN education.enrollments e ON ugp.user_id = e.user_id
|
||||||
|
LEFT JOIN education.user_achievements ua ON ugp.user_id = ua.user_id
|
||||||
|
GROUP BY ugp.user_id, ugp.total_xp, ugp.current_level, ugp.xp_to_next_level,
|
||||||
|
ugp.current_streak_days, ugp.longest_streak_days, ugp.total_courses_completed,
|
||||||
|
ugp.total_lessons_completed, ugp.total_quizzes_passed, ugp.total_certificates_earned,
|
||||||
|
ugp.average_quiz_score;
|
||||||
|
|
||||||
|
COMMENT ON VIEW education.v_user_statistics IS 'Estadísticas completas del usuario (gamificación + progreso)';
|
||||||
|
|
||||||
|
-- Vista: Cursos populares
|
||||||
|
CREATE OR REPLACE VIEW education.v_popular_courses AS
|
||||||
|
SELECT
|
||||||
|
c.id,
|
||||||
|
c.title,
|
||||||
|
c.slug,
|
||||||
|
c.thumbnail_url,
|
||||||
|
c.difficulty_level,
|
||||||
|
c.avg_rating,
|
||||||
|
c.total_reviews,
|
||||||
|
c.total_enrollments,
|
||||||
|
COUNT(DISTINCT e.id) as recent_enrollments_30d,
|
||||||
|
COUNT(DISTINCT CASE WHEN e.status = 'completed' THEN e.id END) as completions
|
||||||
|
FROM education.courses c
|
||||||
|
LEFT JOIN education.enrollments e ON c.id = e.course_id
|
||||||
|
AND e.enrolled_at >= NOW() - INTERVAL '30 days'
|
||||||
|
WHERE c.status = 'published'
|
||||||
|
GROUP BY c.id
|
||||||
|
ORDER BY recent_enrollments_30d DESC, c.avg_rating DESC
|
||||||
|
LIMIT 50;
|
||||||
|
|
||||||
|
COMMENT ON VIEW education.v_popular_courses IS 'Top 50 cursos más populares (enrollments últimos 30 días)';
|
||||||
132
ddl/schemas/education/install.sh
Executable file
132
ddl/schemas/education/install.sh
Executable file
@ -0,0 +1,132 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# INSTALL SCRIPT - Schema Education
|
||||||
|
# =====================================================
|
||||||
|
# Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
# Módulo: OQI-002 - Education
|
||||||
|
# Especificación: ET-EDU-001-database.md
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DB_HOST="${DB_HOST:-localhost}"
|
||||||
|
DB_PORT="${DB_PORT:-5432}"
|
||||||
|
DB_NAME="${DB_NAME:-trading_platform}"
|
||||||
|
DB_USER="${DB_USER:-postgres}"
|
||||||
|
SCHEMA_NAME="education"
|
||||||
|
|
||||||
|
echo -e "${GREEN}=================================================${NC}"
|
||||||
|
echo -e "${GREEN} OrbiQuant IA - Education Schema Installation${NC}"
|
||||||
|
echo -e "${GREEN}=================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if psql is available
|
||||||
|
if ! command -v psql &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: psql command not found${NC}"
|
||||||
|
echo "Please install PostgreSQL client"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
echo "Configuration:"
|
||||||
|
echo " Database: $DB_NAME"
|
||||||
|
echo " Host: $DB_HOST:$DB_PORT"
|
||||||
|
echo " User: $DB_USER"
|
||||||
|
echo " Schema: $SCHEMA_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Function to execute SQL file
|
||||||
|
execute_sql() {
|
||||||
|
local file=$1
|
||||||
|
local description=$2
|
||||||
|
|
||||||
|
echo -e "${YELLOW}▶${NC} $description"
|
||||||
|
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo -e "${RED} ✗ File not found: $file${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$file" > /dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN} ✓ Success${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ Failed${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create schema if not exists
|
||||||
|
echo -e "${YELLOW}▶${NC} Creating schema: $SCHEMA_NAME"
|
||||||
|
PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "CREATE SCHEMA IF NOT EXISTS $SCHEMA_NAME;" > /dev/null 2>&1
|
||||||
|
echo -e "${GREEN} ✓ Schema created/verified${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Install ENUMs
|
||||||
|
echo -e "${GREEN}[1/3] Installing ENUMs...${NC}"
|
||||||
|
execute_sql "$SCRIPT_DIR/00-enums.sql" "Creating ENUM types"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Install Tables
|
||||||
|
echo -e "${GREEN}[2/3] Installing Tables...${NC}"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/01-categories.sql" "Creating table: categories"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/02-courses.sql" "Creating table: courses"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/03-modules.sql" "Creating table: modules"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/04-lessons.sql" "Creating table: lessons"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/05-enrollments.sql" "Creating table: enrollments"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/06-progress.sql" "Creating table: progress"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/07-quizzes.sql" "Creating table: quizzes"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/08-quiz_questions.sql" "Creating table: quiz_questions"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/09-quiz_attempts.sql" "Creating table: quiz_attempts"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/10-certificates.sql" "Creating table: certificates"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/11-user_achievements.sql" "Creating table: user_achievements"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/12-user_gamification_profile.sql" "Creating table: user_gamification_profile"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/13-user_activity_log.sql" "Creating table: user_activity_log"
|
||||||
|
execute_sql "$SCRIPT_DIR/tables/14-course_reviews.sql" "Creating table: course_reviews"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Install Functions and Triggers
|
||||||
|
echo -e "${GREEN}[3/3] Installing Functions and Triggers...${NC}"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/01-update_updated_at.sql" "Creating trigger: update_updated_at"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/02-update_enrollment_progress.sql" "Creating function: update_enrollment_progress"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/03-auto_complete_enrollment.sql" "Creating function: auto_complete_enrollment"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/04-generate_certificate.sql" "Creating function: generate_certificate_number"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/05-update_course_stats.sql" "Creating function: update_course_stats"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/06-update_enrollment_count.sql" "Creating function: update_enrollment_count"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/07-update_gamification_profile.sql" "Creating functions: gamification"
|
||||||
|
execute_sql "$SCRIPT_DIR/functions/08-views.sql" "Creating views"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo -e "${YELLOW}▶${NC} Verifying installation..."
|
||||||
|
|
||||||
|
TABLE_COUNT=$(PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '$SCHEMA_NAME' AND table_type = 'BASE TABLE';" 2>/dev/null | xargs)
|
||||||
|
|
||||||
|
if [ "$TABLE_COUNT" -eq "14" ]; then
|
||||||
|
echo -e "${GREEN} ✓ All 14 tables created successfully${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ Expected 14 tables, found $TABLE_COUNT${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}=================================================${NC}"
|
||||||
|
echo -e "${GREEN} Installation Complete!${NC}"
|
||||||
|
echo -e "${GREEN}=================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Schema '$SCHEMA_NAME' has been installed successfully."
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Review the README.md for usage examples"
|
||||||
|
echo " 2. Run seed data scripts if needed"
|
||||||
|
echo " 3. Configure Row Level Security (RLS) policies"
|
||||||
|
echo ""
|
||||||
238
ddl/schemas/education/seeds-example.sql
Normal file
238
ddl/schemas/education/seeds-example.sql
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- SEED DATA - Schema Education (EJEMPLO)
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
-- NOTA: Este es un archivo de ejemplo.
|
||||||
|
-- Los datos reales deben ir en /apps/database/seeds/
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 1. CATEGORIES
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
INSERT INTO education.categories (id, name, slug, description, color, display_order, is_active) VALUES
|
||||||
|
('11111111-1111-1111-1111-111111111111', 'Trading Básico', 'trading-basico', 'Fundamentos del trading y mercados financieros', '#3B82F6', 1, true),
|
||||||
|
('22222222-2222-2222-2222-222222222222', 'Análisis Técnico', 'analisis-tecnico', 'Herramientas y técnicas de análisis técnico', '#10B981', 2, true),
|
||||||
|
('33333333-3333-3333-3333-333333333333', 'Análisis Fundamental', 'analisis-fundamental', 'Evaluación fundamental de activos', '#F59E0B', 3, true),
|
||||||
|
('44444444-4444-4444-4444-444444444444', 'Gestión de Riesgo', 'gestion-riesgo', 'Estrategias de gestión de riesgo y capital', '#EF4444', 4, true),
|
||||||
|
('55555555-5555-5555-5555-555555555555', 'Trading Algorítmico', 'trading-algoritmico', 'Automatización y estrategias algorítmicas', '#8B5CF6', 5, true),
|
||||||
|
('66666666-6666-6666-6666-666666666666', 'Psicología del Trading', 'psicologia-trading', 'Aspectos psicológicos y emocionales del trading', '#EC4899', 6, true);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 2. COURSES
|
||||||
|
-- =====================================================
|
||||||
|
-- NOTA: instructor_id debe existir en auth.users
|
||||||
|
-- Para este ejemplo, usar un UUID válido de tu sistema
|
||||||
|
|
||||||
|
INSERT INTO education.courses (
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
slug,
|
||||||
|
short_description,
|
||||||
|
full_description,
|
||||||
|
category_id,
|
||||||
|
difficulty_level,
|
||||||
|
instructor_id,
|
||||||
|
instructor_name,
|
||||||
|
is_free,
|
||||||
|
xp_reward,
|
||||||
|
status,
|
||||||
|
published_at,
|
||||||
|
total_modules,
|
||||||
|
total_lessons
|
||||||
|
) VALUES
|
||||||
|
(
|
||||||
|
'c1111111-1111-1111-1111-111111111111',
|
||||||
|
'Introducción al Trading',
|
||||||
|
'introduccion-trading',
|
||||||
|
'Aprende los conceptos básicos del trading desde cero',
|
||||||
|
'Este curso te enseñará los fundamentos del trading, incluyendo tipos de mercados, instrumentos financieros, y cómo realizar tus primeras operaciones de forma segura.',
|
||||||
|
'11111111-1111-1111-1111-111111111111',
|
||||||
|
'beginner',
|
||||||
|
'00000000-0000-0000-0000-000000000001', -- Reemplazar con ID real
|
||||||
|
'Instructor Demo',
|
||||||
|
true,
|
||||||
|
500,
|
||||||
|
'published',
|
||||||
|
NOW(),
|
||||||
|
3,
|
||||||
|
12
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 3. MODULES
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
INSERT INTO education.modules (id, course_id, title, description, display_order, duration_minutes) VALUES
|
||||||
|
('m1111111-1111-1111-1111-111111111111', 'c1111111-1111-1111-1111-111111111111', 'Módulo 1: Fundamentos', 'Conceptos básicos del trading', 1, 120),
|
||||||
|
('m2222222-2222-2222-2222-222222222222', 'c1111111-1111-1111-1111-111111111111', 'Módulo 2: Mercados Financieros', 'Tipos de mercados y activos', 2, 180),
|
||||||
|
('m3333333-3333-3333-3333-333333333333', 'c1111111-1111-1111-1111-111111111111', 'Módulo 3: Primeros Pasos', 'Cómo empezar a operar', 3, 150);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 4. LESSONS
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
INSERT INTO education.lessons (
|
||||||
|
id,
|
||||||
|
module_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
content_type,
|
||||||
|
video_url,
|
||||||
|
video_duration_seconds,
|
||||||
|
display_order,
|
||||||
|
is_preview,
|
||||||
|
xp_reward
|
||||||
|
) VALUES
|
||||||
|
(
|
||||||
|
'l1111111-1111-1111-1111-111111111111',
|
||||||
|
'm1111111-1111-1111-1111-111111111111',
|
||||||
|
'¿Qué es el Trading?',
|
||||||
|
'Introducción a los conceptos básicos del trading',
|
||||||
|
'video',
|
||||||
|
'https://example.com/videos/lesson-1.mp4',
|
||||||
|
900,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
10
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'l2222222-2222-2222-2222-222222222222',
|
||||||
|
'm1111111-1111-1111-1111-111111111111',
|
||||||
|
'Tipos de Traders',
|
||||||
|
'Conoce los diferentes estilos de trading',
|
||||||
|
'video',
|
||||||
|
'https://example.com/videos/lesson-2.mp4',
|
||||||
|
1200,
|
||||||
|
2,
|
||||||
|
false,
|
||||||
|
10
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'l3333333-3333-3333-3333-333333333333',
|
||||||
|
'm1111111-1111-1111-1111-111111111111',
|
||||||
|
'Terminología Básica',
|
||||||
|
'Vocabulario esencial del trading',
|
||||||
|
'article',
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
3,
|
||||||
|
false,
|
||||||
|
15
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 5. QUIZZES
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
INSERT INTO education.quizzes (
|
||||||
|
id,
|
||||||
|
module_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
passing_score_percentage,
|
||||||
|
max_attempts,
|
||||||
|
xp_reward,
|
||||||
|
xp_perfect_score_bonus
|
||||||
|
) VALUES
|
||||||
|
(
|
||||||
|
'q1111111-1111-1111-1111-111111111111',
|
||||||
|
'm1111111-1111-1111-1111-111111111111',
|
||||||
|
'Quiz: Fundamentos del Trading',
|
||||||
|
'Evalúa tus conocimientos sobre los conceptos básicos',
|
||||||
|
70,
|
||||||
|
3,
|
||||||
|
50,
|
||||||
|
20
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 6. QUIZ QUESTIONS
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
INSERT INTO education.quiz_questions (
|
||||||
|
id,
|
||||||
|
quiz_id,
|
||||||
|
question_text,
|
||||||
|
question_type,
|
||||||
|
options,
|
||||||
|
explanation,
|
||||||
|
points,
|
||||||
|
display_order
|
||||||
|
) VALUES
|
||||||
|
(
|
||||||
|
'qq111111-1111-1111-1111-111111111111',
|
||||||
|
'q1111111-1111-1111-1111-111111111111',
|
||||||
|
'¿Qué es el trading?',
|
||||||
|
'multiple_choice',
|
||||||
|
'[
|
||||||
|
{"id": "a", "text": "Comprar y vender activos financieros", "isCorrect": true},
|
||||||
|
{"id": "b", "text": "Solo comprar acciones", "isCorrect": false},
|
||||||
|
{"id": "c", "text": "Invertir a largo plazo únicamente", "isCorrect": false},
|
||||||
|
{"id": "d", "text": "Ahorrar dinero en un banco", "isCorrect": false}
|
||||||
|
]'::jsonb,
|
||||||
|
'El trading implica la compra y venta de activos financieros con el objetivo de obtener ganancias a corto o mediano plazo.',
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'qq222222-2222-2222-2222-222222222222',
|
||||||
|
'q1111111-1111-1111-1111-111111111111',
|
||||||
|
'¿El trading es una actividad de riesgo?',
|
||||||
|
'true_false',
|
||||||
|
'[
|
||||||
|
{"id": "true", "text": "Verdadero", "isCorrect": true},
|
||||||
|
{"id": "false", "text": "Falso", "isCorrect": false}
|
||||||
|
]'::jsonb,
|
||||||
|
'Sí, el trading es una actividad que conlleva riesgos y es posible perder dinero. Por eso es importante la gestión de riesgo.',
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- EJEMPLO DE USO - ENROLLMENTS
|
||||||
|
-- =====================================================
|
||||||
|
-- NOTA: Estos son ejemplos comentados. NO ejecutar sin IDs reales.
|
||||||
|
|
||||||
|
/*
|
||||||
|
-- Enrollar un usuario a un curso
|
||||||
|
INSERT INTO education.enrollments (user_id, course_id, total_lessons)
|
||||||
|
VALUES (
|
||||||
|
'00000000-0000-0000-0000-000000000001', -- ID del usuario
|
||||||
|
'c1111111-1111-1111-1111-111111111111', -- ID del curso
|
||||||
|
12 -- Total de lecciones del curso
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Registrar progreso en una lección
|
||||||
|
INSERT INTO education.progress (
|
||||||
|
user_id,
|
||||||
|
lesson_id,
|
||||||
|
enrollment_id,
|
||||||
|
is_completed,
|
||||||
|
watch_percentage
|
||||||
|
) VALUES (
|
||||||
|
'00000000-0000-0000-0000-000000000001',
|
||||||
|
'l1111111-1111-1111-1111-111111111111',
|
||||||
|
'<enrollment-id>',
|
||||||
|
true,
|
||||||
|
100.00
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- Verificación
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
SELECT 'Categories created:' as info, COUNT(*) as count FROM education.categories;
|
||||||
|
SELECT 'Courses created:' as info, COUNT(*) as count FROM education.courses;
|
||||||
|
SELECT 'Modules created:' as info, COUNT(*) as count FROM education.modules;
|
||||||
|
SELECT 'Lessons created:' as info, COUNT(*) as count FROM education.lessons;
|
||||||
|
SELECT 'Quizzes created:' as info, COUNT(*) as count FROM education.quizzes;
|
||||||
|
SELECT 'Questions created:' as info, COUNT(*) as count FROM education.quiz_questions;
|
||||||
42
ddl/schemas/education/tables/01-categories.sql
Normal file
42
ddl/schemas/education/tables/01-categories.sql
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.categories
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.categories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Información básica
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Jerarquía
|
||||||
|
parent_id UUID REFERENCES education.categories(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Ordenamiento y visualización
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
icon_url VARCHAR(500),
|
||||||
|
color VARCHAR(7), -- Código hex #RRGGBB
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT valid_color_format CHECK (color ~ '^#[0-9A-Fa-f]{6}$')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_categories_parent ON education.categories(parent_id);
|
||||||
|
CREATE INDEX idx_categories_slug ON education.categories(slug);
|
||||||
|
CREATE INDEX idx_categories_active ON education.categories(is_active) WHERE is_active = true;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.categories IS 'Categorías de cursos con soporte para jerarquía';
|
||||||
|
COMMENT ON COLUMN education.categories.parent_id IS 'Categoría padre para jerarquía';
|
||||||
|
COMMENT ON COLUMN education.categories.display_order IS 'Orden de visualización';
|
||||||
|
COMMENT ON COLUMN education.categories.color IS 'Color en formato hexadecimal #RRGGBB';
|
||||||
74
ddl/schemas/education/tables/02-courses.sql
Normal file
74
ddl/schemas/education/tables/02-courses.sql
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.courses
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.courses (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Información básica
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
slug VARCHAR(200) NOT NULL UNIQUE,
|
||||||
|
short_description VARCHAR(500),
|
||||||
|
full_description TEXT,
|
||||||
|
|
||||||
|
-- Categorización
|
||||||
|
category_id UUID NOT NULL REFERENCES education.categories(id) ON DELETE RESTRICT,
|
||||||
|
difficulty_level education.difficulty_level NOT NULL DEFAULT 'beginner',
|
||||||
|
|
||||||
|
-- Contenido
|
||||||
|
thumbnail_url VARCHAR(500),
|
||||||
|
trailer_url VARCHAR(500), -- Video de presentación
|
||||||
|
|
||||||
|
-- Metadata educativa
|
||||||
|
duration_minutes INTEGER, -- Duración estimada total
|
||||||
|
prerequisites TEXT[], -- IDs de cursos prerequisitos
|
||||||
|
learning_objectives TEXT[], -- Array de objetivos
|
||||||
|
|
||||||
|
-- Instructor
|
||||||
|
instructor_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||||
|
instructor_name VARCHAR(200), -- Denormalizado para performance
|
||||||
|
|
||||||
|
-- Pricing (para futuras features)
|
||||||
|
is_free BOOLEAN DEFAULT true,
|
||||||
|
price_usd DECIMAL(10,2),
|
||||||
|
|
||||||
|
-- Gamificación
|
||||||
|
xp_reward INTEGER DEFAULT 0, -- XP al completar el curso
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status education.course_status DEFAULT 'draft',
|
||||||
|
published_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Estadísticas (denormalizadas)
|
||||||
|
total_modules INTEGER DEFAULT 0,
|
||||||
|
total_lessons INTEGER DEFAULT 0,
|
||||||
|
total_enrollments INTEGER DEFAULT 0,
|
||||||
|
avg_rating DECIMAL(3,2) DEFAULT 0.00,
|
||||||
|
total_reviews INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT valid_rating CHECK (avg_rating >= 0 AND avg_rating <= 5),
|
||||||
|
CONSTRAINT valid_price CHECK (price_usd >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_courses_category ON education.courses(category_id);
|
||||||
|
CREATE INDEX idx_courses_slug ON education.courses(slug);
|
||||||
|
CREATE INDEX idx_courses_status ON education.courses(status);
|
||||||
|
CREATE INDEX idx_courses_difficulty ON education.courses(difficulty_level);
|
||||||
|
CREATE INDEX idx_courses_instructor ON education.courses(instructor_id);
|
||||||
|
CREATE INDEX idx_courses_published ON education.courses(published_at) WHERE status = 'published';
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.courses IS 'Cursos del módulo educativo';
|
||||||
|
COMMENT ON COLUMN education.courses.instructor_name IS 'Denormalizado para performance en queries';
|
||||||
|
COMMENT ON COLUMN education.courses.prerequisites IS 'Array de UUIDs de cursos prerequisitos';
|
||||||
|
COMMENT ON COLUMN education.courses.learning_objectives IS 'Array de objetivos de aprendizaje';
|
||||||
|
COMMENT ON COLUMN education.courses.xp_reward IS 'XP otorgado al completar el curso';
|
||||||
43
ddl/schemas/education/tables/03-modules.sql
Normal file
43
ddl/schemas/education/tables/03-modules.sql
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.modules
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.modules (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relación con curso
|
||||||
|
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Información básica
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Ordenamiento
|
||||||
|
display_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
duration_minutes INTEGER,
|
||||||
|
|
||||||
|
-- Control de acceso
|
||||||
|
is_locked BOOLEAN DEFAULT false, -- Requiere completar módulos anteriores
|
||||||
|
unlock_after_module_id UUID REFERENCES education.modules(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_course_order UNIQUE(course_id, display_order)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_modules_course ON education.modules(course_id);
|
||||||
|
CREATE INDEX idx_modules_order ON education.modules(course_id, display_order);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.modules IS 'Módulos que agrupan lecciones dentro de un curso';
|
||||||
|
COMMENT ON COLUMN education.modules.is_locked IS 'Si requiere completar módulos anteriores para desbloquearse';
|
||||||
|
COMMENT ON COLUMN education.modules.unlock_after_module_id IS 'Módulo que debe completarse antes de acceder a este';
|
||||||
66
ddl/schemas/education/tables/04-lessons.sql
Normal file
66
ddl/schemas/education/tables/04-lessons.sql
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.lessons
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.lessons (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relación con módulo
|
||||||
|
module_id UUID NOT NULL REFERENCES education.modules(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Información básica
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Tipo de contenido
|
||||||
|
content_type education.lesson_content_type NOT NULL DEFAULT 'video',
|
||||||
|
|
||||||
|
-- Contenido video
|
||||||
|
video_url VARCHAR(500), -- URL de Vimeo/S3
|
||||||
|
video_duration_seconds INTEGER,
|
||||||
|
video_provider VARCHAR(50), -- 'vimeo', 's3', etc.
|
||||||
|
video_id VARCHAR(200), -- ID del video en el provider
|
||||||
|
|
||||||
|
-- Contenido texto/article
|
||||||
|
article_content TEXT,
|
||||||
|
|
||||||
|
-- Recursos adicionales
|
||||||
|
attachments JSONB, -- [{name, url, type, size}]
|
||||||
|
|
||||||
|
-- Ordenamiento
|
||||||
|
display_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Configuración
|
||||||
|
is_preview BOOLEAN DEFAULT false, -- Puede verse sin enrollment
|
||||||
|
is_mandatory BOOLEAN DEFAULT true, -- Requerido para completar el curso
|
||||||
|
|
||||||
|
-- Gamificación
|
||||||
|
xp_reward INTEGER DEFAULT 10, -- XP al completar la lección
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_module_order UNIQUE(module_id, display_order),
|
||||||
|
CONSTRAINT video_fields_required CHECK (
|
||||||
|
(content_type != 'video') OR
|
||||||
|
(video_url IS NOT NULL AND video_duration_seconds IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_lessons_module ON education.lessons(module_id);
|
||||||
|
CREATE INDEX idx_lessons_order ON education.lessons(module_id, display_order);
|
||||||
|
CREATE INDEX idx_lessons_type ON education.lessons(content_type);
|
||||||
|
CREATE INDEX idx_lessons_preview ON education.lessons(is_preview) WHERE is_preview = true;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.lessons IS 'Lecciones individuales dentro de módulos';
|
||||||
|
COMMENT ON COLUMN education.lessons.attachments IS 'Archivos adjuntos en formato JSON: [{name, url, type, size}]';
|
||||||
|
COMMENT ON COLUMN education.lessons.is_preview IS 'Puede verse sin enrollment (preview gratuito)';
|
||||||
|
COMMENT ON COLUMN education.lessons.is_mandatory IS 'Requerido para completar el curso';
|
||||||
|
COMMENT ON COLUMN education.lessons.xp_reward IS 'XP otorgado al completar la lección';
|
||||||
56
ddl/schemas/education/tables/05-enrollments.sql
Normal file
56
ddl/schemas/education/tables/05-enrollments.sql
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.enrollments
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.enrollments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relaciones
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status education.enrollment_status DEFAULT 'active',
|
||||||
|
|
||||||
|
-- Progreso
|
||||||
|
progress_percentage DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
completed_lessons INTEGER DEFAULT 0,
|
||||||
|
total_lessons INTEGER DEFAULT 0, -- Snapshot del total al enrollarse
|
||||||
|
|
||||||
|
-- Fechas importantes
|
||||||
|
enrolled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
started_at TIMESTAMPTZ, -- Primera lección vista
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
expires_at TIMESTAMPTZ, -- Para cursos con límite de tiempo
|
||||||
|
|
||||||
|
-- Gamificación
|
||||||
|
total_xp_earned INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_user_course UNIQUE(user_id, course_id),
|
||||||
|
CONSTRAINT valid_progress CHECK (progress_percentage >= 0 AND progress_percentage <= 100),
|
||||||
|
CONSTRAINT valid_completion CHECK (
|
||||||
|
(status != 'completed') OR
|
||||||
|
(completed_at IS NOT NULL AND progress_percentage = 100)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_enrollments_user ON education.enrollments(user_id);
|
||||||
|
CREATE INDEX idx_enrollments_course ON education.enrollments(course_id);
|
||||||
|
CREATE INDEX idx_enrollments_status ON education.enrollments(status);
|
||||||
|
CREATE INDEX idx_enrollments_user_active ON education.enrollments(user_id, status)
|
||||||
|
WHERE status = 'active';
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.enrollments IS 'Inscripciones de usuarios a cursos';
|
||||||
|
COMMENT ON COLUMN education.enrollments.total_lessons IS 'Snapshot del total de lecciones al momento de enrollarse';
|
||||||
|
COMMENT ON COLUMN education.enrollments.started_at IS 'Timestamp de la primera lección vista';
|
||||||
|
COMMENT ON COLUMN education.enrollments.expires_at IS 'Fecha de expiración para cursos con límite de tiempo';
|
||||||
52
ddl/schemas/education/tables/06-progress.sql
Normal file
52
ddl/schemas/education/tables/06-progress.sql
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.progress
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.progress (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relaciones
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
lesson_id UUID NOT NULL REFERENCES education.lessons(id) ON DELETE CASCADE,
|
||||||
|
enrollment_id UUID NOT NULL REFERENCES education.enrollments(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
is_completed BOOLEAN DEFAULT false,
|
||||||
|
|
||||||
|
-- Progreso de video
|
||||||
|
last_position_seconds INTEGER DEFAULT 0,
|
||||||
|
total_watch_time_seconds INTEGER DEFAULT 0, -- Tiempo total visto
|
||||||
|
watch_percentage DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Tracking
|
||||||
|
first_viewed_at TIMESTAMPTZ,
|
||||||
|
last_viewed_at TIMESTAMPTZ,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_user_lesson UNIQUE(user_id, lesson_id),
|
||||||
|
CONSTRAINT valid_watch_percentage CHECK (watch_percentage >= 0 AND watch_percentage <= 100),
|
||||||
|
CONSTRAINT completion_requires_date CHECK (
|
||||||
|
(NOT is_completed) OR (completed_at IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_progress_user ON education.progress(user_id);
|
||||||
|
CREATE INDEX idx_progress_lesson ON education.progress(lesson_id);
|
||||||
|
CREATE INDEX idx_progress_enrollment ON education.progress(enrollment_id);
|
||||||
|
CREATE INDEX idx_progress_completed ON education.progress(is_completed) WHERE is_completed = true;
|
||||||
|
CREATE INDEX idx_progress_user_enrollment ON education.progress(user_id, enrollment_id);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.progress IS 'Progreso individual del usuario en cada lección';
|
||||||
|
COMMENT ON COLUMN education.progress.last_position_seconds IS 'Última posición del video en segundos';
|
||||||
|
COMMENT ON COLUMN education.progress.total_watch_time_seconds IS 'Tiempo total de visualización acumulado';
|
||||||
|
COMMENT ON COLUMN education.progress.watch_percentage IS 'Porcentaje de la lección completada';
|
||||||
57
ddl/schemas/education/tables/07-quizzes.sql
Normal file
57
ddl/schemas/education/tables/07-quizzes.sql
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.quizzes
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.quizzes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relación (puede estar asociado a módulo o lección)
|
||||||
|
module_id UUID REFERENCES education.modules(id) ON DELETE CASCADE,
|
||||||
|
lesson_id UUID REFERENCES education.lessons(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Información básica
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Configuración
|
||||||
|
passing_score_percentage INTEGER DEFAULT 70, -- % mínimo para aprobar
|
||||||
|
max_attempts INTEGER, -- NULL = intentos ilimitados
|
||||||
|
time_limit_minutes INTEGER, -- NULL = sin límite de tiempo
|
||||||
|
|
||||||
|
-- Opciones
|
||||||
|
shuffle_questions BOOLEAN DEFAULT true,
|
||||||
|
shuffle_answers BOOLEAN DEFAULT true,
|
||||||
|
show_correct_answers BOOLEAN DEFAULT true, -- Después de completar
|
||||||
|
|
||||||
|
-- Gamificación
|
||||||
|
xp_reward INTEGER DEFAULT 50,
|
||||||
|
xp_perfect_score_bonus INTEGER DEFAULT 20,
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT valid_passing_score CHECK (passing_score_percentage > 0 AND passing_score_percentage <= 100),
|
||||||
|
CONSTRAINT quiz_association CHECK (
|
||||||
|
(module_id IS NOT NULL AND lesson_id IS NULL) OR
|
||||||
|
(module_id IS NULL AND lesson_id IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_quizzes_module ON education.quizzes(module_id);
|
||||||
|
CREATE INDEX idx_quizzes_lesson ON education.quizzes(lesson_id);
|
||||||
|
CREATE INDEX idx_quizzes_active ON education.quizzes(is_active) WHERE is_active = true;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.quizzes IS 'Quizzes/evaluaciones asociadas a módulos o lecciones';
|
||||||
|
COMMENT ON COLUMN education.quizzes.max_attempts IS 'NULL = intentos ilimitados';
|
||||||
|
COMMENT ON COLUMN education.quizzes.time_limit_minutes IS 'NULL = sin límite de tiempo';
|
||||||
|
COMMENT ON COLUMN education.quizzes.xp_perfect_score_bonus IS 'XP bonus por obtener 100% de score';
|
||||||
56
ddl/schemas/education/tables/08-quiz_questions.sql
Normal file
56
ddl/schemas/education/tables/08-quiz_questions.sql
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.quiz_questions
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.quiz_questions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relación
|
||||||
|
quiz_id UUID NOT NULL REFERENCES education.quizzes(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Pregunta
|
||||||
|
question_text TEXT NOT NULL,
|
||||||
|
question_type education.question_type NOT NULL DEFAULT 'multiple_choice',
|
||||||
|
|
||||||
|
-- Opciones de respuesta (para multiple_choice, true_false, multiple_select)
|
||||||
|
options JSONB, -- [{id, text, isCorrect}]
|
||||||
|
|
||||||
|
-- Respuesta correcta (para fill_blank, code_challenge)
|
||||||
|
correct_answer TEXT,
|
||||||
|
|
||||||
|
-- Explicación
|
||||||
|
explanation TEXT, -- Mostrar después de responder
|
||||||
|
|
||||||
|
-- Recursos adicionales
|
||||||
|
image_url VARCHAR(500),
|
||||||
|
code_snippet TEXT,
|
||||||
|
|
||||||
|
-- Puntuación
|
||||||
|
points INTEGER DEFAULT 1,
|
||||||
|
|
||||||
|
-- Ordenamiento
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT valid_options CHECK (
|
||||||
|
(question_type NOT IN ('multiple_choice', 'true_false', 'multiple_select')) OR
|
||||||
|
(options IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_quiz_questions_quiz ON education.quiz_questions(quiz_id);
|
||||||
|
CREATE INDEX idx_quiz_questions_order ON education.quiz_questions(quiz_id, display_order);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.quiz_questions IS 'Preguntas individuales de los quizzes';
|
||||||
|
COMMENT ON COLUMN education.quiz_questions.options IS 'Array JSON de opciones: [{id, text, isCorrect}]';
|
||||||
|
COMMENT ON COLUMN education.quiz_questions.correct_answer IS 'Respuesta correcta para fill_blank y code_challenge';
|
||||||
|
COMMENT ON COLUMN education.quiz_questions.explanation IS 'Explicación mostrada después de responder';
|
||||||
53
ddl/schemas/education/tables/09-quiz_attempts.sql
Normal file
53
ddl/schemas/education/tables/09-quiz_attempts.sql
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.quiz_attempts
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.quiz_attempts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relaciones
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
quiz_id UUID NOT NULL REFERENCES education.quizzes(id) ON DELETE RESTRICT,
|
||||||
|
enrollment_id UUID REFERENCES education.enrollments(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Estado del intento
|
||||||
|
is_completed BOOLEAN DEFAULT false,
|
||||||
|
is_passed BOOLEAN DEFAULT false,
|
||||||
|
|
||||||
|
-- Respuestas del usuario
|
||||||
|
user_answers JSONB, -- [{questionId, answer, isCorrect, points}]
|
||||||
|
|
||||||
|
-- Puntuación
|
||||||
|
score_points INTEGER DEFAULT 0,
|
||||||
|
max_points INTEGER DEFAULT 0,
|
||||||
|
score_percentage DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Tiempo
|
||||||
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
time_taken_seconds INTEGER,
|
||||||
|
|
||||||
|
-- XP ganado
|
||||||
|
xp_earned INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT valid_score_percentage CHECK (score_percentage >= 0 AND score_percentage <= 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_quiz_attempts_user ON education.quiz_attempts(user_id);
|
||||||
|
CREATE INDEX idx_quiz_attempts_quiz ON education.quiz_attempts(quiz_id);
|
||||||
|
CREATE INDEX idx_quiz_attempts_enrollment ON education.quiz_attempts(enrollment_id);
|
||||||
|
CREATE INDEX idx_quiz_attempts_user_quiz ON education.quiz_attempts(user_id, quiz_id);
|
||||||
|
CREATE INDEX idx_quiz_attempts_completed ON education.quiz_attempts(is_completed, completed_at);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.quiz_attempts IS 'Intentos de los usuarios en los quizzes';
|
||||||
|
COMMENT ON COLUMN education.quiz_attempts.user_answers IS 'Respuestas del usuario: [{questionId, answer, isCorrect, points}]';
|
||||||
|
COMMENT ON COLUMN education.quiz_attempts.time_taken_seconds IS 'Tiempo total empleado en segundos';
|
||||||
54
ddl/schemas/education/tables/10-certificates.sql
Normal file
54
ddl/schemas/education/tables/10-certificates.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.certificates
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.certificates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relaciones
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE RESTRICT,
|
||||||
|
enrollment_id UUID NOT NULL REFERENCES education.enrollments(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Información del certificado
|
||||||
|
certificate_number VARCHAR(50) NOT NULL UNIQUE, -- OQI-CERT-XXXX-YYYY
|
||||||
|
|
||||||
|
-- Datos del certificado (snapshot)
|
||||||
|
user_name VARCHAR(200) NOT NULL,
|
||||||
|
course_title VARCHAR(200) NOT NULL,
|
||||||
|
instructor_name VARCHAR(200),
|
||||||
|
completion_date DATE NOT NULL,
|
||||||
|
|
||||||
|
-- Metadata de logro
|
||||||
|
final_score DECIMAL(5,2), -- Promedio de quizzes
|
||||||
|
total_xp_earned INTEGER,
|
||||||
|
|
||||||
|
-- URL del PDF generado
|
||||||
|
certificate_url VARCHAR(500),
|
||||||
|
|
||||||
|
-- Verificación
|
||||||
|
verification_code VARCHAR(100) UNIQUE, -- Para verificar autenticidad
|
||||||
|
is_verified BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_user_course_cert UNIQUE(user_id, course_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_certificates_user ON education.certificates(user_id);
|
||||||
|
CREATE INDEX idx_certificates_course ON education.certificates(course_id);
|
||||||
|
CREATE INDEX idx_certificates_number ON education.certificates(certificate_number);
|
||||||
|
CREATE INDEX idx_certificates_verification ON education.certificates(verification_code);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.certificates IS 'Certificados emitidos al completar cursos';
|
||||||
|
COMMENT ON COLUMN education.certificates.certificate_number IS 'Número único formato: OQI-CERT-YYYY-NNNNN';
|
||||||
|
COMMENT ON COLUMN education.certificates.verification_code IS 'Código para verificar autenticidad del certificado';
|
||||||
|
COMMENT ON COLUMN education.certificates.final_score IS 'Promedio de todos los quizzes del curso';
|
||||||
47
ddl/schemas/education/tables/11-user_achievements.sql
Normal file
47
ddl/schemas/education/tables/11-user_achievements.sql
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.user_achievements
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: ET-EDU-001-database.md
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.user_achievements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relación
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Tipo de logro
|
||||||
|
achievement_type education.achievement_type NOT NULL,
|
||||||
|
|
||||||
|
-- Información del logro
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
badge_icon_url VARCHAR(500),
|
||||||
|
|
||||||
|
-- Metadata del logro
|
||||||
|
metadata JSONB, -- Información específica del logro
|
||||||
|
|
||||||
|
-- Referencias
|
||||||
|
course_id UUID REFERENCES education.courses(id) ON DELETE SET NULL,
|
||||||
|
quiz_id UUID REFERENCES education.quizzes(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- XP bonus por el logro
|
||||||
|
xp_bonus INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
earned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_user_achievements_user ON education.user_achievements(user_id);
|
||||||
|
CREATE INDEX idx_user_achievements_type ON education.user_achievements(achievement_type);
|
||||||
|
CREATE INDEX idx_user_achievements_earned ON education.user_achievements(earned_at DESC);
|
||||||
|
CREATE INDEX idx_user_achievements_course ON education.user_achievements(course_id);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.user_achievements IS 'Logros/badges obtenidos por los usuarios';
|
||||||
|
COMMENT ON COLUMN education.user_achievements.metadata IS 'Información adicional específica del tipo de logro';
|
||||||
|
COMMENT ON COLUMN education.user_achievements.xp_bonus IS 'XP bonus otorgado por este logro';
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.user_gamification_profile
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: Tabla adicional para gamificación
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.user_gamification_profile (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- XP y nivel
|
||||||
|
total_xp INTEGER NOT NULL DEFAULT 0,
|
||||||
|
current_level INTEGER NOT NULL DEFAULT 1,
|
||||||
|
xp_to_next_level INTEGER NOT NULL DEFAULT 100,
|
||||||
|
|
||||||
|
-- Streaks
|
||||||
|
current_streak_days INTEGER DEFAULT 0,
|
||||||
|
longest_streak_days INTEGER DEFAULT 0,
|
||||||
|
last_activity_date DATE,
|
||||||
|
|
||||||
|
-- Estadísticas
|
||||||
|
total_courses_completed INTEGER DEFAULT 0,
|
||||||
|
total_lessons_completed INTEGER DEFAULT 0,
|
||||||
|
total_quizzes_passed INTEGER DEFAULT 0,
|
||||||
|
total_certificates_earned INTEGER DEFAULT 0,
|
||||||
|
average_quiz_score DECIMAL(5,2) DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Ranking
|
||||||
|
weekly_xp INTEGER DEFAULT 0,
|
||||||
|
monthly_xp INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_user_gamification UNIQUE(user_id),
|
||||||
|
CONSTRAINT valid_level CHECK (current_level >= 1),
|
||||||
|
CONSTRAINT valid_xp CHECK (total_xp >= 0),
|
||||||
|
CONSTRAINT valid_streak CHECK (current_streak_days >= 0 AND longest_streak_days >= 0),
|
||||||
|
CONSTRAINT valid_avg_score CHECK (average_quiz_score >= 0 AND average_quiz_score <= 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_gamification_user ON education.user_gamification_profile(user_id);
|
||||||
|
CREATE INDEX idx_gamification_level ON education.user_gamification_profile(current_level DESC);
|
||||||
|
CREATE INDEX idx_gamification_xp ON education.user_gamification_profile(total_xp DESC);
|
||||||
|
CREATE INDEX idx_gamification_weekly ON education.user_gamification_profile(weekly_xp DESC);
|
||||||
|
CREATE INDEX idx_gamification_monthly ON education.user_gamification_profile(monthly_xp DESC);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.user_gamification_profile IS 'Perfil de gamificación del usuario con XP, niveles, streaks y estadísticas';
|
||||||
|
COMMENT ON COLUMN education.user_gamification_profile.current_streak_days IS 'Días consecutivos de actividad actual';
|
||||||
|
COMMENT ON COLUMN education.user_gamification_profile.longest_streak_days IS 'Racha más larga de días consecutivos';
|
||||||
|
COMMENT ON COLUMN education.user_gamification_profile.weekly_xp IS 'XP acumulado en la semana actual (para leaderboards)';
|
||||||
|
COMMENT ON COLUMN education.user_gamification_profile.monthly_xp IS 'XP acumulado en el mes actual (para leaderboards)';
|
||||||
43
ddl/schemas/education/tables/13-user_activity_log.sql
Normal file
43
ddl/schemas/education/tables/13-user_activity_log.sql
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.user_activity_log
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: Tabla adicional para tracking de actividad
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.user_activity_log (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Tipo de actividad
|
||||||
|
activity_type VARCHAR(50) NOT NULL, -- lesson_view, quiz_complete, course_enroll, etc.
|
||||||
|
|
||||||
|
-- Referencias opcionales
|
||||||
|
course_id UUID REFERENCES education.courses(id) ON DELETE SET NULL,
|
||||||
|
lesson_id UUID REFERENCES education.lessons(id) ON DELETE SET NULL,
|
||||||
|
quiz_id UUID REFERENCES education.quizzes(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
xp_earned INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_activity_user ON education.user_activity_log(user_id);
|
||||||
|
CREATE INDEX idx_activity_type ON education.user_activity_log(activity_type);
|
||||||
|
CREATE INDEX idx_activity_created ON education.user_activity_log(created_at DESC);
|
||||||
|
CREATE INDEX idx_activity_user_date ON education.user_activity_log(user_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_activity_course ON education.user_activity_log(course_id) WHERE course_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.user_activity_log IS 'Log de actividades del usuario en el módulo educativo';
|
||||||
|
COMMENT ON COLUMN education.user_activity_log.activity_type IS 'Tipos: lesson_view, lesson_complete, quiz_start, quiz_complete, course_enroll, etc.';
|
||||||
|
COMMENT ON COLUMN education.user_activity_log.metadata IS 'Información adicional específica del tipo de actividad';
|
||||||
|
COMMENT ON COLUMN education.user_activity_log.xp_earned IS 'XP ganado en esta actividad (si aplica)';
|
||||||
48
ddl/schemas/education/tables/14-course_reviews.sql
Normal file
48
ddl/schemas/education/tables/14-course_reviews.sql
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- TABLE: education.course_reviews
|
||||||
|
-- =====================================================
|
||||||
|
-- Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
-- Módulo: OQI-002 - Education
|
||||||
|
-- Especificación: Tabla adicional para reviews de cursos
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE education.course_reviews (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE,
|
||||||
|
enrollment_id UUID NOT NULL REFERENCES education.enrollments(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Review
|
||||||
|
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||||
|
title VARCHAR(200),
|
||||||
|
content TEXT,
|
||||||
|
|
||||||
|
-- Moderación
|
||||||
|
is_approved BOOLEAN DEFAULT false,
|
||||||
|
is_featured BOOLEAN DEFAULT false,
|
||||||
|
approved_by UUID REFERENCES auth.users(id),
|
||||||
|
approved_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Votos útiles
|
||||||
|
helpful_votes INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_user_course_review UNIQUE(user_id, course_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_reviews_course ON education.course_reviews(course_id);
|
||||||
|
CREATE INDEX idx_reviews_user ON education.course_reviews(user_id);
|
||||||
|
CREATE INDEX idx_reviews_rating ON education.course_reviews(rating);
|
||||||
|
CREATE INDEX idx_reviews_approved ON education.course_reviews(is_approved) WHERE is_approved = true;
|
||||||
|
CREATE INDEX idx_reviews_featured ON education.course_reviews(is_featured) WHERE is_featured = true;
|
||||||
|
CREATE INDEX idx_reviews_helpful ON education.course_reviews(helpful_votes DESC);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE education.course_reviews IS 'Reviews y calificaciones de cursos por usuarios';
|
||||||
|
COMMENT ON COLUMN education.course_reviews.is_approved IS 'Review aprobada por moderador';
|
||||||
|
COMMENT ON COLUMN education.course_reviews.is_featured IS 'Review destacada para mostrar en página del curso';
|
||||||
|
COMMENT ON COLUMN education.course_reviews.helpful_votes IS 'Número de votos útiles de otros usuarios';
|
||||||
55
ddl/schemas/education/uninstall.sh
Executable file
55
ddl/schemas/education/uninstall.sh
Executable file
@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# UNINSTALL SCRIPT - Schema Education
|
||||||
|
# =====================================================
|
||||||
|
# Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
# Módulo: OQI-002 - Education
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DB_HOST="${DB_HOST:-localhost}"
|
||||||
|
DB_PORT="${DB_PORT:-5432}"
|
||||||
|
DB_NAME="${DB_NAME:-trading_platform}"
|
||||||
|
DB_USER="${DB_USER:-postgres}"
|
||||||
|
SCHEMA_NAME="education"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}=================================================${NC}"
|
||||||
|
echo -e "${YELLOW} OrbiQuant IA - Education Schema Uninstall${NC}"
|
||||||
|
echo -e "${YELLOW}=================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${RED}WARNING: This will DROP the entire '$SCHEMA_NAME' schema and ALL its data!${NC}"
|
||||||
|
echo ""
|
||||||
|
read -p "Are you sure you want to continue? (type 'yes' to confirm): " CONFIRM
|
||||||
|
|
||||||
|
if [ "$CONFIRM" != "yes" ]; then
|
||||||
|
echo "Uninstall cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}▶${NC} Dropping schema: $SCHEMA_NAME (CASCADE)"
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "DROP SCHEMA IF EXISTS $SCHEMA_NAME CASCADE;" > /dev/null 2>&1; then
|
||||||
|
echo -e "${GREEN} ✓ Schema dropped successfully${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ Failed to drop schema${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}=================================================${NC}"
|
||||||
|
echo -e "${GREEN} Uninstall Complete!${NC}"
|
||||||
|
echo -e "${GREEN}=================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Schema '$SCHEMA_NAME' has been removed."
|
||||||
|
echo ""
|
||||||
145
ddl/schemas/education/verify.sh
Executable file
145
ddl/schemas/education/verify.sh
Executable file
@ -0,0 +1,145 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# VERIFY SCRIPT - Schema Education
|
||||||
|
# =====================================================
|
||||||
|
# Proyecto: OrbiQuant IA (Trading Platform)
|
||||||
|
# Módulo: OQI-002 - Education
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DB_HOST="${DB_HOST:-localhost}"
|
||||||
|
DB_PORT="${DB_PORT:-5432}"
|
||||||
|
DB_NAME="${DB_NAME:-trading_platform}"
|
||||||
|
DB_USER="${DB_USER:-postgres}"
|
||||||
|
SCHEMA_NAME="education"
|
||||||
|
|
||||||
|
echo -e "${BLUE}=================================================${NC}"
|
||||||
|
echo -e "${BLUE} OrbiQuant IA - Education Schema Verification${NC}"
|
||||||
|
echo -e "${BLUE}=================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if psql is available
|
||||||
|
if ! command -v psql &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: psql command not found${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to run query and return result
|
||||||
|
run_query() {
|
||||||
|
local query=$1
|
||||||
|
PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "$query" 2>/dev/null | xargs
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Configuration:"
|
||||||
|
echo " Database: $DB_NAME"
|
||||||
|
echo " Host: $DB_HOST:$DB_PORT"
|
||||||
|
echo " Schema: $SCHEMA_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if schema exists
|
||||||
|
echo -e "${YELLOW}▶${NC} Checking schema existence..."
|
||||||
|
SCHEMA_EXISTS=$(run_query "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '$SCHEMA_NAME';")
|
||||||
|
if [ "$SCHEMA_EXISTS" -eq "1" ]; then
|
||||||
|
echo -e "${GREEN} ✓ Schema exists${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ Schema not found${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check ENUMs
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}▶${NC} Checking ENUMs..."
|
||||||
|
EXPECTED_ENUMS=("difficulty_level" "course_status" "enrollment_status" "lesson_content_type" "question_type" "achievement_type")
|
||||||
|
ENUM_COUNT=0
|
||||||
|
|
||||||
|
for enum_name in "${EXPECTED_ENUMS[@]}"; do
|
||||||
|
EXISTS=$(run_query "SELECT COUNT(*) FROM pg_type WHERE typname = '$enum_name' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = '$SCHEMA_NAME');")
|
||||||
|
if [ "$EXISTS" -eq "1" ]; then
|
||||||
|
echo -e "${GREEN} ✓ $enum_name${NC}"
|
||||||
|
((ENUM_COUNT++))
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ $enum_name${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check tables
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}▶${NC} Checking tables..."
|
||||||
|
EXPECTED_TABLES=("categories" "courses" "modules" "lessons" "enrollments" "progress" "quizzes" "quiz_questions" "quiz_attempts" "certificates" "user_achievements" "user_gamification_profile" "user_activity_log" "course_reviews")
|
||||||
|
TABLE_COUNT=0
|
||||||
|
|
||||||
|
for table_name in "${EXPECTED_TABLES[@]}"; do
|
||||||
|
EXISTS=$(run_query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '$SCHEMA_NAME' AND table_name = '$table_name';")
|
||||||
|
if [ "$EXISTS" -eq "1" ]; then
|
||||||
|
ROW_COUNT=$(run_query "SELECT COUNT(*) FROM $SCHEMA_NAME.$table_name;")
|
||||||
|
echo -e "${GREEN} ✓ $table_name${NC} ($ROW_COUNT rows)"
|
||||||
|
((TABLE_COUNT++))
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ $table_name${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check functions
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}▶${NC} Checking functions..."
|
||||||
|
EXPECTED_FUNCTIONS=("update_updated_at_column" "update_enrollment_progress" "auto_complete_enrollment" "generate_certificate_number" "update_course_rating_stats" "update_enrollment_count" "update_user_xp" "update_user_streak")
|
||||||
|
FUNCTION_COUNT=0
|
||||||
|
|
||||||
|
for function_name in "${EXPECTED_FUNCTIONS[@]}"; do
|
||||||
|
EXISTS=$(run_query "SELECT COUNT(*) FROM pg_proc WHERE proname = '$function_name' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = '$SCHEMA_NAME');")
|
||||||
|
if [ "$EXISTS" -ge "1" ]; then
|
||||||
|
echo -e "${GREEN} ✓ $function_name${NC}"
|
||||||
|
((FUNCTION_COUNT++))
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ $function_name${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check views
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}▶${NC} Checking views..."
|
||||||
|
EXPECTED_VIEWS=("v_courses_with_stats" "v_user_course_progress" "v_leaderboard_weekly" "v_leaderboard_monthly" "v_leaderboard_alltime" "v_user_statistics" "v_popular_courses")
|
||||||
|
VIEW_COUNT=0
|
||||||
|
|
||||||
|
for view_name in "${EXPECTED_VIEWS[@]}"; do
|
||||||
|
EXISTS=$(run_query "SELECT COUNT(*) FROM information_schema.views WHERE table_schema = '$SCHEMA_NAME' AND table_name = '$view_name';")
|
||||||
|
if [ "$EXISTS" -eq "1" ]; then
|
||||||
|
echo -e "${GREEN} ✓ $view_name${NC}"
|
||||||
|
((VIEW_COUNT++))
|
||||||
|
else
|
||||||
|
echo -e "${RED} ✗ $view_name${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}=================================================${NC}"
|
||||||
|
echo -e "${BLUE} Verification Summary${NC}"
|
||||||
|
echo -e "${BLUE}=================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "ENUMs: $ENUM_COUNT / ${#EXPECTED_ENUMS[@]}"
|
||||||
|
echo "Tables: $TABLE_COUNT / ${#EXPECTED_TABLES[@]}"
|
||||||
|
echo "Functions: $FUNCTION_COUNT / ${#EXPECTED_FUNCTIONS[@]}"
|
||||||
|
echo "Views: $VIEW_COUNT / ${#EXPECTED_VIEWS[@]}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
TOTAL_EXPECTED=$((${#EXPECTED_ENUMS[@]} + ${#EXPECTED_TABLES[@]} + ${#EXPECTED_FUNCTIONS[@]} + ${#EXPECTED_VIEWS[@]}))
|
||||||
|
TOTAL_FOUND=$((ENUM_COUNT + TABLE_COUNT + FUNCTION_COUNT + VIEW_COUNT))
|
||||||
|
|
||||||
|
if [ "$TOTAL_FOUND" -eq "$TOTAL_EXPECTED" ]; then
|
||||||
|
echo -e "${GREEN}✓ All components verified successfully!${NC}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠ Some components are missing ($TOTAL_FOUND / $TOTAL_EXPECTED)${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
131
ddl/schemas/financial/00-enums.sql
Normal file
131
ddl/schemas/financial/00-enums.sql
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - FINANCIAL SCHEMA ENUMS
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Type definitions for financial domain
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Tipos de wallet
|
||||||
|
CREATE TYPE financial.wallet_type AS ENUM (
|
||||||
|
'trading', -- Para operaciones de trading
|
||||||
|
'investment', -- Para cuentas PAMM
|
||||||
|
'earnings', -- Para ganancias/distribuciones
|
||||||
|
'referral' -- Para bonos de referidos
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estados de wallet
|
||||||
|
CREATE TYPE financial.wallet_status AS ENUM (
|
||||||
|
'active',
|
||||||
|
'frozen',
|
||||||
|
'closed'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipos de transacción
|
||||||
|
CREATE TYPE financial.transaction_type AS ENUM (
|
||||||
|
'deposit',
|
||||||
|
'withdrawal',
|
||||||
|
'transfer_in',
|
||||||
|
'transfer_out',
|
||||||
|
'fee',
|
||||||
|
'refund',
|
||||||
|
'earning',
|
||||||
|
'distribution',
|
||||||
|
'bonus'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estados de transacción
|
||||||
|
CREATE TYPE financial.transaction_status AS ENUM (
|
||||||
|
'pending',
|
||||||
|
'processing',
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
'cancelled',
|
||||||
|
'reversed'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Planes de suscripción
|
||||||
|
CREATE TYPE financial.subscription_plan AS ENUM (
|
||||||
|
'free',
|
||||||
|
'basic',
|
||||||
|
'pro',
|
||||||
|
'premium',
|
||||||
|
'enterprise'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estados de suscripción
|
||||||
|
CREATE TYPE financial.subscription_status AS ENUM (
|
||||||
|
'active',
|
||||||
|
'past_due',
|
||||||
|
'cancelled',
|
||||||
|
'incomplete',
|
||||||
|
'trialing',
|
||||||
|
'unpaid',
|
||||||
|
'paused'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Monedas soportadas
|
||||||
|
CREATE TYPE financial.currency_code AS ENUM (
|
||||||
|
'USD',
|
||||||
|
'MXN',
|
||||||
|
'EUR'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Métodos de pago
|
||||||
|
CREATE TYPE financial.payment_method AS ENUM (
|
||||||
|
'card',
|
||||||
|
'bank_transfer',
|
||||||
|
'wire',
|
||||||
|
'crypto',
|
||||||
|
'paypal',
|
||||||
|
'stripe'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estados de pago
|
||||||
|
CREATE TYPE financial.payment_status AS ENUM (
|
||||||
|
'pending',
|
||||||
|
'processing',
|
||||||
|
'succeeded',
|
||||||
|
'failed',
|
||||||
|
'cancelled',
|
||||||
|
'refunded'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipos de invoice
|
||||||
|
CREATE TYPE financial.invoice_type AS ENUM (
|
||||||
|
'subscription',
|
||||||
|
'one_time',
|
||||||
|
'usage'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estados de invoice
|
||||||
|
CREATE TYPE financial.invoice_status AS ENUM (
|
||||||
|
'draft',
|
||||||
|
'open',
|
||||||
|
'paid',
|
||||||
|
'void',
|
||||||
|
'uncollectible'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Acciones de auditoría
|
||||||
|
CREATE TYPE financial.audit_action AS ENUM (
|
||||||
|
'created',
|
||||||
|
'balance_updated',
|
||||||
|
'status_changed',
|
||||||
|
'limit_changed',
|
||||||
|
'frozen',
|
||||||
|
'unfrozen',
|
||||||
|
'closed'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE financial.wallet_type IS 'Tipos de wallets en el sistema';
|
||||||
|
COMMENT ON TYPE financial.wallet_status IS 'Estados posibles de una wallet';
|
||||||
|
COMMENT ON TYPE financial.transaction_type IS 'Tipos de transacciones financieras';
|
||||||
|
COMMENT ON TYPE financial.transaction_status IS 'Estados del ciclo de vida de una transacción';
|
||||||
|
COMMENT ON TYPE financial.subscription_plan IS 'Planes de suscripción disponibles';
|
||||||
|
COMMENT ON TYPE financial.subscription_status IS 'Estados de suscripción según Stripe';
|
||||||
|
COMMENT ON TYPE financial.currency_code IS 'Códigos de moneda ISO 4217 soportados';
|
||||||
|
COMMENT ON TYPE financial.payment_method IS 'Métodos de pago aceptados';
|
||||||
|
COMMENT ON TYPE financial.payment_status IS 'Estados de procesamiento de pagos';
|
||||||
|
COMMENT ON TYPE financial.invoice_type IS 'Tipos de factura';
|
||||||
|
COMMENT ON TYPE financial.invoice_status IS 'Estados de factura';
|
||||||
|
COMMENT ON TYPE financial.audit_action IS 'Acciones auditables en wallets';
|
||||||
283
ddl/schemas/financial/functions/01-update_wallet_balance.sql
Normal file
283
ddl/schemas/financial/functions/01-update_wallet_balance.sql
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - UPDATE WALLET BALANCE FUNCTION
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Safely update wallet balance with audit trail
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.update_wallet_balance(
|
||||||
|
p_wallet_id UUID,
|
||||||
|
p_amount DECIMAL(20,8),
|
||||||
|
p_operation VARCHAR(20), -- 'add', 'subtract', 'set'
|
||||||
|
p_transaction_id UUID DEFAULT NULL,
|
||||||
|
p_actor_id UUID DEFAULT NULL,
|
||||||
|
p_actor_type VARCHAR(50) DEFAULT 'system',
|
||||||
|
p_reason TEXT DEFAULT NULL,
|
||||||
|
p_metadata JSONB DEFAULT '{}'
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
success BOOLEAN,
|
||||||
|
new_balance DECIMAL(20,8),
|
||||||
|
new_available DECIMAL(20,8),
|
||||||
|
error_message TEXT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_wallet RECORD;
|
||||||
|
v_old_balance DECIMAL(20,8);
|
||||||
|
v_old_available DECIMAL(20,8);
|
||||||
|
v_new_balance DECIMAL(20,8);
|
||||||
|
v_new_available DECIMAL(20,8);
|
||||||
|
BEGIN
|
||||||
|
-- Lock wallet row for update
|
||||||
|
SELECT * INTO v_wallet
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE id = p_wallet_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
-- Validar que existe
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN QUERY SELECT false, 0::DECIMAL, 0::DECIMAL, 'Wallet not found';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Validar que está activa
|
||||||
|
IF v_wallet.status != 'active' THEN
|
||||||
|
RETURN QUERY SELECT false, v_wallet.balance, v_wallet.available_balance,
|
||||||
|
'Wallet is not active (status: ' || v_wallet.status::TEXT || ')';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Guardar valores antiguos
|
||||||
|
v_old_balance := v_wallet.balance;
|
||||||
|
v_old_available := v_wallet.available_balance;
|
||||||
|
|
||||||
|
-- Calcular nuevo balance según operación
|
||||||
|
CASE p_operation
|
||||||
|
WHEN 'add' THEN
|
||||||
|
v_new_balance := v_old_balance + p_amount;
|
||||||
|
v_new_available := v_old_available + p_amount;
|
||||||
|
WHEN 'subtract' THEN
|
||||||
|
v_new_balance := v_old_balance - p_amount;
|
||||||
|
v_new_available := v_old_available - p_amount;
|
||||||
|
WHEN 'set' THEN
|
||||||
|
v_new_balance := p_amount;
|
||||||
|
v_new_available := p_amount - v_wallet.pending_balance;
|
||||||
|
ELSE
|
||||||
|
RETURN QUERY SELECT false, v_old_balance, v_old_available,
|
||||||
|
'Invalid operation: ' || p_operation;
|
||||||
|
RETURN;
|
||||||
|
END CASE;
|
||||||
|
|
||||||
|
-- Validar que no quede negativo
|
||||||
|
IF v_new_balance < 0 THEN
|
||||||
|
RETURN QUERY SELECT false, v_old_balance, v_old_available,
|
||||||
|
'Insufficient balance (current: ' || v_old_balance::TEXT || ', required: ' || p_amount::TEXT || ')';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_new_available < 0 THEN
|
||||||
|
RETURN QUERY SELECT false, v_old_balance, v_old_available,
|
||||||
|
'Insufficient available balance (current: ' || v_old_available::TEXT || ')';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Validar min_balance si existe
|
||||||
|
IF v_wallet.min_balance IS NOT NULL AND v_new_available < v_wallet.min_balance THEN
|
||||||
|
RETURN QUERY SELECT false, v_old_balance, v_old_available,
|
||||||
|
'Would violate minimum balance requirement (min: ' || v_wallet.min_balance::TEXT || ')';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Actualizar wallet
|
||||||
|
UPDATE financial.wallets
|
||||||
|
SET
|
||||||
|
balance = v_new_balance,
|
||||||
|
available_balance = v_new_available,
|
||||||
|
last_transaction_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
|
||||||
|
-- Registrar en audit log
|
||||||
|
INSERT INTO financial.wallet_audit_log (
|
||||||
|
wallet_id,
|
||||||
|
action,
|
||||||
|
actor_id,
|
||||||
|
actor_type,
|
||||||
|
old_values,
|
||||||
|
new_values,
|
||||||
|
balance_before,
|
||||||
|
balance_after,
|
||||||
|
transaction_id,
|
||||||
|
reason,
|
||||||
|
metadata
|
||||||
|
) VALUES (
|
||||||
|
p_wallet_id,
|
||||||
|
'balance_updated',
|
||||||
|
p_actor_id,
|
||||||
|
p_actor_type,
|
||||||
|
jsonb_build_object(
|
||||||
|
'balance', v_old_balance,
|
||||||
|
'available_balance', v_old_available
|
||||||
|
),
|
||||||
|
jsonb_build_object(
|
||||||
|
'balance', v_new_balance,
|
||||||
|
'available_balance', v_new_available
|
||||||
|
),
|
||||||
|
v_old_balance,
|
||||||
|
v_new_balance,
|
||||||
|
p_transaction_id,
|
||||||
|
p_reason,
|
||||||
|
p_metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Retornar éxito
|
||||||
|
RETURN QUERY SELECT true, v_new_balance, v_new_available, NULL::TEXT;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.update_wallet_balance IS 'Safely update wallet balance with validation and audit trail';
|
||||||
|
|
||||||
|
-- Función helper para reservar fondos (pending balance)
|
||||||
|
CREATE OR REPLACE FUNCTION financial.reserve_wallet_funds(
|
||||||
|
p_wallet_id UUID,
|
||||||
|
p_amount DECIMAL(20,8),
|
||||||
|
p_reason TEXT DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
success BOOLEAN,
|
||||||
|
new_available DECIMAL(20,8),
|
||||||
|
new_pending DECIMAL(20,8),
|
||||||
|
error_message TEXT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_wallet RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Lock wallet
|
||||||
|
SELECT * INTO v_wallet
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE id = p_wallet_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN QUERY SELECT false, 0::DECIMAL, 0::DECIMAL, 'Wallet not found';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_wallet.status != 'active' THEN
|
||||||
|
RETURN QUERY SELECT false, v_wallet.available_balance, v_wallet.pending_balance,
|
||||||
|
'Wallet is not active';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_wallet.available_balance < p_amount THEN
|
||||||
|
RETURN QUERY SELECT false, v_wallet.available_balance, v_wallet.pending_balance,
|
||||||
|
'Insufficient available balance';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Mover de available a pending
|
||||||
|
UPDATE financial.wallets
|
||||||
|
SET
|
||||||
|
available_balance = available_balance - p_amount,
|
||||||
|
pending_balance = pending_balance + p_amount,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
|
||||||
|
-- Audit log
|
||||||
|
INSERT INTO financial.wallet_audit_log (
|
||||||
|
wallet_id, action, actor_type, reason,
|
||||||
|
old_values, new_values
|
||||||
|
) VALUES (
|
||||||
|
p_wallet_id, 'balance_updated', 'system', p_reason,
|
||||||
|
jsonb_build_object('available', v_wallet.available_balance, 'pending', v_wallet.pending_balance),
|
||||||
|
jsonb_build_object('available', v_wallet.available_balance - p_amount, 'pending', v_wallet.pending_balance + p_amount)
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT
|
||||||
|
true,
|
||||||
|
v_wallet.available_balance - p_amount,
|
||||||
|
v_wallet.pending_balance + p_amount,
|
||||||
|
NULL::TEXT;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.reserve_wallet_funds IS 'Reserve funds by moving from available to pending balance';
|
||||||
|
|
||||||
|
-- Función helper para liberar fondos reservados
|
||||||
|
CREATE OR REPLACE FUNCTION financial.release_wallet_funds(
|
||||||
|
p_wallet_id UUID,
|
||||||
|
p_amount DECIMAL(20,8),
|
||||||
|
p_to_available BOOLEAN DEFAULT true,
|
||||||
|
p_reason TEXT DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
success BOOLEAN,
|
||||||
|
new_available DECIMAL(20,8),
|
||||||
|
new_pending DECIMAL(20,8),
|
||||||
|
error_message TEXT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_wallet RECORD;
|
||||||
|
v_new_balance DECIMAL(20,8);
|
||||||
|
BEGIN
|
||||||
|
-- Lock wallet
|
||||||
|
SELECT * INTO v_wallet
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE id = p_wallet_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN QUERY SELECT false, 0::DECIMAL, 0::DECIMAL, 'Wallet not found';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_wallet.pending_balance < p_amount THEN
|
||||||
|
RETURN QUERY SELECT false, v_wallet.available_balance, v_wallet.pending_balance,
|
||||||
|
'Insufficient pending balance';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Liberar fondos
|
||||||
|
IF p_to_available THEN
|
||||||
|
-- Devolver a available
|
||||||
|
v_new_balance := v_wallet.balance;
|
||||||
|
UPDATE financial.wallets
|
||||||
|
SET
|
||||||
|
available_balance = available_balance + p_amount,
|
||||||
|
pending_balance = pending_balance - p_amount,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
ELSE
|
||||||
|
-- Remover completamente (ej: después de withdrawal exitoso)
|
||||||
|
v_new_balance := v_wallet.balance - p_amount;
|
||||||
|
UPDATE financial.wallets
|
||||||
|
SET
|
||||||
|
balance = balance - p_amount,
|
||||||
|
pending_balance = pending_balance - p_amount,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Audit log
|
||||||
|
INSERT INTO financial.wallet_audit_log (
|
||||||
|
wallet_id, action, actor_type, reason, metadata
|
||||||
|
) VALUES (
|
||||||
|
p_wallet_id, 'balance_updated', 'system', p_reason,
|
||||||
|
jsonb_build_object('released_amount', p_amount, 'to_available', p_to_available)
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT available_balance, pending_balance INTO v_wallet
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
|
||||||
|
RETURN QUERY SELECT true, v_wallet.available_balance, v_wallet.pending_balance, NULL::TEXT;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.release_wallet_funds IS 'Release reserved funds back to available or remove from balance';
|
||||||
327
ddl/schemas/financial/functions/02-process_transaction.sql
Normal file
327
ddl/schemas/financial/functions/02-process_transaction.sql
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - PROCESS TRANSACTION FUNCTION
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Create and process wallet transactions atomically
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.process_transaction(
|
||||||
|
p_wallet_id UUID,
|
||||||
|
p_transaction_type financial.transaction_type,
|
||||||
|
p_amount DECIMAL(20,8),
|
||||||
|
p_currency financial.currency_code,
|
||||||
|
p_fee DECIMAL(15,8) DEFAULT 0,
|
||||||
|
p_description TEXT DEFAULT NULL,
|
||||||
|
p_reference_id VARCHAR(100) DEFAULT NULL,
|
||||||
|
p_destination_wallet_id UUID DEFAULT NULL,
|
||||||
|
p_idempotency_key VARCHAR(255) DEFAULT NULL,
|
||||||
|
p_metadata JSONB DEFAULT '{}',
|
||||||
|
p_auto_complete BOOLEAN DEFAULT false
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
success BOOLEAN,
|
||||||
|
transaction_id UUID,
|
||||||
|
new_balance DECIMAL(20,8),
|
||||||
|
error_message TEXT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_wallet RECORD;
|
||||||
|
v_tx_id UUID;
|
||||||
|
v_existing_status financial.transaction_status; -- Para validación de idempotencia
|
||||||
|
v_balance_before DECIMAL(20,8);
|
||||||
|
v_balance_after DECIMAL(20,8);
|
||||||
|
v_update_result RECORD;
|
||||||
|
v_dest_tx_id UUID;
|
||||||
|
BEGIN
|
||||||
|
-- Validar idempotency
|
||||||
|
IF p_idempotency_key IS NOT NULL THEN
|
||||||
|
SELECT id, status INTO v_tx_id, v_existing_status
|
||||||
|
FROM financial.wallet_transactions
|
||||||
|
WHERE idempotency_key = p_idempotency_key;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
-- Transacción ya existe
|
||||||
|
SELECT balance INTO v_balance_after
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
|
||||||
|
RETURN QUERY SELECT
|
||||||
|
true,
|
||||||
|
v_tx_id,
|
||||||
|
v_balance_after,
|
||||||
|
'Transaction already exists (idempotent)'::TEXT;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Lock wallet
|
||||||
|
SELECT * INTO v_wallet
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE id = p_wallet_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN QUERY SELECT false, NULL::UUID, 0::DECIMAL, 'Wallet not found';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_wallet.status != 'active' THEN
|
||||||
|
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
|
||||||
|
'Wallet is not active (status: ' || v_wallet.status::TEXT || ')';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Validar currency match
|
||||||
|
IF v_wallet.currency != p_currency THEN
|
||||||
|
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
|
||||||
|
'Currency mismatch (wallet: ' || v_wallet.currency::TEXT || ', transaction: ' || p_currency::TEXT || ')';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Validar destination para transfers
|
||||||
|
IF p_transaction_type IN ('transfer_out', 'transfer_in') AND p_destination_wallet_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
|
||||||
|
'Transfer requires destination_wallet_id';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- No permitir self-transfers
|
||||||
|
IF p_destination_wallet_id = p_wallet_id THEN
|
||||||
|
RETURN QUERY SELECT false, NULL::UUID, v_wallet.balance,
|
||||||
|
'Cannot transfer to same wallet';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_balance_before := v_wallet.balance;
|
||||||
|
|
||||||
|
-- Crear transacción
|
||||||
|
INSERT INTO financial.wallet_transactions (
|
||||||
|
wallet_id,
|
||||||
|
transaction_type,
|
||||||
|
status,
|
||||||
|
amount,
|
||||||
|
fee,
|
||||||
|
currency,
|
||||||
|
balance_before,
|
||||||
|
destination_wallet_id,
|
||||||
|
reference_id,
|
||||||
|
description,
|
||||||
|
metadata,
|
||||||
|
idempotency_key,
|
||||||
|
processed_at
|
||||||
|
) VALUES (
|
||||||
|
p_wallet_id,
|
||||||
|
p_transaction_type,
|
||||||
|
CASE WHEN p_auto_complete THEN 'completed'::financial.transaction_status
|
||||||
|
ELSE 'pending'::financial.transaction_status END,
|
||||||
|
p_amount,
|
||||||
|
p_fee,
|
||||||
|
p_currency,
|
||||||
|
v_balance_before,
|
||||||
|
p_destination_wallet_id,
|
||||||
|
p_reference_id,
|
||||||
|
p_description,
|
||||||
|
p_metadata,
|
||||||
|
p_idempotency_key,
|
||||||
|
CASE WHEN p_auto_complete THEN NOW() ELSE NULL END
|
||||||
|
)
|
||||||
|
RETURNING id INTO v_tx_id;
|
||||||
|
|
||||||
|
-- Si es auto_complete, procesar inmediatamente
|
||||||
|
IF p_auto_complete THEN
|
||||||
|
-- Determinar operación de balance
|
||||||
|
CASE p_transaction_type
|
||||||
|
WHEN 'deposit', 'transfer_in', 'earning', 'distribution', 'bonus', 'refund' THEN
|
||||||
|
-- Aumentar balance
|
||||||
|
SELECT * INTO v_update_result
|
||||||
|
FROM financial.update_wallet_balance(
|
||||||
|
p_wallet_id,
|
||||||
|
p_amount - p_fee,
|
||||||
|
'add',
|
||||||
|
v_tx_id,
|
||||||
|
NULL,
|
||||||
|
'system',
|
||||||
|
'Transaction: ' || p_transaction_type::TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
WHEN 'withdrawal', 'transfer_out', 'fee' THEN
|
||||||
|
-- Disminuir balance
|
||||||
|
SELECT * INTO v_update_result
|
||||||
|
FROM financial.update_wallet_balance(
|
||||||
|
p_wallet_id,
|
||||||
|
p_amount + p_fee,
|
||||||
|
'subtract',
|
||||||
|
v_tx_id,
|
||||||
|
NULL,
|
||||||
|
'system',
|
||||||
|
'Transaction: ' || p_transaction_type::TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RETURN QUERY SELECT false, v_tx_id, v_balance_before,
|
||||||
|
'Unknown transaction type: ' || p_transaction_type::TEXT;
|
||||||
|
RETURN;
|
||||||
|
END CASE;
|
||||||
|
|
||||||
|
-- Verificar éxito de actualización
|
||||||
|
IF NOT v_update_result.success THEN
|
||||||
|
-- Marcar transacción como fallida
|
||||||
|
UPDATE financial.wallet_transactions
|
||||||
|
SET
|
||||||
|
status = 'failed',
|
||||||
|
failed_reason = v_update_result.error_message,
|
||||||
|
failed_at = NOW()
|
||||||
|
WHERE id = v_tx_id;
|
||||||
|
|
||||||
|
RETURN QUERY SELECT false, v_tx_id, v_balance_before, v_update_result.error_message;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_balance_after := v_update_result.new_balance;
|
||||||
|
|
||||||
|
-- Actualizar balance_after en transacción
|
||||||
|
UPDATE financial.wallet_transactions
|
||||||
|
SET
|
||||||
|
balance_after = v_balance_after,
|
||||||
|
completed_at = NOW()
|
||||||
|
WHERE id = v_tx_id;
|
||||||
|
|
||||||
|
-- Si es transfer_out, crear transfer_in en destino
|
||||||
|
IF p_transaction_type = 'transfer_out' AND p_destination_wallet_id IS NOT NULL THEN
|
||||||
|
SELECT * INTO v_update_result
|
||||||
|
FROM financial.process_transaction(
|
||||||
|
p_destination_wallet_id,
|
||||||
|
'transfer_in',
|
||||||
|
p_amount - p_fee, -- El fee lo paga el origen
|
||||||
|
p_currency,
|
||||||
|
0, -- Sin fee adicional en destino
|
||||||
|
'Transfer from wallet ' || p_wallet_id::TEXT,
|
||||||
|
p_reference_id,
|
||||||
|
p_wallet_id, -- Origen como destino inverso
|
||||||
|
p_idempotency_key || '_dest', -- Idempotency para destino
|
||||||
|
p_metadata,
|
||||||
|
true -- Auto-complete
|
||||||
|
);
|
||||||
|
|
||||||
|
IF v_update_result.success THEN
|
||||||
|
v_dest_tx_id := v_update_result.transaction_id;
|
||||||
|
|
||||||
|
-- Vincular transacciones
|
||||||
|
UPDATE financial.wallet_transactions
|
||||||
|
SET related_transaction_id = v_dest_tx_id
|
||||||
|
WHERE id = v_tx_id;
|
||||||
|
|
||||||
|
UPDATE financial.wallet_transactions
|
||||||
|
SET related_transaction_id = v_tx_id
|
||||||
|
WHERE id = v_dest_tx_id;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Actualizar totals en wallet
|
||||||
|
IF p_transaction_type IN ('deposit', 'transfer_in') THEN
|
||||||
|
UPDATE financial.wallets
|
||||||
|
SET total_deposits = total_deposits + p_amount
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
ELSIF p_transaction_type IN ('withdrawal', 'transfer_out') THEN
|
||||||
|
UPDATE financial.wallets
|
||||||
|
SET total_withdrawals = total_withdrawals + p_amount
|
||||||
|
WHERE id = p_wallet_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
-- Transaction pending, no balance update yet
|
||||||
|
v_balance_after := v_balance_before;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY SELECT true, v_tx_id, v_balance_after, NULL::TEXT;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.process_transaction IS 'Create and optionally complete a wallet transaction atomically';
|
||||||
|
|
||||||
|
-- Función para completar transacción pendiente
|
||||||
|
CREATE OR REPLACE FUNCTION financial.complete_transaction(
|
||||||
|
p_transaction_id UUID
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
success BOOLEAN,
|
||||||
|
new_balance DECIMAL(20,8),
|
||||||
|
error_message TEXT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_tx RECORD;
|
||||||
|
v_update_result RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Lock transaction
|
||||||
|
SELECT * INTO v_tx
|
||||||
|
FROM financial.wallet_transactions
|
||||||
|
WHERE id = p_transaction_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN QUERY SELECT false, 0::DECIMAL, 'Transaction not found';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_tx.status != 'pending' THEN
|
||||||
|
RETURN QUERY SELECT false, 0::DECIMAL,
|
||||||
|
'Transaction is not pending (status: ' || v_tx.status::TEXT || ')';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Procesar según tipo
|
||||||
|
CASE v_tx.transaction_type
|
||||||
|
WHEN 'deposit', 'transfer_in', 'earning', 'distribution', 'bonus', 'refund' THEN
|
||||||
|
SELECT * INTO v_update_result
|
||||||
|
FROM financial.update_wallet_balance(
|
||||||
|
v_tx.wallet_id,
|
||||||
|
v_tx.amount - v_tx.fee,
|
||||||
|
'add',
|
||||||
|
p_transaction_id
|
||||||
|
);
|
||||||
|
|
||||||
|
WHEN 'withdrawal', 'transfer_out', 'fee' THEN
|
||||||
|
SELECT * INTO v_update_result
|
||||||
|
FROM financial.update_wallet_balance(
|
||||||
|
v_tx.wallet_id,
|
||||||
|
v_tx.amount + v_tx.fee,
|
||||||
|
'subtract',
|
||||||
|
p_transaction_id
|
||||||
|
);
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RETURN QUERY SELECT false, 0::DECIMAL, 'Unknown transaction type';
|
||||||
|
RETURN;
|
||||||
|
END CASE;
|
||||||
|
|
||||||
|
IF NOT v_update_result.success THEN
|
||||||
|
-- Marcar como fallida
|
||||||
|
UPDATE financial.wallet_transactions
|
||||||
|
SET
|
||||||
|
status = 'failed',
|
||||||
|
failed_reason = v_update_result.error_message,
|
||||||
|
failed_at = NOW()
|
||||||
|
WHERE id = p_transaction_id;
|
||||||
|
|
||||||
|
RETURN QUERY SELECT false, 0::DECIMAL, v_update_result.error_message;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Marcar como completada
|
||||||
|
UPDATE financial.wallet_transactions
|
||||||
|
SET
|
||||||
|
status = 'completed',
|
||||||
|
balance_after = v_update_result.new_balance,
|
||||||
|
completed_at = NOW(),
|
||||||
|
processed_at = COALESCE(processed_at, NOW())
|
||||||
|
WHERE id = p_transaction_id;
|
||||||
|
|
||||||
|
RETURN QUERY SELECT true, v_update_result.new_balance, NULL::TEXT;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.complete_transaction IS 'Complete a pending wallet transaction';
|
||||||
278
ddl/schemas/financial/functions/03-triggers.sql
Normal file
278
ddl/schemas/financial/functions/03-triggers.sql
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - FINANCIAL SCHEMA TRIGGERS
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Automated triggers for data integrity and audit
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Update timestamps
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.update_timestamp()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Apply to all tables with updated_at
|
||||||
|
CREATE TRIGGER trigger_wallets_updated_at
|
||||||
|
BEFORE UPDATE ON financial.wallets
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.update_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_transactions_updated_at
|
||||||
|
BEFORE UPDATE ON financial.wallet_transactions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.update_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_subscriptions_updated_at
|
||||||
|
BEFORE UPDATE ON financial.subscriptions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.update_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_payments_updated_at
|
||||||
|
BEFORE UPDATE ON financial.payments
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.update_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_invoices_updated_at
|
||||||
|
BEFORE UPDATE ON financial.invoices
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.update_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_exchange_rates_updated_at
|
||||||
|
BEFORE UPDATE ON financial.currency_exchange_rates
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.update_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_wallet_limits_updated_at
|
||||||
|
BEFORE UPDATE ON financial.wallet_limits
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.update_timestamp();
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Auto-generate invoice number
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.generate_invoice_number()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.invoice_number IS NULL THEN
|
||||||
|
NEW.invoice_number := 'INV-' || TO_CHAR(NOW(), 'YYYYMM') || '-' ||
|
||||||
|
LPAD(nextval('financial.invoice_number_seq')::TEXT, 6, '0');
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_invoice_number
|
||||||
|
BEFORE INSERT ON financial.invoices
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.generate_invoice_number();
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Validate wallet balance consistency
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.validate_wallet_balance()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Validar que balance = available + pending
|
||||||
|
IF NEW.balance != (NEW.available_balance + NEW.pending_balance) THEN
|
||||||
|
RAISE EXCEPTION 'Balance consistency error: balance (%) != available (%) + pending (%)',
|
||||||
|
NEW.balance, NEW.available_balance, NEW.pending_balance;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Validar que no haya negativos
|
||||||
|
IF NEW.balance < 0 OR NEW.available_balance < 0 OR NEW.pending_balance < 0 THEN
|
||||||
|
RAISE EXCEPTION 'Negative balance detected: balance=%, available=%, pending=%',
|
||||||
|
NEW.balance, NEW.available_balance, NEW.pending_balance;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_wallet_balance_validation
|
||||||
|
BEFORE INSERT OR UPDATE ON financial.wallets
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.validate_wallet_balance();
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Audit wallet status changes
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.audit_wallet_status_change()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Solo auditar si cambió el status
|
||||||
|
IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN
|
||||||
|
INSERT INTO financial.wallet_audit_log (
|
||||||
|
wallet_id,
|
||||||
|
action,
|
||||||
|
actor_type,
|
||||||
|
old_values,
|
||||||
|
new_values,
|
||||||
|
reason
|
||||||
|
) VALUES (
|
||||||
|
NEW.id,
|
||||||
|
'status_changed',
|
||||||
|
'system',
|
||||||
|
jsonb_build_object('status', OLD.status),
|
||||||
|
jsonb_build_object('status', NEW.status),
|
||||||
|
'Status changed from ' || OLD.status::TEXT || ' to ' || NEW.status::TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Si se cerró, registrar timestamp
|
||||||
|
IF NEW.status = 'closed' AND NEW.closed_at IS NULL THEN
|
||||||
|
NEW.closed_at := NOW();
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_wallet_status_audit
|
||||||
|
BEFORE UPDATE ON financial.wallets
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.audit_wallet_status_change();
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Prevent modification of completed transactions
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.protect_completed_transactions()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF OLD.status = 'completed' AND NEW.status != 'completed' THEN
|
||||||
|
RAISE EXCEPTION 'Cannot modify completed transaction %', OLD.id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF OLD.status = 'completed' AND (
|
||||||
|
OLD.amount != NEW.amount OR
|
||||||
|
OLD.wallet_id != NEW.wallet_id OR
|
||||||
|
OLD.transaction_type != NEW.transaction_type
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Cannot modify core fields of completed transaction %', OLD.id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_protect_completed_tx
|
||||||
|
BEFORE UPDATE ON financial.wallet_transactions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.protect_completed_transactions();
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Set payment succeeded_at timestamp
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.set_payment_timestamps()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Set succeeded_at when status changes to succeeded
|
||||||
|
IF NEW.status = 'succeeded' AND OLD.status != 'succeeded' THEN
|
||||||
|
NEW.succeeded_at := NOW();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Set failed_at when status changes to failed
|
||||||
|
IF NEW.status = 'failed' AND OLD.status != 'failed' THEN
|
||||||
|
NEW.failed_at := NOW();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_payment_timestamps
|
||||||
|
BEFORE UPDATE ON financial.payments
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.set_payment_timestamps();
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Update subscription ended_at
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.set_subscription_ended_at()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Set ended_at when status changes to cancelled and cancel_at_period_end is false
|
||||||
|
IF NEW.status = 'cancelled' AND
|
||||||
|
OLD.status != 'cancelled' AND
|
||||||
|
NOT NEW.cancel_at_period_end AND
|
||||||
|
NEW.ended_at IS NULL THEN
|
||||||
|
NEW.ended_at := NOW();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_subscription_ended_at
|
||||||
|
BEFORE UPDATE ON financial.subscriptions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.set_subscription_ended_at();
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- TRIGGER: Validate transaction currency matches wallet
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION financial.validate_transaction_currency()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_wallet_currency financial.currency_code;
|
||||||
|
BEGIN
|
||||||
|
-- Get wallet currency
|
||||||
|
SELECT currency INTO v_wallet_currency
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE id = NEW.wallet_id;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'Wallet % not found', NEW.wallet_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Validate currency match
|
||||||
|
IF NEW.currency != v_wallet_currency THEN
|
||||||
|
RAISE EXCEPTION 'Transaction currency (%) does not match wallet currency (%)',
|
||||||
|
NEW.currency, v_wallet_currency;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_transaction_currency_validation
|
||||||
|
BEFORE INSERT ON financial.wallet_transactions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION financial.validate_transaction_currency();
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.update_timestamp IS 'Auto-update updated_at timestamp';
|
||||||
|
COMMENT ON FUNCTION financial.generate_invoice_number IS 'Auto-generate invoice number with format INV-YYYYMM-XXXXXX';
|
||||||
|
COMMENT ON FUNCTION financial.validate_wallet_balance IS 'Ensure balance = available + pending';
|
||||||
|
COMMENT ON FUNCTION financial.audit_wallet_status_change IS 'Log wallet status changes to audit log';
|
||||||
|
COMMENT ON FUNCTION financial.protect_completed_transactions IS 'Prevent modification of completed transactions';
|
||||||
|
COMMENT ON FUNCTION financial.set_payment_timestamps IS 'Auto-set succeeded_at and failed_at timestamps';
|
||||||
|
COMMENT ON FUNCTION financial.set_subscription_ended_at IS 'Auto-set ended_at when subscription is cancelled';
|
||||||
|
COMMENT ON FUNCTION financial.validate_transaction_currency IS 'Ensure transaction currency matches wallet currency';
|
||||||
258
ddl/schemas/financial/functions/04-views.sql
Normal file
258
ddl/schemas/financial/functions/04-views.sql
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - FINANCIAL SCHEMA VIEWS
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Useful views for common financial queries
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Active user wallets summary
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_user_wallets_summary AS
|
||||||
|
SELECT
|
||||||
|
w.user_id,
|
||||||
|
w.wallet_type,
|
||||||
|
w.currency,
|
||||||
|
w.balance,
|
||||||
|
w.available_balance,
|
||||||
|
w.pending_balance,
|
||||||
|
w.status,
|
||||||
|
w.last_transaction_at,
|
||||||
|
w.total_deposits,
|
||||||
|
w.total_withdrawals,
|
||||||
|
w.created_at,
|
||||||
|
-- Transaction counts
|
||||||
|
(SELECT COUNT(*)
|
||||||
|
FROM financial.wallet_transactions wt
|
||||||
|
WHERE wt.wallet_id = w.id AND wt.status = 'completed') as total_transactions,
|
||||||
|
(SELECT COUNT(*)
|
||||||
|
FROM financial.wallet_transactions wt
|
||||||
|
WHERE wt.wallet_id = w.id AND wt.status = 'pending') as pending_transactions,
|
||||||
|
-- Latest transaction
|
||||||
|
(SELECT wt.created_at
|
||||||
|
FROM financial.wallet_transactions wt
|
||||||
|
WHERE wt.wallet_id = w.id
|
||||||
|
ORDER BY wt.created_at DESC
|
||||||
|
LIMIT 1) as last_tx_date
|
||||||
|
FROM financial.wallets w
|
||||||
|
WHERE w.status = 'active';
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_user_wallets_summary IS 'Active wallets with transaction statistics';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: User total balance across all wallets (USD)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_user_total_balance AS
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
SUM(CASE WHEN currency = 'USD' THEN balance ELSE 0 END) as total_usd,
|
||||||
|
SUM(CASE WHEN currency = 'MXN' THEN balance ELSE 0 END) as total_mxn,
|
||||||
|
SUM(CASE WHEN currency = 'EUR' THEN balance ELSE 0 END) as total_eur,
|
||||||
|
-- Totals by wallet type
|
||||||
|
SUM(CASE WHEN wallet_type = 'trading' AND currency = 'USD' THEN balance ELSE 0 END) as trading_usd,
|
||||||
|
SUM(CASE WHEN wallet_type = 'investment' AND currency = 'USD' THEN balance ELSE 0 END) as investment_usd,
|
||||||
|
SUM(CASE WHEN wallet_type = 'earnings' AND currency = 'USD' THEN balance ELSE 0 END) as earnings_usd,
|
||||||
|
SUM(CASE WHEN wallet_type = 'referral' AND currency = 'USD' THEN balance ELSE 0 END) as referral_usd,
|
||||||
|
COUNT(*) as wallet_count,
|
||||||
|
MAX(last_transaction_at) as last_activity
|
||||||
|
FROM financial.wallets
|
||||||
|
WHERE status = 'active'
|
||||||
|
GROUP BY user_id;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_user_total_balance IS 'Aggregated balance per user across all wallets';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Recent transactions (last 30 days)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_recent_transactions AS
|
||||||
|
SELECT
|
||||||
|
wt.id,
|
||||||
|
wt.wallet_id,
|
||||||
|
w.user_id,
|
||||||
|
w.wallet_type,
|
||||||
|
wt.transaction_type,
|
||||||
|
wt.status,
|
||||||
|
wt.amount,
|
||||||
|
wt.fee,
|
||||||
|
wt.net_amount,
|
||||||
|
wt.currency,
|
||||||
|
wt.description,
|
||||||
|
wt.reference_id,
|
||||||
|
wt.balance_before,
|
||||||
|
wt.balance_after,
|
||||||
|
wt.created_at,
|
||||||
|
wt.completed_at,
|
||||||
|
-- Days since transaction
|
||||||
|
EXTRACT(DAY FROM NOW() - wt.created_at) as days_ago
|
||||||
|
FROM financial.wallet_transactions wt
|
||||||
|
JOIN financial.wallets w ON w.id = wt.wallet_id
|
||||||
|
WHERE wt.created_at >= NOW() - INTERVAL '30 days'
|
||||||
|
ORDER BY wt.created_at DESC;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_recent_transactions IS 'Wallet transactions from last 30 days';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Active subscriptions with user details
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_active_subscriptions AS
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.user_id,
|
||||||
|
s.plan,
|
||||||
|
s.status,
|
||||||
|
s.price,
|
||||||
|
s.currency,
|
||||||
|
s.billing_interval,
|
||||||
|
s.current_period_start,
|
||||||
|
s.current_period_end,
|
||||||
|
s.next_payment_at,
|
||||||
|
s.cancel_at_period_end,
|
||||||
|
s.stripe_subscription_id,
|
||||||
|
-- Days until renewal
|
||||||
|
EXTRACT(DAY FROM s.current_period_end - NOW()) as days_until_renewal,
|
||||||
|
-- Is in trial
|
||||||
|
(s.status = 'trialing') as is_trial,
|
||||||
|
-- Trial days remaining
|
||||||
|
CASE
|
||||||
|
WHEN s.trial_end IS NOT NULL THEN EXTRACT(DAY FROM s.trial_end - NOW())
|
||||||
|
ELSE NULL
|
||||||
|
END as trial_days_remaining
|
||||||
|
FROM financial.subscriptions s
|
||||||
|
WHERE s.status IN ('active', 'trialing', 'past_due')
|
||||||
|
ORDER BY s.current_period_end ASC;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_active_subscriptions IS 'Active subscriptions with renewal information';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Pending payments
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_pending_payments AS
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.user_id,
|
||||||
|
p.subscription_id,
|
||||||
|
p.amount,
|
||||||
|
p.currency,
|
||||||
|
p.payment_method,
|
||||||
|
p.status,
|
||||||
|
p.description,
|
||||||
|
p.stripe_payment_intent_id,
|
||||||
|
p.created_at,
|
||||||
|
-- Days pending
|
||||||
|
EXTRACT(DAY FROM NOW() - p.created_at) as days_pending
|
||||||
|
FROM financial.payments p
|
||||||
|
WHERE p.status IN ('pending', 'processing')
|
||||||
|
ORDER BY p.created_at ASC;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_pending_payments IS 'Payments awaiting completion';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Unpaid invoices
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_unpaid_invoices AS
|
||||||
|
SELECT
|
||||||
|
i.id,
|
||||||
|
i.user_id,
|
||||||
|
i.invoice_number,
|
||||||
|
i.total,
|
||||||
|
i.amount_due,
|
||||||
|
i.currency,
|
||||||
|
i.due_date,
|
||||||
|
i.status,
|
||||||
|
i.invoice_date,
|
||||||
|
i.hosted_invoice_url,
|
||||||
|
-- Days overdue
|
||||||
|
CASE
|
||||||
|
WHEN i.due_date IS NOT NULL AND i.due_date < NOW()
|
||||||
|
THEN EXTRACT(DAY FROM NOW() - i.due_date)
|
||||||
|
ELSE 0
|
||||||
|
END as days_overdue,
|
||||||
|
-- Is overdue
|
||||||
|
(i.due_date IS NOT NULL AND i.due_date < NOW()) as is_overdue
|
||||||
|
FROM financial.invoices i
|
||||||
|
WHERE i.status = 'open' AND i.paid = false
|
||||||
|
ORDER BY i.due_date ASC NULLS LAST;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_unpaid_invoices IS 'Open invoices with overdue status';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Daily transaction volume
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_daily_transaction_volume AS
|
||||||
|
SELECT
|
||||||
|
DATE(wt.created_at) as transaction_date,
|
||||||
|
wt.transaction_type,
|
||||||
|
wt.currency,
|
||||||
|
COUNT(*) as transaction_count,
|
||||||
|
SUM(wt.amount) as total_amount,
|
||||||
|
SUM(wt.fee) as total_fees,
|
||||||
|
SUM(wt.net_amount) as total_net_amount,
|
||||||
|
AVG(wt.amount) as avg_amount
|
||||||
|
FROM financial.wallet_transactions wt
|
||||||
|
WHERE wt.status = 'completed'
|
||||||
|
AND wt.created_at >= NOW() - INTERVAL '90 days'
|
||||||
|
GROUP BY DATE(wt.created_at), wt.transaction_type, wt.currency
|
||||||
|
ORDER BY transaction_date DESC, transaction_type;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_daily_transaction_volume IS 'Daily aggregated transaction statistics';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Wallet activity summary (last 7 days)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_wallet_activity_7d AS
|
||||||
|
SELECT
|
||||||
|
w.id as wallet_id,
|
||||||
|
w.user_id,
|
||||||
|
w.wallet_type,
|
||||||
|
w.currency,
|
||||||
|
w.balance,
|
||||||
|
-- Transaction counts by type
|
||||||
|
COUNT(CASE WHEN wt.transaction_type = 'deposit' THEN 1 END) as deposits_7d,
|
||||||
|
COUNT(CASE WHEN wt.transaction_type = 'withdrawal' THEN 1 END) as withdrawals_7d,
|
||||||
|
COUNT(CASE WHEN wt.transaction_type IN ('transfer_in', 'transfer_out') THEN 1 END) as transfers_7d,
|
||||||
|
-- Amounts
|
||||||
|
SUM(CASE WHEN wt.transaction_type = 'deposit' THEN wt.amount ELSE 0 END) as deposit_amount_7d,
|
||||||
|
SUM(CASE WHEN wt.transaction_type = 'withdrawal' THEN wt.amount ELSE 0 END) as withdrawal_amount_7d,
|
||||||
|
-- Total activity
|
||||||
|
COUNT(wt.id) as total_transactions_7d
|
||||||
|
FROM financial.wallets w
|
||||||
|
LEFT JOIN financial.wallet_transactions wt ON wt.wallet_id = w.id
|
||||||
|
AND wt.created_at >= NOW() - INTERVAL '7 days'
|
||||||
|
AND wt.status = 'completed'
|
||||||
|
WHERE w.status = 'active'
|
||||||
|
GROUP BY w.id, w.user_id, w.wallet_type, w.currency, w.balance;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_wallet_activity_7d IS 'Wallet activity summary for last 7 days';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- VIEW: Subscription revenue metrics
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW financial.v_subscription_revenue AS
|
||||||
|
SELECT
|
||||||
|
s.plan,
|
||||||
|
s.billing_interval,
|
||||||
|
s.currency,
|
||||||
|
COUNT(*) as active_count,
|
||||||
|
SUM(s.price) as total_monthly_value,
|
||||||
|
AVG(s.price) as avg_price,
|
||||||
|
-- MRR calculation (Monthly Recurring Revenue)
|
||||||
|
SUM(CASE
|
||||||
|
WHEN s.billing_interval = 'month' THEN s.price
|
||||||
|
WHEN s.billing_interval = 'year' THEN s.price / 12
|
||||||
|
ELSE 0
|
||||||
|
END) as monthly_recurring_revenue
|
||||||
|
FROM financial.subscriptions s
|
||||||
|
WHERE s.status IN ('active', 'trialing')
|
||||||
|
GROUP BY s.plan, s.billing_interval, s.currency
|
||||||
|
ORDER BY s.plan, s.billing_interval;
|
||||||
|
|
||||||
|
COMMENT ON VIEW financial.v_subscription_revenue IS 'Subscription metrics and MRR calculation';
|
||||||
85
ddl/schemas/financial/tables/01-wallets.sql
Normal file
85
ddl/schemas/financial/tables/01-wallets.sql
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - WALLETS TABLE (UNIFIED)
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Single source of truth for all wallet types
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
-- UNIFICACIÓN: Esta tabla reemplaza definiciones previas en:
|
||||||
|
-- - OQI-004 (trading schema)
|
||||||
|
-- - OQI-005 (investment schema)
|
||||||
|
-- - OQI-008 (financial schema - legacy)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.wallets (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Tipo y estado
|
||||||
|
wallet_type financial.wallet_type NOT NULL,
|
||||||
|
status financial.wallet_status NOT NULL DEFAULT 'active',
|
||||||
|
|
||||||
|
-- Balance (precisión de 8 decimales para soportar crypto)
|
||||||
|
balance DECIMAL(20,8) NOT NULL DEFAULT 0.00,
|
||||||
|
available_balance DECIMAL(20,8) NOT NULL DEFAULT 0.00,
|
||||||
|
pending_balance DECIMAL(20,8) NOT NULL DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Moneda
|
||||||
|
currency financial.currency_code NOT NULL DEFAULT 'USD',
|
||||||
|
|
||||||
|
-- Stripe integration (si aplica)
|
||||||
|
stripe_account_id VARCHAR(255),
|
||||||
|
stripe_customer_id VARCHAR(255),
|
||||||
|
|
||||||
|
-- Límites operacionales
|
||||||
|
daily_withdrawal_limit DECIMAL(15,2),
|
||||||
|
monthly_withdrawal_limit DECIMAL(15,2),
|
||||||
|
min_balance DECIMAL(15,2) DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Tracking de uso
|
||||||
|
last_transaction_at TIMESTAMPTZ,
|
||||||
|
total_deposits DECIMAL(20,8) DEFAULT 0.00,
|
||||||
|
total_withdrawals DECIMAL(20,8) DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Metadata extensible
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
closed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT positive_balance CHECK (balance >= 0),
|
||||||
|
CONSTRAINT positive_available CHECK (available_balance >= 0),
|
||||||
|
CONSTRAINT positive_pending CHECK (pending_balance >= 0),
|
||||||
|
CONSTRAINT available_lte_balance CHECK (available_balance <= balance),
|
||||||
|
CONSTRAINT balance_equation CHECK (balance = available_balance + pending_balance),
|
||||||
|
CONSTRAINT positive_limits CHECK (
|
||||||
|
(daily_withdrawal_limit IS NULL OR daily_withdrawal_limit > 0) AND
|
||||||
|
(monthly_withdrawal_limit IS NULL OR monthly_withdrawal_limit > 0)
|
||||||
|
),
|
||||||
|
CONSTRAINT unique_user_wallet_type UNIQUE(user_id, wallet_type, currency),
|
||||||
|
CONSTRAINT closed_status_has_date CHECK (
|
||||||
|
(status = 'closed' AND closed_at IS NOT NULL) OR
|
||||||
|
(status != 'closed' AND closed_at IS NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes para performance
|
||||||
|
CREATE INDEX idx_wallets_user_id ON financial.wallets(user_id);
|
||||||
|
CREATE INDEX idx_wallets_wallet_type ON financial.wallets(wallet_type);
|
||||||
|
CREATE INDEX idx_wallets_status ON financial.wallets(status) WHERE status = 'active';
|
||||||
|
CREATE INDEX idx_wallets_currency ON financial.wallets(currency);
|
||||||
|
CREATE INDEX idx_wallets_user_type_currency ON financial.wallets(user_id, wallet_type, currency);
|
||||||
|
CREATE INDEX idx_wallets_stripe_account ON financial.wallets(stripe_account_id) WHERE stripe_account_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wallets_last_transaction ON financial.wallets(last_transaction_at DESC NULLS LAST);
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.wallets IS 'Unified wallet table - single source of truth for all wallet types';
|
||||||
|
COMMENT ON COLUMN financial.wallets.wallet_type IS 'Type of wallet: trading, investment, earnings, referral';
|
||||||
|
COMMENT ON COLUMN financial.wallets.balance IS 'Total balance = available + pending';
|
||||||
|
COMMENT ON COLUMN financial.wallets.available_balance IS 'Balance available for immediate use';
|
||||||
|
COMMENT ON COLUMN financial.wallets.pending_balance IS 'Balance in pending transactions';
|
||||||
|
COMMENT ON COLUMN financial.wallets.stripe_account_id IS 'Stripe Connect account ID for payouts';
|
||||||
|
COMMENT ON COLUMN financial.wallets.stripe_customer_id IS 'Stripe Customer ID for payments';
|
||||||
|
COMMENT ON COLUMN financial.wallets.metadata IS 'Extensible JSON field for additional data';
|
||||||
101
ddl/schemas/financial/tables/02-wallet_transactions.sql
Normal file
101
ddl/schemas/financial/tables/02-wallet_transactions.sql
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - WALLET TRANSACTIONS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Complete transaction history for all wallets
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.wallet_transactions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Wallet relacionada
|
||||||
|
wallet_id UUID NOT NULL REFERENCES financial.wallets(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Tipo y estado
|
||||||
|
transaction_type financial.transaction_type NOT NULL,
|
||||||
|
status financial.transaction_status NOT NULL DEFAULT 'pending',
|
||||||
|
|
||||||
|
-- Montos (precisión de 8 decimales)
|
||||||
|
amount DECIMAL(20,8) NOT NULL,
|
||||||
|
fee DECIMAL(15,8) DEFAULT 0,
|
||||||
|
net_amount DECIMAL(20,8) GENERATED ALWAYS AS (amount - fee) STORED,
|
||||||
|
|
||||||
|
-- Moneda
|
||||||
|
currency financial.currency_code NOT NULL,
|
||||||
|
|
||||||
|
-- Balances snapshot (para auditoría)
|
||||||
|
balance_before DECIMAL(20,8),
|
||||||
|
balance_after DECIMAL(20,8),
|
||||||
|
|
||||||
|
-- Referencias externas
|
||||||
|
stripe_payment_intent_id VARCHAR(255),
|
||||||
|
stripe_transfer_id VARCHAR(255),
|
||||||
|
stripe_charge_id VARCHAR(255),
|
||||||
|
reference_id VARCHAR(100), -- ID de referencia interna (ej: trade_id, investment_id)
|
||||||
|
|
||||||
|
-- Para transfers entre wallets
|
||||||
|
destination_wallet_id UUID REFERENCES financial.wallets(id) ON DELETE RESTRICT,
|
||||||
|
related_transaction_id UUID REFERENCES financial.wallet_transactions(id), -- TX relacionada (para transfers bidireccionales)
|
||||||
|
|
||||||
|
-- Descripción y notas
|
||||||
|
description TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Procesamiento
|
||||||
|
processed_at TIMESTAMPTZ,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
failed_at TIMESTAMPTZ,
|
||||||
|
failed_reason TEXT,
|
||||||
|
|
||||||
|
-- Idempotency
|
||||||
|
idempotency_key VARCHAR(255) UNIQUE,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT positive_amount CHECK (amount > 0),
|
||||||
|
CONSTRAINT positive_fee CHECK (fee >= 0),
|
||||||
|
CONSTRAINT fee_lte_amount CHECK (fee <= amount),
|
||||||
|
CONSTRAINT destination_for_transfers CHECK (
|
||||||
|
(transaction_type IN ('transfer_in', 'transfer_out') AND destination_wallet_id IS NOT NULL) OR
|
||||||
|
(transaction_type NOT IN ('transfer_in', 'transfer_out'))
|
||||||
|
),
|
||||||
|
CONSTRAINT no_self_transfer CHECK (wallet_id != destination_wallet_id),
|
||||||
|
CONSTRAINT completed_has_timestamp CHECK (
|
||||||
|
(status = 'completed' AND completed_at IS NOT NULL) OR
|
||||||
|
(status != 'completed')
|
||||||
|
),
|
||||||
|
CONSTRAINT failed_has_reason CHECK (
|
||||||
|
(status = 'failed' AND failed_reason IS NOT NULL AND failed_at IS NOT NULL) OR
|
||||||
|
(status != 'failed')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes para performance y queries comunes
|
||||||
|
CREATE INDEX idx_wt_wallet_id ON financial.wallet_transactions(wallet_id);
|
||||||
|
CREATE INDEX idx_wt_transaction_type ON financial.wallet_transactions(transaction_type);
|
||||||
|
CREATE INDEX idx_wt_status ON financial.wallet_transactions(status);
|
||||||
|
CREATE INDEX idx_wt_created_at ON financial.wallet_transactions(created_at DESC);
|
||||||
|
CREATE INDEX idx_wt_reference_id ON financial.wallet_transactions(reference_id) WHERE reference_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wt_stripe_payment ON financial.wallet_transactions(stripe_payment_intent_id) WHERE stripe_payment_intent_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wt_stripe_transfer ON financial.wallet_transactions(stripe_transfer_id) WHERE stripe_transfer_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wt_destination ON financial.wallet_transactions(destination_wallet_id) WHERE destination_wallet_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wt_wallet_status_created ON financial.wallet_transactions(wallet_id, status, created_at DESC);
|
||||||
|
CREATE INDEX idx_wt_idempotency ON financial.wallet_transactions(idempotency_key) WHERE idempotency_key IS NOT NULL;
|
||||||
|
|
||||||
|
-- Composite index para queries de rango por wallet
|
||||||
|
CREATE INDEX idx_wt_wallet_date_range ON financial.wallet_transactions(wallet_id, created_at DESC)
|
||||||
|
WHERE status = 'completed';
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.wallet_transactions IS 'Complete transaction history for all wallet operations';
|
||||||
|
COMMENT ON COLUMN financial.wallet_transactions.transaction_type IS 'Type of transaction: deposit, withdrawal, transfer, fee, etc.';
|
||||||
|
COMMENT ON COLUMN financial.wallet_transactions.net_amount IS 'Amount after fees (computed column)';
|
||||||
|
COMMENT ON COLUMN financial.wallet_transactions.reference_id IS 'Internal reference to related entity (trade, investment, etc.)';
|
||||||
|
COMMENT ON COLUMN financial.wallet_transactions.destination_wallet_id IS 'Target wallet for transfer operations';
|
||||||
|
COMMENT ON COLUMN financial.wallet_transactions.related_transaction_id IS 'Related transaction for bidirectional transfers';
|
||||||
|
COMMENT ON COLUMN financial.wallet_transactions.idempotency_key IS 'Unique key to prevent duplicate transactions';
|
||||||
|
COMMENT ON COLUMN financial.wallet_transactions.metadata IS 'Extensible JSON field for transaction-specific data';
|
||||||
107
ddl/schemas/financial/tables/03-subscriptions.sql
Normal file
107
ddl/schemas/financial/tables/03-subscriptions.sql
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - SUBSCRIPTIONS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: User subscription management with Stripe integration
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
-- DECISION: Planes en USD como moneda base
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.subscriptions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Plan y estado
|
||||||
|
plan financial.subscription_plan NOT NULL,
|
||||||
|
status financial.subscription_status NOT NULL DEFAULT 'incomplete',
|
||||||
|
|
||||||
|
-- Stripe integration
|
||||||
|
stripe_subscription_id VARCHAR(255) UNIQUE,
|
||||||
|
stripe_customer_id VARCHAR(255),
|
||||||
|
stripe_price_id VARCHAR(255),
|
||||||
|
stripe_product_id VARCHAR(255),
|
||||||
|
|
||||||
|
-- Pricing
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
currency financial.currency_code NOT NULL DEFAULT 'USD',
|
||||||
|
billing_interval VARCHAR(20) NOT NULL DEFAULT 'month', -- month, year
|
||||||
|
|
||||||
|
-- Billing periods
|
||||||
|
current_period_start TIMESTAMPTZ,
|
||||||
|
current_period_end TIMESTAMPTZ,
|
||||||
|
trial_start TIMESTAMPTZ,
|
||||||
|
trial_end TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Cancelación
|
||||||
|
cancelled_at TIMESTAMPTZ,
|
||||||
|
cancel_at_period_end BOOLEAN DEFAULT false,
|
||||||
|
cancellation_reason TEXT,
|
||||||
|
cancellation_feedback JSONB,
|
||||||
|
|
||||||
|
-- Downgrade/Upgrade tracking
|
||||||
|
previous_plan financial.subscription_plan,
|
||||||
|
scheduled_plan financial.subscription_plan,
|
||||||
|
scheduled_plan_effective_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Payment tracking
|
||||||
|
last_payment_at TIMESTAMPTZ,
|
||||||
|
next_payment_at TIMESTAMPTZ,
|
||||||
|
failed_payment_count INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Features/Quotas (se pueden mover a tabla separada si crece)
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ended_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT positive_price CHECK (price >= 0),
|
||||||
|
CONSTRAINT valid_billing_interval CHECK (billing_interval IN ('month', 'year')),
|
||||||
|
CONSTRAINT trial_dates_order CHECK (
|
||||||
|
(trial_start IS NULL AND trial_end IS NULL) OR
|
||||||
|
(trial_start IS NOT NULL AND trial_end IS NOT NULL AND trial_start < trial_end)
|
||||||
|
),
|
||||||
|
CONSTRAINT period_dates_order CHECK (
|
||||||
|
(current_period_start IS NULL AND current_period_end IS NULL) OR
|
||||||
|
(current_period_start IS NOT NULL AND current_period_end IS NOT NULL AND current_period_start < current_period_end)
|
||||||
|
),
|
||||||
|
CONSTRAINT cancel_date_valid CHECK (
|
||||||
|
(cancelled_at IS NULL) OR
|
||||||
|
(cancelled_at >= created_at)
|
||||||
|
),
|
||||||
|
CONSTRAINT ended_when_cancelled CHECK (
|
||||||
|
(ended_at IS NULL) OR
|
||||||
|
(cancelled_at IS NOT NULL AND ended_at >= cancelled_at)
|
||||||
|
),
|
||||||
|
CONSTRAINT scheduled_plan_different CHECK (
|
||||||
|
scheduled_plan IS NULL OR scheduled_plan != plan
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_subscriptions_user_id ON financial.subscriptions(user_id);
|
||||||
|
CREATE INDEX idx_subscriptions_status ON financial.subscriptions(status);
|
||||||
|
CREATE INDEX idx_subscriptions_plan ON financial.subscriptions(plan);
|
||||||
|
CREATE INDEX idx_subscriptions_stripe_sub ON financial.subscriptions(stripe_subscription_id) WHERE stripe_subscription_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_subscriptions_stripe_customer ON financial.subscriptions(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_subscriptions_active ON financial.subscriptions(user_id, status) WHERE status = 'active';
|
||||||
|
CREATE INDEX idx_subscriptions_period_end ON financial.subscriptions(current_period_end) WHERE status = 'active';
|
||||||
|
CREATE INDEX idx_subscriptions_trial_end ON financial.subscriptions(trial_end) WHERE status = 'trialing';
|
||||||
|
CREATE INDEX idx_subscriptions_next_payment ON financial.subscriptions(next_payment_at) WHERE next_payment_at IS NOT NULL;
|
||||||
|
|
||||||
|
-- Unique constraint: un usuario solo puede tener una suscripción activa a la vez
|
||||||
|
CREATE UNIQUE INDEX idx_subscriptions_user_active ON financial.subscriptions(user_id)
|
||||||
|
WHERE status IN ('active', 'trialing', 'past_due');
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.subscriptions IS 'User subscription management with Stripe integration';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.plan IS 'Subscription plan tier';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.status IS 'Subscription status (Stripe-compatible states)';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.price IS 'Subscription price in specified currency';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.billing_interval IS 'Billing frequency: month or year';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.cancel_at_period_end IS 'If true, subscription will cancel at end of current period';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.scheduled_plan IS 'Plan to switch to at scheduled_plan_effective_at';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.failed_payment_count IS 'Number of consecutive failed payment attempts';
|
||||||
|
COMMENT ON COLUMN financial.subscriptions.metadata IS 'Plan features, quotas, and additional configuration';
|
||||||
120
ddl/schemas/financial/tables/04-invoices.sql
Normal file
120
ddl/schemas/financial/tables/04-invoices.sql
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - INVOICES TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Invoice records for subscriptions and one-time charges
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.invoices (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Referencias
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||||
|
subscription_id UUID REFERENCES financial.subscriptions(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Stripe integration
|
||||||
|
stripe_invoice_id VARCHAR(255) UNIQUE,
|
||||||
|
stripe_customer_id VARCHAR(255),
|
||||||
|
|
||||||
|
-- Invoice details
|
||||||
|
invoice_number VARCHAR(100) UNIQUE, -- Número de factura interno
|
||||||
|
invoice_type financial.invoice_type NOT NULL,
|
||||||
|
status financial.invoice_status NOT NULL DEFAULT 'draft',
|
||||||
|
|
||||||
|
-- Amounts
|
||||||
|
subtotal DECIMAL(15,2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(15,2) DEFAULT 0,
|
||||||
|
total DECIMAL(15,2) NOT NULL,
|
||||||
|
amount_paid DECIMAL(15,2) DEFAULT 0,
|
||||||
|
amount_due DECIMAL(15,2) GENERATED ALWAYS AS (total - amount_paid) STORED,
|
||||||
|
|
||||||
|
currency financial.currency_code NOT NULL DEFAULT 'USD',
|
||||||
|
|
||||||
|
-- Dates
|
||||||
|
invoice_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
due_date TIMESTAMPTZ,
|
||||||
|
period_start TIMESTAMPTZ,
|
||||||
|
period_end TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Payment
|
||||||
|
paid BOOLEAN DEFAULT false,
|
||||||
|
paid_at TIMESTAMPTZ,
|
||||||
|
attempted BOOLEAN DEFAULT false,
|
||||||
|
attempt_count INTEGER DEFAULT 0,
|
||||||
|
next_payment_attempt TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- URLs
|
||||||
|
hosted_invoice_url TEXT, -- Stripe hosted invoice URL
|
||||||
|
invoice_pdf_url TEXT,
|
||||||
|
|
||||||
|
-- Billing details
|
||||||
|
billing_email VARCHAR(255),
|
||||||
|
billing_name VARCHAR(255),
|
||||||
|
billing_address JSONB,
|
||||||
|
|
||||||
|
-- Line items (si se quiere detalle simple)
|
||||||
|
line_items JSONB DEFAULT '[]',
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
description TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
finalized_at TIMESTAMPTZ,
|
||||||
|
voided_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT positive_subtotal CHECK (subtotal >= 0),
|
||||||
|
CONSTRAINT positive_tax CHECK (tax >= 0),
|
||||||
|
CONSTRAINT positive_total CHECK (total >= 0),
|
||||||
|
CONSTRAINT positive_paid CHECK (amount_paid >= 0),
|
||||||
|
CONSTRAINT paid_lte_total CHECK (amount_paid <= total),
|
||||||
|
CONSTRAINT total_equals_subtotal_plus_tax CHECK (total = subtotal + tax),
|
||||||
|
CONSTRAINT paid_has_timestamp CHECK (
|
||||||
|
(paid = false) OR
|
||||||
|
(paid = true AND paid_at IS NOT NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT finalized_for_open_paid CHECK (
|
||||||
|
(status IN ('open', 'paid') AND finalized_at IS NOT NULL) OR
|
||||||
|
(status NOT IN ('open', 'paid'))
|
||||||
|
),
|
||||||
|
CONSTRAINT void_has_timestamp CHECK (
|
||||||
|
(status = 'void' AND voided_at IS NOT NULL) OR
|
||||||
|
(status != 'void')
|
||||||
|
),
|
||||||
|
CONSTRAINT due_date_after_invoice CHECK (
|
||||||
|
due_date IS NULL OR due_date >= invoice_date
|
||||||
|
),
|
||||||
|
CONSTRAINT period_dates_order CHECK (
|
||||||
|
(period_start IS NULL AND period_end IS NULL) OR
|
||||||
|
(period_start IS NOT NULL AND period_end IS NOT NULL AND period_start < period_end)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Sequence para invoice numbers
|
||||||
|
CREATE SEQUENCE financial.invoice_number_seq START 1000;
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_invoices_user_id ON financial.invoices(user_id);
|
||||||
|
CREATE INDEX idx_invoices_subscription_id ON financial.invoices(subscription_id) WHERE subscription_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_invoices_status ON financial.invoices(status);
|
||||||
|
CREATE INDEX idx_invoices_stripe_id ON financial.invoices(stripe_invoice_id) WHERE stripe_invoice_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_invoices_invoice_number ON financial.invoices(invoice_number);
|
||||||
|
CREATE INDEX idx_invoices_due_date ON financial.invoices(due_date) WHERE due_date IS NOT NULL AND status = 'open';
|
||||||
|
CREATE INDEX idx_invoices_invoice_date ON financial.invoices(invoice_date DESC);
|
||||||
|
CREATE INDEX idx_invoices_user_date ON financial.invoices(user_id, invoice_date DESC);
|
||||||
|
CREATE INDEX idx_invoices_unpaid ON financial.invoices(user_id, status) WHERE paid = false AND status = 'open';
|
||||||
|
CREATE INDEX idx_invoices_period ON financial.invoices(period_start, period_end) WHERE period_start IS NOT NULL;
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.invoices IS 'Invoice records for subscriptions and one-time charges';
|
||||||
|
COMMENT ON COLUMN financial.invoices.invoice_number IS 'Internal unique invoice number (auto-generated)';
|
||||||
|
COMMENT ON COLUMN financial.invoices.invoice_type IS 'Type: subscription, one_time, or usage-based';
|
||||||
|
COMMENT ON COLUMN financial.invoices.amount_due IS 'Computed: total - amount_paid';
|
||||||
|
COMMENT ON COLUMN financial.invoices.line_items IS 'JSON array of invoice line items with description, amount, quantity';
|
||||||
|
COMMENT ON COLUMN financial.invoices.billing_address IS 'JSON object with billing address details';
|
||||||
|
COMMENT ON COLUMN financial.invoices.hosted_invoice_url IS 'URL to Stripe-hosted invoice page';
|
||||||
|
COMMENT ON COLUMN financial.invoices.attempt_count IS 'Number of payment attempts made';
|
||||||
86
ddl/schemas/financial/tables/05-payments.sql
Normal file
86
ddl/schemas/financial/tables/05-payments.sql
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - PAYMENTS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Payment transaction records
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.payments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Referencias
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||||
|
subscription_id UUID REFERENCES financial.subscriptions(id) ON DELETE SET NULL,
|
||||||
|
invoice_id UUID REFERENCES financial.invoices(id) ON DELETE SET NULL,
|
||||||
|
wallet_transaction_id UUID REFERENCES financial.wallet_transactions(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Stripe integration
|
||||||
|
stripe_payment_intent_id VARCHAR(255) UNIQUE,
|
||||||
|
stripe_charge_id VARCHAR(255),
|
||||||
|
stripe_payment_method_id VARCHAR(255),
|
||||||
|
|
||||||
|
-- Payment details
|
||||||
|
amount DECIMAL(15,2) NOT NULL,
|
||||||
|
currency financial.currency_code NOT NULL,
|
||||||
|
payment_method financial.payment_method NOT NULL,
|
||||||
|
status financial.payment_status NOT NULL DEFAULT 'pending',
|
||||||
|
|
||||||
|
-- Descripción
|
||||||
|
description TEXT,
|
||||||
|
statement_descriptor VARCHAR(255), -- Lo que ve el usuario en su estado de cuenta
|
||||||
|
|
||||||
|
-- Refunds
|
||||||
|
refunded BOOLEAN DEFAULT false,
|
||||||
|
refund_amount DECIMAL(15,2),
|
||||||
|
refund_reason TEXT,
|
||||||
|
refunded_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Error handling
|
||||||
|
failure_code VARCHAR(100),
|
||||||
|
failure_message TEXT,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
succeeded_at TIMESTAMPTZ,
|
||||||
|
failed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT positive_amount CHECK (amount > 0),
|
||||||
|
CONSTRAINT positive_refund CHECK (refund_amount IS NULL OR refund_amount > 0),
|
||||||
|
CONSTRAINT refund_lte_amount CHECK (refund_amount IS NULL OR refund_amount <= amount),
|
||||||
|
CONSTRAINT refunded_has_data CHECK (
|
||||||
|
(refunded = false AND refund_amount IS NULL AND refunded_at IS NULL) OR
|
||||||
|
(refunded = true AND refund_amount IS NOT NULL AND refunded_at IS NOT NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT succeeded_has_timestamp CHECK (
|
||||||
|
(status = 'succeeded' AND succeeded_at IS NOT NULL) OR
|
||||||
|
(status != 'succeeded')
|
||||||
|
),
|
||||||
|
CONSTRAINT failed_has_data CHECK (
|
||||||
|
(status = 'failed' AND failed_at IS NOT NULL) OR
|
||||||
|
(status != 'failed')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_payments_user_id ON financial.payments(user_id);
|
||||||
|
CREATE INDEX idx_payments_subscription_id ON financial.payments(subscription_id) WHERE subscription_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_payments_invoice_id ON financial.payments(invoice_id) WHERE invoice_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_payments_status ON financial.payments(status);
|
||||||
|
CREATE INDEX idx_payments_stripe_intent ON financial.payments(stripe_payment_intent_id) WHERE stripe_payment_intent_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_payments_created_at ON financial.payments(created_at DESC);
|
||||||
|
CREATE INDEX idx_payments_user_created ON financial.payments(user_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_payments_payment_method ON financial.payments(payment_method);
|
||||||
|
CREATE INDEX idx_payments_refunded ON financial.payments(refunded) WHERE refunded = true;
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.payments IS 'Payment transaction records with Stripe integration';
|
||||||
|
COMMENT ON COLUMN financial.payments.stripe_payment_intent_id IS 'Stripe PaymentIntent ID';
|
||||||
|
COMMENT ON COLUMN financial.payments.payment_method IS 'Payment method used: card, bank_transfer, crypto, etc.';
|
||||||
|
COMMENT ON COLUMN financial.payments.statement_descriptor IS 'Text shown on customer bank statement';
|
||||||
|
COMMENT ON COLUMN financial.payments.wallet_transaction_id IS 'Related wallet transaction if payment funds a wallet';
|
||||||
|
COMMENT ON COLUMN financial.payments.metadata IS 'Additional payment metadata and context';
|
||||||
68
ddl/schemas/financial/tables/06-wallet_audit_log.sql
Normal file
68
ddl/schemas/financial/tables/06-wallet_audit_log.sql
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - WALLET AUDIT LOG TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Audit trail for all wallet state changes
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.wallet_audit_log (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Wallet referencia
|
||||||
|
wallet_id UUID NOT NULL REFERENCES financial.wallets(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Acción
|
||||||
|
action financial.audit_action NOT NULL,
|
||||||
|
|
||||||
|
-- Actor (quien realizó el cambio)
|
||||||
|
actor_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
actor_type VARCHAR(50) DEFAULT 'user', -- user, system, admin, api
|
||||||
|
|
||||||
|
-- Cambios registrados
|
||||||
|
old_values JSONB,
|
||||||
|
new_values JSONB,
|
||||||
|
|
||||||
|
-- Balance snapshot
|
||||||
|
balance_before DECIMAL(20,8),
|
||||||
|
balance_after DECIMAL(20,8),
|
||||||
|
|
||||||
|
-- Transacción relacionada (si aplica)
|
||||||
|
transaction_id UUID REFERENCES financial.wallet_transactions(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
reason TEXT,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- IP y user agent (para auditoría de seguridad)
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
-- Timestamp
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT balance_change_has_amounts CHECK (
|
||||||
|
(action = 'balance_updated' AND balance_before IS NOT NULL AND balance_after IS NOT NULL) OR
|
||||||
|
(action != 'balance_updated')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_wal_wallet_id ON financial.wallet_audit_log(wallet_id);
|
||||||
|
CREATE INDEX idx_wal_action ON financial.wallet_audit_log(action);
|
||||||
|
CREATE INDEX idx_wal_actor_id ON financial.wallet_audit_log(actor_id) WHERE actor_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wal_created_at ON financial.wallet_audit_log(created_at DESC);
|
||||||
|
CREATE INDEX idx_wal_wallet_created ON financial.wallet_audit_log(wallet_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_wal_transaction_id ON financial.wallet_audit_log(transaction_id) WHERE transaction_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Partitioning hint: Esta tabla puede crecer mucho, considerar particionamiento por created_at
|
||||||
|
-- PARTITION BY RANGE (created_at);
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.wallet_audit_log IS 'Immutable audit trail for all wallet state changes';
|
||||||
|
COMMENT ON COLUMN financial.wallet_audit_log.action IS 'Type of action performed on wallet';
|
||||||
|
COMMENT ON COLUMN financial.wallet_audit_log.actor_id IS 'User who performed the action (NULL for system actions)';
|
||||||
|
COMMENT ON COLUMN financial.wallet_audit_log.actor_type IS 'Type of actor: user, system, admin, api';
|
||||||
|
COMMENT ON COLUMN financial.wallet_audit_log.old_values IS 'JSON snapshot of values before change';
|
||||||
|
COMMENT ON COLUMN financial.wallet_audit_log.new_values IS 'JSON snapshot of values after change';
|
||||||
|
COMMENT ON COLUMN financial.wallet_audit_log.metadata IS 'Additional context and metadata';
|
||||||
132
ddl/schemas/financial/tables/07-currency_exchange_rates.sql
Normal file
132
ddl/schemas/financial/tables/07-currency_exchange_rates.sql
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - CURRENCY EXCHANGE RATES TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Historical exchange rates for multi-currency support
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.currency_exchange_rates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Par de monedas
|
||||||
|
from_currency financial.currency_code NOT NULL,
|
||||||
|
to_currency financial.currency_code NOT NULL,
|
||||||
|
|
||||||
|
-- Tasa de cambio
|
||||||
|
rate DECIMAL(18,8) NOT NULL,
|
||||||
|
|
||||||
|
-- Fuente de datos
|
||||||
|
source VARCHAR(100) NOT NULL DEFAULT 'manual', -- manual, api, stripe, coinbase, etc.
|
||||||
|
provider VARCHAR(100), -- nombre del proveedor si es API
|
||||||
|
|
||||||
|
-- Validez temporal
|
||||||
|
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
valid_to TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT positive_rate CHECK (rate > 0),
|
||||||
|
CONSTRAINT different_currencies CHECK (from_currency != to_currency),
|
||||||
|
CONSTRAINT valid_dates_order CHECK (
|
||||||
|
valid_to IS NULL OR valid_to > valid_from
|
||||||
|
),
|
||||||
|
CONSTRAINT unique_rate_period UNIQUE(from_currency, to_currency, valid_from)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_cer_currencies ON financial.currency_exchange_rates(from_currency, to_currency);
|
||||||
|
CREATE INDEX idx_cer_valid_from ON financial.currency_exchange_rates(valid_from DESC);
|
||||||
|
-- Index for currently valid rates (without time-based predicate for immutability)
|
||||||
|
CREATE INDEX idx_cer_valid_period ON financial.currency_exchange_rates(from_currency, to_currency, valid_from DESC)
|
||||||
|
WHERE valid_to IS NULL;
|
||||||
|
CREATE INDEX idx_cer_source ON financial.currency_exchange_rates(source);
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.currency_exchange_rates IS 'Historical exchange rates for currency conversion';
|
||||||
|
COMMENT ON COLUMN financial.currency_exchange_rates.rate IS 'Exchange rate: 1 from_currency = rate * to_currency';
|
||||||
|
COMMENT ON COLUMN financial.currency_exchange_rates.source IS 'Source of exchange rate data';
|
||||||
|
COMMENT ON COLUMN financial.currency_exchange_rates.valid_from IS 'Start of rate validity period';
|
||||||
|
COMMENT ON COLUMN financial.currency_exchange_rates.valid_to IS 'End of rate validity period (NULL = currently valid)';
|
||||||
|
COMMENT ON COLUMN financial.currency_exchange_rates.metadata IS 'Additional rate metadata (bid, ask, spread, etc.)';
|
||||||
|
|
||||||
|
-- Función helper para obtener tasa de cambio actual
|
||||||
|
CREATE OR REPLACE FUNCTION financial.get_exchange_rate(
|
||||||
|
p_from_currency financial.currency_code,
|
||||||
|
p_to_currency financial.currency_code,
|
||||||
|
p_at_time TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
)
|
||||||
|
RETURNS DECIMAL(18,8)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_rate DECIMAL(18,8);
|
||||||
|
BEGIN
|
||||||
|
-- Si son la misma moneda, retornar 1
|
||||||
|
IF p_from_currency = p_to_currency THEN
|
||||||
|
RETURN 1.0;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Buscar tasa de cambio válida
|
||||||
|
SELECT rate INTO v_rate
|
||||||
|
FROM financial.currency_exchange_rates
|
||||||
|
WHERE from_currency = p_from_currency
|
||||||
|
AND to_currency = p_to_currency
|
||||||
|
AND valid_from <= p_at_time
|
||||||
|
AND (valid_to IS NULL OR valid_to > p_at_time)
|
||||||
|
ORDER BY valid_from DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- Si no se encuentra, intentar inversa
|
||||||
|
IF v_rate IS NULL THEN
|
||||||
|
SELECT 1.0 / rate INTO v_rate
|
||||||
|
FROM financial.currency_exchange_rates
|
||||||
|
WHERE from_currency = p_to_currency
|
||||||
|
AND to_currency = p_from_currency
|
||||||
|
AND valid_from <= p_at_time
|
||||||
|
AND (valid_to IS NULL OR valid_to > p_at_time)
|
||||||
|
ORDER BY valid_from DESC
|
||||||
|
LIMIT 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Si aún no hay tasa, retornar NULL
|
||||||
|
RETURN v_rate;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.get_exchange_rate IS 'Get exchange rate between currencies at specific time';
|
||||||
|
|
||||||
|
-- Función para convertir montos
|
||||||
|
CREATE OR REPLACE FUNCTION financial.convert_currency(
|
||||||
|
p_amount DECIMAL,
|
||||||
|
p_from_currency financial.currency_code,
|
||||||
|
p_to_currency financial.currency_code,
|
||||||
|
p_at_time TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
)
|
||||||
|
RETURNS DECIMAL(20,8)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_rate DECIMAL(18,8);
|
||||||
|
BEGIN
|
||||||
|
-- Obtener tasa de cambio
|
||||||
|
v_rate := financial.get_exchange_rate(p_from_currency, p_to_currency, p_at_time);
|
||||||
|
|
||||||
|
-- Si no hay tasa, retornar NULL
|
||||||
|
IF v_rate IS NULL THEN
|
||||||
|
RETURN NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Convertir y retornar
|
||||||
|
RETURN p_amount * v_rate;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.convert_currency IS 'Convert amount between currencies at specific time';
|
||||||
102
ddl/schemas/financial/tables/08-wallet_limits.sql
Normal file
102
ddl/schemas/financial/tables/08-wallet_limits.sql
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ORBIQUANT IA - WALLET LIMITS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Configurable limits and thresholds for wallets
|
||||||
|
-- Schema: financial
|
||||||
|
-- =====================================================
|
||||||
|
-- Separado de wallets para permitir límites más complejos
|
||||||
|
-- y dinámicos basados en plan, nivel de verificación, etc.
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE financial.wallet_limits (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Wallet o configuración global
|
||||||
|
wallet_id UUID REFERENCES financial.wallets(id) ON DELETE CASCADE,
|
||||||
|
wallet_type financial.wallet_type, -- Para límites por tipo de wallet
|
||||||
|
subscription_plan financial.subscription_plan, -- Para límites por plan
|
||||||
|
|
||||||
|
-- Límites de transacción única
|
||||||
|
min_deposit DECIMAL(15,2),
|
||||||
|
max_deposit DECIMAL(15,2),
|
||||||
|
min_withdrawal DECIMAL(15,2),
|
||||||
|
max_withdrawal DECIMAL(15,2),
|
||||||
|
min_transfer DECIMAL(15,2),
|
||||||
|
max_transfer DECIMAL(15,2),
|
||||||
|
|
||||||
|
-- Límites periódicos
|
||||||
|
daily_deposit_limit DECIMAL(15,2),
|
||||||
|
daily_withdrawal_limit DECIMAL(15,2),
|
||||||
|
daily_transfer_limit DECIMAL(15,2),
|
||||||
|
|
||||||
|
weekly_deposit_limit DECIMAL(15,2),
|
||||||
|
weekly_withdrawal_limit DECIMAL(15,2),
|
||||||
|
weekly_transfer_limit DECIMAL(15,2),
|
||||||
|
|
||||||
|
monthly_deposit_limit DECIMAL(15,2),
|
||||||
|
monthly_withdrawal_limit DECIMAL(15,2),
|
||||||
|
monthly_transfer_limit DECIMAL(15,2),
|
||||||
|
|
||||||
|
-- Límites de volumen
|
||||||
|
max_pending_transactions INTEGER,
|
||||||
|
max_daily_transaction_count INTEGER,
|
||||||
|
|
||||||
|
-- Balance limits
|
||||||
|
min_balance DECIMAL(15,2) DEFAULT 0,
|
||||||
|
max_balance DECIMAL(15,2),
|
||||||
|
|
||||||
|
-- Moneda de los límites
|
||||||
|
currency financial.currency_code NOT NULL DEFAULT 'USD',
|
||||||
|
|
||||||
|
-- Prioridad (mayor número = mayor prioridad)
|
||||||
|
priority INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Vigencia
|
||||||
|
active BOOLEAN DEFAULT true,
|
||||||
|
valid_from TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
valid_to TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
description TEXT,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT wallet_or_type_or_plan CHECK (
|
||||||
|
(wallet_id IS NOT NULL AND wallet_type IS NULL AND subscription_plan IS NULL) OR
|
||||||
|
(wallet_id IS NULL AND wallet_type IS NOT NULL AND subscription_plan IS NULL) OR
|
||||||
|
(wallet_id IS NULL AND wallet_type IS NULL AND subscription_plan IS NOT NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT positive_limits CHECK (
|
||||||
|
(min_deposit IS NULL OR min_deposit > 0) AND
|
||||||
|
(max_deposit IS NULL OR max_deposit > 0) AND
|
||||||
|
(min_withdrawal IS NULL OR min_withdrawal > 0) AND
|
||||||
|
(max_withdrawal IS NULL OR max_withdrawal > 0) AND
|
||||||
|
(min_transfer IS NULL OR min_transfer > 0) AND
|
||||||
|
(max_transfer IS NULL OR max_transfer > 0)
|
||||||
|
),
|
||||||
|
CONSTRAINT min_max_deposit CHECK (min_deposit IS NULL OR max_deposit IS NULL OR min_deposit <= max_deposit),
|
||||||
|
CONSTRAINT min_max_withdrawal CHECK (min_withdrawal IS NULL OR max_withdrawal IS NULL OR min_withdrawal <= max_withdrawal),
|
||||||
|
CONSTRAINT min_max_transfer CHECK (min_transfer IS NULL OR max_transfer IS NULL OR min_transfer <= max_transfer),
|
||||||
|
CONSTRAINT valid_dates_order CHECK (valid_to IS NULL OR valid_to > valid_from)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_wl_wallet_id ON financial.wallet_limits(wallet_id) WHERE wallet_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wl_wallet_type ON financial.wallet_limits(wallet_type) WHERE wallet_type IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wl_subscription_plan ON financial.wallet_limits(subscription_plan) WHERE subscription_plan IS NOT NULL;
|
||||||
|
CREATE INDEX idx_wl_active ON financial.wallet_limits(active, priority DESC) WHERE active = true;
|
||||||
|
-- Index for currently valid limits (without time-based predicate for immutability)
|
||||||
|
CREATE INDEX idx_wl_valid_period ON financial.wallet_limits(valid_from, valid_to)
|
||||||
|
WHERE active = true AND valid_to IS NULL;
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
COMMENT ON TABLE financial.wallet_limits IS 'Configurable transaction limits for wallets';
|
||||||
|
COMMENT ON COLUMN financial.wallet_limits.wallet_id IS 'Specific wallet (takes highest priority)';
|
||||||
|
COMMENT ON COLUMN financial.wallet_limits.wallet_type IS 'Limits for all wallets of this type';
|
||||||
|
COMMENT ON COLUMN financial.wallet_limits.subscription_plan IS 'Limits based on subscription plan';
|
||||||
|
COMMENT ON COLUMN financial.wallet_limits.priority IS 'Higher number = higher priority when multiple limits apply';
|
||||||
|
COMMENT ON COLUMN financial.wallet_limits.currency IS 'Currency for all limit amounts';
|
||||||
68
ddl/schemas/financial/tables/09-customers.sql
Normal file
68
ddl/schemas/financial/tables/09-customers.sql
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- FINANCIAL SCHEMA - Tabla: customers
|
||||||
|
-- ============================================================================
|
||||||
|
-- Clientes de Stripe y datos de facturacion
|
||||||
|
-- Vincula usuarios con su informacion de pago
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS financial.customers (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relacion con usuario
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Stripe
|
||||||
|
stripe_customer_id VARCHAR(100) UNIQUE,
|
||||||
|
stripe_default_payment_method_id VARCHAR(100),
|
||||||
|
|
||||||
|
-- Datos de facturacion
|
||||||
|
billing_name VARCHAR(255),
|
||||||
|
billing_email VARCHAR(255),
|
||||||
|
billing_phone VARCHAR(50),
|
||||||
|
|
||||||
|
-- Direccion de facturacion
|
||||||
|
billing_address_line1 VARCHAR(255),
|
||||||
|
billing_address_line2 VARCHAR(255),
|
||||||
|
billing_city VARCHAR(100),
|
||||||
|
billing_state VARCHAR(100),
|
||||||
|
billing_postal_code VARCHAR(20),
|
||||||
|
billing_country VARCHAR(2), -- ISO 3166-1 alpha-2
|
||||||
|
|
||||||
|
-- Datos fiscales (Mexico)
|
||||||
|
tax_id VARCHAR(20), -- RFC
|
||||||
|
tax_id_type VARCHAR(20) DEFAULT 'mx_rfc', -- Tipo de ID fiscal
|
||||||
|
legal_name VARCHAR(255), -- Razon social
|
||||||
|
|
||||||
|
-- Preferencias
|
||||||
|
currency financial.currency_code NOT NULL DEFAULT 'USD',
|
||||||
|
locale VARCHAR(10) DEFAULT 'en-US',
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
delinquent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
delinquent_since TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT uq_customers_user UNIQUE(user_id),
|
||||||
|
CONSTRAINT chk_valid_country CHECK (billing_country IS NULL OR LENGTH(billing_country) = 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indices
|
||||||
|
CREATE INDEX idx_customers_user ON financial.customers(user_id);
|
||||||
|
CREATE INDEX idx_customers_stripe ON financial.customers(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_customers_email ON financial.customers(billing_email) WHERE billing_email IS NOT NULL;
|
||||||
|
CREATE INDEX idx_customers_delinquent ON financial.customers(delinquent) WHERE delinquent = TRUE;
|
||||||
|
CREATE INDEX idx_customers_tax_id ON financial.customers(tax_id) WHERE tax_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE financial.customers IS 'Clientes de Stripe con datos de facturacion';
|
||||||
|
COMMENT ON COLUMN financial.customers.stripe_customer_id IS 'ID del cliente en Stripe (cus_xxx)';
|
||||||
|
COMMENT ON COLUMN financial.customers.tax_id IS 'RFC para Mexico, VAT para EU, etc.';
|
||||||
|
COMMENT ON COLUMN financial.customers.delinquent IS 'True si tiene pagos vencidos';
|
||||||
180
ddl/schemas/financial/tables/10-payment_methods.sql
Normal file
180
ddl/schemas/financial/tables/10-payment_methods.sql
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- FINANCIAL SCHEMA - Tabla: payment_methods
|
||||||
|
-- ============================================================================
|
||||||
|
-- Metodos de pago guardados por usuarios para pagos recurrentes
|
||||||
|
-- Integra con Stripe para almacenamiento seguro de tarjetas y cuentas
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Enum para tipo de metodo de pago guardado
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'saved_payment_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'financial')) THEN
|
||||||
|
CREATE TYPE financial.saved_payment_type AS ENUM (
|
||||||
|
'card',
|
||||||
|
'bank_account',
|
||||||
|
'sepa_debit',
|
||||||
|
'crypto_wallet'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
-- Enum para estado del metodo de pago
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'payment_method_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'financial')) THEN
|
||||||
|
CREATE TYPE financial.payment_method_status AS ENUM (
|
||||||
|
'pending_verification',
|
||||||
|
'active',
|
||||||
|
'expired',
|
||||||
|
'failed',
|
||||||
|
'removed'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS financial.payment_methods (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relaciones
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
customer_id UUID REFERENCES financial.customers(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- Stripe integration
|
||||||
|
stripe_payment_method_id VARCHAR(100) UNIQUE,
|
||||||
|
stripe_fingerprint VARCHAR(100), -- Para detectar duplicados
|
||||||
|
|
||||||
|
-- Tipo y estado
|
||||||
|
payment_type financial.saved_payment_type NOT NULL,
|
||||||
|
status financial.payment_method_status NOT NULL DEFAULT 'pending_verification',
|
||||||
|
|
||||||
|
-- Informacion del metodo (datos no sensibles)
|
||||||
|
-- Para tarjetas: last4, brand, exp_month, exp_year
|
||||||
|
-- Para bancos: last4, bank_name, account_type
|
||||||
|
display_info JSONB NOT NULL DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Metodo por defecto
|
||||||
|
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Datos de tarjeta (solo informacion visible)
|
||||||
|
card_brand VARCHAR(20), -- 'visa', 'mastercard', 'amex', etc.
|
||||||
|
card_last4 VARCHAR(4),
|
||||||
|
card_exp_month INTEGER,
|
||||||
|
card_exp_year INTEGER,
|
||||||
|
card_funding VARCHAR(20), -- 'credit', 'debit', 'prepaid'
|
||||||
|
|
||||||
|
-- Datos de cuenta bancaria (solo informacion visible)
|
||||||
|
bank_name VARCHAR(100),
|
||||||
|
bank_last4 VARCHAR(4),
|
||||||
|
bank_account_type VARCHAR(20), -- 'checking', 'savings'
|
||||||
|
|
||||||
|
-- Datos de crypto wallet
|
||||||
|
crypto_network VARCHAR(20), -- 'ethereum', 'bitcoin', 'polygon'
|
||||||
|
crypto_address_last8 VARCHAR(8),
|
||||||
|
|
||||||
|
-- Verificacion
|
||||||
|
verified_at TIMESTAMPTZ,
|
||||||
|
verification_method VARCHAR(50), -- 'micro_deposits', '3d_secure', 'instant'
|
||||||
|
|
||||||
|
-- Billing address (para 3DS y validacion)
|
||||||
|
billing_address JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMPTZ, -- Para tarjetas con fecha de expiracion
|
||||||
|
removed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT chk_card_info CHECK (
|
||||||
|
payment_type != 'card' OR (
|
||||||
|
card_brand IS NOT NULL AND
|
||||||
|
card_last4 IS NOT NULL AND
|
||||||
|
card_exp_month IS NOT NULL AND
|
||||||
|
card_exp_year IS NOT NULL
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_bank_info CHECK (
|
||||||
|
payment_type != 'bank_account' OR (
|
||||||
|
bank_name IS NOT NULL AND
|
||||||
|
bank_last4 IS NOT NULL
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_crypto_info CHECK (
|
||||||
|
payment_type != 'crypto_wallet' OR (
|
||||||
|
crypto_network IS NOT NULL AND
|
||||||
|
crypto_address_last8 IS NOT NULL
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_valid_exp_month CHECK (
|
||||||
|
card_exp_month IS NULL OR (card_exp_month >= 1 AND card_exp_month <= 12)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_valid_exp_year CHECK (
|
||||||
|
card_exp_year IS NULL OR card_exp_year >= 2024
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indices
|
||||||
|
CREATE INDEX idx_payment_methods_user ON financial.payment_methods(user_id);
|
||||||
|
CREATE INDEX idx_payment_methods_customer ON financial.payment_methods(customer_id);
|
||||||
|
CREATE INDEX idx_payment_methods_stripe ON financial.payment_methods(stripe_payment_method_id);
|
||||||
|
CREATE INDEX idx_payment_methods_status ON financial.payment_methods(status);
|
||||||
|
CREATE INDEX idx_payment_methods_default ON financial.payment_methods(user_id, is_default)
|
||||||
|
WHERE is_default = TRUE;
|
||||||
|
CREATE INDEX idx_payment_methods_fingerprint ON financial.payment_methods(stripe_fingerprint)
|
||||||
|
WHERE stripe_fingerprint IS NOT NULL;
|
||||||
|
CREATE INDEX idx_payment_methods_active ON financial.payment_methods(user_id, payment_type)
|
||||||
|
WHERE status = 'active';
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE financial.payment_methods IS 'Metodos de pago guardados por usuarios con integracion Stripe';
|
||||||
|
COMMENT ON COLUMN financial.payment_methods.stripe_payment_method_id IS 'ID del PaymentMethod en Stripe';
|
||||||
|
COMMENT ON COLUMN financial.payment_methods.stripe_fingerprint IS 'Fingerprint para detectar tarjetas duplicadas';
|
||||||
|
COMMENT ON COLUMN financial.payment_methods.display_info IS 'Informacion visible del metodo para UI';
|
||||||
|
COMMENT ON COLUMN financial.payment_methods.is_default IS 'Metodo de pago por defecto del usuario';
|
||||||
|
COMMENT ON COLUMN financial.payment_methods.billing_address IS 'Direccion de facturacion para 3D Secure';
|
||||||
|
|
||||||
|
-- Trigger para asegurar un solo metodo por defecto por usuario
|
||||||
|
CREATE OR REPLACE FUNCTION financial.ensure_single_default_payment_method()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.is_default = TRUE THEN
|
||||||
|
UPDATE financial.payment_methods
|
||||||
|
SET is_default = FALSE, updated_at = NOW()
|
||||||
|
WHERE user_id = NEW.user_id
|
||||||
|
AND id != NEW.id
|
||||||
|
AND is_default = TRUE;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER tr_ensure_single_default_payment_method
|
||||||
|
BEFORE INSERT OR UPDATE OF is_default ON financial.payment_methods
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.is_default = TRUE)
|
||||||
|
EXECUTE FUNCTION financial.ensure_single_default_payment_method();
|
||||||
|
|
||||||
|
-- Funcion para marcar tarjetas expiradas
|
||||||
|
CREATE OR REPLACE FUNCTION financial.check_expired_cards()
|
||||||
|
RETURNS INTEGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
UPDATE financial.payment_methods
|
||||||
|
SET status = 'expired', updated_at = NOW()
|
||||||
|
WHERE payment_type = 'card'
|
||||||
|
AND status = 'active'
|
||||||
|
AND (
|
||||||
|
card_exp_year < EXTRACT(YEAR FROM CURRENT_DATE) OR
|
||||||
|
(card_exp_year = EXTRACT(YEAR FROM CURRENT_DATE) AND card_exp_month < EXTRACT(MONTH FROM CURRENT_DATE))
|
||||||
|
);
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_count = ROW_COUNT;
|
||||||
|
RETURN v_count;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION financial.check_expired_cards() IS 'Marca como expiradas las tarjetas vencidas. Ejecutar mensualmente.';
|
||||||
52
ddl/schemas/investment/00-enums.sql
Normal file
52
ddl/schemas/investment/00-enums.sql
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- INVESTMENT SCHEMA - ENUMS
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Enumerations for PAMM investment system
|
||||||
|
-- Schema: investment
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Agentes de inversión (Trading Agents)
|
||||||
|
CREATE TYPE investment.trading_agent AS ENUM (
|
||||||
|
'atlas', -- Conservador: 3-5% mensual
|
||||||
|
'orion', -- Moderado: 5-10% mensual
|
||||||
|
'nova' -- Agresivo: 10%+ mensual
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Perfil de riesgo (unificado con cuestionario)
|
||||||
|
CREATE TYPE investment.risk_profile AS ENUM (
|
||||||
|
'conservative',
|
||||||
|
'moderate',
|
||||||
|
'aggressive'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado de cuenta PAMM
|
||||||
|
CREATE TYPE investment.account_status AS ENUM (
|
||||||
|
'pending_kyc',
|
||||||
|
'active',
|
||||||
|
'suspended',
|
||||||
|
'closed'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Frecuencia de distribución (DECISIÓN: mensual por defecto)
|
||||||
|
CREATE TYPE investment.distribution_frequency AS ENUM (
|
||||||
|
'monthly',
|
||||||
|
'quarterly'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de transacción
|
||||||
|
CREATE TYPE investment.transaction_type AS ENUM (
|
||||||
|
'deposit',
|
||||||
|
'withdrawal',
|
||||||
|
'distribution'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado de transacción
|
||||||
|
CREATE TYPE investment.transaction_status AS ENUM (
|
||||||
|
'pending',
|
||||||
|
'processing',
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
'cancelled'
|
||||||
|
);
|
||||||
60
ddl/schemas/investment/tables/01-products.sql
Normal file
60
ddl/schemas/investment/tables/01-products.sql
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- INVESTMENT SCHEMA - PRODUCTS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: PAMM investment products
|
||||||
|
-- Schema: investment
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE investment.products (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Identificación
|
||||||
|
code VARCHAR(20) NOT NULL UNIQUE, -- PAMM-ATLAS, PAMM-ORION, PAMM-NOVA
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Agente asociado
|
||||||
|
trading_agent investment.trading_agent NOT NULL,
|
||||||
|
|
||||||
|
-- Parámetros de inversión
|
||||||
|
min_investment DECIMAL(15,2) NOT NULL,
|
||||||
|
max_investment DECIMAL(15,2),
|
||||||
|
|
||||||
|
-- Rentabilidad objetivo
|
||||||
|
target_return_min DECIMAL(5,2), -- % mensual mínimo esperado
|
||||||
|
target_return_max DECIMAL(5,2), -- % mensual máximo esperado
|
||||||
|
|
||||||
|
-- Distribución de ganancias
|
||||||
|
distribution_frequency investment.distribution_frequency DEFAULT 'monthly',
|
||||||
|
investor_share_percent DECIMAL(5,2) DEFAULT 80.00, -- 80% para inversor
|
||||||
|
platform_share_percent DECIMAL(5,2) DEFAULT 20.00, -- 20% para plataforma
|
||||||
|
|
||||||
|
-- Perfil de riesgo recomendado
|
||||||
|
recommended_risk_profile investment.risk_profile NOT NULL,
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
is_accepting_new_investors BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
-- Límites
|
||||||
|
total_capacity DECIMAL(15,2), -- Capacidad total del producto
|
||||||
|
current_aum DECIMAL(15,2) DEFAULT 0.00, -- Assets Under Management
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_products_agent ON investment.products(trading_agent);
|
||||||
|
CREATE INDEX idx_products_active ON investment.products(is_active) WHERE is_active = true;
|
||||||
|
CREATE INDEX idx_products_risk_profile ON investment.products(recommended_risk_profile);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE investment.products IS 'PAMM investment products linked to trading agents';
|
||||||
|
COMMENT ON COLUMN investment.products.code IS 'Unique product code (e.g., PAMM-ATLAS)';
|
||||||
|
COMMENT ON COLUMN investment.products.current_aum IS 'Current Assets Under Management';
|
||||||
|
COMMENT ON COLUMN investment.products.investor_share_percent IS 'Percentage of profits distributed to investors (80%)';
|
||||||
|
COMMENT ON COLUMN investment.products.platform_share_percent IS 'Percentage of profits retained by platform (20%)';
|
||||||
63
ddl/schemas/investment/tables/02-risk_questionnaire.sql
Normal file
63
ddl/schemas/investment/tables/02-risk_questionnaire.sql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- INVESTMENT SCHEMA - RISK QUESTIONNAIRE TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Risk assessment questionnaire (15 questions)
|
||||||
|
-- Schema: investment
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE investment.risk_questionnaire (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Usuario
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Respuestas (15 preguntas)
|
||||||
|
responses JSONB NOT NULL, -- [{question_id, answer, score}]
|
||||||
|
|
||||||
|
-- Resultado
|
||||||
|
total_score INTEGER NOT NULL CHECK (total_score >= 0 AND total_score <= 100),
|
||||||
|
calculated_profile investment.risk_profile NOT NULL,
|
||||||
|
|
||||||
|
-- Recomendación de agente
|
||||||
|
recommended_agent investment.trading_agent,
|
||||||
|
|
||||||
|
-- Validez
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL, -- Válido por 1 año
|
||||||
|
-- Note: is_expired removed - compute dynamically as (expires_at < NOW())
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
completion_time_seconds INTEGER, -- Tiempo que tardó en completar
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_questionnaire_user ON investment.risk_questionnaire(user_id);
|
||||||
|
CREATE INDEX idx_questionnaire_profile ON investment.risk_questionnaire(calculated_profile);
|
||||||
|
-- Index for valid questionnaires (without time-based predicate for immutability)
|
||||||
|
CREATE INDEX idx_questionnaire_valid ON investment.risk_questionnaire(user_id, expires_at DESC);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE investment.risk_questionnaire IS 'Risk assessment questionnaire responses (valid for 1 year)';
|
||||||
|
COMMENT ON COLUMN investment.risk_questionnaire.responses IS 'Array of question responses with scores: [{question_id, answer, score}]';
|
||||||
|
COMMENT ON COLUMN investment.risk_questionnaire.total_score IS 'Sum of all question scores (0-100)';
|
||||||
|
COMMENT ON COLUMN investment.risk_questionnaire.calculated_profile IS 'Risk profile calculated from total_score';
|
||||||
|
COMMENT ON COLUMN investment.risk_questionnaire.recommended_agent IS 'Trading agent recommendation based on risk profile';
|
||||||
|
COMMENT ON COLUMN investment.risk_questionnaire.expires_at IS 'Questionnaire expires after 1 year, user must retake';
|
||||||
|
|
||||||
|
-- Ejemplo de estructura de responses JSONB:
|
||||||
|
COMMENT ON COLUMN investment.risk_questionnaire.responses IS
|
||||||
|
'Example: [
|
||||||
|
{"question_id": "Q1", "answer": "A", "score": 5},
|
||||||
|
{"question_id": "Q2", "answer": "B", "score": 10},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
Scoring logic:
|
||||||
|
- Conservative (0-40): Atlas agent recommended
|
||||||
|
- Moderate (41-70): Orion agent recommended
|
||||||
|
- Aggressive (71-100): Nova agent recommended';
|
||||||
67
ddl/schemas/investment/tables/03-accounts.sql
Normal file
67
ddl/schemas/investment/tables/03-accounts.sql
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- INVESTMENT SCHEMA - ACCOUNTS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Individual investor PAMM accounts
|
||||||
|
-- Schema: investment
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE investment.accounts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Propietario
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Producto PAMM
|
||||||
|
product_id UUID NOT NULL REFERENCES investment.products(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Identificación
|
||||||
|
account_number VARCHAR(20) NOT NULL UNIQUE, -- INV-202512-00001
|
||||||
|
|
||||||
|
-- Balance
|
||||||
|
initial_balance DECIMAL(15,2) NOT NULL,
|
||||||
|
current_balance DECIMAL(15,2) NOT NULL,
|
||||||
|
total_deposits DECIMAL(15,2) DEFAULT 0.00,
|
||||||
|
total_withdrawals DECIMAL(15,2) DEFAULT 0.00,
|
||||||
|
total_distributions DECIMAL(15,2) DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Rentabilidad
|
||||||
|
total_return_percent DECIMAL(10,4) DEFAULT 0.00,
|
||||||
|
total_return_amount DECIMAL(15,2) DEFAULT 0.00,
|
||||||
|
|
||||||
|
-- Perfil de riesgo del usuario
|
||||||
|
user_risk_profile investment.risk_profile NOT NULL,
|
||||||
|
questionnaire_id UUID REFERENCES investment.risk_questionnaire(id),
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status investment.account_status DEFAULT 'pending_kyc',
|
||||||
|
|
||||||
|
-- KYC/Compliance
|
||||||
|
kyc_verified BOOLEAN DEFAULT false,
|
||||||
|
kyc_verified_at TIMESTAMPTZ,
|
||||||
|
kyc_verified_by VARCHAR(100),
|
||||||
|
|
||||||
|
-- Fechas importantes
|
||||||
|
opened_at TIMESTAMPTZ,
|
||||||
|
closed_at TIMESTAMPTZ,
|
||||||
|
last_distribution_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_accounts_user ON investment.accounts(user_id);
|
||||||
|
CREATE INDEX idx_accounts_product ON investment.accounts(product_id);
|
||||||
|
CREATE INDEX idx_accounts_status ON investment.accounts(status);
|
||||||
|
CREATE INDEX idx_accounts_active ON investment.accounts(status) WHERE status = 'active';
|
||||||
|
CREATE INDEX idx_accounts_number ON investment.accounts(account_number);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE investment.accounts IS 'Individual investor PAMM accounts';
|
||||||
|
COMMENT ON COLUMN investment.accounts.account_number IS 'Unique account identifier (INV-YYYYMM-NNNNN)';
|
||||||
|
COMMENT ON COLUMN investment.accounts.current_balance IS 'Current account balance including all deposits, withdrawals, and distributions';
|
||||||
|
COMMENT ON COLUMN investment.accounts.total_return_percent IS 'Cumulative return percentage since account opening';
|
||||||
|
COMMENT ON COLUMN investment.accounts.user_risk_profile IS 'Risk profile from questionnaire, must match product recommendation';
|
||||||
69
ddl/schemas/investment/tables/04-distributions.sql
Normal file
69
ddl/schemas/investment/tables/04-distributions.sql
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- INVESTMENT SCHEMA - DISTRIBUTIONS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Profit distributions (80/20 split)
|
||||||
|
-- Schema: investment
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE investment.distributions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Producto PAMM
|
||||||
|
product_id UUID NOT NULL REFERENCES investment.products(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Periodo
|
||||||
|
period_start TIMESTAMPTZ NOT NULL,
|
||||||
|
period_end TIMESTAMPTZ NOT NULL,
|
||||||
|
period_label VARCHAR(20) NOT NULL, -- 2025-12, 2025-Q4
|
||||||
|
|
||||||
|
-- Performance del agente de trading
|
||||||
|
total_profit_amount DECIMAL(15,2) NOT NULL, -- Ganancia total generada
|
||||||
|
total_profit_percent DECIMAL(10,4) NOT NULL, -- % de retorno
|
||||||
|
|
||||||
|
-- Distribución 80/20
|
||||||
|
investor_total_amount DECIMAL(15,2) NOT NULL, -- 80% para inversores
|
||||||
|
platform_total_amount DECIMAL(15,2) NOT NULL, -- 20% para plataforma
|
||||||
|
|
||||||
|
-- Cuentas participantes
|
||||||
|
participating_accounts INTEGER NOT NULL,
|
||||||
|
total_aum_at_period_start DECIMAL(15,2) NOT NULL,
|
||||||
|
total_aum_at_period_end DECIMAL(15,2) NOT NULL,
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status VARCHAR(20) DEFAULT 'pending', -- pending, processing, completed, failed
|
||||||
|
|
||||||
|
-- Procesamiento
|
||||||
|
calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
approved_by VARCHAR(100),
|
||||||
|
approved_at TIMESTAMPTZ,
|
||||||
|
distributed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
notes TEXT,
|
||||||
|
distribution_metadata JSONB, -- Detalles adicionales
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Validación
|
||||||
|
CONSTRAINT valid_period CHECK (period_end > period_start),
|
||||||
|
CONSTRAINT valid_split CHECK (
|
||||||
|
investor_total_amount + platform_total_amount = total_profit_amount
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_distributions_product ON investment.distributions(product_id);
|
||||||
|
CREATE INDEX idx_distributions_period ON investment.distributions(period_start, period_end);
|
||||||
|
CREATE INDEX idx_distributions_status ON investment.distributions(status);
|
||||||
|
CREATE UNIQUE INDEX idx_distributions_product_period ON investment.distributions(product_id, period_label);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE investment.distributions IS 'Periodic profit distributions with 80/20 split';
|
||||||
|
COMMENT ON COLUMN investment.distributions.period_label IS 'Human-readable period identifier (YYYY-MM or YYYY-QN)';
|
||||||
|
COMMENT ON COLUMN investment.distributions.total_profit_amount IS 'Total profit generated by trading agent during period';
|
||||||
|
COMMENT ON COLUMN investment.distributions.investor_total_amount IS '80% of total profit distributed to all investors';
|
||||||
|
COMMENT ON COLUMN investment.distributions.platform_total_amount IS '20% of total profit retained by platform';
|
||||||
|
COMMENT ON COLUMN investment.distributions.total_aum_at_period_start IS 'Total Assets Under Management at period start';
|
||||||
69
ddl/schemas/investment/tables/05-transactions.sql
Normal file
69
ddl/schemas/investment/tables/05-transactions.sql
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- INVESTMENT SCHEMA - TRANSACTIONS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Deposits, withdrawals, and distributions
|
||||||
|
-- Schema: investment
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE investment.transactions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Cuenta asociada
|
||||||
|
account_id UUID NOT NULL REFERENCES investment.accounts(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Identificación
|
||||||
|
transaction_number VARCHAR(30) NOT NULL UNIQUE, -- TXN-202512-00001
|
||||||
|
|
||||||
|
-- Tipo y monto
|
||||||
|
transaction_type investment.transaction_type NOT NULL,
|
||||||
|
amount DECIMAL(15,2) NOT NULL CHECK (amount > 0),
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status investment.transaction_status DEFAULT 'pending',
|
||||||
|
|
||||||
|
-- Detalles de pago (para deposits/withdrawals)
|
||||||
|
payment_method VARCHAR(50), -- bank_transfer, card, crypto
|
||||||
|
payment_reference VARCHAR(100),
|
||||||
|
payment_metadata JSONB,
|
||||||
|
|
||||||
|
-- Distribución (para transaction_type = 'distribution')
|
||||||
|
distribution_id UUID REFERENCES investment.distributions(id),
|
||||||
|
|
||||||
|
-- Balance después de transacción
|
||||||
|
balance_before DECIMAL(15,2),
|
||||||
|
balance_after DECIMAL(15,2),
|
||||||
|
|
||||||
|
-- Procesamiento
|
||||||
|
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
processed_at TIMESTAMPTZ,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
failed_at TIMESTAMPTZ,
|
||||||
|
failure_reason TEXT,
|
||||||
|
|
||||||
|
-- Aprobación (para withdrawals)
|
||||||
|
requires_approval BOOLEAN DEFAULT false,
|
||||||
|
approved_by VARCHAR(100),
|
||||||
|
approved_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_transactions_account ON investment.transactions(account_id);
|
||||||
|
CREATE INDEX idx_transactions_type ON investment.transactions(transaction_type);
|
||||||
|
CREATE INDEX idx_transactions_status ON investment.transactions(status);
|
||||||
|
CREATE INDEX idx_transactions_number ON investment.transactions(transaction_number);
|
||||||
|
CREATE INDEX idx_transactions_distribution ON investment.transactions(distribution_id);
|
||||||
|
CREATE INDEX idx_transactions_requested ON investment.transactions(requested_at DESC);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE investment.transactions IS 'All account transactions: deposits, withdrawals, and distributions';
|
||||||
|
COMMENT ON COLUMN investment.transactions.transaction_number IS 'Unique transaction identifier (TXN-YYYYMM-NNNNN)';
|
||||||
|
COMMENT ON COLUMN investment.transactions.payment_method IS 'Payment method for deposits/withdrawals';
|
||||||
|
COMMENT ON COLUMN investment.transactions.distribution_id IS 'Link to distribution record if transaction_type is distribution';
|
||||||
|
COMMENT ON COLUMN investment.transactions.requires_approval IS 'Whether withdrawal requires manual approval';
|
||||||
119
ddl/schemas/investment/tables/06-withdrawal_requests.sql
Normal file
119
ddl/schemas/investment/tables/06-withdrawal_requests.sql
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- INVESTMENT SCHEMA - Tabla: withdrawal_requests
|
||||||
|
-- ============================================================================
|
||||||
|
-- Solicitudes de retiro de cuentas PAMM
|
||||||
|
-- Requiere aprobacion manual para montos grandes
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Enum para estado de solicitud
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'withdrawal_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'investment')) THEN
|
||||||
|
CREATE TYPE investment.withdrawal_status AS ENUM (
|
||||||
|
'pending',
|
||||||
|
'under_review',
|
||||||
|
'approved',
|
||||||
|
'processing',
|
||||||
|
'completed',
|
||||||
|
'rejected',
|
||||||
|
'cancelled'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS investment.withdrawal_requests (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relaciones
|
||||||
|
account_id UUID NOT NULL REFERENCES investment.accounts(id) ON DELETE RESTRICT,
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||||
|
|
||||||
|
-- Solicitud
|
||||||
|
request_number VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
amount DECIMAL(20, 8) NOT NULL,
|
||||||
|
currency VARCHAR(10) NOT NULL DEFAULT 'USD',
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status investment.withdrawal_status NOT NULL DEFAULT 'pending',
|
||||||
|
|
||||||
|
-- Destino del retiro
|
||||||
|
destination_type VARCHAR(20) NOT NULL, -- 'wallet', 'bank', 'crypto'
|
||||||
|
destination_details JSONB NOT NULL DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Fees
|
||||||
|
fee_amount DECIMAL(20, 8) NOT NULL DEFAULT 0,
|
||||||
|
fee_percentage DECIMAL(5, 2) NOT NULL DEFAULT 0,
|
||||||
|
net_amount DECIMAL(20, 8) GENERATED ALWAYS AS (amount - fee_amount) STORED,
|
||||||
|
|
||||||
|
-- Aprobacion
|
||||||
|
requires_approval BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
reviewed_by UUID REFERENCES auth.users(id),
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
review_notes TEXT,
|
||||||
|
|
||||||
|
-- Procesamiento
|
||||||
|
processed_at TIMESTAMPTZ,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
transaction_reference VARCHAR(100),
|
||||||
|
|
||||||
|
-- Rechazo/Cancelacion
|
||||||
|
rejection_reason TEXT,
|
||||||
|
cancelled_at TIMESTAMPTZ,
|
||||||
|
cancellation_reason TEXT,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT chk_positive_amount CHECK (amount > 0),
|
||||||
|
CONSTRAINT chk_valid_fee CHECK (fee_amount >= 0 AND fee_amount <= amount),
|
||||||
|
CONSTRAINT chk_fee_percentage CHECK (fee_percentage >= 0 AND fee_percentage <= 100),
|
||||||
|
CONSTRAINT chk_destination_type CHECK (destination_type IN ('wallet', 'bank', 'crypto'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indices
|
||||||
|
CREATE INDEX idx_withdrawal_requests_account ON investment.withdrawal_requests(account_id);
|
||||||
|
CREATE INDEX idx_withdrawal_requests_user ON investment.withdrawal_requests(user_id);
|
||||||
|
CREATE INDEX idx_withdrawal_requests_status ON investment.withdrawal_requests(status);
|
||||||
|
CREATE INDEX idx_withdrawal_requests_created ON investment.withdrawal_requests(created_at DESC);
|
||||||
|
CREATE INDEX idx_withdrawal_requests_pending ON investment.withdrawal_requests(status, created_at)
|
||||||
|
WHERE status IN ('pending', 'under_review');
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE investment.withdrawal_requests IS 'Solicitudes de retiro de cuentas PAMM';
|
||||||
|
COMMENT ON COLUMN investment.withdrawal_requests.request_number IS 'Numero unico de solicitud (WR-YYYYMMDD-XXXX)';
|
||||||
|
COMMENT ON COLUMN investment.withdrawal_requests.requires_approval IS 'True si el monto requiere aprobacion manual';
|
||||||
|
COMMENT ON COLUMN investment.withdrawal_requests.destination_details IS 'Detalles del destino (IBAN, wallet address, etc.)';
|
||||||
|
|
||||||
|
-- Funcion para generar numero de solicitud
|
||||||
|
CREATE OR REPLACE FUNCTION investment.generate_withdrawal_request_number()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_date TEXT;
|
||||||
|
v_seq INTEGER;
|
||||||
|
BEGIN
|
||||||
|
v_date := TO_CHAR(NOW(), 'YYYYMMDD');
|
||||||
|
|
||||||
|
SELECT COALESCE(MAX(
|
||||||
|
CAST(SUBSTRING(request_number FROM 'WR-[0-9]{8}-([0-9]+)') AS INTEGER)
|
||||||
|
), 0) + 1
|
||||||
|
INTO v_seq
|
||||||
|
FROM investment.withdrawal_requests
|
||||||
|
WHERE request_number LIKE 'WR-' || v_date || '-%';
|
||||||
|
|
||||||
|
NEW.request_number := 'WR-' || v_date || '-' || LPAD(v_seq::TEXT, 4, '0');
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER tr_generate_withdrawal_request_number
|
||||||
|
BEFORE INSERT ON investment.withdrawal_requests
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.request_number IS NULL)
|
||||||
|
EXECUTE FUNCTION investment.generate_withdrawal_request_number();
|
||||||
114
ddl/schemas/investment/tables/07-daily_performance.sql
Normal file
114
ddl/schemas/investment/tables/07-daily_performance.sql
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- INVESTMENT SCHEMA - Tabla: daily_performance
|
||||||
|
-- ============================================================================
|
||||||
|
-- Snapshots diarios de rendimiento de cuentas PAMM
|
||||||
|
-- Usado para graficos, reportes y calculo de metricas
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS investment.daily_performance (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Relaciones
|
||||||
|
account_id UUID NOT NULL REFERENCES investment.accounts(id) ON DELETE CASCADE,
|
||||||
|
product_id UUID NOT NULL REFERENCES investment.products(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Fecha del snapshot
|
||||||
|
snapshot_date DATE NOT NULL,
|
||||||
|
|
||||||
|
-- Balance
|
||||||
|
opening_balance DECIMAL(20, 8) NOT NULL,
|
||||||
|
closing_balance DECIMAL(20, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- Rendimiento del dia
|
||||||
|
daily_pnl DECIMAL(20, 8) NOT NULL DEFAULT 0,
|
||||||
|
daily_return_percentage DECIMAL(10, 6) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Rendimiento acumulado
|
||||||
|
cumulative_pnl DECIMAL(20, 8) NOT NULL DEFAULT 0,
|
||||||
|
cumulative_return_percentage DECIMAL(10, 6) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Movimientos del dia
|
||||||
|
deposits DECIMAL(20, 8) NOT NULL DEFAULT 0,
|
||||||
|
withdrawals DECIMAL(20, 8) NOT NULL DEFAULT 0,
|
||||||
|
distributions_received DECIMAL(20, 8) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Metricas del agente de trading
|
||||||
|
trades_executed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
winning_trades INTEGER NOT NULL DEFAULT 0,
|
||||||
|
losing_trades INTEGER NOT NULL DEFAULT 0,
|
||||||
|
win_rate DECIMAL(5, 2),
|
||||||
|
|
||||||
|
-- Volatilidad y riesgo
|
||||||
|
max_drawdown DECIMAL(10, 6),
|
||||||
|
sharpe_ratio DECIMAL(10, 6),
|
||||||
|
volatility DECIMAL(10, 6),
|
||||||
|
|
||||||
|
-- High/Low del dia
|
||||||
|
high_water_mark DECIMAL(20, 8),
|
||||||
|
lowest_point DECIMAL(20, 8),
|
||||||
|
|
||||||
|
-- Metadata del snapshot
|
||||||
|
snapshot_source VARCHAR(50) DEFAULT 'cron', -- 'cron', 'manual', 'system'
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT uq_daily_performance_account_date UNIQUE(account_id, snapshot_date),
|
||||||
|
CONSTRAINT chk_valid_balances CHECK (opening_balance >= 0 AND closing_balance >= 0),
|
||||||
|
CONSTRAINT chk_valid_movements CHECK (deposits >= 0 AND withdrawals >= 0),
|
||||||
|
CONSTRAINT chk_valid_trades CHECK (
|
||||||
|
trades_executed >= 0 AND
|
||||||
|
winning_trades >= 0 AND
|
||||||
|
losing_trades >= 0 AND
|
||||||
|
winning_trades + losing_trades <= trades_executed
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_valid_win_rate CHECK (win_rate IS NULL OR (win_rate >= 0 AND win_rate <= 100))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indices
|
||||||
|
CREATE INDEX idx_daily_performance_account ON investment.daily_performance(account_id);
|
||||||
|
CREATE INDEX idx_daily_performance_product ON investment.daily_performance(product_id);
|
||||||
|
CREATE INDEX idx_daily_performance_date ON investment.daily_performance(snapshot_date DESC);
|
||||||
|
CREATE INDEX idx_daily_performance_account_date ON investment.daily_performance(account_id, snapshot_date DESC);
|
||||||
|
|
||||||
|
-- Index for recent performance (removed time-based predicate for immutability)
|
||||||
|
CREATE INDEX idx_daily_performance_recent ON investment.daily_performance(account_id, snapshot_date DESC);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE investment.daily_performance IS 'Snapshots diarios de rendimiento de cuentas PAMM';
|
||||||
|
COMMENT ON COLUMN investment.daily_performance.snapshot_date IS 'Fecha del snapshot (una entrada por dia por cuenta)';
|
||||||
|
COMMENT ON COLUMN investment.daily_performance.daily_return_percentage IS 'Retorno del dia como porcentaje';
|
||||||
|
COMMENT ON COLUMN investment.daily_performance.cumulative_return_percentage IS 'Retorno acumulado desde apertura de cuenta';
|
||||||
|
COMMENT ON COLUMN investment.daily_performance.max_drawdown IS 'Maximo drawdown del dia';
|
||||||
|
COMMENT ON COLUMN investment.daily_performance.high_water_mark IS 'Punto mas alto alcanzado';
|
||||||
|
|
||||||
|
-- Vista para resumen mensual
|
||||||
|
CREATE OR REPLACE VIEW investment.v_monthly_performance AS
|
||||||
|
SELECT
|
||||||
|
account_id,
|
||||||
|
product_id,
|
||||||
|
DATE_TRUNC('month', snapshot_date) AS month,
|
||||||
|
MIN(opening_balance) AS month_opening,
|
||||||
|
MAX(closing_balance) AS month_closing,
|
||||||
|
SUM(daily_pnl) AS total_pnl,
|
||||||
|
AVG(daily_return_percentage) AS avg_daily_return,
|
||||||
|
SUM(deposits) AS total_deposits,
|
||||||
|
SUM(withdrawals) AS total_withdrawals,
|
||||||
|
SUM(distributions_received) AS total_distributions,
|
||||||
|
SUM(trades_executed) AS total_trades,
|
||||||
|
SUM(winning_trades) AS total_winning,
|
||||||
|
SUM(losing_trades) AS total_losing,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(winning_trades) + SUM(losing_trades) > 0
|
||||||
|
THEN ROUND(SUM(winning_trades)::DECIMAL / (SUM(winning_trades) + SUM(losing_trades)) * 100, 2)
|
||||||
|
ELSE NULL
|
||||||
|
END AS monthly_win_rate,
|
||||||
|
MIN(lowest_point) AS monthly_low,
|
||||||
|
MAX(high_water_mark) AS monthly_high,
|
||||||
|
COUNT(*) AS trading_days
|
||||||
|
FROM investment.daily_performance
|
||||||
|
GROUP BY account_id, product_id, DATE_TRUNC('month', snapshot_date);
|
||||||
|
|
||||||
|
COMMENT ON VIEW investment.v_monthly_performance IS 'Resumen mensual de rendimiento agregado';
|
||||||
63
ddl/schemas/llm/00-enums.sql
Normal file
63
ddl/schemas/llm/00-enums.sql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- LLM SCHEMA - ENUMS
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Enumerations for LLM agent system
|
||||||
|
-- Schema: llm
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Rol del mensaje
|
||||||
|
CREATE TYPE llm.message_role AS ENUM (
|
||||||
|
'user',
|
||||||
|
'assistant',
|
||||||
|
'system',
|
||||||
|
'tool'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado de la conversación
|
||||||
|
CREATE TYPE llm.conversation_status AS ENUM (
|
||||||
|
'active',
|
||||||
|
'archived',
|
||||||
|
'deleted'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de conversación
|
||||||
|
CREATE TYPE llm.conversation_type AS ENUM (
|
||||||
|
'general', -- Conversación general
|
||||||
|
'trading_advice', -- Consulta sobre trading
|
||||||
|
'education', -- Preguntas educativas
|
||||||
|
'market_analysis', -- Análisis de mercado
|
||||||
|
'support', -- Soporte técnico
|
||||||
|
'onboarding' -- Onboarding de usuario
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tono de comunicación
|
||||||
|
CREATE TYPE llm.communication_tone AS ENUM (
|
||||||
|
'casual',
|
||||||
|
'professional',
|
||||||
|
'technical'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Nivel de verbosidad
|
||||||
|
CREATE TYPE llm.verbosity_level AS ENUM (
|
||||||
|
'brief',
|
||||||
|
'normal',
|
||||||
|
'detailed'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Frecuencia de alertas
|
||||||
|
CREATE TYPE llm.alert_frequency AS ENUM (
|
||||||
|
'low',
|
||||||
|
'normal',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de memoria
|
||||||
|
CREATE TYPE llm.memory_type AS ENUM (
|
||||||
|
'fact', -- Hecho sobre el usuario
|
||||||
|
'preference', -- Preferencia del usuario
|
||||||
|
'context', -- Contexto de conversaciones
|
||||||
|
'goal', -- Objetivo del usuario
|
||||||
|
'constraint' -- Restricción o límite
|
||||||
|
);
|
||||||
13
ddl/schemas/llm/00-extensions.sql
Normal file
13
ddl/schemas/llm/00-extensions.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- OrbiQuant IA - Trading Platform
|
||||||
|
-- Schema: llm
|
||||||
|
-- File: 00-extensions.sql
|
||||||
|
-- Description: PostgreSQL extensions required for LLM schema (embeddings)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- pgvector extension for vector similarity search
|
||||||
|
-- Required for storing and querying embeddings
|
||||||
|
-- Installation: https://github.com/pgvector/pgvector
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "vector";
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION "vector" IS 'Vector similarity search extension (pgvector) for LLM embeddings';
|
||||||
63
ddl/schemas/llm/tables/01-conversations.sql
Normal file
63
ddl/schemas/llm/tables/01-conversations.sql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- LLM SCHEMA - CONVERSATIONS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Chat conversations with LLM agent
|
||||||
|
-- Schema: llm
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE llm.conversations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Usuario
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Identificación
|
||||||
|
title VARCHAR(200), -- Auto-generado o definido por usuario
|
||||||
|
|
||||||
|
-- Tipo y contexto
|
||||||
|
conversation_type llm.conversation_type DEFAULT 'general',
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status llm.conversation_status DEFAULT 'active',
|
||||||
|
|
||||||
|
-- Resumen de conversación (generado por LLM)
|
||||||
|
summary TEXT,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
total_messages INTEGER DEFAULT 0,
|
||||||
|
total_tokens_used INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- Tags para búsqueda
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Contexto de negocio
|
||||||
|
related_symbols VARCHAR(20)[] DEFAULT '{}', -- Símbolos discutidos
|
||||||
|
related_topics TEXT[] DEFAULT '{}', -- Temas discutidos
|
||||||
|
|
||||||
|
-- Fechas
|
||||||
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_message_at TIMESTAMPTZ,
|
||||||
|
archived_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_conversations_user ON llm.conversations(user_id);
|
||||||
|
CREATE INDEX idx_conversations_status ON llm.conversations(status);
|
||||||
|
CREATE INDEX idx_conversations_type ON llm.conversations(conversation_type);
|
||||||
|
CREATE INDEX idx_conversations_active ON llm.conversations(user_id, last_message_at DESC)
|
||||||
|
WHERE status = 'active';
|
||||||
|
CREATE INDEX idx_conversations_tags ON llm.conversations USING GIN(tags);
|
||||||
|
CREATE INDEX idx_conversations_symbols ON llm.conversations USING GIN(related_symbols);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE llm.conversations IS 'Chat conversations between users and LLM agent';
|
||||||
|
COMMENT ON COLUMN llm.conversations.title IS 'Conversation title (auto-generated from first messages or user-defined)';
|
||||||
|
COMMENT ON COLUMN llm.conversations.summary IS 'AI-generated summary of conversation content';
|
||||||
|
COMMENT ON COLUMN llm.conversations.total_tokens_used IS 'Cumulative token count for cost tracking';
|
||||||
|
COMMENT ON COLUMN llm.conversations.related_symbols IS 'Trading symbols mentioned in conversation';
|
||||||
|
COMMENT ON COLUMN llm.conversations.related_topics IS 'Topics discussed (e.g., technical_analysis, risk_management)';
|
||||||
98
ddl/schemas/llm/tables/02-messages.sql
Normal file
98
ddl/schemas/llm/tables/02-messages.sql
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- LLM SCHEMA - MESSAGES TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Individual messages in conversations
|
||||||
|
-- Schema: llm
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE llm.messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Conversación
|
||||||
|
conversation_id UUID NOT NULL REFERENCES llm.conversations(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Rol y contenido
|
||||||
|
role llm.message_role NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- Metadata de LLM
|
||||||
|
model_name VARCHAR(100), -- claude-opus-4-5, gpt-4, etc.
|
||||||
|
prompt_tokens INTEGER,
|
||||||
|
completion_tokens INTEGER,
|
||||||
|
total_tokens INTEGER,
|
||||||
|
|
||||||
|
-- Contexto utilizado
|
||||||
|
context_used JSONB, -- RAG context, market data, user profile, etc.
|
||||||
|
|
||||||
|
-- Tools/Functions llamadas
|
||||||
|
tool_calls JSONB, -- Function calls realizadas
|
||||||
|
tool_results JSONB, -- Resultados de tool calls
|
||||||
|
|
||||||
|
-- Metadata de procesamiento
|
||||||
|
response_time_ms INTEGER,
|
||||||
|
temperature DECIMAL(3,2),
|
||||||
|
|
||||||
|
-- Feedback del usuario
|
||||||
|
user_rating INTEGER CHECK (user_rating >= 1 AND user_rating <= 5),
|
||||||
|
user_feedback TEXT,
|
||||||
|
|
||||||
|
-- Referencias
|
||||||
|
references_symbols VARCHAR(20)[] DEFAULT '{}',
|
||||||
|
references_concepts TEXT[] DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_messages_conversation ON llm.messages(conversation_id);
|
||||||
|
CREATE INDEX idx_messages_role ON llm.messages(role);
|
||||||
|
CREATE INDEX idx_messages_created ON llm.messages(created_at DESC);
|
||||||
|
CREATE INDEX idx_messages_conversation_created ON llm.messages(conversation_id, created_at ASC);
|
||||||
|
CREATE INDEX idx_messages_rated ON llm.messages(user_rating) WHERE user_rating IS NOT NULL;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE llm.messages IS 'Individual messages in LLM conversations';
|
||||||
|
COMMENT ON COLUMN llm.messages.role IS 'Message sender: user, assistant, system, or tool';
|
||||||
|
COMMENT ON COLUMN llm.messages.content IS 'Message text content';
|
||||||
|
COMMENT ON COLUMN llm.messages.model_name IS 'LLM model used to generate response';
|
||||||
|
COMMENT ON COLUMN llm.messages.context_used IS 'Context provided to LLM (RAG docs, market data, user profile)';
|
||||||
|
COMMENT ON COLUMN llm.messages.tool_calls IS 'Functions/tools called by LLM during response generation';
|
||||||
|
COMMENT ON COLUMN llm.messages.user_rating IS 'User satisfaction rating (1-5 stars)';
|
||||||
|
|
||||||
|
-- Ejemplo de context_used JSONB:
|
||||||
|
COMMENT ON COLUMN llm.messages.context_used IS
|
||||||
|
'Example: {
|
||||||
|
"market_data": {
|
||||||
|
"symbol": "BTCUSDT",
|
||||||
|
"price": 45234.12,
|
||||||
|
"change_24h": 0.0234
|
||||||
|
},
|
||||||
|
"user_profile": {
|
||||||
|
"risk_profile": "moderate",
|
||||||
|
"preferred_symbols": ["BTCUSDT", "ETHUSDT"]
|
||||||
|
},
|
||||||
|
"rag_documents": [
|
||||||
|
{"doc_id": "123", "relevance": 0.89, "snippet": "..."},
|
||||||
|
{"doc_id": "456", "relevance": 0.76, "snippet": "..."}
|
||||||
|
]
|
||||||
|
}';
|
||||||
|
|
||||||
|
-- Ejemplo de tool_calls JSONB:
|
||||||
|
COMMENT ON COLUMN llm.messages.tool_calls IS
|
||||||
|
'Example: [
|
||||||
|
{
|
||||||
|
"tool": "get_market_data",
|
||||||
|
"params": {"symbol": "BTCUSDT", "timeframe": "1h"},
|
||||||
|
"result": {...}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "calculate_indicator",
|
||||||
|
"params": {"indicator": "rsi", "period": 14},
|
||||||
|
"result": {"rsi": 65.42}
|
||||||
|
}
|
||||||
|
]';
|
||||||
68
ddl/schemas/llm/tables/03-user_preferences.sql
Normal file
68
ddl/schemas/llm/tables/03-user_preferences.sql
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- LLM SCHEMA - USER PREFERENCES TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: User preferences for LLM agent interactions
|
||||||
|
-- Schema: llm
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE llm.user_preferences (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Preferencias de comunicación
|
||||||
|
language VARCHAR(5) DEFAULT 'es', -- ISO 639-1
|
||||||
|
tone llm.communication_tone DEFAULT 'professional',
|
||||||
|
verbosity llm.verbosity_level DEFAULT 'normal',
|
||||||
|
|
||||||
|
-- Preferencias de trading
|
||||||
|
preferred_symbols VARCHAR(20)[] DEFAULT '{}',
|
||||||
|
preferred_timeframe VARCHAR(10) DEFAULT '1h',
|
||||||
|
risk_tolerance VARCHAR(20) DEFAULT 'moderate', -- conservative, moderate, aggressive
|
||||||
|
|
||||||
|
-- Preferencias de notificación
|
||||||
|
proactive_alerts BOOLEAN DEFAULT true,
|
||||||
|
alert_frequency llm.alert_frequency DEFAULT 'normal',
|
||||||
|
notification_hours_start TIME DEFAULT '08:00:00',
|
||||||
|
notification_hours_end TIME DEFAULT '22:00:00',
|
||||||
|
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
|
||||||
|
|
||||||
|
-- Intereses
|
||||||
|
topics_of_interest TEXT[] DEFAULT '{}', -- trading, education, news, market_analysis
|
||||||
|
|
||||||
|
-- Nivel de experiencia (para personalizar explicaciones)
|
||||||
|
trading_experience_level VARCHAR(20) DEFAULT 'beginner', -- beginner, intermediate, advanced, expert
|
||||||
|
|
||||||
|
-- Preferencias de análisis
|
||||||
|
preferred_analysis_types TEXT[] DEFAULT '{}', -- technical, fundamental, sentiment, onchain
|
||||||
|
|
||||||
|
-- Formato de respuestas
|
||||||
|
include_charts BOOLEAN DEFAULT true,
|
||||||
|
include_data_tables BOOLEAN DEFAULT true,
|
||||||
|
include_explanations BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
onboarding_completed BOOLEAN DEFAULT false,
|
||||||
|
onboarding_completed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE UNIQUE INDEX idx_user_preferences_user ON llm.user_preferences(user_id);
|
||||||
|
CREATE INDEX idx_user_preferences_language ON llm.user_preferences(language);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE llm.user_preferences IS 'User preferences for personalized LLM agent interactions';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.language IS 'Preferred language for responses (ISO 639-1 code)';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.tone IS 'Communication style: casual, professional, or technical';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.verbosity IS 'Response length preference: brief, normal, or detailed';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.preferred_symbols IS 'Trading pairs user is most interested in';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.proactive_alerts IS 'Whether agent should send proactive notifications';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.alert_frequency IS 'How often to receive alerts: low, normal, or high';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.notification_hours_start IS 'Start of notification window in user timezone';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.notification_hours_end IS 'End of notification window in user timezone';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.topics_of_interest IS 'Topics user wants to learn about or discuss';
|
||||||
|
COMMENT ON COLUMN llm.user_preferences.trading_experience_level IS 'User experience level for tailoring explanations';
|
||||||
82
ddl/schemas/llm/tables/04-user_memory.sql
Normal file
82
ddl/schemas/llm/tables/04-user_memory.sql
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- LLM SCHEMA - USER MEMORY TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Persistent memory about users for personalization
|
||||||
|
-- Schema: llm
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE llm.user_memory (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Tipo de memoria
|
||||||
|
memory_type llm.memory_type NOT NULL,
|
||||||
|
|
||||||
|
-- Contenido
|
||||||
|
key VARCHAR(200) NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- Importancia
|
||||||
|
importance_score DECIMAL(3,2) DEFAULT 0.50 CHECK (importance_score >= 0.00 AND importance_score <= 1.00),
|
||||||
|
|
||||||
|
-- Fuente
|
||||||
|
source_conversation_id UUID REFERENCES llm.conversations(id) ON DELETE SET NULL,
|
||||||
|
extracted_from TEXT, -- Fragmento del que se extrajo la memoria
|
||||||
|
extraction_method VARCHAR(50) DEFAULT 'llm', -- llm, manual, system
|
||||||
|
|
||||||
|
-- Validez
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Confirmación (algunas memorias pueden requerir confirmación del usuario)
|
||||||
|
requires_confirmation BOOLEAN DEFAULT false,
|
||||||
|
confirmed_by_user BOOLEAN DEFAULT false,
|
||||||
|
confirmed_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
metadata JSONB,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT unique_user_memory_key UNIQUE(user_id, memory_type, key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_memory_user ON llm.user_memory(user_id);
|
||||||
|
CREATE INDEX idx_memory_type ON llm.user_memory(memory_type);
|
||||||
|
CREATE INDEX idx_memory_active ON llm.user_memory(is_active) WHERE is_active = true;
|
||||||
|
CREATE INDEX idx_memory_importance ON llm.user_memory(importance_score DESC);
|
||||||
|
CREATE INDEX idx_memory_conversation ON llm.user_memory(source_conversation_id);
|
||||||
|
CREATE INDEX idx_memory_expires ON llm.user_memory(expires_at) WHERE expires_at IS NOT NULL;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE llm.user_memory IS 'Persistent memory about users for LLM personalization and context';
|
||||||
|
COMMENT ON COLUMN llm.user_memory.memory_type IS 'Type of memory: fact, preference, context, goal, or constraint';
|
||||||
|
COMMENT ON COLUMN llm.user_memory.key IS 'Memory identifier (e.g., "favorite_symbol", "trading_goal", "risk_limit")';
|
||||||
|
COMMENT ON COLUMN llm.user_memory.value IS 'Memory content (e.g., "BTCUSDT", "save for house", "max 5% per trade")';
|
||||||
|
COMMENT ON COLUMN llm.user_memory.importance_score IS 'Importance weight (0.00-1.00) for retrieval prioritization';
|
||||||
|
COMMENT ON COLUMN llm.user_memory.extracted_from IS 'Original text from which memory was extracted';
|
||||||
|
COMMENT ON COLUMN llm.user_memory.requires_confirmation IS 'Whether this memory needs explicit user confirmation';
|
||||||
|
|
||||||
|
-- Ejemplos de memorias por tipo:
|
||||||
|
COMMENT ON COLUMN llm.user_memory.memory_type IS
|
||||||
|
'Memory type examples:
|
||||||
|
- fact: "trading_since" = "2020", "max_loss_experienced" = "15%"
|
||||||
|
- preference: "favorite_indicator" = "RSI", "avoids_margin_trading" = "true"
|
||||||
|
- context: "recent_portfolio_loss" = "trying to recover", "learning_focus" = "risk management"
|
||||||
|
- goal: "monthly_target" = "5% return", "learning_goal" = "master technical analysis"
|
||||||
|
- constraint: "max_risk_per_trade" = "2%", "no_trading_during_work" = "9am-5pm"';
|
||||||
|
|
||||||
|
-- Ejemplo de metadata JSONB:
|
||||||
|
COMMENT ON COLUMN llm.user_memory.metadata IS
|
||||||
|
'Example: {
|
||||||
|
"confidence": 0.85,
|
||||||
|
"last_mentioned": "2025-12-05T10:30:00Z",
|
||||||
|
"mention_count": 5,
|
||||||
|
"related_memories": ["mem_123", "mem_456"],
|
||||||
|
"tags": ["trading_style", "risk_management"]
|
||||||
|
}';
|
||||||
122
ddl/schemas/llm/tables/05-embeddings.sql
Normal file
122
ddl/schemas/llm/tables/05-embeddings.sql
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- LLM SCHEMA - EMBEDDINGS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Vector embeddings for RAG and semantic search
|
||||||
|
-- Schema: llm
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- NOTA: Requiere extensión pgvector
|
||||||
|
-- CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
|
||||||
|
CREATE TABLE llm.embeddings (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Tipo de contenido
|
||||||
|
content_type VARCHAR(50) NOT NULL, -- message, document, faq, tutorial, article
|
||||||
|
|
||||||
|
-- Referencia al contenido original
|
||||||
|
content_id UUID, -- ID del mensaje, documento, etc.
|
||||||
|
|
||||||
|
-- Contenido
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
content_hash VARCHAR(64), -- SHA-256 para deduplicación
|
||||||
|
|
||||||
|
-- Metadata del contenido
|
||||||
|
title VARCHAR(500),
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Vector embedding (dimensión depende del modelo)
|
||||||
|
-- OpenAI text-embedding-3-small: 1536 dims
|
||||||
|
-- OpenAI text-embedding-3-large: 3072 dims
|
||||||
|
-- Voyage AI: 1024 dims
|
||||||
|
embedding vector(1536), -- Ajustar según modelo usado
|
||||||
|
|
||||||
|
-- Modelo usado para generar embedding
|
||||||
|
embedding_model VARCHAR(100) NOT NULL, -- text-embedding-3-small, voyage-2, etc.
|
||||||
|
|
||||||
|
-- Metadata para filtrado
|
||||||
|
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, -- Si es contenido específico de usuario
|
||||||
|
is_public BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
-- Categorización
|
||||||
|
category VARCHAR(100), -- education, trading, market_news, platform_help
|
||||||
|
subcategory VARCHAR(100),
|
||||||
|
tags TEXT[] DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Relevancia
|
||||||
|
importance_score DECIMAL(3,2) DEFAULT 0.50,
|
||||||
|
|
||||||
|
-- Contexto adicional
|
||||||
|
context_metadata JSONB, -- Metadata adicional para mejorar recuperación
|
||||||
|
|
||||||
|
-- Fuente
|
||||||
|
source_url VARCHAR(500),
|
||||||
|
source_type VARCHAR(50), -- internal, external, generated
|
||||||
|
|
||||||
|
-- Validez
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_embeddings_type ON llm.embeddings(content_type);
|
||||||
|
CREATE INDEX idx_embeddings_user ON llm.embeddings(user_id) WHERE user_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_embeddings_category ON llm.embeddings(category);
|
||||||
|
CREATE INDEX idx_embeddings_tags ON llm.embeddings USING GIN(tags);
|
||||||
|
CREATE INDEX idx_embeddings_active ON llm.embeddings(is_active) WHERE is_active = true;
|
||||||
|
CREATE INDEX idx_embeddings_hash ON llm.embeddings(content_hash);
|
||||||
|
|
||||||
|
-- Índice para búsqueda vectorial (HNSW para mejor performance)
|
||||||
|
-- Requiere pgvector
|
||||||
|
CREATE INDEX idx_embeddings_vector_hnsw ON llm.embeddings
|
||||||
|
USING hnsw (embedding vector_cosine_ops);
|
||||||
|
|
||||||
|
-- Índice alternativo: IVFFlat (más rápido de construir, menos preciso)
|
||||||
|
-- CREATE INDEX idx_embeddings_vector_ivfflat ON llm.embeddings
|
||||||
|
-- USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE llm.embeddings IS 'Vector embeddings for RAG and semantic search using pgvector';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.content_type IS 'Type of content: message, document, faq, tutorial, article';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.content_id IS 'Reference to original content (e.g., message ID)';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.content IS 'Text content that was embedded';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.content_hash IS 'SHA-256 hash for deduplication';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.embedding IS 'Vector embedding (dimension depends on model)';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.embedding_model IS 'Model used to generate embedding';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.is_public IS 'Whether embedding is accessible to all users or user-specific';
|
||||||
|
COMMENT ON COLUMN llm.embeddings.importance_score IS 'Relevance score for retrieval prioritization';
|
||||||
|
|
||||||
|
-- Ejemplo de uso para búsqueda semántica:
|
||||||
|
COMMENT ON TABLE llm.embeddings IS
|
||||||
|
'Vector search example:
|
||||||
|
SELECT
|
||||||
|
content,
|
||||||
|
title,
|
||||||
|
1 - (embedding <=> query_embedding) AS similarity
|
||||||
|
FROM llm.embeddings
|
||||||
|
WHERE is_active = true
|
||||||
|
AND category = ''education''
|
||||||
|
ORDER BY embedding <=> query_embedding
|
||||||
|
LIMIT 5;
|
||||||
|
|
||||||
|
Operators:
|
||||||
|
- <-> : L2 distance
|
||||||
|
- <#> : inner product
|
||||||
|
- <=> : cosine distance (recommended)';
|
||||||
|
|
||||||
|
-- Ejemplo de context_metadata JSONB:
|
||||||
|
COMMENT ON COLUMN llm.embeddings.context_metadata IS
|
||||||
|
'Example: {
|
||||||
|
"language": "es",
|
||||||
|
"difficulty_level": "beginner",
|
||||||
|
"reading_time_minutes": 5,
|
||||||
|
"author": "system",
|
||||||
|
"last_updated": "2025-12-01",
|
||||||
|
"related_symbols": ["BTCUSDT"],
|
||||||
|
"related_topics": ["technical_analysis", "rsi"]
|
||||||
|
}';
|
||||||
29
ddl/schemas/market_data/00-enums.sql
Normal file
29
ddl/schemas/market_data/00-enums.sql
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Schema: market_data
|
||||||
|
-- File: 00-enums.sql
|
||||||
|
-- Description: ENUMs para datos de mercado OHLCV
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Tipo de activo
|
||||||
|
CREATE TYPE market_data.asset_type AS ENUM (
|
||||||
|
'forex', -- Pares de divisas (EURUSD, GBPUSD, etc.)
|
||||||
|
'crypto', -- Criptomonedas (BTCUSD, ETHUSD, etc.)
|
||||||
|
'commodity', -- Commodities (XAUUSD, XAGUSD, etc.)
|
||||||
|
'index', -- Índices (SPX500, NAS100, etc.)
|
||||||
|
'stock' -- Acciones
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Temporalidad
|
||||||
|
CREATE TYPE market_data.timeframe AS ENUM (
|
||||||
|
'1m',
|
||||||
|
'5m',
|
||||||
|
'15m',
|
||||||
|
'30m',
|
||||||
|
'1h',
|
||||||
|
'4h',
|
||||||
|
'1d',
|
||||||
|
'1w'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TYPE market_data.asset_type IS 'Clasificación de tipo de activo financiero';
|
||||||
|
COMMENT ON TYPE market_data.timeframe IS 'Temporalidades soportadas para datos OHLCV';
|
||||||
83
ddl/schemas/market_data/functions/01-aggregate_15m.sql
Normal file
83
ddl/schemas/market_data/functions/01-aggregate_15m.sql
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Schema: market_data
|
||||||
|
-- Function: aggregate_5m_to_15m
|
||||||
|
-- Description: Genera datos de 15m a partir de 5m para un ticker
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION market_data.aggregate_5m_to_15m(
|
||||||
|
p_ticker_id INTEGER,
|
||||||
|
p_start_date TIMESTAMPTZ DEFAULT NULL,
|
||||||
|
p_end_date TIMESTAMPTZ DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS INTEGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_inserted INTEGER;
|
||||||
|
v_start TIMESTAMPTZ;
|
||||||
|
v_end TIMESTAMPTZ;
|
||||||
|
BEGIN
|
||||||
|
-- Determinar rango de fechas
|
||||||
|
v_start := COALESCE(p_start_date, '2015-01-01'::TIMESTAMPTZ);
|
||||||
|
v_end := COALESCE(p_end_date, NOW());
|
||||||
|
|
||||||
|
-- Insertar datos agregados de 15m
|
||||||
|
WITH aggregated AS (
|
||||||
|
SELECT
|
||||||
|
ticker_id,
|
||||||
|
date_trunc('hour', timestamp) +
|
||||||
|
INTERVAL '15 minutes' * (EXTRACT(MINUTE FROM timestamp)::INT / 15) AS ts_15m,
|
||||||
|
(array_agg(open ORDER BY timestamp))[1] AS open,
|
||||||
|
MAX(high) AS high,
|
||||||
|
MIN(low) AS low,
|
||||||
|
(array_agg(close ORDER BY timestamp DESC))[1] AS close,
|
||||||
|
SUM(volume) AS volume,
|
||||||
|
AVG(vwap) AS vwap,
|
||||||
|
COUNT(*) AS candle_count
|
||||||
|
FROM market_data.ohlcv_5m
|
||||||
|
WHERE ticker_id = p_ticker_id
|
||||||
|
AND timestamp >= v_start
|
||||||
|
AND timestamp < v_end
|
||||||
|
GROUP BY ticker_id, ts_15m
|
||||||
|
HAVING COUNT(*) >= 2 -- Al menos 2 velas de 5m
|
||||||
|
)
|
||||||
|
INSERT INTO market_data.ohlcv_15m (ticker_id, timestamp, open, high, low, close, volume, vwap, candle_count)
|
||||||
|
SELECT ticker_id, ts_15m, open, high, low, close, volume, vwap, candle_count
|
||||||
|
FROM aggregated
|
||||||
|
ON CONFLICT (ticker_id, timestamp) DO UPDATE SET
|
||||||
|
open = EXCLUDED.open,
|
||||||
|
high = EXCLUDED.high,
|
||||||
|
low = EXCLUDED.low,
|
||||||
|
close = EXCLUDED.close,
|
||||||
|
volume = EXCLUDED.volume,
|
||||||
|
vwap = EXCLUDED.vwap,
|
||||||
|
candle_count = EXCLUDED.candle_count;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS v_inserted = ROW_COUNT;
|
||||||
|
|
||||||
|
RETURN v_inserted;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION market_data.aggregate_5m_to_15m IS 'Genera datos OHLCV de 15m agregando velas de 5m';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Function: aggregate_all_15m
|
||||||
|
-- Description: Genera datos de 15m para todos los tickers activos
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION market_data.aggregate_all_15m()
|
||||||
|
RETURNS TABLE (ticker_symbol VARCHAR, rows_inserted INTEGER) AS $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
v_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT id, symbol FROM market_data.tickers WHERE is_active = true
|
||||||
|
LOOP
|
||||||
|
v_count := market_data.aggregate_5m_to_15m(r.id);
|
||||||
|
ticker_symbol := r.symbol;
|
||||||
|
rows_inserted := v_count;
|
||||||
|
RETURN NEXT;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION market_data.aggregate_all_15m IS 'Genera datos 15m para todos los tickers activos';
|
||||||
54
ddl/schemas/market_data/tables/01-tickers.sql
Normal file
54
ddl/schemas/market_data/tables/01-tickers.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Schema: market_data
|
||||||
|
-- Table: tickers
|
||||||
|
-- Description: Catálogo de activos/tickers para datos de mercado
|
||||||
|
-- Dependencies: market_data.asset_type
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE market_data.tickers (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Identificación
|
||||||
|
symbol VARCHAR(20) NOT NULL UNIQUE, -- XAUUSD, EURUSD, BTCUSD
|
||||||
|
name VARCHAR(100) NOT NULL, -- Gold/US Dollar, Euro/US Dollar
|
||||||
|
|
||||||
|
-- Clasificación
|
||||||
|
asset_type market_data.asset_type NOT NULL,
|
||||||
|
base_currency VARCHAR(10) NOT NULL, -- XAU, EUR, BTC
|
||||||
|
quote_currency VARCHAR(10) NOT NULL, -- USD
|
||||||
|
|
||||||
|
-- Configuración ML
|
||||||
|
is_ml_enabled BOOLEAN DEFAULT true, -- Habilitado para ML signals
|
||||||
|
supported_timeframes VARCHAR(50)[] DEFAULT ARRAY['5m', '15m'],
|
||||||
|
|
||||||
|
-- Polygon.io mapping
|
||||||
|
polygon_ticker VARCHAR(20), -- C:XAUUSD, X:BTCUSD
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_tickers_symbol ON market_data.tickers(symbol);
|
||||||
|
CREATE INDEX idx_tickers_asset_type ON market_data.tickers(asset_type);
|
||||||
|
CREATE INDEX idx_tickers_ml_enabled ON market_data.tickers(is_ml_enabled) WHERE is_ml_enabled = true;
|
||||||
|
CREATE INDEX idx_tickers_polygon ON market_data.tickers(polygon_ticker);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE market_data.tickers IS 'Catálogo de activos financieros para datos OHLCV';
|
||||||
|
COMMENT ON COLUMN market_data.tickers.symbol IS 'Símbolo del activo (XAUUSD, BTCUSD, etc.)';
|
||||||
|
COMMENT ON COLUMN market_data.tickers.polygon_ticker IS 'Símbolo en formato Polygon.io (C:XAUUSD, X:BTCUSD)';
|
||||||
|
COMMENT ON COLUMN market_data.tickers.is_ml_enabled IS 'Indica si el activo está habilitado para ML signals';
|
||||||
|
|
||||||
|
-- Seed: 6 activos principales
|
||||||
|
INSERT INTO market_data.tickers (symbol, name, asset_type, base_currency, quote_currency, polygon_ticker) VALUES
|
||||||
|
('XAUUSD', 'Gold/US Dollar', 'commodity', 'XAU', 'USD', 'C:XAUUSD'),
|
||||||
|
('EURUSD', 'Euro/US Dollar', 'forex', 'EUR', 'USD', 'C:EURUSD'),
|
||||||
|
('BTCUSD', 'Bitcoin/US Dollar', 'crypto', 'BTC', 'USD', 'X:BTCUSD'),
|
||||||
|
('GBPUSD', 'British Pound/US Dollar', 'forex', 'GBP', 'USD', 'C:GBPUSD'),
|
||||||
|
('USDJPY', 'US Dollar/Japanese Yen', 'forex', 'USD', 'JPY', 'C:USDJPY'),
|
||||||
|
('AUDUSD', 'Australian Dollar/US Dollar', 'forex', 'AUD', 'USD', 'C:AUDUSD');
|
||||||
44
ddl/schemas/market_data/tables/02-ohlcv_5m.sql
Normal file
44
ddl/schemas/market_data/tables/02-ohlcv_5m.sql
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Schema: market_data
|
||||||
|
-- Table: ohlcv_5m
|
||||||
|
-- Description: Datos OHLCV agregados a 5 minutos
|
||||||
|
-- Dependencies: market_data.tickers
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE market_data.ohlcv_5m (
|
||||||
|
id BIGSERIAL,
|
||||||
|
ticker_id INTEGER NOT NULL REFERENCES market_data.tickers(id),
|
||||||
|
|
||||||
|
-- Timestamp
|
||||||
|
timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
|
||||||
|
-- OHLCV
|
||||||
|
open DECIMAL(20,8) NOT NULL,
|
||||||
|
high DECIMAL(20,8) NOT NULL,
|
||||||
|
low DECIMAL(20,8) NOT NULL,
|
||||||
|
close DECIMAL(20,8) NOT NULL,
|
||||||
|
volume DECIMAL(20,4) DEFAULT 0,
|
||||||
|
|
||||||
|
-- Datos adicionales de Polygon
|
||||||
|
vwap DECIMAL(20,8), -- Volume Weighted Average Price
|
||||||
|
ts_epoch BIGINT, -- Timestamp original en ms
|
||||||
|
|
||||||
|
-- Metadatos
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraint único
|
||||||
|
CONSTRAINT ohlcv_5m_unique UNIQUE (ticker_id, timestamp),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices optimizados para consultas ML
|
||||||
|
CREATE INDEX idx_ohlcv_5m_ticker_ts ON market_data.ohlcv_5m(ticker_id, timestamp DESC);
|
||||||
|
CREATE INDEX idx_ohlcv_5m_timestamp ON market_data.ohlcv_5m(timestamp DESC);
|
||||||
|
-- Índice para consultas recientes (sin filtro temporal - NOW() no es IMMUTABLE)
|
||||||
|
CREATE INDEX idx_ohlcv_5m_ticker_recent ON market_data.ohlcv_5m(ticker_id, timestamp DESC)
|
||||||
|
WHERE timestamp >= '2024-01-01'::TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE market_data.ohlcv_5m IS 'Datos OHLCV agregados a 5 minutos - Fuente: Polygon.io';
|
||||||
|
COMMENT ON COLUMN market_data.ohlcv_5m.vwap IS 'Volume Weighted Average Price del período';
|
||||||
|
COMMENT ON COLUMN market_data.ohlcv_5m.ts_epoch IS 'Timestamp original de Polygon en milisegundos';
|
||||||
43
ddl/schemas/market_data/tables/03-ohlcv_15m.sql
Normal file
43
ddl/schemas/market_data/tables/03-ohlcv_15m.sql
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Schema: market_data
|
||||||
|
-- Table: ohlcv_15m
|
||||||
|
-- Description: Datos OHLCV agregados a 15 minutos (derivados de 5m)
|
||||||
|
-- Dependencies: market_data.tickers, market_data.ohlcv_5m
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE market_data.ohlcv_15m (
|
||||||
|
id BIGSERIAL,
|
||||||
|
ticker_id INTEGER NOT NULL REFERENCES market_data.tickers(id),
|
||||||
|
|
||||||
|
-- Timestamp (inicio del período de 15 minutos)
|
||||||
|
timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
|
||||||
|
-- OHLCV agregados
|
||||||
|
open DECIMAL(20,8) NOT NULL,
|
||||||
|
high DECIMAL(20,8) NOT NULL,
|
||||||
|
low DECIMAL(20,8) NOT NULL,
|
||||||
|
close DECIMAL(20,8) NOT NULL,
|
||||||
|
volume DECIMAL(20,4) DEFAULT 0,
|
||||||
|
|
||||||
|
-- Datos agregados
|
||||||
|
vwap DECIMAL(20,8), -- VWAP del período
|
||||||
|
candle_count INTEGER DEFAULT 3, -- Número de velas 5m agregadas
|
||||||
|
|
||||||
|
-- Metadatos
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraint único
|
||||||
|
CONSTRAINT ohlcv_15m_unique UNIQUE (ticker_id, timestamp),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices optimizados para consultas ML
|
||||||
|
CREATE INDEX idx_ohlcv_15m_ticker_ts ON market_data.ohlcv_15m(ticker_id, timestamp DESC);
|
||||||
|
CREATE INDEX idx_ohlcv_15m_timestamp ON market_data.ohlcv_15m(timestamp DESC);
|
||||||
|
-- Índice para consultas recientes (sin filtro temporal - NOW() no es IMMUTABLE)
|
||||||
|
CREATE INDEX idx_ohlcv_15m_ticker_recent ON market_data.ohlcv_15m(ticker_id, timestamp DESC)
|
||||||
|
WHERE timestamp >= '2024-01-01'::TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE market_data.ohlcv_15m IS 'Datos OHLCV agregados a 15 minutos - Derivados de ohlcv_5m';
|
||||||
|
COMMENT ON COLUMN market_data.ohlcv_15m.candle_count IS 'Número de velas de 5m que componen esta vela de 15m';
|
||||||
20
ddl/schemas/market_data/tables/04-staging.sql
Normal file
20
ddl/schemas/market_data/tables/04-staging.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Schema: market_data
|
||||||
|
-- Table: ohlcv_5m_staging
|
||||||
|
-- Description: Tabla temporal para carga masiva de datos
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE market_data.ohlcv_5m_staging (
|
||||||
|
ticker_id INTEGER,
|
||||||
|
timestamp TIMESTAMPTZ,
|
||||||
|
open DECIMAL(20,8),
|
||||||
|
high DECIMAL(20,8),
|
||||||
|
low DECIMAL(20,8),
|
||||||
|
close DECIMAL(20,8),
|
||||||
|
volume DECIMAL(20,4),
|
||||||
|
vwap DECIMAL(20,8),
|
||||||
|
ts_epoch BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Sin índices para carga rápida
|
||||||
|
COMMENT ON TABLE market_data.ohlcv_5m_staging IS 'Tabla staging para carga masiva de datos OHLCV';
|
||||||
68
ddl/schemas/ml/00-enums.sql
Normal file
68
ddl/schemas/ml/00-enums.sql
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ML SCHEMA - ENUMS
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Enumerations for ML signals system
|
||||||
|
-- Schema: ml
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Tipo de modelo ML
|
||||||
|
CREATE TYPE ml.model_type AS ENUM (
|
||||||
|
'classification',
|
||||||
|
'regression',
|
||||||
|
'time_series',
|
||||||
|
'clustering',
|
||||||
|
'anomaly_detection',
|
||||||
|
'reinforcement_learning'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Framework de ML
|
||||||
|
CREATE TYPE ml.framework AS ENUM (
|
||||||
|
'sklearn',
|
||||||
|
'tensorflow',
|
||||||
|
'pytorch',
|
||||||
|
'xgboost',
|
||||||
|
'lightgbm',
|
||||||
|
'prophet',
|
||||||
|
'custom'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado del modelo
|
||||||
|
CREATE TYPE ml.model_status AS ENUM (
|
||||||
|
'development',
|
||||||
|
'testing',
|
||||||
|
'staging',
|
||||||
|
'production',
|
||||||
|
'deprecated',
|
||||||
|
'archived'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tipo de predicción
|
||||||
|
CREATE TYPE ml.prediction_type AS ENUM (
|
||||||
|
'price_direction', -- UP/DOWN/NEUTRAL
|
||||||
|
'price_target', -- Precio objetivo
|
||||||
|
'volatility', -- Alta/Media/Baja
|
||||||
|
'trend', -- Tendencia
|
||||||
|
'signal', -- BUY/SELL/HOLD
|
||||||
|
'risk_score' -- Score de riesgo
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Resultado de predicción
|
||||||
|
CREATE TYPE ml.prediction_result AS ENUM (
|
||||||
|
'buy',
|
||||||
|
'sell',
|
||||||
|
'hold',
|
||||||
|
'up',
|
||||||
|
'down',
|
||||||
|
'neutral'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado de outcome
|
||||||
|
CREATE TYPE ml.outcome_status AS ENUM (
|
||||||
|
'pending',
|
||||||
|
'correct',
|
||||||
|
'incorrect',
|
||||||
|
'partially_correct',
|
||||||
|
'expired'
|
||||||
|
);
|
||||||
392
ddl/schemas/ml/functions/05-calculate_prediction_accuracy.sql
Normal file
392
ddl/schemas/ml/functions/05-calculate_prediction_accuracy.sql
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ML SCHEMA - CALCULATE PREDICTION ACCURACY FUNCTION
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Function to calculate LLM prediction accuracy metrics
|
||||||
|
-- Schema: ml
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2026-01-04
|
||||||
|
-- Module: OQI-010-llm-trading-integration
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Function: ml.calculate_llm_prediction_accuracy
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Calculates accuracy metrics for LLM predictions
|
||||||
|
-- Parameters:
|
||||||
|
-- p_symbol: Trading symbol (required)
|
||||||
|
-- p_days: Number of days to analyze (default: 30)
|
||||||
|
-- Returns:
|
||||||
|
-- total_predictions: Total number of resolved predictions
|
||||||
|
-- direction_accuracy: Percentage of correct direction predictions
|
||||||
|
-- target_hit_rate: Percentage of predictions that hit take profit
|
||||||
|
-- avg_pnl_pips: Average profit/loss in pips
|
||||||
|
-- profit_factor: Ratio of gross profit to gross loss
|
||||||
|
-- win_rate: Percentage of profitable trades
|
||||||
|
-- avg_resolution_candles: Average candles to resolution
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION ml.calculate_llm_prediction_accuracy(
|
||||||
|
p_symbol VARCHAR,
|
||||||
|
p_days INT DEFAULT 30
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
total_predictions INT,
|
||||||
|
direction_accuracy DECIMAL(5,4),
|
||||||
|
target_hit_rate DECIMAL(5,4),
|
||||||
|
stop_hit_rate DECIMAL(5,4),
|
||||||
|
avg_pnl_pips DECIMAL(10,2),
|
||||||
|
profit_factor DECIMAL(10,4),
|
||||||
|
win_rate DECIMAL(5,4),
|
||||||
|
avg_resolution_candles DECIMAL(10,2)
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::INT AS total_predictions,
|
||||||
|
|
||||||
|
-- Direction accuracy (correct predictions / total)
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.direction_correct = TRUE THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS direction_accuracy,
|
||||||
|
|
||||||
|
-- Target hit rate (predictions that reached take profit)
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.target_reached = TRUE THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS target_hit_rate,
|
||||||
|
|
||||||
|
-- Stop hit rate (predictions that hit stop loss)
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.stop_hit = TRUE THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS stop_hit_rate,
|
||||||
|
|
||||||
|
-- Average PnL in pips
|
||||||
|
COALESCE(AVG(o.pnl_pips)::DECIMAL(10,2), 0.0) AS avg_pnl_pips,
|
||||||
|
|
||||||
|
-- Profit factor (gross profit / gross loss)
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(SUM(CASE WHEN o.pnl_pips < 0 THEN ABS(o.pnl_pips) ELSE 0 END), 0) > 0
|
||||||
|
THEN (
|
||||||
|
COALESCE(SUM(CASE WHEN o.pnl_pips > 0 THEN o.pnl_pips ELSE 0 END), 0) /
|
||||||
|
SUM(CASE WHEN o.pnl_pips < 0 THEN ABS(o.pnl_pips) ELSE 0 END)
|
||||||
|
)::DECIMAL(10,4)
|
||||||
|
ELSE NULL
|
||||||
|
END AS profit_factor,
|
||||||
|
|
||||||
|
-- Win rate (profitable trades / total)
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.pnl_pips > 0 THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS win_rate,
|
||||||
|
|
||||||
|
-- Average candles to resolution
|
||||||
|
COALESCE(AVG(o.resolution_candles)::DECIMAL(10,2), 0.0) AS avg_resolution_candles
|
||||||
|
|
||||||
|
FROM ml.llm_predictions p
|
||||||
|
INNER JOIN ml.llm_prediction_outcomes o ON p.id = o.prediction_id
|
||||||
|
WHERE p.symbol = p_symbol
|
||||||
|
AND p.created_at >= NOW() - (p_days || ' days')::INTERVAL
|
||||||
|
AND o.resolved_at IS NOT NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION ml.calculate_llm_prediction_accuracy(VARCHAR, INT) IS
|
||||||
|
'Calculates comprehensive accuracy metrics for LLM predictions.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- p_symbol: Trading symbol to analyze (e.g., XAUUSD, BTCUSDT)
|
||||||
|
- p_days: Number of days to look back (default: 30)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- total_predictions: Count of resolved predictions in period
|
||||||
|
- direction_accuracy: Ratio of correct direction predictions (0.0 to 1.0)
|
||||||
|
- target_hit_rate: Ratio of predictions that hit take profit (0.0 to 1.0)
|
||||||
|
- stop_hit_rate: Ratio of predictions that hit stop loss (0.0 to 1.0)
|
||||||
|
- avg_pnl_pips: Average profit/loss in pips
|
||||||
|
- profit_factor: Gross profit / Gross loss (>1.0 is profitable)
|
||||||
|
- win_rate: Ratio of profitable trades (0.0 to 1.0)
|
||||||
|
- avg_resolution_candles: Average candles until outcome determined
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
SELECT * FROM ml.calculate_llm_prediction_accuracy(''XAUUSD'', 30);
|
||||||
|
SELECT * FROM ml.calculate_llm_prediction_accuracy(''BTCUSDT'', 7);
|
||||||
|
';
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Function: ml.calculate_llm_prediction_accuracy_by_phase
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Calculates accuracy metrics grouped by AMD phase
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION ml.calculate_llm_prediction_accuracy_by_phase(
|
||||||
|
p_symbol VARCHAR,
|
||||||
|
p_days INT DEFAULT 30
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
amd_phase VARCHAR(50),
|
||||||
|
total_predictions INT,
|
||||||
|
direction_accuracy DECIMAL(5,4),
|
||||||
|
avg_pnl_pips DECIMAL(10,2),
|
||||||
|
win_rate DECIMAL(5,4)
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
p.amd_phase,
|
||||||
|
COUNT(*)::INT AS total_predictions,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.direction_correct = TRUE THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS direction_accuracy,
|
||||||
|
COALESCE(AVG(o.pnl_pips)::DECIMAL(10,2), 0.0) AS avg_pnl_pips,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.pnl_pips > 0 THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS win_rate
|
||||||
|
FROM ml.llm_predictions p
|
||||||
|
INNER JOIN ml.llm_prediction_outcomes o ON p.id = o.prediction_id
|
||||||
|
WHERE p.symbol = p_symbol
|
||||||
|
AND p.created_at >= NOW() - (p_days || ' days')::INTERVAL
|
||||||
|
AND o.resolved_at IS NOT NULL
|
||||||
|
AND p.amd_phase IS NOT NULL
|
||||||
|
GROUP BY p.amd_phase
|
||||||
|
ORDER BY total_predictions DESC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION ml.calculate_llm_prediction_accuracy_by_phase(VARCHAR, INT) IS
|
||||||
|
'Calculates prediction accuracy metrics grouped by AMD phase.
|
||||||
|
|
||||||
|
Useful for understanding which AMD phases produce the most accurate predictions.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
SELECT * FROM ml.calculate_llm_prediction_accuracy_by_phase(''XAUUSD'', 30);
|
||||||
|
';
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Function: ml.calculate_llm_prediction_accuracy_by_killzone
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Calculates accuracy metrics grouped by ICT Killzone
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION ml.calculate_llm_prediction_accuracy_by_killzone(
|
||||||
|
p_symbol VARCHAR,
|
||||||
|
p_days INT DEFAULT 30
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
killzone VARCHAR(50),
|
||||||
|
total_predictions INT,
|
||||||
|
direction_accuracy DECIMAL(5,4),
|
||||||
|
avg_pnl_pips DECIMAL(10,2),
|
||||||
|
win_rate DECIMAL(5,4)
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
p.killzone,
|
||||||
|
COUNT(*)::INT AS total_predictions,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.direction_correct = TRUE THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS direction_accuracy,
|
||||||
|
COALESCE(AVG(o.pnl_pips)::DECIMAL(10,2), 0.0) AS avg_pnl_pips,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.pnl_pips > 0 THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS win_rate
|
||||||
|
FROM ml.llm_predictions p
|
||||||
|
INNER JOIN ml.llm_prediction_outcomes o ON p.id = o.prediction_id
|
||||||
|
WHERE p.symbol = p_symbol
|
||||||
|
AND p.created_at >= NOW() - (p_days || ' days')::INTERVAL
|
||||||
|
AND o.resolved_at IS NOT NULL
|
||||||
|
AND p.killzone IS NOT NULL
|
||||||
|
GROUP BY p.killzone
|
||||||
|
ORDER BY total_predictions DESC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION ml.calculate_llm_prediction_accuracy_by_killzone(VARCHAR, INT) IS
|
||||||
|
'Calculates prediction accuracy metrics grouped by ICT Killzone.
|
||||||
|
|
||||||
|
Useful for understanding which trading sessions produce the best predictions.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
SELECT * FROM ml.calculate_llm_prediction_accuracy_by_killzone(''XAUUSD'', 30);
|
||||||
|
';
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Function: ml.calculate_llm_prediction_accuracy_by_confluence
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Calculates accuracy metrics grouped by confluence score ranges
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION ml.calculate_llm_prediction_accuracy_by_confluence(
|
||||||
|
p_symbol VARCHAR,
|
||||||
|
p_days INT DEFAULT 30
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
confluence_range VARCHAR(20),
|
||||||
|
total_predictions INT,
|
||||||
|
direction_accuracy DECIMAL(5,4),
|
||||||
|
avg_pnl_pips DECIMAL(10,2),
|
||||||
|
win_rate DECIMAL(5,4)
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN p.confluence_score >= 0.8 THEN '0.8-1.0 (High)'
|
||||||
|
WHEN p.confluence_score >= 0.6 THEN '0.6-0.8 (Medium-High)'
|
||||||
|
WHEN p.confluence_score >= 0.4 THEN '0.4-0.6 (Medium)'
|
||||||
|
WHEN p.confluence_score >= 0.2 THEN '0.2-0.4 (Medium-Low)'
|
||||||
|
ELSE '0.0-0.2 (Low)'
|
||||||
|
END AS confluence_range,
|
||||||
|
COUNT(*)::INT AS total_predictions,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.direction_correct = TRUE THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS direction_accuracy,
|
||||||
|
COALESCE(AVG(o.pnl_pips)::DECIMAL(10,2), 0.0) AS avg_pnl_pips,
|
||||||
|
COALESCE(
|
||||||
|
AVG(CASE WHEN o.pnl_pips > 0 THEN 1.0 ELSE 0.0 END)::DECIMAL(5,4),
|
||||||
|
0.0
|
||||||
|
) AS win_rate
|
||||||
|
FROM ml.llm_predictions p
|
||||||
|
INNER JOIN ml.llm_prediction_outcomes o ON p.id = o.prediction_id
|
||||||
|
WHERE p.symbol = p_symbol
|
||||||
|
AND p.created_at >= NOW() - (p_days || ' days')::INTERVAL
|
||||||
|
AND o.resolved_at IS NOT NULL
|
||||||
|
AND p.confluence_score IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
CASE
|
||||||
|
WHEN p.confluence_score >= 0.8 THEN '0.8-1.0 (High)'
|
||||||
|
WHEN p.confluence_score >= 0.6 THEN '0.6-0.8 (Medium-High)'
|
||||||
|
WHEN p.confluence_score >= 0.4 THEN '0.4-0.6 (Medium)'
|
||||||
|
WHEN p.confluence_score >= 0.2 THEN '0.2-0.4 (Medium-Low)'
|
||||||
|
ELSE '0.0-0.2 (Low)'
|
||||||
|
END
|
||||||
|
ORDER BY confluence_range DESC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION ml.calculate_llm_prediction_accuracy_by_confluence(VARCHAR, INT) IS
|
||||||
|
'Calculates prediction accuracy metrics grouped by confluence score ranges.
|
||||||
|
|
||||||
|
Validates whether higher confluence scores correlate with better accuracy.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
SELECT * FROM ml.calculate_llm_prediction_accuracy_by_confluence(''XAUUSD'', 30);
|
||||||
|
';
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Function: ml.get_active_risk_events
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Returns all unresolved risk events for a user
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION ml.get_active_risk_events(
|
||||||
|
p_user_id UUID DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
id UUID,
|
||||||
|
event_type VARCHAR(50),
|
||||||
|
severity VARCHAR(20),
|
||||||
|
details JSONB,
|
||||||
|
action_taken VARCHAR(100),
|
||||||
|
created_at TIMESTAMPTZ
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.event_type,
|
||||||
|
r.severity,
|
||||||
|
r.details,
|
||||||
|
r.action_taken,
|
||||||
|
r.created_at
|
||||||
|
FROM ml.risk_events r
|
||||||
|
WHERE r.resolved = FALSE
|
||||||
|
AND (p_user_id IS NULL OR r.user_id = p_user_id OR r.user_id IS NULL)
|
||||||
|
ORDER BY
|
||||||
|
CASE r.severity
|
||||||
|
WHEN 'emergency' THEN 1
|
||||||
|
WHEN 'critical' THEN 2
|
||||||
|
WHEN 'warning' THEN 3
|
||||||
|
ELSE 4
|
||||||
|
END,
|
||||||
|
r.created_at DESC;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION ml.get_active_risk_events(UUID) IS
|
||||||
|
'Returns all unresolved risk events, optionally filtered by user.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- p_user_id: User ID to filter by (NULL for all events including system-wide)
|
||||||
|
|
||||||
|
Returns events ordered by severity (emergency first) then by time.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
SELECT * FROM ml.get_active_risk_events();
|
||||||
|
SELECT * FROM ml.get_active_risk_events(''550e8400-e29b-41d4-a716-446655440000'');
|
||||||
|
';
|
||||||
|
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Function: ml.check_circuit_breaker_status
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- Checks if circuit breaker is active for a user
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION ml.check_circuit_breaker_status(
|
||||||
|
p_user_id UUID DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
is_active BOOLEAN,
|
||||||
|
event_id UUID,
|
||||||
|
trigger_reason TEXT,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
details JSONB
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
TRUE AS is_active,
|
||||||
|
r.id AS event_id,
|
||||||
|
r.details->>'trigger_reason' AS trigger_reason,
|
||||||
|
r.created_at,
|
||||||
|
r.details
|
||||||
|
FROM ml.risk_events r
|
||||||
|
WHERE r.event_type = 'CIRCUIT_BREAKER'
|
||||||
|
AND r.resolved = FALSE
|
||||||
|
AND (p_user_id IS NULL OR r.user_id = p_user_id OR r.user_id IS NULL)
|
||||||
|
ORDER BY r.created_at DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If no rows returned, return false status
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RETURN QUERY SELECT FALSE, NULL::UUID, NULL::TEXT, NULL::TIMESTAMPTZ, NULL::JSONB;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql STABLE;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION ml.check_circuit_breaker_status(UUID) IS
|
||||||
|
'Checks if circuit breaker is currently active for a user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- is_active: TRUE if circuit breaker is engaged
|
||||||
|
- event_id: ID of the active circuit breaker event
|
||||||
|
- trigger_reason: Reason the circuit breaker was triggered
|
||||||
|
- created_at: When the circuit breaker was activated
|
||||||
|
- details: Full details of the event
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
SELECT * FROM ml.check_circuit_breaker_status();
|
||||||
|
SELECT is_active FROM ml.check_circuit_breaker_status(''550e8400-e29b-41d4-a716-446655440000'');
|
||||||
|
';
|
||||||
65
ddl/schemas/ml/tables/01-models.sql
Normal file
65
ddl/schemas/ml/tables/01-models.sql
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ML SCHEMA - MODELS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: ML models registry
|
||||||
|
-- Schema: ml
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE ml.models (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Identificación
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
display_name VARCHAR(200) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
|
||||||
|
-- Tipo y framework
|
||||||
|
model_type ml.model_type NOT NULL,
|
||||||
|
framework ml.framework NOT NULL,
|
||||||
|
|
||||||
|
-- Categoría
|
||||||
|
category VARCHAR(50) NOT NULL, -- sentiment, technical, fundamental, hybrid
|
||||||
|
|
||||||
|
-- Alcance
|
||||||
|
applies_to_symbols VARCHAR(20)[] DEFAULT '{}', -- ['BTCUSDT', 'ETHUSDT'] o [] para todos
|
||||||
|
applies_to_timeframes VARCHAR(10)[] DEFAULT '{}', -- ['1h', '4h', '1d'] o [] para todos
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
status ml.model_status DEFAULT 'development',
|
||||||
|
|
||||||
|
-- Versión actual en producción
|
||||||
|
current_version_id UUID,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
owner VARCHAR(100) NOT NULL,
|
||||||
|
repository_url VARCHAR(500),
|
||||||
|
documentation_url VARCHAR(500),
|
||||||
|
|
||||||
|
-- Métricas agregadas (de todas las versiones)
|
||||||
|
total_predictions INTEGER DEFAULT 0,
|
||||||
|
total_correct_predictions INTEGER DEFAULT 0,
|
||||||
|
overall_accuracy DECIMAL(5,4),
|
||||||
|
|
||||||
|
-- Fechas
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deployed_at TIMESTAMPTZ,
|
||||||
|
deprecated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_models_name ON ml.models(name);
|
||||||
|
CREATE INDEX idx_models_status ON ml.models(status);
|
||||||
|
CREATE INDEX idx_models_type ON ml.models(model_type);
|
||||||
|
CREATE INDEX idx_models_category ON ml.models(category);
|
||||||
|
CREATE INDEX idx_models_production ON ml.models(status) WHERE status = 'production';
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE ml.models IS 'Registry of ML models for trading signals';
|
||||||
|
COMMENT ON COLUMN ml.models.name IS 'Unique technical name (e.g., sentiment_analyzer_v1)';
|
||||||
|
COMMENT ON COLUMN ml.models.applies_to_symbols IS 'Symbols this model can analyze. Empty array = all symbols';
|
||||||
|
COMMENT ON COLUMN ml.models.applies_to_timeframes IS 'Timeframes this model supports. Empty array = all timeframes';
|
||||||
|
COMMENT ON COLUMN ml.models.current_version_id IS 'Reference to current production version';
|
||||||
|
COMMENT ON COLUMN ml.models.overall_accuracy IS 'Aggregated accuracy across all versions and predictions';
|
||||||
102
ddl/schemas/ml/tables/02-model_versions.sql
Normal file
102
ddl/schemas/ml/tables/02-model_versions.sql
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ML SCHEMA - MODEL VERSIONS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: Versioned ML model artifacts and metadata
|
||||||
|
-- Schema: ml
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE ml.model_versions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Modelo padre
|
||||||
|
model_id UUID NOT NULL REFERENCES ml.models(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Versión
|
||||||
|
version VARCHAR(50) NOT NULL, -- Semantic versioning: 1.0.0, 1.1.0, 2.0.0
|
||||||
|
|
||||||
|
-- Artefacto
|
||||||
|
artifact_path VARCHAR(500) NOT NULL, -- S3 path, local path, registry URL
|
||||||
|
artifact_size_bytes BIGINT,
|
||||||
|
checksum VARCHAR(64), -- SHA-256
|
||||||
|
|
||||||
|
-- Métricas de entrenamiento
|
||||||
|
training_metrics JSONB, -- {accuracy, precision, recall, f1, loss}
|
||||||
|
validation_metrics JSONB, -- {accuracy, precision, recall, f1, loss}
|
||||||
|
test_metrics JSONB, -- {accuracy, precision, recall, f1, loss}
|
||||||
|
|
||||||
|
-- Features
|
||||||
|
feature_set JSONB NOT NULL, -- Lista de features usadas
|
||||||
|
feature_importance JSONB, -- Importancia de cada feature
|
||||||
|
|
||||||
|
-- Hiperparámetros
|
||||||
|
hyperparameters JSONB, -- Parámetros del modelo
|
||||||
|
|
||||||
|
-- Dataset info
|
||||||
|
training_dataset_size INTEGER,
|
||||||
|
training_dataset_path VARCHAR(500),
|
||||||
|
data_version VARCHAR(50),
|
||||||
|
|
||||||
|
-- Estado
|
||||||
|
is_production BOOLEAN DEFAULT false,
|
||||||
|
deployed_at TIMESTAMPTZ,
|
||||||
|
deployment_metadata JSONB,
|
||||||
|
|
||||||
|
-- Metadata de entrenamiento
|
||||||
|
training_started_at TIMESTAMPTZ,
|
||||||
|
training_completed_at TIMESTAMPTZ,
|
||||||
|
training_duration_seconds INTEGER,
|
||||||
|
trained_by VARCHAR(100),
|
||||||
|
training_environment JSONB, -- Python version, library versions, etc.
|
||||||
|
|
||||||
|
-- Performance en producción
|
||||||
|
production_predictions INTEGER DEFAULT 0,
|
||||||
|
production_accuracy DECIMAL(5,4),
|
||||||
|
|
||||||
|
-- Notas
|
||||||
|
release_notes TEXT,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Constraints
|
||||||
|
CONSTRAINT unique_model_version UNIQUE(model_id, version)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_model_versions_model ON ml.model_versions(model_id);
|
||||||
|
CREATE INDEX idx_model_versions_production ON ml.model_versions(is_production)
|
||||||
|
WHERE is_production = true;
|
||||||
|
CREATE INDEX idx_model_versions_version ON ml.model_versions(version);
|
||||||
|
CREATE INDEX idx_model_versions_deployed ON ml.model_versions(deployed_at DESC);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE ml.model_versions IS 'Versioned artifacts and metadata for ML models';
|
||||||
|
COMMENT ON COLUMN ml.model_versions.version IS 'Semantic version (major.minor.patch)';
|
||||||
|
COMMENT ON COLUMN ml.model_versions.artifact_path IS 'Location of serialized model file';
|
||||||
|
COMMENT ON COLUMN ml.model_versions.checksum IS 'SHA-256 hash for artifact integrity verification';
|
||||||
|
COMMENT ON COLUMN ml.model_versions.feature_set IS 'Array of feature names used by this version';
|
||||||
|
COMMENT ON COLUMN ml.model_versions.hyperparameters IS 'Model hyperparameters for reproducibility';
|
||||||
|
COMMENT ON COLUMN ml.model_versions.is_production IS 'Whether this version is currently deployed in production';
|
||||||
|
|
||||||
|
-- Ejemplo de training_metrics JSONB:
|
||||||
|
COMMENT ON COLUMN ml.model_versions.training_metrics IS
|
||||||
|
'Example: {
|
||||||
|
"accuracy": 0.8542,
|
||||||
|
"precision": 0.8234,
|
||||||
|
"recall": 0.7891,
|
||||||
|
"f1_score": 0.8058,
|
||||||
|
"loss": 0.3421,
|
||||||
|
"auc_roc": 0.9123
|
||||||
|
}';
|
||||||
|
|
||||||
|
-- Ejemplo de feature_set JSONB:
|
||||||
|
COMMENT ON COLUMN ml.model_versions.feature_set IS
|
||||||
|
'Example: [
|
||||||
|
"rsi_14",
|
||||||
|
"macd_signal",
|
||||||
|
"volume_sma_20",
|
||||||
|
"price_change_1h",
|
||||||
|
"sentiment_score"
|
||||||
|
]';
|
||||||
94
ddl/schemas/ml/tables/03-predictions.sql
Normal file
94
ddl/schemas/ml/tables/03-predictions.sql
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- ML SCHEMA - PREDICTIONS TABLE
|
||||||
|
-- =====================================================
|
||||||
|
-- Description: ML model predictions and signals
|
||||||
|
-- Schema: ml
|
||||||
|
-- Author: Database Agent
|
||||||
|
-- Date: 2025-12-06
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
CREATE TABLE ml.predictions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Modelo y versión
|
||||||
|
model_id UUID NOT NULL REFERENCES ml.models(id) ON DELETE CASCADE,
|
||||||
|
model_version_id UUID NOT NULL REFERENCES ml.model_versions(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Símbolo y timeframe
|
||||||
|
symbol VARCHAR(20) NOT NULL,
|
||||||
|
timeframe VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
-- Tipo de predicción
|
||||||
|
prediction_type ml.prediction_type NOT NULL,
|
||||||
|
|
||||||
|
-- Resultado de predicción
|
||||||
|
prediction_result ml.prediction_result,
|
||||||
|
prediction_value DECIMAL(20,8), -- Para predicciones numéricas
|
||||||
|
|
||||||
|
-- Confianza
|
||||||
|
confidence_score DECIMAL(5,4) NOT NULL CHECK (confidence_score >= 0 AND confidence_score <= 1),
|
||||||
|
|
||||||
|
-- Input features utilizados
|
||||||
|
input_features JSONB NOT NULL,
|
||||||
|
|
||||||
|
-- Output completo del modelo
|
||||||
|
model_output JSONB, -- Raw output del modelo
|
||||||
|
|
||||||
|
-- Contexto de mercado al momento de predicción
|
||||||
|
market_price DECIMAL(20,8),
|
||||||
|
market_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
|
||||||
|
-- Horizonte temporal
|
||||||
|
prediction_horizon VARCHAR(20), -- 1h, 4h, 1d, 1w
|
||||||
|
valid_until TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
prediction_metadata JSONB,
|
||||||
|
|
||||||
|
-- Procesamiento
|
||||||
|
inference_time_ms INTEGER, -- Tiempo de inferencia en milisegundos
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices
|
||||||
|
CREATE INDEX idx_predictions_model ON ml.predictions(model_id);
|
||||||
|
CREATE INDEX idx_predictions_version ON ml.predictions(model_version_id);
|
||||||
|
CREATE INDEX idx_predictions_symbol ON ml.predictions(symbol);
|
||||||
|
CREATE INDEX idx_predictions_symbol_time ON ml.predictions(symbol, market_timestamp DESC);
|
||||||
|
CREATE INDEX idx_predictions_type ON ml.predictions(prediction_type);
|
||||||
|
CREATE INDEX idx_predictions_created ON ml.predictions(created_at DESC);
|
||||||
|
-- Index for predictions with validity period (without time-based predicate for immutability)
|
||||||
|
CREATE INDEX idx_predictions_valid ON ml.predictions(valid_until)
|
||||||
|
WHERE valid_until IS NOT NULL;
|
||||||
|
|
||||||
|
-- Particionamiento por fecha (opcional, para alto volumen)
|
||||||
|
-- CREATE INDEX idx_predictions_timestamp ON ml.predictions(market_timestamp DESC);
|
||||||
|
|
||||||
|
-- Comentarios
|
||||||
|
COMMENT ON TABLE ml.predictions IS 'ML model predictions and trading signals';
|
||||||
|
COMMENT ON COLUMN ml.predictions.prediction_type IS 'Type of prediction being made';
|
||||||
|
COMMENT ON COLUMN ml.predictions.prediction_result IS 'Categorical result (buy/sell/hold/up/down/neutral)';
|
||||||
|
COMMENT ON COLUMN ml.predictions.prediction_value IS 'Numeric prediction value (e.g., target price, probability)';
|
||||||
|
COMMENT ON COLUMN ml.predictions.confidence_score IS 'Model confidence in prediction (0.0 to 1.0)';
|
||||||
|
COMMENT ON COLUMN ml.predictions.input_features IS 'Feature values used for this prediction';
|
||||||
|
COMMENT ON COLUMN ml.predictions.prediction_horizon IS 'Time horizon for prediction validity';
|
||||||
|
COMMENT ON COLUMN ml.predictions.inference_time_ms IS 'Model inference latency in milliseconds';
|
||||||
|
|
||||||
|
-- Ejemplo de input_features JSONB:
|
||||||
|
COMMENT ON COLUMN ml.predictions.input_features IS
|
||||||
|
'Example: {
|
||||||
|
"rsi_14": 65.42,
|
||||||
|
"macd_signal": 0.0234,
|
||||||
|
"volume_sma_20": 1234567.89,
|
||||||
|
"price_change_1h": 0.0145,
|
||||||
|
"sentiment_score": 0.72
|
||||||
|
}';
|
||||||
|
|
||||||
|
-- Ejemplo de model_output JSONB:
|
||||||
|
COMMENT ON COLUMN ml.predictions.model_output IS
|
||||||
|
'Example: {
|
||||||
|
"probabilities": {"buy": 0.72, "sell": 0.15, "hold": 0.13},
|
||||||
|
"raw_score": 0.5823,
|
||||||
|
"feature_contributions": {...}
|
||||||
|
}';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user