- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
25 KiB
ET-INIT-001: Especificación Técnica - Trigger de Inicialización de Usuario
Proyecto: GAMILIT Épica: EAI-001 - Fundamentos Versión: 1.1 Fecha de creación: 2025-11-24 Última actualización: 2025-11-24 Estado: ✅ Implementado Relacionado con: RF-INIT-001, ADR-012
📋 Información de la Especificación
| Atributo | Valor |
|---|---|
| ID | ET-INIT-001 |
| Tipo | Especificación Técnica |
| Categoría | Base de Datos / Triggers |
| Requerimiento | RF-INIT-001 |
| Epic | EAI-001 - Fundamentos |
| Complejidad | Media-Alta |
| Impacto | Crítico |
🎯 Objetivo Técnico
Especificar la implementación técnica del sistema de inicialización automática de usuarios mediante un Database Trigger que se ejecuta al momento de crear un perfil de usuario, inicializando todos los componentes necesarios para gamificación y progreso educativo.
🏗️ Arquitectura de la Solución
Diagrama de Componentes
┌────────────────┐
│ Backend API │
│ (NestJS) │
└───────┬────────┘
│ POST /auth/register
│
▼
┌────────────────────────────────────────┐
│ Database (PostgreSQL 15+) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 1. INSERT INTO │ │
│ │ auth_management.profiles │ │
│ └──────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ 2. ⚡ TRIGGER: │ │
│ │ trg_initialize_user_stats │ │
│ │ AFTER INSERT │ │
│ │ FOR EACH ROW │ │
│ └──────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ 3. FUNCTION: │ │
│ │ gamilit.initialize_user_stats()│ │
│ │ │ │
│ │ ├─ 3.1 INSERT user_stats │ │
│ │ ├─ 3.2 INSERT comodines │ │
│ │ ├─ 3.3 INSERT user_ranks │ │
│ │ └─ 3.4 INSERT module_progress│ │
│ └──────────────────────────────────┘ │
│ │
└────────────────────────────────────────┘
📁 Estructura de Archivos
apps/database/ddl/
├── schemas/gamilit/functions/
│ └── 04-initialize_user_stats.sql ← Función principal
│
└── schemas/auth_management/triggers/
└── 04-trg_initialize_user_stats.sql ← Trigger
docs/
├── 97-adr/
│ └── ADR-012-automatic-user-initialization-trigger.md
│
├── 90-transversal/
│ ├── FLUJO-INICIALIZACION-USUARIO.md
│ ├── DIAGRAMA-DEPENDENCIAS-INITIALIZE-USER-STATS.md
│ └── FUNCIONES-UTILITARIAS-GAMILIT.md
│
└── 01-fase-alcance-inicial/EAI-001-fundamentos/
├── requerimientos/
│ └── RF-INIT-001-inicializacion-automatica-usuario.md
└── especificaciones/
└── ET-INIT-001-trigger-inicializacion.md (este archivo)
🔧 Implementación Técnica
Componente 1: Trigger
Archivo: apps/database/ddl/schemas/auth_management/triggers/04-trg_initialize_user_stats.sql
Especificación:
CREATE TRIGGER trg_initialize_user_stats
AFTER INSERT ON auth_management.profiles
FOR EACH ROW
EXECUTE FUNCTION gamilit.initialize_user_stats();
Características:
| Propiedad | Valor | Razón |
|---|---|---|
| Timing | AFTER INSERT |
Asegurar que profile existe antes de inicialización |
| Level | FOR EACH ROW |
Inicialización individual por usuario |
| Event | INSERT únicamente |
Solo para nuevos usuarios, no para updates |
| Schema | auth_management |
Trigger vive en el schema del objeto |
| Table | profiles |
Tabla que dispara el evento |
| Function | gamilit.initialize_user_stats() |
Función en schema central gamilit |
Decisiones de Diseño:
-
¿Por qué AFTER y no BEFORE?
- AFTER garantiza que el perfil ya está persistido en DB
- BEFORE podría causar problemas si el INSERT falla
- AFTER permite que la inicialización sea asíncrona/eventual
-
¿Por qué en profiles y no en auth.users?
auth.userses tabla de Supabase (auth externo)profileses nuestra tabla local con tenant_id- Necesitamos tenant_id para multi-tenancy
-
¿Por qué una función separada y no código inline?
- Reusabilidad: Función puede llamarse manualmente para migración
- Testabilidad: Función puede probarse independientemente
- Mantenibilidad: Cambios en lógica no requieren DROP/CREATE trigger
Componente 2: Función de Inicialización
Archivo: apps/database/ddl/schemas/gamilit/functions/04-initialize_user_stats.sql
Firma:
CREATE OR REPLACE FUNCTION gamilit.initialize_user_stats()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
Características:
| Propiedad | Valor |
|---|---|
| Return Type | trigger (no devuelve datos, sino NEW/OLD/NULL) |
| Language | plpgsql |
| Volatility | VOLATILE (modifican |
datos) | | Security | SECURITY INVOKER (ejecuta como usuario que dispara) | | Cost Estimate | ~100-500 ms (depende de módulos publicados) |
Implementación de la Función: Paso a Paso
Paso 1: Validación de Rol
IF NEW.role IN ('student', 'admin_teacher', 'super_admin') THEN
-- Continuar con inicialización
ELSE
-- Skip, role no tiene gamificación
RETURN NEW;
END IF;
Propósito: Filtrar roles que NO participan en gamificación
Roles incluidos: student, admin_teacher, super_admin
Roles excluidos: Cualquier otro rol futuro (ej: viewer, auditor)
Paso 2: Inicializar user_stats
Tabla: gamification_system.user_stats
Código:
INSERT INTO gamification_system.user_stats (
user_id,
tenant_id,
ml_coins,
ml_coins_earned_total
) VALUES (
NEW.user_id, -- ⚠️ CRITICAL: usar user_id (auth.users.id), NO NEW.id
NEW.tenant_id,
100, -- Monedas de bienvenida
100
)
ON CONFLICT (user_id) DO NOTHING; -- Idempotencia
Detalles Técnicos:
| Campo | Valor | Tipo | Constraint | Razón |
|---|---|---|---|---|
user_id |
NEW.user_id |
UUID | FK → auth.users.id, PK | Identificador de Supabase |
tenant_id |
NEW.tenant_id |
UUID | FK → tenants.id | Multi-tenancy |
ml_coins |
100 |
INTEGER | CHECK >= 0 | Monedas iniciales |
ml_coins_earned_total |
100 |
INTEGER | CHECK >= 0 | Historial acumulado |
total_xp |
0 (default) |
INTEGER | CHECK >= 0 | Experiencia inicial |
level |
1 (default) |
INTEGER | CHECK > 0 | Nivel inicial |
FK Reference: ⚠️ CRÍTICO
- ✅
user_id→auth.users.id(tabla de Supabase) - ❌ NO usar
NEW.id(profiles.id)
Razón: Gamificación necesita vincular con user_id de Supabase para autenticación y permisos.
Estrategia de Idempotencia:
ON CONFLICT (user_id) DO NOTHING
- Si ya existe user_stats para este user_id → Skip (no error)
- Permite re-ejecutar función sin crear duplicados
user_idtiene UNIQUE constraint
Paso 3: Inicializar comodines_inventory
Tabla: gamification_system.comodines_inventory
Código:
INSERT INTO gamification_system.comodines_inventory (
user_id
) VALUES (
NEW.id -- ⚠️ CRITICAL: usar NEW.id (profiles.id), NO NEW.user_id
)
ON CONFLICT (user_id) DO NOTHING;
Detalles Técnicos:
| Campo | Valor | Tipo | Constraint | Razón |
|---|---|---|---|---|
user_id |
NEW.id |
UUID | FK → profiles.id, PK | Identificador local con tenant |
| Otros campos | Defaults | INTEGER | - | Cantidades en 0 |
FK Reference: ⚠️ CRÍTICO
- ✅
user_id→profiles.id(nuestra tabla local) - ❌ NO usar
NEW.user_id(auth.users.id)
Razón: Inventario necesita contexto de tenant local (profiles tiene tenant_id).
Comentario en Código:
-- IMPORTANT: comodines_inventory.user_id references profiles.id (NOT auth.users.id)
-- CORRECTED: usar NEW.id (profiles.id) porque FK apunta a profiles(id)
Paso 4: Inicializar user_ranks
Tabla: gamification_system.user_ranks
Código:
INSERT INTO gamification_system.user_ranks (
user_id,
tenant_id,
current_rank
)
SELECT
NEW.user_id,
NEW.tenant_id,
'Ajaw'::gamification_system.maya_rank
WHERE NOT EXISTS (
SELECT 1 FROM gamification_system.user_ranks WHERE user_id = NEW.user_id
);
Detalles Técnicos:
| Campo | Valor | Tipo | Constraint | Razón |
|---|---|---|---|---|
user_id |
NEW.user_id |
UUID | FK → auth.users.id | Identificador Supabase |
tenant_id |
NEW.tenant_id |
UUID | FK → tenants.id | Multi-tenancy |
current_rank |
'Ajaw' |
maya_rank | ENUM | Rango más bajo |
FK Reference: ⚠️ CRÍTICO
- ✅
user_id→auth.users.id - ❌ NO usar
NEW.id(profiles.id)
Razón: Igual que user_stats, necesita vincular con auth.users.id
Estrategia de Idempotencia: WHERE NOT EXISTS
BUG FIX #2: Cambio de ON CONFLICT a WHERE NOT EXISTS
Problema anterior:
-- ❌ INCORRECTO (antes del fix)
ON CONFLICT (user_id) DO NOTHING
¿Por qué falla?
- La tabla
user_ranksNO tiene unique constraint enuser_id ON CONFLICTrequiere un constraint único- PostgreSQL rechazaría el DDL
Solución actual:
-- ✅ CORRECTO (después del fix)
WHERE NOT EXISTS (
SELECT 1 FROM gamification_system.user_ranks WHERE user_id = NEW.user_id
)
Beneficios:
- Funciona sin unique constraint
- Previene duplicados verificando antes de INSERT
- Idempotente: puede ejecutarse N veces
Comentario en Código:
-- BUG FIX #2: Use WHERE NOT EXISTS instead of ON CONFLICT (no unique constraint on user_id)
Paso 5: Inicializar module_progress (BUG FIX #1 - GAP-003)
Tabla: progress_tracking.module_progress
Código:
INSERT INTO progress_tracking.module_progress (
user_id,
module_id,
status,
progress_percentage,
created_at,
updated_at
)
SELECT
NEW.id, -- ⚠️ CRITICAL: profiles.id, NO NEW.user_id
m.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;
Detalles Técnicos:
| Campo | Valor | Tipo | Constraint | Razón |
|---|---|---|---|---|
user_id |
NEW.id |
UUID | FK → profiles.id, parte de PK | Identificador local |
module_id |
m.id |
UUID | FK → modules.id, parte de PK | Módulo educativo |
status |
'not_started' |
progress_status | ENUM | Estado inicial |
progress_percentage |
0 |
INTEGER | CHECK 0-100 | Sin progreso aún |
created_at |
NOW() |
TIMESTAMP | NOT NULL | Timestamp de creación |
updated_at |
NOW() |
TIMESTAMP | NOT NULL | Timestamp actualización |
FK Reference: ⚠️ CRÍTICO
- ✅
user_id→profiles.id - ❌ NO usar
NEW.user_id(auth.users.id)
Razón: Igual que comodines, necesita contexto de tenant local.
Filtro de Módulos:
WHERE m.is_published = true AND m.status = 'published'
- Solo módulos visibles para estudiantes
- Módulos en draft o archived quedan fuera
Resultado: Si hay 5 módulos publicados → 5 registros creados
Estrategia de Idempotencia:
ON CONFLICT (user_id, module_id) DO NOTHING
- La tabla tiene unique constraint compuesto:
(user_id, module_id) - Si ya existe progreso para ese usuario+módulo → Skip
- Permite re-ejecutar función sin duplicados
BUG FIX #1 - GAP-003: Este código faltaba completamente
Problema anterior:
-- ❌ FALTABA ESTE BLOQUE COMPLETO (antes de 2025-11-24)
-- Usuarios no tenían module_progress inicializado
-- Dashboard mostraba "No modules available"
Solución: Se agregó el bloque completo (líneas 60-82 del DDL)
Comentarios en Código:
-- BUG FIX #1: Initialize module progress for all active modules
-- CRITICAL: New users must see available modules immediately
-- This was missing and caused "no modules available" errors
-- IMPORTANT: module_progress.user_id references profiles.id (NOT auth.users.id)
-- FIXED: Use NEW.id (profiles.id) not NEW.user_id (auth.users.id)
Evidencia del Fix: Ver VALIDACION-GAP-003-MODULE-PROGRESS.md
Paso 6: Return NEW
END IF;
RETURN NEW;
Propósito:
- En un trigger AFTER INSERT, RETURN NEW es obligatorio
- No afecta los datos (ya están persistidos)
- Es requerido por la sintaxis de PostgreSQL
🔍 Análisis de FK References
Tabla de Referencias
| Tabla | Campo | FK Apunta a | NEW.field | Razón |
|---|---|---|---|---|
user_stats |
user_id |
auth.users.id |
NEW.user_id |
Gamificación usa auth de Supabase |
comodines_inventory |
user_id |
profiles.id |
NEW.id |
Necesita tenant_id local |
user_ranks |
user_id |
auth.users.id |
NEW.user_id |
Gamificación usa auth de Supabase |
module_progress |
user_id |
profiles.id |
NEW.id |
Necesita tenant_id local |
Regla Mnemotécnica
Schema gamification_system:
- ✅ Usa
auth.users.id(Supabase) - ❌ EXCEPTO
comodines_inventory→ usaprofiles.id
Otros schemas (progress_tracking, etc.):
- ✅ Usan
profiles.id(local con tenant)
¿Por qué la excepción?
comodines_inventorynecesita tenant_id para multi-tenancy- No necesita vincular directamente con Supabase auth
- Prioriza consistencia de tenant sobre auth global
⚡ Análisis de Performance
Complejidad Temporal
Operaciones:
| Operación | Complejidad | Tiempo Estimado |
|---|---|---|
| INSERT user_stats | O(1) | ~5 ms |
| INSERT comodines_inventory | O(1) | ~5 ms |
| INSERT user_ranks | O(1) + EXISTS check | ~10 ms |
| SELECT modules publicados | O(M) donde M = módulos | ~20 ms (5 módulos) |
| INSERT module_progress (N veces) | O(N) donde N = módulos | ~50 ms (5 módulos) |
| Total | O(M + N) | ~90 ms |
Escalabilidad:
- Con 5 módulos: ~90 ms ✅ Excelente
- Con 20 módulos: ~200 ms ✅ Aceptable
- Con 100 módulos: ~800 ms ⚠️ Considerar batch processing
Análisis de Índices
Índices Requeridos:
gamification_system.user_stats(user_id)- UNIQUE (PK)gamification_system.comodines_inventory(user_id)- UNIQUE (PK)progress_tracking.module_progress(user_id, module_id)- UNIQUE (composite)educational_content.modules(is_published, status)- INDEX compuesto
Impacto:
- ON CONFLICT requiere índices únicos (ya existen)
- WHERE NOT EXISTS se beneficia de índice en user_id
- SELECT modules con filtro usa índice compuesto
Todos los índices necesarios ya están presentes en el schema.
🧪 Testing
Test 1: Inicialización Completa
Setup:
-- Crear usuario de prueba
INSERT INTO auth.users (id, email) VALUES
('11111111-1111-1111-1111-111111111111', 'test@example.com');
-- Crear perfil (dispara trigger)
INSERT INTO auth_management.profiles (id, user_id, tenant_id, email, role)
VALUES (
'22222222-2222-2222-2222-222222222222',
'11111111-1111-1111-1111-111111111111',
'tenant-uuid',
'test@example.com',
'student'
);
Validación:
-- Query de validación
WITH user_check AS (
SELECT
COUNT(DISTINCT us.user_id) as has_user_stats,
COUNT(DISTINCT ci.user_id) as has_comodines,
COUNT(DISTINCT ur.user_id) as has_ranks,
COUNT(mp.id) as module_count
FROM auth_management.profiles p
LEFT JOIN gamification_system.user_stats us ON us.user_id = p.user_id
LEFT JOIN gamification_system.comodines_inventory ci ON ci.user_id = p.id
LEFT JOIN gamification_system.user_ranks ur ON ur.user_id = p.user_id
LEFT JOIN progress_tracking.module_progress mp ON mp.user_id = p.id
WHERE p.id = '22222222-2222-2222-2222-222222222222'
)
SELECT * FROM user_check;
-- Resultado esperado:
-- has_user_stats = 1
-- has_comodines = 1
-- has_ranks = 1
-- module_count = 5 (o número de módulos publicados)
Cleanup:
DELETE FROM auth_management.profiles WHERE id = '22222222-2222-2222-2222-222222222222';
DELETE FROM auth.users WHERE id = '11111111-1111-1111-1111-111111111111';
Test 2: Idempotencia
Setup:
-- Crear usuario con inicialización completa (Test 1)
-- Re-ejecutar función manualmente
SELECT gamilit.initialize_user_stats();
Validación:
-- Verificar que NO se crearon duplicados
SELECT
(SELECT COUNT(*) FROM gamification_system.user_stats WHERE user_id = 'user-uuid') as count_user_stats,
(SELECT COUNT(*) FROM gamification_system.comodines_inventory WHERE user_id = 'profile-uuid') as count_comodines,
(SELECT COUNT(*) FROM gamification_system.user_ranks WHERE user_id = 'user-uuid') as count_ranks,
(SELECT COUNT(*) FROM progress_tracking.module_progress WHERE user_id = 'profile-uuid') as count_modules;
-- Resultado esperado:
-- count_user_stats = 1 (no duplicado)
-- count_comodines = 1 (no duplicado)
-- count_ranks = 1 (no duplicado)
-- count_modules = 5 (no duplicados)
Test 3: Filtrado de Roles
Setup:
-- Crear perfil con rol excluido
INSERT INTO auth_management.profiles (id, user_id, tenant_id, email, role)
VALUES (
'profile-uuid',
'user-uuid',
'tenant-uuid',
'viewer@example.com',
'viewer' -- Role sin gamificación
);
Validación:
-- Verificar que NO se creó nada
SELECT COUNT(*) FROM gamification_system.user_stats WHERE user_id = 'user-uuid';
-- Resultado esperado: 0
SELECT COUNT(*) FROM gamification_system.comodines_inventory WHERE user_id = 'profile-uuid';
-- Resultado esperado: 0
🐛 Debugging y Troubleshooting
Problema 1: Usuario sin module_progress
Síntoma: Dashboard muestra "No modules available"
Diagnóstico:
-- Verificar progreso del usuario
SELECT COUNT(*) FROM progress_tracking.module_progress
WHERE user_id = '<profile_id>';
-- Si retorna 0, inicialización falló
Posibles Causas:
- No hay módulos publicados (is_published=false)
- Trigger no se disparó (bug en DDL)
- FK reference incorrecto (user_id vs profile_id)
Solución Manual:
-- Ejecutar inicialización manual
DO $$
DECLARE
v_profile RECORD;
BEGIN
SELECT id, user_id, tenant_id, role INTO v_profile
FROM auth_management.profiles
WHERE id = '<profile_id>';
-- Inicializar module_progress
INSERT INTO progress_tracking.module_progress (user_id, module_id, status, progress_percentage, created_at, updated_at)
SELECT v_profile.id, m.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;
END $$;
Problema 2: Duplicate Key Error
Síntoma: Error "duplicate key value violates unique constraint"
Diagnóstico:
-- Verificar si ya existen registros
SELECT user_id FROM gamification_system.user_stats WHERE user_id = '<user_id>';
SELECT user_id FROM gamification_system.comodines_inventory WHERE user_id = '<profile_id>';
Causa Común: Trigger se ejecutó múltiples veces
Solución:
- Verificar idempotencia (ON CONFLICT / WHERE NOT EXISTS)
- Código actual ya maneja esto correctamente
Problema 3: FK Constraint Violation
Síntoma: Error "violates foreign key constraint"
Diagnóstico:
-- Verificar que auth.users existe
SELECT id FROM auth.users WHERE id = '<user_id>';
-- Verificar que profiles existe
SELECT id, user_id FROM auth_management.profiles WHERE id = '<profile_id>';
-- Verificar FK references en user_stats
SELECT constraint_name, table_name, column_name, foreign_table_name, foreign_column_name
FROM information_schema.key_column_usage
WHERE table_name = 'user_stats' AND constraint_name LIKE '%fkey%';
Causa Común: Usar NEW.id en lugar de NEW.user_id (o viceversa)
Solución: Ver sección "Análisis de FK References" arriba
📊 Monitoreo en Producción
Queries de Monitoreo
Query 1: Usuarios sin inicialización completa
SELECT
p.id as profile_id,
p.email,
p.created_at,
CASE WHEN us.user_id IS NULL THEN 0 ELSE 1 END as has_user_stats,
CASE WHEN ci.user_id IS NULL THEN 0 ELSE 1 END as has_comodines,
CASE WHEN ur.user_id IS NULL THEN 0 ELSE 1 END as has_ranks,
CASE WHEN COUNT(mp.id) > 0 THEN 1 ELSE 0 END as has_module_progress,
COUNT(mp.id) as module_count
FROM auth_management.profiles p
LEFT JOIN gamification_system.user_stats us ON us.user_id = p.user_id
LEFT JOIN gamification_system.comodines_inventory ci ON ci.user_id = p.id
LEFT JOIN gamification_system.user_ranks ur ON ur.user_id = p.user_id
LEFT JOIN progress_tracking.module_progress mp ON mp.user_id = p.id
WHERE p.role IN ('student', 'admin_teacher', 'super_admin')
AND p.created_at > NOW() - INTERVAL '24 hours'
GROUP BY p.id, p.email, p.created_at, us.user_id, ci.user_id, ur.user_id
HAVING
COUNT(DISTINCT us.user_id) = 0 OR
COUNT(DISTINCT ci.user_id) = 0 OR
COUNT(DISTINCT ur.user_id) = 0 OR
COUNT(mp.id) = 0;
Alerta: Si retorna > 0 filas → Inicialización falló para algunos usuarios
Query 2: Estadísticas de inicialización
SELECT
COUNT(*) as total_users,
SUM(CASE WHEN has_user_stats = 1 THEN 1 ELSE 0 END) as users_with_user_stats,
SUM(CASE WHEN has_comodines = 1 THEN 1 ELSE 0 END) as users_with_comodines,
SUM(CASE WHEN has_ranks = 1 THEN 1 ELSE 0 END) as users_with_ranks,
SUM(CASE WHEN has_module_progress = 1 THEN 1 ELSE 0 END) as users_with_module_progress,
AVG(module_count) as avg_modules_per_user
FROM (
SELECT
COUNT(DISTINCT us.user_id) as has_user_stats,
COUNT(DISTINCT ci.user_id) as has_comodines,
COUNT(DISTINCT ur.user_id) as has_ranks,
COUNT(DISTINCT mp.user_id) as has_module_progress,
COUNT(mp.id) as module_count
FROM auth_management.profiles p
LEFT JOIN gamification_system.user_stats us ON us.user_id = p.user_id
LEFT JOIN gamification_system.comodines_inventory ci ON ci.user_id = p.id
LEFT JOIN gamification_system.user_ranks ur ON ur.user_id = p.user_id
LEFT JOIN progress_tracking.module_progress mp ON mp.user_id = p.id
WHERE p.role IN ('student', 'admin_teacher', 'super_admin')
GROUP BY p.id
) stats;
Objetivo: 100% en todos los campos
📚 Referencias
Documentación Relacionada
- RF-INIT-001:
docs/01-fase-alcance-inicial/EAI-001-fundamentos/requerimientos/RF-INIT-001-inicializacion-automatica-usuario.md - ADR-012:
docs/97-adr/ADR-012-automatic-user-initialization-trigger.md - FLUJO:
docs/90-transversal/FLUJO-INICIALIZACION-USUARIO.md - DIAGRAMA:
docs/90-transversal/DIAGRAMA-DEPENDENCIAS-INITIALIZE-USER-STATS.md - FUNCIONES:
docs/90-transversal/FUNCIONES-UTILITARIAS-GAMILIT.md - TRACEABILITY:
docs/01-fase-alcance-inicial/EAI-001-fundamentos/implementacion/TRACEABILITY.yml
Código Fuente
- Función:
apps/database/ddl/schemas/gamilit/functions/04-initialize_user_stats.sql(93 líneas) - Trigger:
apps/database/ddl/schemas/auth_management/triggers/04-trg_initialize_user_stats.sql(14 líneas)
Validaciones
- GAP-003:
orchestration/agentes/architecture-analyst/analisis-estado-proyecto-2025-11-24/VALIDACION-GAP-003-MODULE-PROGRESS.md - FINAL:
orchestration/agentes/architecture-analyst/analisis-estado-proyecto-2025-11-24/VALIDACION-FINAL-EXHAUSTIVA.md - CONFLICTOS:
orchestration/agentes/architecture-analyst/analisis-estado-proyecto-2025-11-24/VALIDACION-CONFLICTOS-DUPLICIDADES-REFERENCIAS.md
Fin de la Especificación Técnica ET-INIT-001
Autor: Architecture-Analyst Fecha: 2025-11-24 Versión: 1.1 Estado: ✅ Implementado, Validado y Documentado