workspace/projects/gamilit/docs/90-transversal/arquitectura/FLUJO-INICIALIZACION-USUARIO.md
rckrdmrd 608e1e2a2e
Some checks are pending
CI Pipeline / changes (push) Waiting to run
CI Pipeline / core (push) Blocked by required conditions
CI Pipeline / trading-backend (push) Blocked by required conditions
CI Pipeline / trading-data-service (push) Blocked by required conditions
CI Pipeline / trading-frontend (push) Blocked by required conditions
CI Pipeline / erp-core (push) Blocked by required conditions
CI Pipeline / erp-mecanicas (push) Blocked by required conditions
CI Pipeline / gamilit-backend (push) Blocked by required conditions
CI Pipeline / gamilit-frontend (push) Blocked by required conditions
Multi-project update: gamilit, orchestration, trading-platform
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>
2025-12-18 07:17:46 -06:00

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_name y full_name tengan valores válidos
  • Si están vacíos, extrae el nombre del email automáticamente
  • Computa full_name como concatenación de first_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:

  1. trg_ensure_profile_name (BEFORE INSERT) - Asegura datos del perfil
  2. INSERT en profiles se completa
  3. 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:

  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:

-- 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:

  1. Trigger deshabilitado
  2. No hay módulos publicados
  3. 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.yml
  • orchestration/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