Structure: - control-plane/: Registries, SIMCO directives, CI/CD templates - projects/: Gamilit, ERP-Suite, Trading-Platform, Betting-Analytics - shared/: Libs catalog, knowledge-base Key features: - Centralized port, domain, database, and service registries - 23 SIMCO directives + 6 fundamental principles - NEXUS agent profiles with delegation rules - Validation scripts for workspace integrity - Dockerfiles for all services - Path aliases for quick reference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
20 KiB
Diagrama de Dependencias: initialize_user_stats()
Última actualización: 2025-11-24 Relacionado con: ADR-012, GAP-003 Bug Fix Propósito: Documentar todas las dependencias, FK references y relaciones de la función de inicialización
📋 Descripción General
La función gamilit.initialize_user_stats() tiene dependencias con múltiples tablas y esquemas. Este documento mapea exhaustivamente todas estas relaciones para facilitar el mantenimiento y prevenir errores futuros.
🔄 Diagrama de Dependencias Completo
┌─────────────────────────────────────────────────────────────────┐
│ DISPARADO POR │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ auth_management.profiles (INSERT) │
│ │
│ Trigger: trg_initialize_user_stats │
└──────────────────┬───────────────────┘
│
▼
┌──────────────────────────────────────┐
│ gamilit.initialize_user_stats() │
│ │
│ Función de inicialización │
└──────────────────┬───────────────────┘
│
│
┌──────────────────┴───────────────────┐
│ │
│ LEE DE (SELECT): │
│ │
│ educational_content.modules │
│ WHERE is_published = true │
│ AND status = 'published' │
│ │
└──────────────────┬───────────────────┘
│
│
┌──────────────────┴───────────────────┐
│ │
│ INSERTA EN (4 tablas): │
│ │
├──────────────────────────────────────┤
│ │
│ 1. gamification_system.user_stats │
│ FK: user_id → auth.users.id │
│ │
├──────────────────────────────────────┤
│ │
│ 2. gamification_system. │
│ comodines_inventory │
│ FK: user_id → profiles.id │
│ │
├──────────────────────────────────────┤
│ │
│ 3. gamification_system.user_ranks │
│ FK: user_id → auth.users.id │
│ │
├──────────────────────────────────────┤
│ │
│ 4. progress_tracking.module_progress │
│ FK1: user_id → profiles.id │
│ FK2: module_id → modules.id │
│ │
└──────────────────────────────────────┘
🗃️ Tablas Involucradas
Tabla 1: auth.users (Supabase)
Rol: Proveedor de user_id para tablas de gamificación
Schema: auth (Supabase built-in)
Columnas relevantes:
id(uuid, PK) - UUID del usuario de Supabase Auth
Usado por:
gamification_system.user_stats.user_id(FK)gamification_system.user_ranks.user_id(FK)auth_management.profiles.user_id(FK)
Relación con initialize_user_stats:
- ✅ Referenciado indirectamente vía
NEW.user_iden trigger - ✅ Usado para user_stats y user_ranks
Tabla 2: auth_management.profiles
Rol: Tabla que dispara el trigger de inicialización
Schema: auth_management
Archivo DDL: apps/database/ddl/schemas/auth_management/tables/03-profiles.sql
Columnas relevantes:
id(uuid, PK) - ID del perfiluser_id(uuid, FK) → auth.users.idemail(text, UNIQUE)role(gamilit_role)
Trigger configurado:
CREATE TRIGGER trg_initialize_user_stats
AFTER INSERT ON auth_management.profiles
FOR EACH ROW
EXECUTE FUNCTION gamilit.initialize_user_stats();
Variables disponibles en trigger:
NEW.id→ profiles.id (usado para module_progress, comodines_inventory)NEW.user_id→ auth.users.id (usado para user_stats, user_ranks)
Relación con initialize_user_stats:
- ✅ Dispara el trigger en INSERT
- ✅ Provee NEW.id y NEW.user_id
Tabla 3: educational_content.modules
Rol: Proveedor de módulos disponibles para inicialización
Schema: educational_content
Archivo DDL: apps/database/ddl/schemas/educational_content/tables/01-modules.sql
Columnas relevantes:
id(uuid, PK) - ID del módulois_published(boolean)status(module_status ENUM)
Query ejecutada por initialize_user_stats:
SELECT m.id
FROM educational_content.modules m
WHERE m.is_published = true
AND m.status = 'published';
Resultado típico: 5 módulos (M1-M5)
Relación con initialize_user_stats:
- ✅ Se lee para obtener lista de módulos disponibles
- ✅ No se modifica (solo SELECT)
Tabla 4: gamification_system.user_stats
Rol: Almacenar estadísticas de gamificación del usuario
Schema: gamification_system
Archivo DDL: apps/database/ddl/schemas/gamification_system/tables/01-user_stats.sql
Columnas relevantes:
user_id(uuid, PK, FK) → auth.users.idtotal_xp(bigint)level(integer)ml_coins(integer)current_streak(integer)
FK Constraint:
CONSTRAINT user_stats_user_id_fkey
FOREIGN KEY (user_id)
REFERENCES auth.users(id)
ON DELETE CASCADE
Índices:
user_stats_pkey(PK en user_id)idx_user_stats_user_id(índice en user_id)
INSERT ejecutado:
INSERT INTO gamification_system.user_stats (
user_id, total_xp, level, ml_coins, current_streak
)
VALUES (NEW.user_id, 0, 1, 100, 0)
ON CONFLICT (user_id) DO NOTHING;
Estrategia de conflicto: ON CONFLICT DO NOTHING (idempotente)
Relación con initialize_user_stats:
- ✅ Se inserta un registro
- ✅ FK apunta a auth.users.id (no profiles.id)
Tabla 5: gamification_system.comodines_inventory
Rol: Almacenar inventario de comodines del usuario
Schema: gamification_system
Archivo DDL: apps/database/ddl/schemas/gamification_system/tables/07-comodines_inventory.sql
Columnas relevantes:
user_id(uuid, PK, FK) → auth_management.profiles.idtotal_comodines_earned(integer)total_comodines_used(integer)
FK Constraint:
CONSTRAINT comodines_inventory_user_id_fkey
FOREIGN KEY (user_id)
REFERENCES auth_management.profiles(id)
ON DELETE CASCADE
Índices:
comodines_inventory_pkey(PK en user_id)
INSERT ejecutado:
INSERT INTO gamification_system.comodines_inventory (
user_id, total_comodines_earned, total_comodines_used
)
VALUES (NEW.id, 0, 0)
ON CONFLICT (user_id) DO NOTHING;
Estrategia de conflicto: ON CONFLICT DO NOTHING (idempotente)
Relación con initialize_user_stats:
- ✅ Se inserta un registro
- ✅ FK apunta a profiles.id (no auth.users.id)
Tabla 6: gamification_system.user_ranks
Rol: Almacenar rango Maya del usuario
Schema: gamification_system
Archivo DDL: apps/database/ddl/schemas/gamification_system/tables/02-user_ranks.sql
Columnas relevantes:
user_id(uuid, PK, FK) → auth.users.idcurrent_rank(maya_rank ENUM)rank_progress(integer)
FK Constraint:
CONSTRAINT user_ranks_user_id_fkey
FOREIGN KEY (user_id)
REFERENCES auth.users(id)
ON DELETE CASCADE
Índices:
user_ranks_pkey(PK en user_id)
⚠️ Nota importante: Esta tabla NO tiene constraint UNIQUE en user_id, por lo que no se puede usar ON CONFLICT. Se usa WHERE NOT EXISTS en su lugar.
INSERT ejecutado:
INSERT INTO gamification_system.user_ranks (
user_id, current_rank, rank_progress
)
SELECT NEW.user_id, 'Ajaw'::gamification_system.maya_rank, 0
WHERE NOT EXISTS (
SELECT 1 FROM gamification_system.user_ranks
WHERE user_id = NEW.user_id
);
Estrategia de conflicto: WHERE NOT EXISTS (previene duplicados)
Relación con initialize_user_stats:
- ✅ Se inserta un registro
- ✅ FK apunta a auth.users.id (no profiles.id)
Tabla 7: progress_tracking.module_progress
Rol: Almacenar progreso del usuario en cada módulo
Schema: progress_tracking
Archivo DDL: apps/database/ddl/schemas/progress_tracking/tables/01-module_progress.sql
Columnas relevantes:
user_id(uuid, FK) → auth_management.profiles.idmodule_id(uuid, FK) → educational_content.modules.idstatus(progress_status ENUM)progress_percentage(numeric)
FK Constraints:
-- FK 1: Usuario
CONSTRAINT module_progress_user_id_fkey
FOREIGN KEY (user_id)
REFERENCES auth_management.profiles(id)
ON DELETE CASCADE
-- FK 2: Módulo
CONSTRAINT module_progress_module_id_fkey
FOREIGN KEY (module_id)
REFERENCES educational_content.modules(id)
ON DELETE CASCADE
Índices:
module_progress_pkey(PK compuesto en user_id, module_id)idx_module_progress_user_id(índice en user_id)idx_module_progress_module_id(índice en module_id)
INSERT ejecutado:
INSERT INTO progress_tracking.module_progress (
user_id, module_id, status, progress_percentage
)
SELECT
NEW.id, -- profiles.id
m.id, -- modules.id
'not_started'::progress_tracking.progress_status,
0
FROM educational_content.modules m
WHERE m.is_published = true
AND m.status = 'published'
ON CONFLICT (user_id, module_id) DO NOTHING;
Estrategia de conflicto: ON CONFLICT DO NOTHING (idempotente)
Registros típicos creados: 5 (uno por cada módulo publicado)
Relación con initialize_user_stats:
- ✅ Se insertan múltiples registros (1 por módulo)
- ✅ FK user_id apunta a profiles.id (no auth.users.id)
- ✅ FK module_id apunta a modules.id
🔑 Mapa de Foreign Keys
Resumen de FK References
auth.users.id
↓ (FK)
auth_management.profiles.user_id
↓
├─ [NEW.user_id en trigger]
│ ↓ (usado para)
│ ├─ gamification_system.user_stats.user_id
│ └─ gamification_system.user_ranks.user_id
│
└─ [NEW.id en trigger] (profiles.id)
↓ (usado para)
├─ gamification_system.comodines_inventory.user_id
└─ progress_tracking.module_progress.user_id
educational_content.modules.id
↓ (FK)
progress_tracking.module_progress.module_id
Regla Mnemotécnica
Para recordar qué usar (NEW.id vs NEW.user_id):
Tablas de GAMIFICACIÓN → NEW.user_id (auth.users.id)
├─ user_stats
└─ user_ranks
Tablas de PROGRESS/INVENTORY → NEW.id (profiles.id)
├─ module_progress
└─ comodines_inventory
Razón histórica:
gamification_systemfue diseñado primero, referencia auth.users directamenteprogress_trackingfue diseñado después, referencia profiles (más limpio)
📊 Matriz de Dependencias
| Tabla Destino | Schema | FK Column | Referencia | Valor en Trigger | Constraint |
|---|---|---|---|---|---|
| user_stats | gamification_system | user_id | auth.users.id | NEW.user_id | ON CONFLICT |
| comodines_inventory | gamification_system | user_id | profiles.id | NEW.id | ON CONFLICT |
| user_ranks | gamification_system | user_id | auth.users.id | NEW.user_id | WHERE NOT EXISTS |
| module_progress | progress_tracking | user_id | profiles.id | NEW.id | ON CONFLICT |
| module_progress | progress_tracking | module_id | modules.id | m.id (SELECT) | ON CONFLICT |
🔍 Casos Críticos a Considerar
Caso 1: Cambiar FK de module_progress
Escenario hipotético: Migrar module_progress.user_id de profiles.id a auth.users.id
Impacto:
-- ANTES (actual):
INSERT INTO module_progress (user_id, ...)
VALUES (NEW.id, ...) -- profiles.id
-- DESPUÉS (hipotético):
INSERT INTO module_progress (user_id, ...)
VALUES (NEW.user_id, ...) -- auth.users.id
Cambios requeridos:
- ✅ Modificar constraint FK en tabla module_progress
- ✅ Actualizar función initialize_user_stats (NEW.id → NEW.user_id)
- ✅ Migración de datos existentes
- ✅ Actualizar queries en backend (JOIN diferentes)
Recomendación: ❌ NO HACER - Mantener consistencia actual
Caso 2: Agregar Nueva Tabla de Inicialización
Escenario: Agregar tabla user_preferences que debe inicializarse
Pasos:
- Crear tabla
user_management.user_preferences - Decidir FK: ¿profiles.id o auth.users.id?
- Actualizar
gamilit.initialize_user_stats() - Agregar INSERT con ON CONFLICT
- Actualizar tests de validación
- Actualizar esta documentación
Template de INSERT:
-- Si FK apunta a profiles.id:
INSERT INTO user_management.user_preferences (user_id, ...)
VALUES (NEW.id, ...)
ON CONFLICT (user_id) DO NOTHING;
-- Si FK apunta a auth.users.id:
INSERT INTO user_management.user_preferences (user_id, ...)
VALUES (NEW.user_id, ...)
ON CONFLICT (user_id) DO NOTHING;
Caso 3: Módulos Dinámicos
Escenario: Se publica un nuevo módulo M6 después de que usuarios ya están registrados
Problema:
- Usuarios existentes no tienen module_progress para M6
- Trigger solo funciona en nuevos registros
Soluciones:
Opción A: Migration Manual (Recomendada)
-- Script: apps/database/migrations/YYYY-MM-DD-add-module-6-progress.sql
INSERT INTO progress_tracking.module_progress (
user_id, module_id, status, progress_percentage
)
SELECT
p.id,
'm6-uuid'::uuid,
'not_started',
0
FROM auth_management.profiles p
WHERE p.role IN ('student', 'admin_teacher', 'super_admin')
AND p.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM progress_tracking.module_progress mp
WHERE mp.user_id = p.id AND mp.module_id = 'm6-uuid'::uuid
);
Opción B: Lazy Loading en Backend
// En module.service.ts
async getModuleProgress(userId: string, moduleId: string) {
let progress = await this.findProgress(userId, moduleId);
if (!progress) {
// Crear module_progress si no existe
progress = await this.createProgress({
userId,
moduleId,
status: 'not_started',
progressPercentage: 0,
});
}
return progress;
}
Recomendación: Opción A (migration) para consistencia
🧪 Testing de Dependencias
Test 1: Validar Todas las FK
-- Verificar que todas las FK existen y son correctas
SELECT
tc.table_schema,
tc.table_name,
kcu.column_name,
ccu.table_schema AS foreign_table_schema,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND (
(tc.table_schema = 'gamification_system' AND tc.table_name IN ('user_stats', 'comodines_inventory', 'user_ranks'))
OR
(tc.table_schema = 'progress_tracking' AND tc.table_name = 'module_progress')
)
ORDER BY tc.table_schema, tc.table_name;
Resultado esperado: 5 FK constraints listadas
Test 2: Validar Idempotencia del Trigger
-- Test de idempotencia: Ejecutar trigger múltiples veces
DO $$
DECLARE
test_profile_id uuid;
test_user_id uuid;
BEGIN
-- Crear usuario de prueba
INSERT INTO auth.users (email, encrypted_password)
VALUES ('test-idempotence@test.com', 'hashed')
RETURNING id INTO test_user_id;
-- Crear perfil (dispara trigger 1ra vez)
INSERT INTO auth_management.profiles (user_id, email, role)
VALUES (test_user_id, 'test-idempotence@test.com', 'student')
RETURNING id INTO test_profile_id;
-- Intentar insertar duplicados (simular trigger 2da vez)
-- Todos deben fallar gracefully con ON CONFLICT
PERFORM gamilit.initialize_user_stats();
-- Verificar conteo
ASSERT (SELECT COUNT(*) FROM gamification_system.user_stats WHERE user_id = test_user_id) = 1,
'Debe haber exactamente 1 user_stats';
ASSERT (SELECT COUNT(*) FROM gamification_system.user_ranks WHERE user_id = test_user_id) = 1,
'Debe haber exactamente 1 user_ranks';
ASSERT (SELECT COUNT(*) FROM progress_tracking.module_progress WHERE user_id = test_profile_id) = 5,
'Debe haber exactamente 5 module_progress';
RAISE NOTICE 'Test de idempotencia: OK';
END $$;
Test 3: Validar Cascade Deletes
-- Test de cascade: Eliminar usuario debe eliminar todo
DO $$
DECLARE
test_profile_id uuid;
test_user_id uuid;
BEGIN
-- Crear usuario de prueba
INSERT INTO auth.users (email, encrypted_password)
VALUES ('test-cascade@test.com', 'hashed')
RETURNING id INTO test_user_id;
INSERT INTO auth_management.profiles (user_id, email, role)
VALUES (test_user_id, 'test-cascade@test.com', 'student')
RETURNING id INTO test_profile_id;
-- Eliminar usuario (debe hacer CASCADE)
DELETE FROM auth.users WHERE id = test_user_id;
-- Verificar que todo se eliminó
ASSERT (SELECT COUNT(*) FROM gamification_system.user_stats WHERE user_id = test_user_id) = 0;
ASSERT (SELECT COUNT(*) FROM auth_management.profiles WHERE user_id = test_user_id) = 0;
ASSERT (SELECT COUNT(*) FROM progress_tracking.module_progress WHERE user_id = test_profile_id) = 0;
RAISE NOTICE 'Test de cascade delete: OK';
END $$;
📚 Referencias
Documentación relacionada:
- ADR:
docs/97-adr/ADR-012-automatic-user-initialization-trigger.md - Flujo:
docs/90-transversal/FLUJO-INICIALIZACION-USUARIO.md - Función:
docs/90-transversal/FUNCIONES-UTILITARIAS-GAMILIT.md
Código fuente:
- Trigger:
apps/database/ddl/schemas/auth_management/triggers/04-trg_initialize_user_stats.sql - Función:
apps/database/ddl/schemas/gamilit/functions/04-initialize_user_stats.sql
Tablas DDL:
apps/database/ddl/schemas/auth_management/tables/03-profiles.sqlapps/database/ddl/schemas/gamification_system/tables/01-user_stats.sqlapps/database/ddl/schemas/gamification_system/tables/02-user_ranks.sqlapps/database/ddl/schemas/gamification_system/tables/07-comodines_inventory.sqlapps/database/ddl/schemas/progress_tracking/tables/01-module_progress.sqlapps/database/ddl/schemas/educational_content/tables/01-modules.sql
FIN DEL DOCUMENTO
Última actualización: 2025-11-24 Mantenedores: Architecture-Analyst, Database-Agent Revisión necesaria: Sí, al agregar nuevas tablas de inicialización o modificar FK