Gamilit: - Backend: Teacher services, assignments, gamification, exercise submissions - Frontend: Admin/Teacher/Student portals, module 4-5 mechanics, monitoring - Database: DDL functions, seeds for dev/prod, auth/gamification schemas - Docs: Architecture, features, guides cleanup and reorganization Core/Orchestration: - New workspace directives index - Documentation directive Trading-platform: - Database seeds and inventory updates - Tech leader validation report 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
21 KiB
Flujo Completo: Inicialización de Usuario
Última actualización: 2025-12-18 Relacionado con: ADR-012 Propósito: Documentar el flujo end-to-end desde registro de usuario hasta plataforma lista para usar
📋 Descripción General
Este documento describe el flujo completo de inicialización automática cuando un usuario se registra en GAMILIT. El sistema utiliza un trigger de base de datos para garantizar que cada nuevo usuario tenga todos los registros necesarios creados automáticamente en 0 segundos.
Resultado final:
- ✅ Usuario registrado puede usar la plataforma inmediatamente
- ✅ Ve 5 módulos disponibles sin esperar
- ✅ Gamificación funciona desde el primer momento
- ✅ 0 errores en dashboard
🔄 Flujo End-to-End
1. Usuario se Registra (Frontend)
Componente: apps/frontend/src/apps/student/pages/RegisterPage.tsx
Acción del usuario:
// Usuario llena formulario de registro
{
email: "student@example.com",
password: "SecurePass123!",
role: "student",
displayName: "Juan Pérez"
}
Request HTTP:
POST /api/v1/auth/register
Content-Type: application/json
{
"email": "student@example.com",
"password": "SecurePass123!",
"role": "student",
"displayName": "Juan Pérez"
}
2. Backend Procesa Registro
Servicio: apps/backend/src/auth/auth.service.ts
Código simplificado:
async register(dto: RegisterDto) {
// Paso 1: Crear usuario en auth.users (autenticación estándar)
const { data: authUser, error } = await this.dbClient.auth.signUp({
email: dto.email,
password: dto.password,
});
if (error) throw new UnauthorizedException(error.message);
// Paso 2: Crear perfil en auth_management.profiles
const { data: profile, error: profileError } = await this.dbClient
.from('profiles')
.insert({
user_id: authUser.user.id, // FK a auth.users
email: dto.email,
role: dto.role,
display_name: dto.displayName,
})
.select()
.single();
// ⚡ En este punto, el TRIGGER se dispara automáticamente
// No se requiere código adicional para inicialización
return {
user: profile,
accessToken: authUser.session.access_token,
};
}
Observaciones importantes:
- ✅ El backend NO necesita código para inicializar gamificación
- ✅ El backend NO necesita código para crear module_progress
- ✅ Todo es manejado automáticamente por el trigger
- ✅ Código más simple y menos propenso a errores
3. Triggers de Base de Datos se Disparan
3.1. Trigger: Aseguramiento de Nombre (BEFORE INSERT)
Trigger: trg_ensure_profile_name
Ubicación: apps/database/ddl/schemas/auth_management/triggers/03b-trg_ensure_profile_name.sql
Definición:
CREATE TRIGGER trg_ensure_profile_name
BEFORE INSERT ON auth_management.profiles
FOR EACH ROW
EXECUTE FUNCTION auth_management.ensure_profile_name();
Propósito:
- Asegura que
first_name,last_nameyfull_nametengan valores válidos - Si están vacíos, extrae el nombre del email automáticamente
- Computa
full_namecomo concatenación defirst_name + last_name - Previene usuarios con "Unknown Student" en la UI
Lógica:
-- Si first_name está vacío, extraer del email
IF NEW.first_name IS NULL OR TRIM(NEW.first_name) = '' THEN
NEW.first_name := INITCAP(SPLIT_PART(NEW.email, '@', 1));
END IF;
-- Si last_name está vacío, poner valor por defecto
IF NEW.last_name IS NULL OR TRIM(NEW.last_name) = '' THEN
NEW.last_name := 'Usuario';
END IF;
-- Siempre computar full_name como concatenación
NEW.full_name := TRIM(COALESCE(NEW.first_name, '') || ' ' || COALESCE(NEW.last_name, ''));
3.2. Trigger: Inicialización de Estadísticas (AFTER INSERT)
Trigger: trg_initialize_user_stats
Ubicación: apps/database/ddl/schemas/auth_management/triggers/04-trg_initialize_user_stats.sql
Definición:
CREATE TRIGGER trg_initialize_user_stats
AFTER INSERT ON auth_management.profiles
FOR EACH ROW
EXECUTE FUNCTION gamilit.initialize_user_stats();
Orden de ejecución:
trg_ensure_profile_name(BEFORE INSERT) - Asegura datos del perfil- INSERT en
profilesse completa trg_initialize_user_stats(AFTER INSERT) - Inicializa gamificación
4. Función de Inicialización Ejecuta
Función: gamilit.initialize_user_stats()
Ubicación: apps/database/ddl/schemas/gamilit/functions/04-initialize_user_stats.sql
Acciones realizadas (en orden):
4.1. Inicializar Estadísticas de Gamificación
INSERT INTO gamification_system.user_stats (
user_id,
total_xp,
level,
ml_coins,
current_streak,
created_at,
updated_at
)
VALUES (
NEW.user_id, -- auth.users.id
0, -- XP inicial
1, -- Nivel inicial
100, -- 100 ML Coins de bienvenida
0, -- Racha inicial
NOW(),
NOW()
)
ON CONFLICT (user_id) DO NOTHING;
Resultado:
- ✅ Usuario tiene 100 ML Coins para gastar
- ✅ Nivel 1 asignado
- ✅ Estadísticas listas para tracking
4.2. Inicializar Inventario de Comodines
INSERT INTO gamification_system.comodines_inventory (
user_id,
total_comodines_earned,
total_comodines_used,
created_at,
updated_at
)
VALUES (
NEW.id, -- profiles.id
0,
0,
NOW(),
NOW()
)
ON CONFLICT (user_id) DO NOTHING;
Resultado:
- ✅ Inventario vacío creado
- ✅ Listo para acumular comodines
4.3. Asignar Rango Maya Inicial
INSERT INTO gamification_system.user_ranks (
user_id,
current_rank,
rank_progress,
created_at,
updated_at
)
SELECT
NEW.user_id, -- auth.users.id
'Ajaw'::gamification_system.maya_rank,
0,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM gamification_system.user_ranks
WHERE user_id = NEW.user_id
);
Resultado:
- ✅ Rango inicial "Ajaw" asignado
- ✅ Sistema de progresión activado
4.4. Crear Progreso para Todos los Módulos Publicados
INSERT INTO progress_tracking.module_progress (
user_id,
module_id,
status,
progress_percentage,
created_at,
updated_at
)
SELECT
NEW.id, -- profiles.id
m.id, -- modules.id
'not_started'::progress_tracking.progress_status,
0,
NOW(),
NOW()
FROM educational_content.modules m
WHERE m.is_published = true
AND m.status = 'published'
ON CONFLICT (user_id, module_id) DO NOTHING;
Resultado:
- ✅ Un registro por cada módulo publicado (típicamente 5 registros)
- ✅ Usuario ve módulos disponibles inmediatamente
- ✅ Sin errores de "no modules available"
5. Backend Retorna Respuesta
Response HTTP:
{
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "student@example.com",
"role": "student",
"displayName": "Juan Pérez",
"createdAt": "2025-11-24T10:30:00.000Z"
},
"accessToken": "eyJhbGc..."
}
Estado de base de datos en este momento:
-- user_stats: 1 registro ✅
-- comodines_inventory: 1 registro ✅
-- user_ranks: 1 registro ✅
-- module_progress: 5 registros ✅
-- TOTAL: 8 registros creados automáticamente
6. Frontend Carga Dashboard
Componente: apps/frontend/src/apps/student/pages/DashboardPage.tsx
Queries automáticas:
// 1. Cargar módulos disponibles
const { data: modules } = useQuery({
queryKey: ['modules'],
queryFn: () => api.get('/api/v1/modules'),
});
// Retorna: 5 módulos con status 'not_started'
// 2. Cargar estadísticas
const { data: stats } = useQuery({
queryKey: ['user-stats'],
queryFn: () => api.get('/api/v1/gamification/stats'),
});
// Retorna: { level: 1, ml_coins: 100, xp: 0, streak: 0 }
// 3. Cargar rango actual
const { data: rank } = useQuery({
queryKey: ['user-rank'],
queryFn: () => api.get('/api/v1/gamification/rank'),
});
// Retorna: { current_rank: 'Ajaw', progress: 0 }
Resultado visual:
┌─────────────────────────────────────┐
│ Dashboard - Bienvenido Juan Pérez │
├─────────────────────────────────────┤
│ Nivel: 1 │ Coins: 100 💰 │
│ Rango: Ajaw │ XP: 0/100 │
├─────────────────────────────────────┤
│ Módulos Disponibles: │
│ ✅ M1: María y su Huerto │
│ ✅ M2: Detective Numérico │
│ ✅ M3: La Gran Carrera │
│ ✅ M4: Mercado de las Fracciones │
│ ✅ M5: Aventura en el Museo │
└─────────────────────────────────────┘
✅ Usuario puede empezar a usar la plataforma inmediatamente
📊 Diagrama de Secuencia Completo
┌────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐
│ Usuario│ │ Frontend │ │ Backend │ │ Database │ │ Trigger │
└───┬────┘ └────┬─────┘ └────┬────┘ └────┬─────┘ └────┬────┘
│ │ │ │ │
│ Registro │ │ │ │
│─────────────>│ │ │ │
│ │ │ │ │
│ │ POST /register │ │
│ │─────────────>│ │ │
│ │ │ │ │
│ │ │ INSERT │ │
│ │ │ auth.users │ │
│ │ │─────────────>│ │
│ │ │<─────────────│ │
│ │ │ User ID │ │
│ │ │ │ │
│ │ │ INSERT │ │
│ │ │ profiles │ │
│ │ │─────────────>│ │
│ │ │ │ │
│ │ │ │ ⚡ TRIGGER │
│ │ │ │──────────────>│
│ │ │ │ │
│ │ │ │ INSERT │
│ │ │ │ user_stats │
│ │ │ │<─────────────│
│ │ │ │ │
│ │ │ │ INSERT │
│ │ │ │ comodines │
│ │ │ │<─────────────│
│ │ │ │ │
│ │ │ │ INSERT │
│ │ │ │ user_ranks │
│ │ │ │<─────────────│
│ │ │ │ │
│ │ │ │ INSERT │
│ │ │ │ module_progress (x5)
│ │ │ │<─────────────│
│ │ │ │ │
│ │ │<─────────────│ │
│ │ │ Profile + Session │
│ │<─────────────│ │ │
│ │ User + Token │ │ │
│<─────────────│ │ │ │
│ │ │ │ │
│ Redirect │ │ │ │
│ Dashboard │ │ │ │
│─────────────>│ │ │ │
│ │ │ │ │
│ │ GET /modules │ │ │
│ │─────────────>│ │ │
│ │ │ SELECT │ │
│ │ │ module_progress │
│ │ │─────────────>│ │
│ │ │<─────────────│ │
│ │ │ 5 modules │ │
│ │<─────────────│ │ │
│ │ 5 modules │ │ │
│ │ │ │ │
│ 🎉 Plataforma│ │ │ │
│ Lista │ │ │ │
│<─────────────│ │ │ │
Tiempo total: ~300ms
- Registro: ~100ms
- Trigger: ~50ms (4 INSERTs en paralelo)
- Primera carga: ~150ms
🔍 Validación del Flujo
Query de Validación Post-Registro
-- Verificar que nuevo usuario tiene todo inicializado
WITH new_user AS (
SELECT id, user_id, email
FROM auth_management.profiles
WHERE email = 'student@example.com'
)
SELECT
'auth_management.profiles' as tabla,
1 as esperado,
COUNT(*) as real,
CASE WHEN COUNT(*) = 1 THEN '✅' ELSE '❌' END as status
FROM new_user
UNION ALL
SELECT
'gamification_system.user_stats',
1,
COUNT(*),
CASE WHEN COUNT(*) = 1 THEN '✅' ELSE '❌' END
FROM new_user nu
JOIN gamification_system.user_stats us ON us.user_id = nu.user_id
UNION ALL
SELECT
'gamification_system.comodines_inventory',
1,
COUNT(*),
CASE WHEN COUNT(*) = 1 THEN '✅' ELSE '❌' END
FROM new_user nu
JOIN gamification_system.comodines_inventory ci ON ci.user_id = nu.id
UNION ALL
SELECT
'gamification_system.user_ranks',
1,
COUNT(*),
CASE WHEN COUNT(*) = 1 THEN '✅' ELSE '❌' END
FROM new_user nu
JOIN gamification_system.user_ranks ur ON ur.user_id = nu.user_id
UNION ALL
SELECT
'progress_tracking.module_progress',
5, -- Asumiendo 5 módulos publicados
COUNT(*),
CASE WHEN COUNT(*) = 5 THEN '✅' ELSE '❌' END
FROM new_user nu
JOIN progress_tracking.module_progress mp ON mp.user_id = nu.id;
Resultado esperado:
tabla | esperado | real | status
-------------------------------------------+----------+------+--------
auth_management.profiles | 1 | 1 | ✅
gamification_system.user_stats | 1 | 1 | ✅
gamification_system.comodines_inventory | 1 | 1 | ✅
gamification_system.user_ranks | 1 | 1 | ✅
progress_tracking.module_progress | 5 | 5 | ✅
🎯 Casos de Uso Comunes
Caso 1: Usuario se Registra por Primera Vez
Escenario:
- Usuario nuevo
- Email no existe en sistema
- Primer registro
Flujo:
- POST /api/v1/auth/register
- Trigger crea 8 registros (1+1+1+5)
- Usuario ve dashboard completo
- ✅ Resultado: Usuario listo para usar plataforma
Caso 2: Re-registro del Mismo Usuario (Idempotencia)
Escenario:
- Usuario intenta registrarse 2 veces con mismo email
- Sistema debe manejar gracefully
Flujo:
- POST /api/v1/auth/register (segunda vez)
- Trigger intenta crear registros
ON CONFLICT DO NOTHINGpreviene duplicados- ✅ Resultado: Sin errores, registros existentes no se alteran
SQL:
-- Todas las inserciones usan ON CONFLICT
ON CONFLICT (user_id) DO NOTHING;
ON CONFLICT (user_id, module_id) DO NOTHING;
Caso 3: Se Publica un Nuevo Módulo (M6)
Escenario:
- Admin publica módulo M6
- Usuarios existentes deben poder acceder
Opción 1: Migration Manual
-- Crear module_progress para usuarios existentes
INSERT INTO progress_tracking.module_progress (
user_id, module_id, status, progress_percentage
)
SELECT
p.id,
'm6-new-module'::uuid,
'not_started',
0
FROM auth_management.profiles p
WHERE p.role IN ('student', 'admin_teacher', 'super_admin')
AND NOT EXISTS (
SELECT 1 FROM progress_tracking.module_progress mp
WHERE mp.user_id = p.id AND mp.module_id = 'm6-new-module'::uuid
);
Opción 2: Lazy Loading en Backend
- Crear module_progress al primer acceso
- Más simple pero menos consistente
Recomendación: Opción 1 (migration) para consistencia
🚨 Troubleshooting
Problema 1: Usuario Sin Módulos
Síntoma:
Dashboard muestra: "No modules available"
Diagnóstico:
SELECT COUNT(*) FROM progress_tracking.module_progress
WHERE user_id = 'USER_PROFILE_ID';
-- Resultado esperado: 5
-- Si retorna 0: Trigger no se ejecutó
Causas posibles:
- ❌ Trigger deshabilitado
- ❌ No hay módulos publicados
- ❌ FK incorrecta (user_id → auth.users.id en vez de profiles.id)
Solución:
-- Verificar trigger existe
SELECT * FROM pg_trigger WHERE tgname = 'trg_initialize_user_stats';
-- Verificar módulos publicados
SELECT COUNT(*) FROM educational_content.modules
WHERE is_published = true AND status = 'published';
-- Re-inicializar manualmente
SELECT gamilit.initialize_user_stats() FROM auth_management.profiles
WHERE id = 'USER_PROFILE_ID';
Problema 2: Usuario Sin Stats
Síntoma:
Dashboard muestra: "Error loading stats"
Diagnóstico:
SELECT * FROM gamification_system.user_stats
WHERE user_id = 'USER_AUTH_ID';
-- Resultado esperado: 1 fila
Solución:
-- Insertar stats manualmente
INSERT INTO gamification_system.user_stats (
user_id, total_xp, level, ml_coins, current_streak
)
VALUES ('USER_AUTH_ID', 0, 1, 100, 0)
ON CONFLICT (user_id) DO NOTHING;
Problema 3: Duplicados en module_progress
Síntoma:
ERROR: duplicate key value violates unique constraint "module_progress_pkey"
Diagnóstico:
SELECT user_id, module_id, COUNT(*)
FROM progress_tracking.module_progress
GROUP BY user_id, module_id
HAVING COUNT(*) > 1;
Solución:
-- Eliminar duplicados, mantener el más antiguo
WITH duplicates AS (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY user_id, module_id ORDER BY created_at
) as rn
FROM progress_tracking.module_progress
)
DELETE FROM progress_tracking.module_progress
WHERE id IN (
SELECT id FROM duplicates WHERE rn > 1
);
📚 Referencias
Documentación relacionada:
- ADR:
docs/97-adr/ADR-012-automatic-user-initialization-trigger.md - Función:
docs/90-transversal/FUNCIONES-UTILITARIAS-GAMILIT.md - Dependencias:
docs/90-transversal/DIAGRAMA-DEPENDENCIAS-INITIALIZE-USER-STATS.md
Código fuente:
- Trigger aseguramiento nombre:
apps/database/ddl/schemas/auth_management/triggers/03b-trg_ensure_profile_name.sql - Trigger inicialización:
apps/database/ddl/schemas/auth_management/triggers/04-trg_initialize_user_stats.sql - Función inicialización:
apps/database/ddl/schemas/gamilit/functions/04-initialize_user_stats.sql - Backend:
apps/backend/src/modules/auth/services/auth.service.ts - Frontend:
apps/frontend/src/apps/student/pages/DashboardPage.tsx
Inventarios:
docs/90-transversal/inventarios/DATABASE_INVENTORY.ymlorchestration/inventarios/MASTER_INVENTORY.yml
FIN DEL DOCUMENTO
Última actualización: 2025-12-18 Mantenedores: Architecture-Analyst, Database-Agent Revisión necesaria: Sí, al agregar nuevos módulos o cambiar proceso de registro