workspace/projects/gamilit/docs/90-transversal/arquitectura/DIAGRAMA-DEPENDENCIAS-INITIALIZE-USER-STATS.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

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 (sistema)

Rol: Proveedor de user_id para tablas de gamificación

Schema: auth (sistema auth)

Columnas relevantes:

  • id (uuid, PK) - UUID del usuario de autenticación estándar

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_id en 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 perfil
  • user_id (uuid, FK) → auth.users.id
  • email (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ódulo
  • is_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.id
  • total_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.id
  • total_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.id
  • current_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.id
  • module_id (uuid, FK) → educational_content.modules.id
  • status (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_system fue diseñado primero, referencia auth.users directamente
  • progress_tracking fue 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:

  1. Modificar constraint FK en tabla module_progress
  2. Actualizar función initialize_user_stats (NEW.id → NEW.user_id)
  3. Migración de datos existentes
  4. 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:

  1. Crear tabla user_management.user_preferences
  2. Decidir FK: ¿profiles.id o auth.users.id?
  3. Actualizar gamilit.initialize_user_stats()
  4. Agregar INSERT con ON CONFLICT
  5. Actualizar tests de validación
  6. 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.sql
  • apps/database/ddl/schemas/gamification_system/tables/01-user_stats.sql
  • apps/database/ddl/schemas/gamification_system/tables/02-user_ranks.sql
  • apps/database/ddl/schemas/gamification_system/tables/07-comodines_inventory.sql
  • apps/database/ddl/schemas/progress_tracking/tables/01-module_progress.sql
  • apps/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