workspace/projects/gamilit/docs/01-fase-alcance-inicial/EAI-003-gamificacion/requerimientos/RF-GAM-001-achievements.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

18 KiB

RF-GAM-001: Sistema de Logros (Achievements)

📋 Metadata

Campo Valor
ID RF-GAM-001
Módulo Gamificación
Prioridad Alta
Estado Implementado
Versión 1.0
Fecha creación 2025-11-07
Última actualización 2025-11-07

🔗 Referencias

Especificación Técnica

📐 ET-GAM-001: Sistema de Achievements

Implementación DDL

🗄️ ENUMs Canónicos:

achievement_type:

  • Ubicación: apps/database/ddl/00-prerequisites.sql:51-54
  • Tipo: gamification_system.achievement_type
  • Valores: badge, milestone, special, rank_promotion

achievement_category:

  • Ubicación: apps/database/ddl/00-prerequisites.sql:47-50
  • Tipo: gamification_system.achievement_category
  • Valores: progress, streak, completion, social, special, mastery, exploration

🗄️ Tablas:

  1. gamification_system.achievementsapps/database/ddl/schemas/gamification_system/tables/01-achievements.sql
    • Columnas: type achievement_type, category achievement_category
  2. gamification_system.user_achievementsapps/database/ddl/schemas/gamification_system/tables/02-user_achievements.sql
    • Tracking de achievements desbloqueados por usuario

🗄️ Funciones:

  • gamification_system.check_and_unlock_achievement() → Verifica y desbloquea achievements
  • gamification_system.award_achievement_rewards() → Otorga recompensas (XP, ML Coins)

🗄️ Triggers:

  • trg_achievement_unlocked → Ejecuta al desbloquear achievement
  • trg_check_rank_promotion → Verifica si XP alcanza siguiente rango

Backend

💻 Implementación:

  • Enums:
    • apps/backend/src/modules/gamification/enums/achievement-type.enum.ts
    • apps/backend/src/modules/gamification/enums/achievement-category.enum.ts
  • Service: apps/backend/src/modules/gamification/services/achievement.service.ts
  • Listeners: apps/backend/src/modules/gamification/listeners/achievement.listener.ts
  • DTOs: apps/backend/src/modules/gamification/dto/unlock-achievement.dto.ts

Frontend

🎨 Componentes:

  • Types: apps/frontend/src/types/gamification.types.ts
  • Componentes:
    • apps/frontend/src/components/gamification/AchievementGallery.tsx
    • apps/frontend/src/components/gamification/AchievementCard.tsx
    • apps/frontend/src/components/gamification/AchievementUnlockedModal.tsx
    • apps/frontend/src/components/gamification/AchievementProgress.tsx

Mapeo Completo

📊 Ver mapeo completo: Requerimientos → Implementación


📝 Descripción del Requerimiento

Contexto

El sistema educativo Gamilit necesita motivar y mantener comprometidos a los estudiantes mediante mecánicas de gamificación. Los achievements (logros) son un elemento fundamental para:

  • Reconocer el progreso del estudiante
  • Motivar la continuidad en el aprendizaje
  • Celebrar hitos importantes
  • Fomentar comportamientos positivos (rachas, exploración, dominio)
  • Proporcionar feedback visual del desarrollo

Necesidad del Negocio

Problema: Sin un sistema de achievements:

  • Estudiantes pierden motivación tras completar ejercicios
  • No hay reconocimiento visible del progreso
  • Difícil fomentar hábitos de estudio consistentes
  • Falta de incentivos para explorar contenido nuevo

Solución: Implementar un sistema de achievements clasificados por tipo y categoría que recompense diversos comportamientos y logros, proporcionando feedback constante y motivación intrínseca.


🎯 Requerimiento Funcional

RF-GAM-001.1: Tipos de Achievements

El sistema DEBE soportar 4 tipos de achievements:

1. Badge (Insignia) 🏅

Descripción: Insignias coleccionables por completar acciones específicas

Características:

  • Coleccionables (el estudiante puede tener múltiples)
  • Visuales llamativas con iconografía única
  • Permanentes una vez desbloqueados
  • Mostrados en perfil público del estudiante

Ejemplos:

  • "Primer Paso" - Completar primer ejercicio
  • "Lector Ávido" - Leer 10 textos completos
  • "Explorador" - Probar 5 mecánicas diferentes

Recompensas Típicas:

  • 10-50 XP
  • Badge visible en perfil
  • Entrada en galería de achievements

2. Milestone (Hito) 📍

Descripción: Hitos que marcan progreso significativo en el aprendizaje

Características:

  • Marca progreso cuantificable
  • Desbloqueables en secuencia
  • Asociados a métricas específicas

Ejemplos:

  • "Módulo Maestro" - Completar módulo al 100%
  • "50 Ejercicios" - Resolver 50 ejercicios correctamente
  • "Nivel 10" - Alcanzar nivel de usuario 10

Recompensas Típicas:

  • 50-200 XP
  • 20-100 ML Coins
  • Badge + título especial

3. Special (Especial)

Descripción: Achievements especiales para eventos, temporadas, o logros únicos

Características:

  • Tiempo limitado (eventos)
  • Únicos o muy difíciles de obtener
  • Mayor valor social (prestigio)

Ejemplos:

  • "Racha de Fuego" - 30 días consecutivos practicando
  • "Perfeccionista" - Completar módulo con 100% accuracy en primer intento
  • "Pionero" - Ser de los primeros 100 usuarios en probar nueva feature

Recompensas Típicas:

  • 200-500 XP
  • 100-300 ML Coins
  • Badge dorado/animado
  • Título exclusivo

4. Rank Promotion (Promoción de Rango) 👑

Descripción: Achievement otorgado al ascender de rango Maya

Características:

  • Automático al alcanzar XP requerido
  • Jerárquico (solo puedes tener un rank activo)
  • Visible en toda la UI del estudiante

Ejemplos:

  • "Nacom" - Alcanzar 1000 XP (Rango 2)
  • "Ah K'in" - Alcanzar 5000 XP (Rango 3)
  • "Halach Uinic" - Alcanzar 20000 XP (Rango 4)

Recompensas:

  • Nuevo título de rango
  • Desbloqueo de features exclusivas del rango
  • Badge de rango + animación especial
  • Notificación destacada a amigos

RF-GAM-001.2: Categorías de Achievements

El sistema DEBE clasificar achievements en 7 categorías:

1. Progress (Progreso) 📈

Relacionados con avance general en el sistema

  • Ejemplos: "Nivel 5", "50% del curso", "Primera semana completada"

2. Streak (Racha) 🔥

Relacionados con consistencia y hábitos

  • Ejemplos: "3 días consecutivos", "Racha de 30 días", "Sin faltar en un mes"

3. Completion (Completitud)

Relacionados con finalizar contenido

  • Ejemplos: "Módulo completado", "Todos los ejercicios del tema", "Curso finalizado"

4. Social (Social) 👥

Relacionados con interacción social

  • Ejemplos: "Primer amigo", "Equipo de 5", "Ayudó a 10 compañeros"

5. Special (Especiales) 🌟

Eventos, temporadas, logros únicos

  • Ejemplos: "Evento de Halloween", "Beta Tester", "Aniversario 1 año"

6. Mastery (Dominio) 🎓

Relacionados con dominar contenido

  • Ejemplos: "100% accuracy en 10 ejercicios", "Maestro de gramática", "Sin errores en módulo"

7. Exploration (Exploración) 🗺️

Relacionados con descubrir contenido nuevo

  • Ejemplos: "Probó todas las mecánicas", "Exploró 3 módulos", "Primera simulación"

RF-GAM-001.3: Matriz de Achievements

El sistema DEBE permitir combinar tipos y categorías:

Tipo Category Ejemplo Criterio Recompensa
badge progress "Principiante" Completar tutorial 10 XP
badge streak "Constante" 7 días consecutivos 50 XP
badge social "Amigable" Agregar 5 amigos 30 XP
milestone completion "Módulo 1 Completo" 100% Módulo 1 100 XP + 50 ML Coins
milestone mastery "Maestro del Análisis" 10 ejercicios >90% 150 XP
special streak "Racha Épica" 30 días 300 XP + Badge oro
special exploration "Aventurero" Probar 20 mecánicas 200 XP
rank_promotion progress "Nacom" 1000 XP alcanzados Rango 2 + features

RF-GAM-001.4: Criterios de Desbloqueo

Cada achievement DEBE tener criterios explícitos almacenados en JSONB:

{
  "achievement_id": "ach_001",
  "name": "Racha de Fuego",
  "type": "special",
  "category": "streak",
  "criteria": {
    "type": "streak_days",
    "required_days": 30,
    "consecutive": true,
    "min_activities_per_day": 1
  },
  "rewards": {
    "xp": 300,
    "ml_coins": 100,
    "badge_url": "/badges/fire-streak.png",
    "title": "Imparable"
  }
}

Tipos de Criterios:

  • streak_days: Días consecutivos
  • exercise_count: Cantidad de ejercicios
  • completion_percentage: Porcentaje de completitud
  • accuracy_threshold: Precisión mínima
  • xp_reached: XP alcanzado
  • social_connections: Conexiones sociales
  • exploration_count: Contenido explorado

RF-GAM-001.5: Sistema de Recompensas

Al desbloquear achievement, el sistema DEBE:

  1. Registrar desbloqueo:
INSERT INTO gamification_system.user_achievements (
    user_id, achievement_id, unlocked_at
) VALUES (
    user_id, achievement_id, NOW()
);
  1. Otorgar recompensas:
  • Sumar XP a user_stats.total_xp
  • Sumar ML Coins a user_stats.ml_coins
  • Verificar si XP alcanza nuevo rango → disparar rank_promotion
  1. Notificar usuario:
await notificationService.send({
  userId,
  type: 'achievement_unlocked',
  priority: 'medium',
  data: {
    achievementName: 'Racha de Fuego',
    badgeUrl: '/badges/fire-streak.png',
    xpEarned: 300,
    mlCoinsEarned: 100,
  }
});
  1. Disparar confetti/animación:
  • Frontend muestra modal de celebración
  • Animación de confetti
  • Sonido de logro
  1. Actualizar galería:
  • Achievement aparece como "desbloqueado" en galería
  • Badge visible en perfil público

📊 Casos de Uso

UC-GAM-001: Estudiante desbloquea achievement por completar módulo

Actor: Estudiante Precondiciones: Estudiante completó último ejercicio del módulo

Flujo:

  1. Estudiante completa ejercicio final del módulo
  2. Sistema actualiza module_progress.status'completed'
  3. Trigger automático verifica criterios de achievements:
    SELECT * FROM gamification_system.achievements
    WHERE criteria->>'type' = 'completion_percentage'
      AND criteria->>'module_id' = current_module_id
      AND criteria->>'percentage' = '100';
    
  4. Sistema encuentra achievement "Módulo Maestro"
  5. Sistema ejecuta gamification_system.check_and_unlock_achievement(user_id, achievement_id)
  6. Sistema registra desbloqueo en user_achievements
  7. Sistema otorga recompensas:
    • +100 XP → user_stats.total_xp
    • +50 ML Coins → user_stats.ml_coins
  8. Sistema envía notificación achievement_unlocked
  9. Frontend muestra modal de celebración con confetti
  10. Achievement aparece en galería del estudiante

Resultado: Achievement desbloqueado, recompensas otorgadas, estudiante motivado


UC-GAM-002: Estudiante ve galería de achievements

Actor: Estudiante Precondiciones: Usuario autenticado

Flujo:

  1. Estudiante navega a "Mis Logros"
  2. Sistema carga achievements:
    const achievements = await achievementService.getGallery(userId);
    // Retorna: { unlocked: [], locked: [], progress: {} }
    
  3. Frontend renderiza galería en grid:
    • Achievements desbloqueados: Full color, con fecha
    • Achievements bloqueados: Silueta con "???" (si es secreto)
    • Achievements en progreso: Barra de progreso visible
  4. Estudiante hace click en achievement desbloqueado
  5. Sistema muestra detalle:
    • Nombre y descripción
    • Fecha de desbloqueo
    • Recompensas obtenidas
    • Rareza (% de usuarios que lo tienen)
  6. Estudiante puede compartir achievement en redes

Resultado: Estudiante visualiza su colección y progreso


UC-GAM-003: Achievement de racha de 7 días consecutivos

Actor: Sistema (automático) Precondiciones: Estudiante tiene 6 días consecutivos registrados

Flujo:

  1. Estudiante completa al menos 1 ejercicio hoy (día 7)
  2. Cron job nocturno ejecuta:
    await streakService.updateStreaks(); // Actualiza todas las rachas
    
  3. Sistema detecta que estudiante alcanzó 7 días consecutivos
  4. Sistema verifica criterios del achievement "Semana Perfecta":
    {
      "type": "streak_days",
      "required_days": 7,
      "consecutive": true
    }
    
  5. Criterio cumplido → Sistema desbloquea achievement
  6. Sistema otorga recompensas:
    • +50 XP
    • Badge "Semana Perfecta"
  7. Al día siguiente: Usuario recibe notificación matutina:

    "🔥 ¡7 días consecutivos! Desbloqueaste 'Semana Perfecta'"

Resultado: Racha reconocida, estudiante motivado a continuar


🔐 Consideraciones de Seguridad

1. Prevención de Manipulación

Problema: Usuario podría intentar manipular criterios de achievements

Soluciones:

// Backend valida criterios antes de desbloquear
async function unlockAchievement(userId: string, achievementId: string) {
  // 1. Verificar que criterios se cumplan REALMENTE
  const criteriaMetFlagg = await verifyCriteria(userId, achievementId);
  if (!criteriaMetFlagg) {
    throw new ForbiddenException('Criteria not met');
  }

  // 2. Verificar que no esté ya desbloqueado
  const alreadyUnlocked = await checkIfUnlocked(userId, achievementId);
  if (alreadyUnlocked) {
    throw new ConflictException('Already unlocked');
  }

  // 3. Rate limiting (max 5 achievements por minuto)
  await rateLimiter.check(`unlock:${userId}`, 5, 60);

  // 4. Desbloquear
  await unlockAndReward(userId, achievementId);
}

2. Integridad de Recompensas

Problema: Recompensas podrían duplicarse por race conditions

Solución:

-- Transaction con locks
BEGIN;

-- Lock row del user
SELECT * FROM gamification_system.user_stats
WHERE user_id = $1
FOR UPDATE;

-- Insertar achievement (UNIQUE constraint previene duplicados)
INSERT INTO gamification_system.user_achievements (user_id, achievement_id)
VALUES ($1, $2)
ON CONFLICT (user_id, achievement_id) DO NOTHING;

-- Otorgar recompensas solo si se insertó
GET DIAGNOSTICS rows_inserted = ROW_COUNT;
IF rows_inserted > 0 THEN
    UPDATE gamification_system.user_stats
    SET total_xp = total_xp + $3,
        ml_coins = ml_coins + $4
    WHERE user_id = $1;
END IF;

COMMIT;

3. Secretos vs Públicos

Algunos achievements pueden ser secretos (sorpresas):

interface Achievement {
  id: string;
  name: string;
  description: string;
  isSecret: boolean; // Si true, no mostrar en galería hasta desbloquear
  hints?: string[]; // Pistas opcionales
}

Criterios de Aceptación

AC-001: ENUMs Implementados

  • ENUM achievement_type con 4 valores
  • ENUM achievement_category con 7 valores
  • Tabla achievements usa ambos ENUMs
  • Tabla user_achievements tracking desbloqueos

AC-002: Criterios Verificables

  • Todos los achievements tienen criterios en JSONB
  • Sistema verifica criterios antes de desbloquear
  • Triggers automáticos verifican después de acciones clave

AC-003: Recompensas Otorgadas

  • XP se suma correctamente a user_stats
  • ML Coins se suman correctamente
  • Badges aparecen en perfil del usuario
  • Rank promotions se disparan automáticamente

AC-004: Notificaciones Enviadas

  • Notificación achievement_unlocked enviada
  • Modal de celebración muestra en frontend
  • Confetti/animación visible

AC-005: Galería Funcional

  • Galería muestra achievements desbloqueados
  • Achievements bloqueados visibles (o secretos)
  • Progreso visible para achievements en curso
  • Detalle completo al hacer click

🧪 Testing

Test Case 1: Desbloquear achievement al completar módulo

test('Achievement unlocks when module completed', async () => {
  const user = await createUser();
  const module = await createModule();

  // Completar todos los ejercicios del módulo
  await completeAllExercises(user.id, module.id);

  // Verificar achievement desbloqueado
  const achievements = await getUnlockedAchievements(user.id);
  expect(achievements).toContainEqual(
    expect.objectContaining({
      name: 'Módulo Maestro',
      type: 'milestone',
      category: 'completion',
    })
  );

  // Verificar recompensas
  const userStats = await getUserStats(user.id);
  expect(userStats.total_xp).toBeGreaterThanOrEqual(100);
  expect(userStats.ml_coins).toBeGreaterThanOrEqual(50);
});

Test Case 2: No duplicar achievement

test('Cannot unlock same achievement twice', async () => {
  const user = await createUser();
  const achievement = await createAchievement();

  // Desbloquear primera vez
  await unlockAchievement(user.id, achievement.id);

  // Intentar desbloquear segunda vez
  await expect(
    unlockAchievement(user.id, achievement.id)
  ).rejects.toThrow(ConflictException);

  // Verificar que solo hay 1 registro
  const count = await countUserAchievements(user.id, achievement.id);
  expect(count).toBe(1);
});

Test Case 3: Galería muestra correctamente

test('Gallery shows unlocked and locked achievements', async () => {
  const user = await createUser();

  // Crear 3 achievements
  const ach1 = await createAchievement({ name: 'First' });
  const ach2 = await createAchievement({ name: 'Second' });
  const ach3 = await createAchievement({ name: 'Third' });

  // Desbloquear solo el primero
  await unlockAchievement(user.id, ach1.id);

  // Obtener galería
  const gallery = await getAchievementGallery(user.id);

  expect(gallery.unlocked).toHaveLength(1);
  expect(gallery.unlocked[0].name).toBe('First');
  expect(gallery.locked).toHaveLength(2);
});

📚 Referencias Adicionales

Documentos Relacionados

Teoría de Gamificación


📅 Historial de Cambios

Versión Fecha Autor Cambios
1.0 2025-11-07 Database Team Creación inicial del requerimiento

Documento: docs/01-requerimientos/02-gamificacion/RF-GAM-001-achievements.md Ruta relativa desde docs/: 01-requerimientos/02-gamificacion/RF-GAM-001-achievements.md