# 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:**
```sql
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:**
```sql
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:**
```sql
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:**
```sql
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:**
```sql
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:**
```sql
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:**
```sql
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:**
```sql
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:**
```sql
-- 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:**
```sql
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:**
```sql
-- 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:**
```sql
-- 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)**
```sql
-- 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**
```typescript
// 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

```sql
-- 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

```sql
-- 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

```sql
-- 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
