# πŸ—οΈ ARQUITECTURA DEL SISTEMA DE RECOMPENSAS Y PROGRESO **VersiΓ³n:** v2.8.0 **Fecha:** 2025-11-29 **Estado:** βœ… IMPLEMENTADO Y VERIFICADO --- ## πŸ“‹ Índice 1. [VisiΓ³n General](#visiΓ³n-general) 2. [Arquitectura de Componentes](#arquitectura-de-componentes) 3. [Dual-Table Pattern](#dual-table-pattern) 4. [Flujo de Datos](#flujo-de-datos) 5. [Patrones de DiseΓ±o](#patrones-de-diseΓ±o) 6. [Seguridad y Rendimiento](#seguridad-y-rendimiento) --- ## 🎯 1. VisiΓ³n General ### Objetivo del Sistema Implementar un sistema completo de **gamificaciΓ³n educativa** que: - βœ… Otorga **XP y ML Coins** por completar ejercicios - βœ… Actualiza **estadΓ­sticas del usuario** automΓ‘ticamente - βœ… Rastrea **progreso de mΓ³dulos** en tiempo real - βœ… Previene **duplicaciΓ³n de recompensas** - βœ… Mantiene **integridad de datos** con triggers --- ## πŸ›οΈ 2. Arquitectura de Componentes ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ FRONTEND β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ β”‚ β”‚ ModuleDetailPage │──│ useModuleDetail │──│ ExerciseCard β”‚β”‚ β”‚ β”‚ (Component) β”‚ β”‚ (Hook) β”‚ β”‚ (Component) β”‚β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ GET /modules/:id β”‚ GET /exercises β”‚ β”‚ GET /exercises β”‚ (with completed) β”‚ β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BACKEND (NestJS) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ API LAYER (Controllers + Guards) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ ModulesController β”‚ β”‚ ExercisesController β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + JwtAuthGuard β”‚ β”‚ + JwtAuthGuard β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ SERVICE LAYER β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ ModulesService β”‚ β”‚ ExerciseAttemptService β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ ExercisesServiceβ”‚ β”‚ ExerciseSubmissionService β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DATABASE (PostgreSQL) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ educational_content (schema) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ modules β”‚ β”‚exercises β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ progress_tracking (schema) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ exercise_ β”‚ β”‚ exercise_attempts β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ submissions β”‚ β”‚ (rewards table) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (workflow) β”‚ β”‚ β–² INSERT β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ TRIGGER β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ gamilit (schema) β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ update_user_stats_on_exercise_complete() β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (TRIGGER FUNCTION) β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ UPDATE/INSERT β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ gamification_system (schema) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ user_stats (table) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - total_xp β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - ml_coins β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - ml_coins_earned_total β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - exercises_completed β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## πŸ”„ 3. Dual-Table Pattern ### Problema Resuelto **SeparaciΓ³n de Responsabilidades**: Necesitamos distinguir entre: 1. **Workflow de evaluaciΓ³n** (draft β†’ submitted β†’ graded) 2. **Sistema de recompensas** (XP, ML Coins, achievements) ### SoluciΓ³n: Dos Tablas Complementarias #### πŸ“ `exercise_submissions` - Tabla de Workflow y RevisiΓ³n Manual **PropΓ³sito:** GestiΓ³n del ciclo de vida de submissions que requieren calificaciΓ³n manual ```sql CREATE TABLE progress_tracking.exercise_submissions ( id UUID PRIMARY KEY, user_id UUID NOT NULL, exercise_id UUID NOT NULL, status VARCHAR(20), -- 'draft', 'submitted', 'graded', 'reviewed' answers JSONB, answer_data JSONB, is_correct BOOLEAN DEFAULT false, xp_earned INTEGER DEFAULT 0, ml_coins_earned INTEGER DEFAULT 0, graded_at TIMESTAMP, feedback TEXT, created_at TIMESTAMP, updated_at TIMESTAMP ); ``` **Flujo:** ``` DRAFT β†’ SUBMITTED β†’ GRADED β†’ REVIEWED ↓ ↓ ↓ ↓ Save Submit Teacher Final ``` **Trigger Asociado (v2.8.0):** ```sql CREATE TRIGGER trg_update_user_stats_on_submission AFTER UPDATE ON exercise_submissions FOR EACH ROW WHEN (status IN ('graded','reviewed') AND is_correct = true) EXECUTE FUNCTION gamilit.update_user_stats_on_submission_graded(); ``` --- #### πŸ† `exercise_attempts` - Tabla de Recompensas **PropΓ³sito:** Tracking de intentos y cΓ‘lculo de recompensas ```sql CREATE TABLE progress_tracking.exercise_attempts ( id UUID PRIMARY KEY, user_id UUID NOT NULL, exercise_id UUID NOT NULL, submission_id UUID REFERENCES exercise_submissions(id), -- SCORING score INTEGER NOT NULL, is_correct BOOLEAN, attempt_number INTEGER, -- REWARDS (calculados por ExerciseAttemptService) xp_earned INTEGER NOT NULL, ml_coins_earned INTEGER NOT NULL, -- GAMEPLAY hints_used INTEGER DEFAULT 0, powerups_used JSONB DEFAULT '[]'::JSONB, time_spent INTEGER, created_at TIMESTAMP DEFAULT NOW() ); ``` **Trigger Asociado:** ```sql CREATE TRIGGER trg_update_user_stats_on_exercise AFTER INSERT ON exercise_attempts FOR EACH ROW EXECUTE FUNCTION gamilit.update_user_stats_on_exercise_complete(); ``` --- ### RelaciΓ³n Entre Tablas (v2.8.0 - Dual Trigger) ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ARQUITECTURA BD-FIRST (Triggers = Verdad) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ exercise_submissions β”‚ β”‚ (RevisiΓ³n manual) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ UPDATE (status='graded', is_correct=true) β”‚ β–Ό TRIGGER 31 ─────────────────────────────────────────┐ trg_update_user_stats_on_submission β”‚ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ update_user_stats_on_ β”‚ β”‚ submission_graded() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ exercise_attempts β”‚ β”‚ user_stats β”‚ β”‚ (Autocorregibles) β”‚ β”‚ total_xp, ml_coins, ... β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ INSERT β”‚ β”‚ β”‚ UPDATE total_xp β–Ό β–Ό TRIGGER 21 ────────────┐ TRIGGER 27 trg_update_user_stats_ β”‚ trg_update_missions_on_earn_xp on_exercise β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ update_missions_on_ β”‚ β”‚ update_user_ β”‚β”€β”€β”€β”€β”˜ β”‚ earn_xp() β”‚ β”‚ stats_on_ β”‚ β”‚ β†’ Actualiza misiones β”‚ β”‚ exercise_ β”‚ β”‚ de tipo earn_xp β”‚ β”‚ complete() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Ventajas:** - βœ… **BD-first Architecture**: Triggers como fuente de verdad - βœ… **Dual Path**: Ambos flujos actualizan user_stats y misiones - βœ… **Separation of Concerns**: Cada tabla tiene una responsabilidad clara - βœ… **Atomic Rewards**: InserciΓ³n/ActualizaciΓ³n = recompensas otorgadas automΓ‘ticamente - βœ… **History Tracking**: Se mantiene historial completo de attempts/submissions - βœ… **No Duplicate Rewards**: Anti-refire en triggers previene duplicados - βœ… **Cascade to Missions**: ActualizaciΓ³n de XP dispara automΓ‘ticamente misiones earn_xp --- ## 🌊 4. Flujo de Datos ### 4.1 Submit de Ejercicio (Escritura) ``` 1. Frontend └─▢ POST /api/educational/exercises/:id/submit Body: { answers, startedAt, hintsUsed, powerupsUsed } 2. ExercisesController.submit() └─▢ ExerciseAttemptService.createAttempt() β”‚ β”œβ”€β–Ά Calcular score (0-100) β”œβ”€β–Ά Calcular XP earned (con penalties) β”œβ”€β–Ά Calcular ML Coins earned (con penalties) β”‚ └─▢ INSERT INTO exercise_attempts { xp_earned, ml_coins_earned, is_correct, score } 3. Database Trigger (AUTOMÁTICO) └─▢ gamilit.update_user_stats_on_exercise_complete() β”‚ β”œβ”€β–Ά READ NEW.xp_earned, NEW.ml_coins_earned β”‚ └─▢ UPDATE gamification_system.user_stats SET total_xp = total_xp + NEW.xp_earned SET ml_coins = ml_coins + NEW.ml_coins_earned SET exercises_completed = exercises_completed + 1 4. Respuesta al Frontend └─▢ { score, isPerfect, rewards: { xp, mlCoins }, rankUp } ``` --- ### 4.2 Consulta de Progreso (Lectura) ``` 1. Frontend └─▢ GET /api/educational/modules Header: Authorization: Bearer {JWT} 2. ModulesController.findAll() β”œβ”€β–Ά Obtener todos los mΓ³dulos β”œβ”€β–Ά Obtener submissions del usuario (status='graded') β”œβ”€β–Ά Obtener todos los ejercicios β”‚ └─▢ Para cada mΓ³dulo: β”œβ”€β–Ά Filtrar ejercicios del mΓ³dulo β”œβ”€β–Ά Contar completed (en completedExercisesMap) β”œβ”€β–Ά Calcular progress = (completed / total) * 100 └─▢ Agregar campos: { total_exercises, completed_exercises, progress, completed } 3. Respuesta al Frontend └─▢ [ { id, title, total_exercises: 5, completed_exercises: 2, progress: 40, completed: false }, ... ] ``` --- ## 🎨 5. Patrones de DiseΓ±o Implementados ### 5.1 UPSERT Pattern (Base de Datos) **UbicaciΓ³n:** `update_user_stats_on_exercise_complete()` ```sql -- Intenta UPDATE UPDATE user_stats SET ... WHERE user_id = NEW.user_id; -- Si no existe (NOT FOUND), hace INSERT IF NOT FOUND THEN INSERT INTO user_stats (user_id, ...) VALUES (...); END IF; ``` **Ventaja:** Evita errores por falta de registro inicial de user_stats --- ### 5.2 Map-based Lookup (Backend) **UbicaciΓ³n:** `ModulesController.findAll()`, `ExercisesController.findAll()` ```typescript // Crear Map para O(1) lookup const completedExercisesMap = new Map(); allSubmissions.forEach((submission) => { if (submission.status === 'graded') { completedExercisesMap.set(submission.exercise_id, true); } }); // Usar Map para agregar campo 'completed' exercises.map((exercise) => ({ ...exercise, completed: completedExercisesMap.get(exercise.id) || false })); ``` **Ventaja:** Evita N+1 queries, eficiencia O(n) en lugar de O(nΒ²) --- ### 5.3 Trigger-based Automation (Base de Datos) **PatrΓ³n:** Event-driven updates ```sql CREATE TRIGGER trg_update_user_stats_on_exercise AFTER INSERT ON exercise_attempts FOR EACH ROW EXECUTE FUNCTION update_user_stats_on_exercise_complete(); ``` **Ventajas:** - βœ… ActualizaciΓ³n automΓ‘tica (no requiere cΓ³digo de aplicaciΓ³n) - βœ… Atomic (dentro de la misma transacciΓ³n) - βœ… No se puede olvidar actualizar stats --- ### 5.4 Dependency Injection (Backend) **UbicaciΓ³n:** `ModulesController` ```typescript constructor( private readonly modulesService: ModulesService, private readonly exercisesService: ExercisesService, private readonly exerciseSubmissionService: ExerciseSubmissionService, ) {} ``` **Ventaja:** Testeable, desacoplado, siguiendo principios SOLID --- ### 5.5 Custom Hooks (Frontend) **UbicaciΓ³n:** `useModuleDetail` ```typescript export function useModuleDetail(moduleId: string) { const [module, setModule] = useState(null); const [exercises, setExercises] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // Fetch logic }, [moduleId]); return { module, exercises, loading, error }; } ``` **Ventaja:** Reusable, encapsula lΓ³gica de fetch, separation of concerns --- ## πŸ”’ 6. Seguridad y Rendimiento ### 6.1 Seguridad #### AutenticaciΓ³n JWT ```typescript @UseGuards(JwtAuthGuard) @Get('modules') async findAll(@Request() req: any) { const userId = req.user.id; // ExtraΓ­do del JWT // ... } ``` #### RLS (Row Level Security) ```sql -- PolΓ­ticas en BD aseguran que users solo vean sus datos ALTER TABLE progress_tracking.exercise_submissions ENABLE ROW LEVEL SECURITY; ``` #### SECURITY DEFINER ```sql CREATE OR REPLACE FUNCTION update_user_stats_on_exercise_complete() LANGUAGE plpgsql SECURITY DEFINER -- Ejecuta con permisos del owner de la funciΓ³n ``` --- ### 6.2 Rendimiento #### Índices en BD ```sql -- Índices para queries frecuentes CREATE INDEX idx_exercise_submissions_user ON exercise_submissions(user_id); CREATE INDEX idx_exercise_attempts_user ON exercise_attempts(user_id); CREATE INDEX idx_user_stats_user ON user_stats(user_id); ``` #### Batch Fetch ```typescript // En lugar de N queries, una sola const allSubmissions = await this.exerciseSubmissionService.findByUserId(userId); ``` #### Caching Potencial ```typescript // TODO: Implementar cache en Redis para submissions frecuentes @Cacheable('user-submissions', ttl: 300) async findByUserId(userId: string) { ... } ``` --- ## πŸ“Š 7. MΓ©tricas y Monitoreo ### Puntos de Observabilidad 1. **Trigger Execution Time** - Medir tiempo de `update_user_stats_on_exercise_complete()` - Alert si > 100ms 2. **API Response Time** - GET /modules: objetivo < 200ms - GET /exercises: objetivo < 150ms 3. **Error Rates** - Trigger warnings en logs - Failed submissions - Token expiration --- **Última actualizaciΓ³n:** 2025-11-29 **Autor:** Sistema Gamilit **VersiΓ³n:** 2.8.0