# Funciones Utilitarias Globales - Schema: gamilit

**Schema:** `gamilit`
**Propósito:** Funciones helper reutilizables en toda la base de datos
**Total de funciones:** 13
**Estado:** ✅ Implementado
**Última actualización:** 2025-11-08

---

## 📋 Descripción

El schema `gamilit` contiene funciones utilitarias globales que son utilizadas por múltiples schemas a través de triggers, RLS policies y procedimientos almacenados. Estas funciones proporcionan funcionalidad común y reutilizable.

---

## 📊 Categorías de Funciones

| Categoría | Cantidad | Funciones |
|-----------|----------|-----------|
| **Auditoría** | 1 | audit_profile_changes |
| **Autenticación/Autorización** | 3 | get_current_user_id, get_current_user_role, is_admin |
| **Inicialización** | 2 | initialize_user_stats, set_profile_defaults |
| **Fecha/Hora** | 1 | now_mexico |
| **Triggers Helper** | 1 | update_updated_at_column |
| **Validación** | 2 | validate_email_format, validate_username |
| **Contadores** | 2 | update_classroom_member_count, update_user_last_login |
| **Gamificación** | 1 | update_user_stats_on_exercise_complete |

---

## 🔐 CATEGORÍA: AUDITORÍA

### 1. `audit_profile_changes()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/01-audit_profile_changes.sql`

**Propósito:** Registra cambios en perfiles de usuario para auditoría

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.audit_profile_changes()
RETURNS TRIGGER
```

**Parámetros:**
- Ninguno (función trigger)

**Retorno:** `TRIGGER`

**Uso:**
```sql
CREATE TRIGGER trg_audit_profile_changes
AFTER UPDATE ON auth_management.profiles
FOR EACH ROW
EXECUTE FUNCTION gamilit.audit_profile_changes();
```

**Usado por:**
- Trigger en `auth_management.profiles`
- Sistema de auditoría

**Comportamiento:**
- Registra cambios en campos importantes del perfil
- Almacena estado anterior y nuevo
- Registra timestamp y usuario que realizó el cambio

---

## 🔑 CATEGORÍA: AUTENTICACIÓN/AUTORIZACIÓN

### 2. `get_current_user_id()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/02-get_current_user_id.sql`

**Propósito:** Obtiene el UUID del usuario actual de la sesión PostgreSQL

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.get_current_user_id()
RETURNS UUID
```

**Parámetros:** Ninguno

**Retorno:** `UUID` - ID del usuario actual o NULL

**Uso:**
```sql
SELECT gamilit.get_current_user_id();
-- Retorna: '550e8400-e29b-41d4-a716-446655440000'
```

**Usado por:**
- RLS policies en todos los schemas
- Triggers de auditoría
- Validaciones de permisos

**Implementación:**
Obtiene el user_id del JWT token almacenado en `current_setting('request.jwt.claims')` o de variables de sesión.

**Seguridad:** `STABLE SECURITY DEFINER`

---

### 3. `get_current_user_role()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/03-get_current_user_role.sql`

**Propósito:** Obtiene el rol del usuario actual

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.get_current_user_role()
RETURNS auth_management.gamilit_role
```

**Parámetros:** Ninguno

**Retorno:** `auth_management.gamilit_role` - Rol del usuario (student, admin_teacher, super_admin)

**Uso:**
```sql
SELECT gamilit.get_current_user_role();
-- Retorna: 'student' | 'admin_teacher' | 'super_admin'
```

**Usado por:**
- RLS policies para control de acceso basado en roles
- Validaciones de permisos
- Lógica condicional en funciones

**Ejemplo en RLS:**
```sql
CREATE POLICY teacher_access ON progress_tracking.exercise_attempts
FOR SELECT
USING (gamilit.get_current_user_role() = 'admin_teacher'::auth_management.gamilit_role);
```

**Seguridad:** `STABLE`

---

### 4. `is_admin()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/05-is_admin.sql`

**Propósito:** Verifica si el usuario actual es administrador (teacher o super_admin)

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.is_admin()
RETURNS BOOLEAN
```

**Parámetros:** Ninguno

**Retorno:** `BOOLEAN` - TRUE si es admin, FALSE si no

**Uso:**
```sql
SELECT gamilit.is_admin();
-- Retorna: true | false
```

**Lógica:**
```sql
-- Verifica si el rol está en ('admin_teacher', 'super_admin')
-- Y el status es 'active'
```

**Usado por:**
- RLS policies para funcionalidad admin-only
- Validaciones de permisos críticos
- Funciones que requieren privilegios elevados

**Ejemplo:**
```sql
CREATE POLICY admin_can_delete ON some_table
FOR DELETE
USING (gamilit.is_admin());
```

**Seguridad:** `STABLE SECURITY DEFINER`

**Manejo de errores:** Retorna FALSE en caso de excepción

---

## 🎯 CATEGORÍA: INICIALIZACIÓN

### 5. `initialize_user_stats()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/04-initialize_user_stats.sql`

**Propósito:** Inicializa automáticamente todos los registros necesarios para que un nuevo usuario pueda usar la plataforma inmediatamente

**Última actualización:** 2025-11-24 (Bug fix GAP-003 - Agregada inicialización de module_progress)

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.initialize_user_stats()
RETURNS TRIGGER
```

**Parámetros:** Ninguno (función trigger)

**Retorno:** `TRIGGER`

**Uso:**
```sql
CREATE TRIGGER trg_initialize_user_stats
AFTER INSERT ON auth_management.profiles
FOR EACH ROW
EXECUTE FUNCTION gamilit.initialize_user_stats();
```

**Comportamiento:**
Crea automáticamente registros en **4 tablas** cuando se crea un nuevo usuario:

**1. Estadísticas de Gamificación** (`gamification_system.user_stats`)
- `user_id` → auth.users.id
- `total_xp = 0`
- `level = 1`
- `ml_coins = 100` (monedas de bienvenida)
- `current_streak = 0`
- Estrategia: `ON CONFLICT (user_id) DO NOTHING`

**2. Inventario de Comodines** (`gamification_system.comodines_inventory`)
- `user_id` → profiles.id
- Inventario vacío inicializado
- Estrategia: `ON CONFLICT (user_id) DO NOTHING`

**3. Rango Maya** (`gamification_system.user_ranks`)
- `user_id` → auth.users.id
- `current_rank = 'Ajaw'` (rango inicial)
- Estrategia: `WHERE NOT EXISTS` (tabla sin constraint UNIQUE)

**4. Progreso de Módulos** (`progress_tracking.module_progress`)
- `user_id` → profiles.id
- `module_id` → modules.id
- Un registro por cada módulo publicado
- `status = 'not_started'`
- `progress_percentage = 0`
- Estrategia: `ON CONFLICT (user_id, module_id) DO NOTHING`

**Dependencias:**
- Lee de: `educational_content.modules` (WHERE is_published = true AND status = 'published')
- Inserta en: 4 tablas (user_stats, comodines_inventory, user_ranks, module_progress)

**Resultado:**
- Usuario puede ver todos los módulos disponibles inmediatamente
- Gamificación funciona desde el primer momento
- 0 errores en dashboard de estudiante
- UX perfecta: registro → plataforma lista en 0 segundos

**Usado por:**
- Trigger `trg_initialize_user_stats` en `auth_management.profiles` (AFTER INSERT)

**Documentación adicional:**
- ADR: `docs/97-adr/ADR-012-automatic-user-initialization-trigger.md`
- Flujo completo: `docs/90-transversal/FLUJO-INICIALIZACION-USUARIO.md`
- Dependencias: `docs/90-transversal/DIAGRAMA-DEPENDENCIAS-INITIALIZE-USER-STATS.md`

---

### 6. `set_profile_defaults()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/09-set_profile_defaults.sql`

**Propósito:** Establece valores por defecto en perfil de usuario

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.set_profile_defaults()
RETURNS TRIGGER
```

**Parámetros:** Ninguno (función trigger)

**Retorno:** `TRIGGER`

**Comportamiento:**
- Asigna valores por defecto si no se proporcionaron
- Genera username automático si es NULL
- Establece avatar por defecto
- Configura timezone según región

**Usado por:**
- Trigger en `auth_management.profiles` (BEFORE INSERT)

---

## ⏰ CATEGORÍA: FECHA/HORA

### 7. `now_mexico()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/08-now_mexico.sql`

**Propósito:** Retorna timestamp actual en zona horaria de México (America/Mexico_City)

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.now_mexico()
RETURNS TIMESTAMP WITH TIME ZONE
```

**Parámetros:** Ninguno

**Retorno:** `TIMESTAMPTZ` - Timestamp actual en zona horaria de México

**Uso:**
```sql
SELECT gamilit.now_mexico();
-- Retorna: '2025-11-08 01:45:00-06'

-- En defaults de columnas:
created_at TIMESTAMPTZ DEFAULT gamilit.now_mexico()
```

**Usado por:**
- Defaults de columnas `created_at` y `updated_at` en ~50+ tablas
- Funciones que requieren timestamp actual
- Cálculos de fecha/hora

**Nota:** Marcada como `IMMUTABLE` para optimización

**Diferencia con NOW():**
```sql
-- NOW() retorna UTC o timezone del servidor
-- now_mexico() retorna siempre en timezone de México
```

---

## ⚡ CATEGORÍA: TRIGGERS HELPER

### 8. `update_updated_at_column()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/09-update_updated_at_column.sql`

**Propósito:** Actualiza automáticamente el campo `updated_at` en updates

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.update_updated_at_column()
RETURNS TRIGGER
```

**Parámetros:** Ninguno (función trigger)

**Retorno:** `TRIGGER`

**Comportamiento:**
```sql
-- En cada UPDATE, automáticamente:
NEW.updated_at = gamilit.now_mexico();
RETURN NEW;
```

**Uso:**
```sql
CREATE TRIGGER trg_table_updated_at
BEFORE UPDATE ON schema.table
FOR EACH ROW
EXECUTE FUNCTION gamilit.update_updated_at_column();
```

**Usado por:** ~30+ triggers en todo el sistema

**Tablas que lo usan:**
- `auth_management.*` (12 tablas)
- `gamification_system.*` (13 tablas)
- `educational_content.*` (4 tablas)
- `progress_tracking.*` (5 tablas)
- `social_features.*` (7 tablas)
- `system_configuration.*` (3 tablas)
- Y más...

**Beneficio:** Garantiza que `updated_at` siempre refleja la última modificación

---

## ✅ CATEGORÍA: VALIDACIÓN

### 9. `validate_email_format()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/12-validate_email_format.sql`

**Propósito:** Valida que un email tenga formato correcto

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.validate_email_format(email TEXT)
RETURNS BOOLEAN
```

**Parámetros:**
- `email` (TEXT) - Email a validar

**Retorno:** `BOOLEAN` - TRUE si válido, FALSE si no

**Uso:**
```sql
SELECT gamilit.validate_email_format('user@example.com');
-- Retorna: true

SELECT gamilit.validate_email_format('invalid-email');
-- Retorna: false

-- En constraint:
ALTER TABLE users ADD CONSTRAINT valid_email
CHECK (gamilit.validate_email_format(email));
```

**Regex usado:**
```regex
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$
```

**Usado por:**
- Constraints en `auth_management.profiles`
- Validaciones de formularios (via backend)

---

### 10. `validate_username()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/13-validate_username.sql`

**Propósito:** Valida formato de username (alfanumérico, guiones, sin espacios)

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.validate_username(username TEXT)
RETURNS BOOLEAN
```

**Parámetros:**
- `username` (TEXT) - Username a validar

**Retorno:** `BOOLEAN` - TRUE si válido, FALSE si no

**Uso:**
```sql
SELECT gamilit.validate_username('john_doe123');
-- Retorna: true

SELECT gamilit.validate_username('john doe');  -- espacios
-- Retorna: false

SELECT gamilit.validate_username('a');  -- muy corto
-- Retorna: false
```

**Reglas de validación:**
- Mínimo 3 caracteres
- Máximo 30 caracteres
- Solo letras, números, guiones bajos y guiones medios
- Sin espacios
- Sin caracteres especiales

**Regex:**
```regex
^[A-Za-z0-9_-]{3,30}$
```

**Usado por:**
- Constraints en `auth_management.profiles`
- Validaciones en registro de usuario

---

## 🔢 CATEGORÍA: CONTADORES

### 11. `update_classroom_member_count()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/10-update_classroom_member_count.sql`

**Propósito:** Actualiza contador de miembros en un aula al agregar/remover estudiantes

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.update_classroom_member_count()
RETURNS TRIGGER
```

**Parámetros:** Ninguno (función trigger)

**Retorno:** `TRIGGER`

**Comportamiento:**
```sql
-- Al INSERT en classroom_members:
UPDATE classrooms SET member_count = member_count + 1

-- Al DELETE en classroom_members:
UPDATE classrooms SET member_count = member_count - 1
```

**Uso:**
```sql
CREATE TRIGGER trg_update_classroom_count
AFTER INSERT OR DELETE ON social_features.classroom_members
FOR EACH ROW
EXECUTE FUNCTION gamilit.update_classroom_member_count();
```

**Usado por:**
- Trigger en `social_features.classroom_members`

**Beneficio:** Mantiene contador sincronizado sin queries adicionales

---

### 12. `update_user_last_login()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/11-update_user_last_login.sql`

**Propósito:** Actualiza timestamp de último login del usuario

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.update_user_last_login(p_user_id UUID)
RETURNS VOID
```

**Parámetros:**
- `p_user_id` (UUID) - ID del usuario

**Retorno:** `VOID`

**Uso:**
```sql
-- Llamada desde backend al hacer login exitoso:
SELECT gamilit.update_user_last_login('550e8400-e29b-41d4-a716-446655440000');
```

**Comportamiento:**
```sql
UPDATE auth_management.profiles
SET last_login_at = gamilit.now_mexico()
WHERE id = p_user_id;
```

**Usado por:**
- Auth service en backend (al hacer login)
- Analytics para calcular usuarios activos

---

## 🎮 CATEGORÍA: GAMIFICACIÓN

### 13. `update_user_stats_on_exercise_complete()`

**Archivo:** `apps/database/ddl/schemas/gamilit/functions/14-update_user_stats_on_exercise_complete.sql`

**Propósito:** Actualiza estadísticas de usuario al completar un ejercicio (XP, ML Coins, racha)

**Firma:**
```sql
CREATE OR REPLACE FUNCTION gamilit.update_user_stats_on_exercise_complete()
RETURNS TRIGGER
```

**Parámetros:** Ninguno (función trigger)

**Retorno:** `TRIGGER`

**Comportamiento:**
```sql
-- Al completar ejercicio con éxito:
1. Incrementa XP según dificultad del ejercicio
2. Otorga ML Coins
3. Actualiza racha diaria (streak)
4. Recalcula nivel si XP suficiente
5. Verifica y desbloquea achievements
```

**Uso:**
```sql
CREATE TRIGGER trg_update_stats_on_exercise
AFTER INSERT ON progress_tracking.exercise_attempts
FOR EACH ROW
WHEN (NEW.status = 'completed' AND NEW.is_correct = true)
EXECUTE FUNCTION gamilit.update_user_stats_on_exercise_complete();
```

**Usado por:**
- Trigger en `progress_tracking.exercise_attempts`

**Lógica de recompensas:**
```sql
-- XP según dificultad:
beginner: 10 XP
intermediate: 20 XP
advanced: 30 XP
expert: 50 XP

-- ML Coins: Variable según configuración (default: 10 coins)
-- Streak: +1 si ejercicio completado en día actual
```

**Interdependencias:**
- Lee de `educational_content.exercises` (dificultad)
- Actualiza `gamification_system.user_stats`
- Llama a `gamification_system.check_and_award_achievements()`

---

## 📊 DIAGRAMA DE DEPENDENCIAS

```
Funciones Base (sin dependencias):
├── now_mexico()
└── validate_email_format()
    validate_username()

Funciones de Sesión:
├── get_current_user_id()
└── get_current_user_role() ──> usa: get_current_user_id()
    └── is_admin() ──> usa: get_current_user_role()

Triggers de Auditoría/Actualización:
├── update_updated_at_column() ──> usa: now_mexico()
├── audit_profile_changes() ──> usa: get_current_user_id(), now_mexico()
└── update_user_last_login() ──> usa: now_mexico()

Triggers de Inicialización:
├── set_profile_defaults()
└── initialize_user_stats() ──> crea registro en user_stats

Triggers de Contadores:
└── update_classroom_member_count()

Triggers de Gamificación:
└── update_user_stats_on_exercise_complete()
    ├── usa: gamification_system.check_and_award_achievements()
    └── usa: gamification_system.calculate_level_from_xp()
```

---

## 🔗 SCHEMAS QUE USAN ESTAS FUNCIONES

| Schema | Funciones Usadas | Uso Principal |
|--------|------------------|---------------|
| **auth_management** | get_current_user_id, get_current_user_role, is_admin, update_updated_at_column, audit_profile_changes, initialize_user_stats, set_profile_defaults, now_mexico | RLS, triggers, validación |
| **gamification_system** | get_current_user_id, update_updated_at_column, update_user_stats_on_exercise_complete, now_mexico | RLS, triggers, gamificación |
| **educational_content** | get_current_user_id, get_current_user_role, update_updated_at_column, now_mexico | RLS, triggers |
| **progress_tracking** | get_current_user_id, get_current_user_role, update_updated_at_column, update_user_stats_on_exercise_complete, now_mexico | RLS, triggers, tracking |
| **social_features** | get_current_user_id, is_admin, update_updated_at_column, update_classroom_member_count, now_mexico | RLS, triggers, contadores |
| **system_configuration** | is_admin, update_updated_at_column, now_mexico | RLS, triggers |
| **audit_logging** | get_current_user_id, now_mexico | Auditoría |
| **content_management** | get_current_user_role, is_admin, now_mexico | RLS, permisos |

---

## ⚠️ CONSIDERACIONES IMPORTANTES

### Seguridad

1. **SECURITY DEFINER:**
   - `is_admin()` - Ejecuta con permisos del creador
   - **Riesgo:** Puede escalar privilegios si no se valida correctamente
   - **Mitigación:** Incluye validaciones exhaustivas

2. **STABLE vs VOLATILE:**
   - `get_current_user_id()` - STABLE (no modifica BD)
   - `get_current_user_role()` - STABLE (cacheable en query)
   - `now_mexico()` - IMMUTABLE (optimización incorrecta, debería ser STABLE)

3. **Validación de entrada:**
   - `validate_email_format()` y `validate_username()` usan regex
   - Protegen contra inyección SQL en constraints

### Performance

1. **Funciones más llamadas:**
   - `update_updated_at_column()` - ~30+ triggers
   - `now_mexico()` - ~50+ defaults + triggers
   - `get_current_user_id()` - Cientos de RLS policies

2. **Optimización:**
   - Funciones STABLE se cachean por transacción
   - Evitar lógica compleja en funciones de triggers masivos

3. **Monitoreo:**
   - Rastrear tiempo de ejecución de `update_user_stats_on_exercise_complete()`
   - Es la función más compleja (actualiza múltiples tablas)

### Mantenimiento

1. **Cambios en firmas:**
   - Verificar TODAS las dependencias antes de modificar
   - Usar `pg_depend` para encontrar objetos dependientes

2. **Versionado:**
   - Documentar cambios en funciones críticas
   - Considerar functions versionadas (`get_user_role_v2`)

3. **Testing:**
   - Tests unitarios para cada función
   - Tests de integración para funciones complejas

---

## 🎯 MEJORES PRÁCTICAS

### Al Usar Estas Funciones

1. **RLS Policies:**
   ```sql
   -- ✅ Correcto:
   USING (user_id = gamilit.get_current_user_id())

   -- ❌ Evitar llamar múltiples veces en mismo query:
   USING (user_id = gamilit.get_current_user_id() OR
          created_by = gamilit.get_current_user_id())

   -- ✅ Mejor:
   USING (gamilit.get_current_user_id() IN (user_id, created_by))
   ```

2. **Triggers:**
   ```sql
   -- ✅ Especificar WHEN para evitar ejecuciones innecesarias:
   CREATE TRIGGER trg_name
   AFTER UPDATE ON table
   FOR EACH ROW
   WHEN (NEW.status <> OLD.status)  -- Solo si cambió status
   EXECUTE FUNCTION gamilit.some_function();
   ```

3. **Validaciones:**
   ```sql
   -- ✅ Usar en constraints:
   ALTER TABLE users ADD CONSTRAINT valid_username
   CHECK (gamilit.validate_username(username));

   -- También validar en backend como primera línea de defensa
   ```

---

## 📝 PRÓXIMOS PASOS

### Mejoras Sugeridas

1. **Documentación de código:**
   - Agregar ejemplos de uso en comentarios SQL
   - Documentar casos edge y manejo de errores

2. **Testing:**
   - Crear suite de tests para cada función
   - Tests de performance para funciones críticas

3. **Monitoring:**
   - Agregar métricas de llamadas y tiempo de ejecución
   - Alertas para funciones que excedan thresholds

4. **Nuevas funciones útiles:**
   - `gamilit.is_teacher()` - Verificar solo admin_teacher
   - `gamilit.is_super_admin()` - Verificar solo super_admin
   - `gamilit.get_user_tenant_id()` - Obtener tenant del usuario
   - `gamilit.log_error()` - Función helper para logging

---

## 🔗 Referencias

- **Directorio:** `apps/database/ddl/schemas/gamilit/functions/`
- **_MAP.md:** `apps/database/ddl/schemas/gamilit/functions/_MAP.md`
- **Dependencias:** Ver diagrama de dependencias arriba
- **Usado por:** Todos los schemas de GAMILIT

---

**Creado:** 2025-11-08
**Tipo:** Documentación retroactiva
**Total de funciones:** 13
**Estado:** ✅ Documentación completa
