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>
648 lines
19 KiB
Markdown
648 lines
19 KiB
Markdown
# 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:**
|
|
```typescript
|
|
// Usuario llena formulario de registro
|
|
{
|
|
email: "student@example.com",
|
|
password: "SecurePass123!",
|
|
role: "student",
|
|
displayName: "Juan Pérez"
|
|
}
|
|
```
|
|
|
|
**Request HTTP:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```sql
|
|
-- 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:**
|
|
```typescript
|
|
// 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
|
|
|
|
```sql
|
|
-- 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:**
|
|
1. POST /api/v1/auth/register
|
|
2. Trigger crea 8 registros (1+1+1+5)
|
|
3. Usuario ve dashboard completo
|
|
4. ✅ 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:**
|
|
1. POST /api/v1/auth/register (segunda vez)
|
|
2. Trigger intenta crear registros
|
|
3. `ON CONFLICT DO NOTHING` previene duplicados
|
|
4. ✅ Resultado: Sin errores, registros existentes no se alteran
|
|
|
|
**SQL:**
|
|
```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**
|
|
```sql
|
|
-- 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:**
|
|
```sql
|
|
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:**
|
|
1. ❌ Trigger deshabilitado
|
|
2. ❌ No hay módulos publicados
|
|
3. ❌ FK incorrecta (user_id → auth.users.id en vez de profiles.id)
|
|
|
|
**Solución:**
|
|
```sql
|
|
-- 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:**
|
|
```sql
|
|
SELECT * FROM gamification_system.user_stats
|
|
WHERE user_id = 'USER_AUTH_ID';
|
|
-- Resultado esperado: 1 fila
|
|
```
|
|
|
|
**Solución:**
|
|
```sql
|
|
-- 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:**
|
|
```sql
|
|
SELECT user_id, module_id, COUNT(*)
|
|
FROM progress_tracking.module_progress
|
|
GROUP BY user_id, module_id
|
|
HAVING COUNT(*) > 1;
|
|
```
|
|
|
|
**Solución:**
|
|
```sql
|
|
-- 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.yml`
|
|
- `orchestration/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
|