workspace/projects/gamilit/docs/sistema-recompensas/02-FLUJO-END-TO-END.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- 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>
2025-12-08 10:44:23 -06:00

27 KiB

🔄 FLUJO END-TO-END - SISTEMA DE RECOMPENSAS

Versión: v2.8.0 Fecha: 2025-11-29


📊 Diagrama Completo del Flujo

┌─────────────┐
│   USUARIO   │
│  (Student)  │
└──────┬──────┘
       │
       │ 1. Ingresa al módulo
       ▼
┌──────────────────────────────────────────────────────────────────┐
│                    FRONTEND (React + TypeScript)                 │
└──────────────────────────────────────────────────────────────────┘
       │
       │ 2. useModuleDetail(moduleId)
       ▼
   ┌────────────────────────────┐
   │ GET /modules/:id           │
   │ GET /exercises (filtered)  │
   └────────────┬───────────────┘
                │
                │ 3. Renderiza ejercicios
                │    con badge "Completado" si completed=true
                ▼
   ┌────────────────────────────┐
   │ Usuario selecciona         │
   │ ejercicio y lo resuelve    │
   └────────────┬───────────────┘
                │
                │ 4. Click "Enviar Respuesta"
                ▼
┌──────────────────────────────────────────────────────────────────┐
│              POST /exercises/:id/submit                          │
│              Body: { answers, startedAt, hintsUsed, powerups }   │
└──────────────────────────────────────────────────────────────────┘
                │
                │ 5. JWT Auth
                ▼
┌──────────────────────────────────────────────────────────────────┐
│                    BACKEND (NestJS)                              │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  ExercisesController.submit()                              │ │
│  └────────────────────────┬───────────────────────────────────┘ │
│                           │                                      │
│                           │ 6. Validar ejercicio existe          │
│                           ▼                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  ExerciseSubmissionService.createSubmission()              │ │
│  │    - status: 'submitted'                                   │ │
│  │    - answers, started_at, submitted_at                     │ │
│  └────────────────────────┬───────────────────────────────────┘ │
│                           │                                      │
│                           │ 7. Crear submission                  │
│                           ▼                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  ExerciseAttemptService.createAttempt()                    │ │
│  │    ├─▶ Calcular score (0-100)                              │ │
│  │    ├─▶ Calcular XP (base_xp - penalties)                   │ │
│  │    ├─▶ Calcular ML Coins (base_coins - penalties)          │ │
│  │    │   Penalties:                                          │ │
│  │    │     - Cada hint: -10% XP, -5% coins                   │ │
│  │    │     - Powerup usado: -15% XP, -10% coins              │ │
│  │    │                                                        │ │
│  │    └─▶ INSERT INTO exercise_attempts                       │ │
│  │        (xp_earned, ml_coins_earned, is_correct, score)     │ │
│  └────────────────────────┬───────────────────────────────────┘ │
└────────────────────────────┼──────────────────────────────────────┘
                             │
                             │ 8. INSERT dispara trigger
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                    DATABASE (PostgreSQL)                         │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  Trigger: trg_update_user_stats_on_exercise                │ │
│  │  AFTER INSERT ON exercise_attempts                         │ │
│  │  FOR EACH ROW EXECUTE FUNCTION                             │ │
│  │    gamilit.update_user_stats_on_exercise_complete()        │ │
│  └────────────────────────┬───────────────────────────────────┘ │
│                           │                                      │
│                           │ 9. Función trigger se ejecuta        │
│                           ▼                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  update_user_stats_on_exercise_complete()                  │ │
│  │    ├─▶ Leer NEW.xp_earned                                  │ │
│  │    ├─▶ Leer NEW.ml_coins_earned                            │ │
│  │    ├─▶ Leer NEW.is_correct                                 │ │
│  │    │                                                        │ │
│  │    └─▶ UPDATE gamification_system.user_stats               │ │
│  │        SET total_xp = total_xp + NEW.xp_earned             │ │
│  │        SET ml_coins = ml_coins + NEW.ml_coins_earned       │ │
│  │        SET ml_coins_earned_total += NEW.ml_coins_earned    │ │
│  │        SET exercises_completed += 1                        │ │
│  │        SET last_activity_at = NOW()                        │ │
│  │        WHERE user_id = NEW.user_id                         │ │
│  │                                                             │ │
│  │        -- Si no existe (NOT FOUND):                        │ │
│  │        INSERT INTO user_stats                              │ │
│  │          (user_id, total_xp, ml_coins, ...)                │ │
│  │        VALUES (NEW.user_id, NEW.xp_earned,                 │ │
│  │                100 + NEW.ml_coins_earned, ...)             │ │
│  └────────────────────────┬───────────────────────────────────┘ │
└────────────────────────────┼──────────────────────────────────────┘
                             │
                             │ 10. Trigger COMMIT SUCCESS
                             │     (todo en misma transacción)
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                    BACKEND (Response)                            │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  ExercisesController retorna:                              │ │
│  │  {                                                         │ │
│  │    score: 100,                                             │ │
│  │    isPerfect: true,                                        │ │
│  │    rewards: {                                              │ │
│  │      xp: 200,                                              │ │
│  │      mlCoins: 50,                                          │ │
│  │      bonuses: []                                           │ │
│  │    },                                                      │ │
│  │    rankUp: null                                            │ │
│  │  }                                                         │ │
│  └────────────────────────┬───────────────────────────────────┘ │
└────────────────────────────┼──────────────────────────────────────┘
                             │
                             │ 11. Respuesta JSON
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                    FRONTEND (React)                              │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  useModuleDetail actualiza:                                │ │
│  │    - Estado del ejercicio (completed: true)                │ │
│  │    - Re-fetch de progreso del módulo                       │ │
│  │    - Muestra feedback visual:                              │ │
│  │      ✓ Badge "Completado"                                  │ │
│  │      ✓ Botón cambia a "Volver a intentar"                  │ │
│  │      ✓ Toast notification con rewards                      │ │
│  └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
                             │
                             │ 12. Usuario ve:
                             │     • "¡Completado! +200 XP, +50 ML Coins"
                             │     • Badge verde "Completado"
                             │     • Progreso del módulo: 20% (1/5)
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                     USUARIO (Feedback Visual)                    │
│  ✅ Ejercicio marcado como completado                            │
│  💰 Recompensas otorgadas y visibles                             │
│  📊 Progreso de módulo actualizado                               │
│  🏆 Stats de perfil actualizados                                 │
└──────────────────────────────────────────────────────────────────┘

📊 Flujo B: Ejercicios con Revisión Manual (v2.8.0)

Este flujo complementa el flujo principal para ejercicios que requieren calificación por maestro.

┌─────────────┐
│   USUARIO   │
│  (Student)  │
└──────┬──────┘
       │
       │ 1. Ingresa al ejercicio (requires_manual_grading = true)
       ▼
┌──────────────────────────────────────────────────────────────────┐
│                    FRONTEND (React + TypeScript)                 │
│  Renderiza ejercicio que requiere revisión manual                │
└──────────────────────────────────────────────────────────────────┘
       │
       │ 2. Click "Enviar para Revisión"
       ▼
┌──────────────────────────────────────────────────────────────────┐
│              POST /exercises/:id/submit                          │
│              Body: { answers, startedAt }                        │
└──────────────────────────────────────────────────────────────────┘
       │
       │ 3. Backend detecta requires_manual_grading = true
       ▼
┌──────────────────────────────────────────────────────────────────┐
│                    DATABASE (PostgreSQL)                         │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  INSERT INTO exercise_submissions                          │ │
│  │    status = 'submitted', is_correct = NULL                 │ │
│  │    xp_earned = 0, ml_coins_earned = 0                      │ │
│  └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
       │
       │ ⏳ ESPERA: Maestro revisa y califica
       ▼
┌──────────────────────────────────────────────────────────────────┐
│                    TEACHER PORTAL                                │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  Maestro califica submission:                              │ │
│  │    PUT /teacher/submissions/:id/grade                      │ │
│  │    Body: { score: 90, is_correct: true, feedback: "..." }  │ │
│  └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
       │
       │ 4. UPDATE dispara trigger
       ▼
┌──────────────────────────────────────────────────────────────────┐
│                    DATABASE (PostgreSQL)                         │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  UPDATE exercise_submissions                               │ │
│  │    SET status = 'graded',                                  │ │
│  │        is_correct = true,                                  │ │
│  │        xp_earned = 200,                                    │ │
│  │        ml_coins_earned = 50                                │ │
│  └────────────────────────┬───────────────────────────────────┘ │
│                           │                                      │
│                           │ 5. Trigger WHEN se activa             │
│                           ▼                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  Trigger: trg_update_user_stats_on_submission              │ │
│  │  AFTER UPDATE ON exercise_submissions                      │ │
│  │  WHEN (status IN ('graded','reviewed')                     │ │
│  │        AND is_correct = true                               │ │
│  │        AND status/is_correct CHANGED)                      │ │
│  └────────────────────────┬───────────────────────────────────┘ │
│                           │                                      │
│                           │ 6. Función se ejecuta                │
│                           ▼                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  update_user_stats_on_submission_graded()                  │ │
│  │    ├─▶ Leer NEW.xp_earned (200)                           │ │
│  │    ├─▶ Leer NEW.ml_coins_earned (50)                      │ │
│  │    │                                                        │ │
│  │    └─▶ UPDATE gamification_system.user_stats               │ │
│  │        SET total_xp = total_xp + 200                       │ │
│  │        SET ml_coins = ml_coins + 50                        │ │
│  │        SET exercises_completed += 1                        │ │
│  └────────────────────────┬───────────────────────────────────┘ │
│                           │                                      │
│                           │ 7. Cascada: UPDATE en user_stats      │
│                           ▼                                      │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │  Trigger: trg_update_missions_on_earn_xp                   │ │
│  │  AFTER UPDATE ON user_stats                                │ │
│  │  WHEN (NEW.total_xp != OLD.total_xp)                       │ │
│  │    ├─▶ Busca misiones earn_xp activas                     │ │
│  │    └─▶ Incrementa objectives[].current += 200             │ │
│  └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
       │
       │ 8. Transacción COMMIT
       ▼
┌──────────────────────────────────────────────────────────────────┐
│                    NOTIFICACIÓN AL ESTUDIANTE                    │
│  ✅ "¡Tu ejercicio fue calificado! +200 XP, +50 ML Coins"       │
│  🏆 Misión "Ganar 100 XP" actualizada automáticamente           │
└──────────────────────────────────────────────────────────────────┘

Comparación de Flujos

Aspecto Flujo A (Autocorregibles) Flujo B (Revisión Manual)
Tabla principal exercise_attempts exercise_submissions
Trigger evento AFTER INSERT AFTER UPDATE
Cuándo se otorga XP Inmediato al enviar Cuando maestro califica
Reintentos Ilimitados Solo 1 entrega
Anti-farming XP Solo primer acierto N/A (solo 1 entrega)
Actualiza misiones Sí, vía cadena de triggers Sí, vía cadena de triggers

🎯 Puntos Clave del Flujo

1. Inicio del Flujo (Frontend)

  • Usuario navega a ModuleDetailPage
  • Hook useModuleDetail se ejecuta automáticamente
  • Fetch simultáneo de módulo y ejercicios

2. Autenticación (JWT)

  • Cada request incluye Authorization: Bearer {token}
  • Backend valida con JwtAuthGuard
  • Extrae userId del token para queries

3. Cálculo de Recompensas (Backend)

  • ExerciseAttemptService calcula XP y ML Coins
  • Aplica penalties según hints/powerups usados
  • NO sobrescribe con 0 (bug corregido)

4. Trigger Automático (Database)

  • INSERT en exercise_attempts dispara trigger
  • Función lee valores precalculados (NEW.xp_earned, NEW.ml_coins_earned)
  • UPDATE atómico en user_stats

5. Pattern UPSERT (Database)

  • Primero intenta UPDATE
  • Si NOT FOUND, hace INSERT con valores iniciales
  • Evita errores por user_stats faltante

6. Respuesta y Actualización (Frontend)

  • Backend retorna rewards calculados
  • Frontend re-fetch para actualizar UI
  • Badges y progreso se actualizan automáticamente

⏱️ Timeline del Flujo (Performance)

t=0ms     Usuario hace submit
t=20ms    Backend recibe request (JWT validated)
t=50ms    INSERT en exercise_submissions
t=80ms    INSERT en exercise_attempts
t=85ms    ⚡ Trigger ejecuta (muy rápido, <5ms)
t=90ms    UPDATE en user_stats completo
t=100ms   Response enviada al Frontend
t=120ms   Frontend actualiza UI

TOTAL: ~120ms end-to-end

🔍 Verificación del Flujo Correcto

Checkpoints de Validación

  1. exercise_attempts insertado

    SELECT * FROM progress_tracking.exercise_attempts
    WHERE user_id = 'user-uuid'
    ORDER BY created_at DESC LIMIT 1;
    
  2. user_stats actualizado

    SELECT total_xp, ml_coins, ml_coins_earned_total, exercises_completed
    FROM gamification_system.user_stats
    WHERE user_id = 'user-uuid';
    
  3. submission marcada como graded

    SELECT status FROM progress_tracking.exercise_submissions
    WHERE id = 'submission-uuid';
    
  4. Frontend muestra completado

    GET /api/educational/exercises/:id
    // response: { ...exercise, completed: true }
    

🚨 Puntos de Fallo y Manejo

1. Trigger Falla

Problema: Función trigger tiene bug Impacto: Stats no se actualizan pero attempt sí se guarda Solución:

-- Trigger usa EXCEPTION handler
EXCEPTION
    WHEN OTHERS THEN
        RAISE WARNING 'Error updating stats for user %', NEW.user_id;
        RETURN NEW;  -- No bloquea el INSERT
END;

2. JWT Expirado

Problema: Token expiró (> 1 hora) Impacto: Request rechazado con 401 Unauthorized Solución Frontend:

try {
  await submitExercise();
} catch (error) {
  if (error.status === 401) {
    // Redirect to login
    logout();
  }
}

3. Exercise No Existe

Problema: ID de ejercicio inválido Impacto: 404 Not Found Solución Backend:

const exercise = await this.exercisesService.findById(id);
if (!exercise) {
  throw new NotFoundException(`Exercise ${id} not found`);
}

📈 Optimizaciones Implementadas

1. Batch Fetch de Submissions

// ❌ ANTES: N queries (una por cada ejercicio)
for (const exercise of exercises) {
  const submission = await findSubmission(userId, exercise.id);
}

// ✅ AHORA: 1 query para todos
const allSubmissions = await findByUserId(userId);
const completedMap = new Map(submissions.map(s => [s.exercise_id, true]));

2. Cálculo de Progress en Backend

// Evita que Frontend tenga que calcular
// Retorna directamente: { progress: 40, completed: false }

3. Trigger en Lugar de Application Logic

// ❌ EVITADO: Actualizar stats desde NestJS
// ✅ MEJOR: Trigger automático en BD
// Garantiza que SIEMPRE se actualiza, sin olvidos

Última actualización: 2025-11-29 Autor: Sistema Gamilit Versión: 2.8.0