- 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>
15 KiB
Implementación de Notificación de RankUp en Ejercicios
Fecha: 2025-11-26 Versión: 1.0 Estado: Implementado Autor: Architecture-Analyst (Orquestación de Agentes)
Resumen Ejecutivo
Se implementó la funcionalidad de notificación de promoción de rango (RankUp) cuando un estudiante completa ejercicios exitosamente. Anteriormente, aunque el backend procesaba correctamente las promociones de rango via triggers de base de datos, el frontend no mostraba ninguna celebración al usuario.
Ejercicios Afectados
- M1-E5: Sopa de Letras
- M2-E1: Detective Textual
- M2-E2: Causa-Efecto
- M2-E3: Predicción Narrativa
- M2-E4: Puzzle Contexto
- M2-E5: Rueda de Inferencias
Problema Original
Síntomas
- Usuario completaba ejercicios correctamente
- XP y ML Coins se acumulaban en backend
- Rango se actualizaba en base de datos (trigger funcionaba)
- Frontend NO mostraba notificación de subida de rango
- Usuario no sabía que había sido promovido
Causa Raíz
ExerciseSubmissionResponseDtono exponía campos de gamificación (xp_earned,ml_coins_earned,rankUp)claimRewards()no detectaba si había ocurrido una promoción de rango- Componentes de ejercicio no integraban
RankUpModal
Arquitectura de la Solución
Diagrama de Flujo
┌─────────────────────────────────────────────────────────────────────────────────┐
│ FLUJO IMPLEMENTADO │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ FRONTEND BACKEND DATABASE │
│ ───────── ─────── ──────── │
│ │
│ submitExercise() ────────► POST /progress/ claimRewards() │
│ │ submissions/submit │ │
│ │ │ │ │
│ │ [1] Guarda previousRank │ │
│ │ │ │ │
│ │ [2] addXp() ──────────────► TRIGGER FIRES │
│ │ │ check_rank_promotion() │
│ │ │ promote_to_next_rank() │
│ │ │ │ │
│ │ [3] Consulta newRank │ │
│ │ │ │ │
│ │ [4] Compara rangos │ │
│ │ previousRank vs newRank │ │
│ │ │ │ │
│ │ [5] Construye rankUp │ │
│ │ si son diferentes │ │
│ │ │ │ │
│ │ DTO Serialization │
│ │ ✅ CAMPOS INCLUIDOS: │
│ │ - xp_earned │
│ │ - ml_coins_earned │
│ │ - rankUp { newRank, previousRank, bonus, multiplier }│
│ │ │ │
│ ◄──────────────────────────┤ │
│ Recibe response │
│ │ │
│ ▼ │
│ [6] FeedbackModal │
│ (muestra score) │
│ │ │
│ ▼ [7] Si rankUp presente │
│ RankUpModal │
│ (celebración 8s) │
│ │ │
│ ▼ │
│ [8] onComplete() │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Objetos Nuevos y Modificados
1. ExerciseSubmissionResponseDto (Backend)
Archivo: apps/backend/src/modules/progress/dto/exercise-submission-response.dto.ts
Campos Agregados:
// =====================================================
// GAMIFICATION REWARDS
// =====================================================
/**
* XP ganada en esta sumisión
*/
@Expose()
xp_earned?: number;
/**
* ML Coins ganadas en esta sumisión
*/
@Expose()
ml_coins_earned?: number;
/**
* Información de ascenso de rango (si aplica)
* Null si no hubo promoción de rango
*/
@Expose()
rankUp?: {
newRank: string;
previousRank?: string;
bonusMLCoins: number;
newMultiplier: number;
} | null;
Uso:
- Estos campos se serializan automáticamente via
class-transformeral enviar respuesta HTTP xp_earnedyml_coins_earnedsiempre presentes (default 0)rankUpesnullcuando no hay promoción, objeto cuando sí hay
2. ExerciseSubmission Entity (Backend)
Archivo: apps/backend/src/modules/progress/entities/exercise-submission.entity.ts
Columnas Agregadas:
// =====================================================
// GAMIFICATION REWARDS
// =====================================================
/**
* XP ganada por completar este ejercicio correctamente
* Se calcula y persiste al momento de claimRewards()
*/
@Column({ type: 'integer', default: 0 })
xp_earned!: number;
/**
* ML Coins ganadas por completar este ejercicio
* Se calcula y persiste al momento de claimRewards()
*/
@Column({ type: 'integer', default: 0 })
ml_coins_earned!: number;
Beneficios:
- Datos persistentes (se pueden recuperar históricamente)
- Entity y DTO alineados
- Permite analytics de rewards por ejercicio
3. DDL exercise_submissions (Database)
Archivo: apps/database/ddl/schemas/progress_tracking/tables/04-exercise_submissions.sql
SQL Agregado:
-- Nuevas columnas
xp_earned integer DEFAULT 0,
ml_coins_earned integer DEFAULT 0,
-- Nuevos comentarios
COMMENT ON COLUMN progress_tracking.exercise_submissions.xp_earned
IS 'XP earned for completing this exercise correctly';
COMMENT ON COLUMN progress_tracking.exercise_submissions.ml_coins_earned
IS 'ML Coins earned for completing this exercise';
Migración Manual (si base de datos ya existe):
ALTER TABLE progress_tracking.exercise_submissions
ADD COLUMN xp_earned INTEGER DEFAULT 0,
ADD COLUMN ml_coins_earned INTEGER DEFAULT 0;
4. Método claimRewards() (Backend Service)
Archivo: apps/backend/src/modules/progress/services/exercise-submission.service.ts
Tipo de Retorno Actualizado:
async claimRewards(id: string): Promise<{
submission: ExerciseSubmission;
xp_earned: number;
ml_coins_earned: number;
rankUp: {
newRank: string;
previousRank: string;
bonusMLCoins: number;
newMultiplier: number;
} | null;
}>
Lógica de Detección de RankUp:
// 1. Guardar rango ANTES de addXp()
const userStatsBefore = await this.userStatsService.findByUserId(submission.user_id);
const previousRank = userStatsBefore.current_rank;
// 2. Ejecutar addXp() (trigger de BD hace promoción si corresponde)
await this.userStatsService.addXp(submission.user_id, xpEarned);
// 3. Consultar rango DESPUÉS
const userStatsAfter = await this.userStatsService.findByUserId(submission.user_id);
const newRank = userStatsAfter.current_rank;
// 4. Comparar y construir objeto si hubo cambio
let rankUpData = null;
if (previousRank !== newRank) {
rankUpData = {
newRank: newRank,
previousRank: previousRank,
bonusMLCoins: rankBonuses[newRank] || 0,
newMultiplier: rankMultipliers[newRank] || 1.0,
};
}
Configuración de Rangos Maya (hardcoded basado en especificación):
| Rango | XP Requerido | Bonus ML Coins | Multiplicador |
|---|---|---|---|
| Ajaw | 0-499 | 0 | 1.00 |
| Nacom | 500-999 | 100 | 1.10 |
| Ah K'in | 1,000-1,499 | 250 | 1.15 |
| Halach Uinic | 1,500-1,899 | 500 | 1.20 |
| K'uk'ulkan | 1,900+ | 1,000 | 1.25 |
5. Hook useRankUpNotification (Frontend)
Archivo: apps/frontend/src/features/gamification/ranks/hooks/useRankUpNotification.ts
Propósito: Manejar la cascada de modales (FeedbackModal → RankUpModal) de forma reutilizable.
Interface:
interface UseRankUpNotificationReturn {
// State
showFeedbackModal: boolean;
showRankUpModal: boolean;
feedback: FeedbackData | null;
rankUpData: SubmitExerciseResponse['rankUp'] | null;
// Actions
processSubmitResponse: (response: SubmitExerciseResponse) => FeedbackData;
handleFeedbackClose: () => void;
handleRankUpClose: () => void;
reset: () => void;
}
export function useRankUpNotification(onComplete?: () => void): UseRankUpNotificationReturn;
Comportamiento:
processSubmitResponse()- Procesa respuesta del ejercicio, extrae feedback y rankUphandleFeedbackClose()- Al cerrar FeedbackModal, muestra RankUpModal si existe (con delay 300ms)handleRankUpClose()- Al cerrar RankUpModal, llama aonComplete()reset()- Limpia todos los estados
6. RankUpModal (Frontend Component)
Archivo: apps/frontend/src/features/gamification/ranks/components/RankUpModal.tsx
Props:
interface RankUpModalProps {
isOpen: boolean;
onClose: () => void;
}
Características:
- Auto-cierre después de 8 segundos
- 50 partículas de confetti
- Muestra progresión visual (rango anterior → nuevo)
- Detalla beneficios desbloqueados
- Usa hooks internos (
useRank,useProgression) para obtener datos
7. Integración en Componentes de Ejercicio
Archivos Modificados:
SopaLetrasExercise.tsxDetectiveTextualExercise.tsxCausaEfectoExercise.tsxPrediccionNarrativaExercise.tsxPuzzleContextoExercise.tsxRuedaInferenciasExercise.tsx
Patrón de Integración (ejemplo SopaLetras):
// 1. Import
import { RankUpModal } from '@/features/gamification/ranks/components/RankUpModal';
// 2. Estados
const [showRankUpModal, setShowRankUpModal] = useState(false);
const [rankUpData, setRankUpData] = useState<{...} | null>(null);
// 3. En handleCheck, guardar rankUp
const response = await submitExercise(...);
if (response.rankUp) {
setRankUpData(response.rankUp);
}
// 4. Modificar onClose de FeedbackModal
onClose={() => {
setShowFeedback(false);
if (rankUpData) {
setTimeout(() => setShowRankUpModal(true), 300);
} else if (feedback?.type === 'success') {
onComplete?.();
}
}}
// 5. Agregar RankUpModal al JSX
{showRankUpModal && rankUpData && (
<RankUpModal
isOpen={showRankUpModal}
onClose={() => {
setShowRankUpModal(false);
setRankUpData(null);
if (feedback?.type === 'success') {
onComplete?.();
}
}}
/>
)}
Testing
Casos de Prueba Recomendados
| Caso | Precondición | Acción | Resultado Esperado |
|---|---|---|---|
| Sin promoción | Usuario con 300 XP (Ajaw) | Completar ejercicio (100 XP) | FeedbackModal → onComplete (sin RankUpModal) |
| Con promoción | Usuario con 450 XP (Ajaw) | Completar ejercicio (100 XP) | FeedbackModal → RankUpModal → onComplete |
| Rango máximo | Usuario K'uk'ulkan | Completar ejercicio | FeedbackModal → onComplete (sin RankUpModal) |
| XP exacto al umbral | Usuario con 400 XP | Completar ejercicio (100 XP = 500) | FeedbackModal → RankUpModal a Nacom |
Verificación Manual
- Crear usuario de prueba con ~450 XP total
- Completar cualquier ejercicio de M1-E5 o M2-E1 a E5
- Verificar que aparece:
- Primero: FeedbackModal con score
- Después (300ms): RankUpModal con celebración
- Cerrar RankUpModal y verificar que se ejecuta
onComplete()
Dependencias
Backend
class-transformer- Para serialización con @Expose()typeorm- Para @Column en entity
Frontend
react- useState, useCallbackRankUpModalcomponent existenteFeedbackModalcomponent existente- Stores:
useRanksStore,useEconomyStore
Consideraciones de Migración
Base de Datos Existente
Si la base de datos ya tiene registros en exercise_submissions:
-- Agregar columnas a tabla existente
ALTER TABLE progress_tracking.exercise_submissions
ADD COLUMN IF NOT EXISTS xp_earned INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS ml_coins_earned INTEGER DEFAULT 0;
-- Agregar comentarios
COMMENT ON COLUMN progress_tracking.exercise_submissions.xp_earned
IS 'XP earned for completing this exercise correctly';
COMMENT ON COLUMN progress_tracking.exercise_submissions.ml_coins_earned
IS 'ML Coins earned for completing this exercise';
Recreación Completa
cd apps/database
./drop-and-recreate-database.sh
Referencias
- Especificación de Rangos Maya:
docs/00-vision-general/ESPECIFICACION-TECNICA-RANGOS-MAYA-v2.1.md - Seed de Rangos:
apps/database/seeds/dev/gamification_system/03-maya_ranks.sql - Trigger de Promoción:
apps/database/ddl/schemas/gamification_system/triggers/trg_check_rank_promotion_on_xp_gain.sql - Función de Promoción:
apps/database/ddl/schemas/gamification_system/functions/promote_to_next_rank.sql
Changelog
| Versión | Fecha | Cambios |
|---|---|---|
| 1.0 | 2025-11-26 | Implementación inicial de notificación de RankUp |