- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
19 KiB
Flujo Completo: Inicialización de Usuario
Última actualización: 2025-11-24 Relacionado con: ADR-012, GAP-003 Bug Fix 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 (Supabase Auth)
const { data: authUser, error } = await this.supabase.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.supabase
.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. Trigger de Base de Datos se Dispara
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();
Momento de ejecución:
- Inmediatamente DESPUÉS de INSERT en
profiles - Antes de que el backend retorne la respuesta
- Dentro de la misma transacción (atómico)
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:
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 - Backend:
apps/backend/src/auth/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-11-24 Mantenedores: Architecture-Analyst, Database-Agent Revisión necesaria: Sí, al agregar nuevos módulos o cambiar proceso de registro