Structure: - control-plane/: Registries, SIMCO directives, CI/CD templates - projects/: Gamilit, ERP-Suite, Trading-Platform, Betting-Analytics - shared/: Libs catalog, knowledge-base Key features: - Centralized port, domain, database, and service registries - 23 SIMCO directives + 6 fundamental principles - NEXUS agent profiles with delegation rules - Validation scripts for workspace integrity - Dockerfiles for all services - Path aliases for quick reference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
24 KiB
US-AE-005: Parametrización Dinámica de Gamificación
📋 Metadata
| Campo | Valor |
|---|---|
| ID | US-AE-005 |
| Épica | EXT-002 (Admin Extendido CORE) |
| Módulo | Admin - Configuración del Sistema |
| Prioridad | Alta (v2 CORE) |
| Story Points | 12 |
| Presupuesto | $4,800 MXN |
| Sprint | TBD |
| Estado | 🆕 Nueva |
| Versión | 1.0 |
| Fecha creación | 2025-11-08 |
🎯 Historia de Usuario
Como super administrador de la plataforma GAMILIT Quiero configurar dinámicamente los parámetros del sistema de gamificación Para ajustar la dificultad, recompensas y progresión sin necesidad de modificar el código
📝 Descripción
Actualmente (v1), todos los parámetros de gamificación están hardcoded en el código:
- XP otorgado por cada ejercicio
- Multiplicadores de XP por nivel/rango
- Thresholds de XP para subir de rango
- Criterios de desbloqueo de insignias
- Cantidad de ML Coins otorgadas por logros
Esta US permite al super admin configurar estos valores dinámicamente a través de una interfaz web, permitiendo:
- Ajustar la dificultad de progresión en tiempo real
- Experimentar con diferentes economías de gamificación (A/B testing)
- Personalizar la experiencia por institución educativa (multi-tenancy)
Contexto de Alcance v2
Esta funcionalidad es parte del Alcance v2 (Ampliación) - Portal de Admin CORE:
- En v1: Valores hardcoded en código
- En v2: Valores configurables dinámicamente por super admin
🔗 Referencias
Épica y Documentación
- Épica: EXT-002 (Admin Extendido)
- Alcance: 2.2.1.5 Administración y Escalabilidad (v2 - Parametrización)
- RF relacionado: RF-SYS-001 (Sistema de configuración global)
- ET relacionado: ET-SYS-001 (Implementación de system_settings)
- Infraestructura base: EAI-006 (Configuración del Sistema)
Objetos de BD
Tabla principal: system_configuration.system_settings
CREATE TABLE system_configuration.system_settings (
setting_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
category VARCHAR(50) NOT NULL, -- 'gamification', 'system', 'notifications'
key VARCHAR(100) NOT NULL,
value JSONB NOT NULL,
value_type VARCHAR(20) NOT NULL, -- 'number', 'string', 'boolean', 'json'
description TEXT,
is_public BOOLEAN DEFAULT FALSE,
min_value NUMERIC,
max_value NUMERIC,
default_value JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by UUID REFERENCES auth.users(id),
UNIQUE(category, key)
);
Settings de gamificación:
// Categoría: gamification
{
"xp_per_exercise_correct": 10, // XP base por ejercicio correcto
"xp_per_exercise_excellent": 20, // XP por ejercicio con >90% accuracy
"xp_multiplier_streak_3": 1.2, // Multiplicador por racha de 3 días
"xp_multiplier_streak_7": 1.5, // Multiplicador por racha de 7 días
"xp_multiplier_level_2": 1.1, // Multiplicador por estar en nivel 2+
"xp_multiplier_level_5": 1.3, // Multiplicador por estar en nivel 5+
"coins_per_module_complete": 50, // ML Coins por completar módulo
"coins_per_rank_promotion": 100, // ML Coins por subir de rango
"coins_per_achievement": 25, // ML Coins por desbloquear insignia
"rank_thresholds": { // XP requerido para cada rango
"ix_chel_aprendiz": 0,
"ah_puch_explorador": 500,
"kukulkan_sabio": 1500,
"itzamna_maestro": 3500
},
"achievement_criteria_first_module": 1, // Completar 1 módulo
"achievement_criteria_streak_master": 7, // 7 días consecutivos
"achievement_criteria_xp_milestone": 1000, // Alcanzar 1000 XP
"help_cost_hint": 10, // Costo en ML Coins de pista
"help_cost_skip": 25, // Costo de saltar pregunta
"help_cost_extra_time": 15 // Costo de tiempo extra
}
Backend
- Endpoint GET:
GET /api/v1/admin/gamification/settings - Endpoint UPDATE:
PUT /api/v1/admin/gamification/settings - Service:
GamificationConfigService - Cache: Redis con TTL de 5 minutos
Frontend
- Ruta:
/admin/settings/gamification - Componente:
GamificationSettingsPanel.tsx - Tabs: XP & Niveles | ML Coins | Rangos | Insignias | Ayudas
✅ Criterios de Aceptación
CA-1: Ver Configuración Actual
Dado que soy un super admin autenticado Cuando accedo a "Configuración" → "Gamificación" Entonces:
- ✅ Veo una interfaz organizada en 5 tabs:
- XP & Niveles (xp_per_exercise, multiplicadores)
- ML Coins (recompensas por logros)
- Rangos Maya (thresholds de XP)
- Insignias (criterios de desbloqueo)
- Ayudas (costos en ML Coins)
- ✅ Cada setting muestra:
- Nombre descriptivo
- Valor actual
- Valor por defecto (badge "Default")
- Descripción/tooltip explicativo
- Input apropiado (number, slider, JSON editor)
- ✅ Los cambios NO se aplican inmediatamente (modo borrador)
- ✅ Hay un botón "Guardar cambios" (disabled si no hay cambios)
- ✅ Hay un botón "Restaurar defaults"
CA-2: Modificar XP y Niveles
Dado que estoy en el tab "XP & Niveles" Cuando modifico los valores de XP Entonces:
- ✅ Puedo cambiar
xp_per_exercise_correct(min: 1, max: 100) - ✅ Puedo cambiar
xp_per_exercise_excellent(min: xp_correct, max: 200) - ✅ Puedo cambiar multiplicadores de racha (min: 1.0, max: 3.0)
- ✅ Puedo cambiar multiplicadores de nivel (min: 1.0, max: 5.0)
- ✅ Veo validaciones en tiempo real:
xp_excellent>=xp_correct- Multiplicadores entre 1.0 y 3.0
- No valores negativos
- ✅ Veo un preview del impacto:
- "Un alumno nivel 2 con racha de 3 días recibirá: X XP"
- ✅ Los cambios se marcan en amarillo (pending)
- ✅ El botón "Guardar cambios" se habilita
CA-3: Modificar Thresholds de Rangos
Dado que estoy en el tab "Rangos Maya" Cuando modifico los thresholds de XP Entonces:
- ✅ Veo una tabla con los 4 rangos:
Rango Nombre XP Requerido 1 Ix Chel (Aprendiz) 0 (fijo) 2 Ah Puch (Explorador) 500 ✏️ 3 Kukulkan (Sabio) 1500 ✏️ 4 Itzamna (Maestro) 3500 ✏️ - ✅ Puedo editar los valores inline (double-click)
- ✅ Validaciones:
- Rango 2 XP > 0
- Rango 3 XP > Rango 2 XP
- Rango 4 XP > Rango 3 XP
- Máximo 10,000 XP por rango
- ✅ Veo un preview:
- "Con estos valores, un alumno promedio alcanzará Maestro en ~X semanas"
- ✅ Hay un botón "Previsualizar impacto" que muestra:
- Distribución actual de alumnos por rango
- Distribución proyectada con nuevos valores
- Alumnos que subirían/bajarían de rango
CA-4: Modificar Criterios de Insignias
Dado que estoy en el tab "Insignias" Cuando modifico los criterios de desbloqueo Entonces:
- ✅ Veo una lista de todas las insignias configurables:
- "Primer Módulo" → Completar X módulos (default: 1)
- "Maestro de Racha" → X días consecutivos (default: 7)
- "Coleccionista de XP" → Alcanzar X XP (default: 1000)
- (más insignias...)
- ✅ Puedo editar cada criterio numérico
- ✅ Validaciones:
- Valores >= 1
- No exceder límites razonables (ej: 365 días racha)
- ✅ Veo estadísticas:
- "X% de alumnos actuales cumplen este criterio"
- "Si lo subes a Y, solo Z% lo cumplirán"
CA-5: Guardar Cambios
Dado que he modificado uno o más settings Cuando hago clic en "Guardar cambios" Y confirmo en el modal de confirmación Entonces:
- ✅ Veo un modal de confirmación:
- "¿Estás seguro de aplicar estos cambios?"
- Lista de cambios a aplicar (antes → después)
- "Estos cambios afectarán a todos los usuarios inmediatamente"
- Checkboxes de confirmación
- ✅ Al confirmar:
- Se guardan en
system_settings - Se invalida cache de Redis
- Se crea evento en auditoría
- Veo toast "Configuración actualizada"
- Los settings cambian de "pending" a "saved"
- Se guardan en
- ✅ Los cambios se aplican inmediatamente en la plataforma
- ✅ El backend recarga settings desde DB (cache invalidado)
CA-6: Restaurar Defaults
Dado que estoy en cualquier tab Cuando hago clic en "Restaurar defaults" Y confirmo la acción Entonces:
- ✅ Todos los settings del tab actual vuelven a valores por defecto
- ✅ Se marca como pending (no se guarda automáticamente)
- ✅ Debo hacer clic en "Guardar cambios" para aplicar
CA-7: Validaciones y Restricciones
Dado que soy super admin Entonces:
- ✅ Solo yo puedo acceder a esta configuración (role check)
- ✅ No puedo establecer valores que rompan el sistema:
- XP negativo
- Thresholds decrecientes
- Costos de ayudas > balance máximo
- ✅ Veo warnings si:
- Los cambios son muy drásticos (>50% diferencia)
- Pueden afectar negativamente la experiencia
- ✅ Hay un changelog de cambios (auditoría)
CA-8: Preview e Impacto
Dado que he modificado settings Cuando hago clic en "Previsualizar impacto" Entonces:
- ✅ Veo un modal con simulaciones:
- Distribución de alumnos por rango (antes vs después)
- XP promedio necesario para completar un módulo
- Tiempo estimado para alcanzar cada rango
- Economía de ML Coins (ganancia vs gasto promedio)
- ✅ Puedo simular diferentes escenarios
- ✅ Puedo exportar el análisis a PDF/CSV
🏗️ Diseño Técnico
Backend (NestJS)
Service: GamificationConfigService
// apps/backend/src/modules/admin/services/gamification-config.service.ts
import { Injectable } from '@nestjs/common';
import { SupabaseService } from '@/shared/services/supabase.service';
import { CacheService } from '@/shared/services/cache.service';
import { AuditService } from '@/modules/audit/audit.service';
const CACHE_KEY = 'gamification:config';
const CACHE_TTL = 300; // 5 minutos
@Injectable()
export class GamificationConfigService {
constructor(
private supabase: SupabaseService,
private cache: CacheService,
private audit: AuditService,
) {}
async getSettings(): Promise<GamificationSettings> {
// 1. Intentar obtener de caché
const cached = await this.cache.get<GamificationSettings>(CACHE_KEY);
if (cached) return cached;
// 2. Cargar desde DB
const { data, error } = await this.supabase
.from('system_settings')
.select('*')
.eq('category', 'gamification');
if (error) throw new InternalServerErrorException(error.message);
// 3. Convertir a objeto estructurado
const settings = this.transformToSettings(data);
// 4. Guardar en caché
await this.cache.set(CACHE_KEY, settings, CACHE_TTL);
return settings;
}
async updateSettings(
adminId: string,
updates: Partial<GamificationSettings>,
): Promise<{ success: boolean }> {
// 1. Validar cambios
this.validateSettings(updates);
// 2. Calcular preview de impacto
const impact = await this.calculateImpact(updates);
// 3. Guardar en DB
const settingsArray = this.transformToArray(updates);
for (const setting of settingsArray) {
await this.supabase
.from('system_settings')
.upsert({
category: 'gamification',
key: setting.key,
value: setting.value,
value_type: setting.type,
updated_by: adminId,
updated_at: new Date().toISOString(),
});
}
// 4. Invalidar caché
await this.cache.del(CACHE_KEY);
// 5. Auditoría
await this.audit.log({
event_type: 'gamification_settings_updated',
user_id: adminId,
metadata: { changes: updates, impact },
});
// 6. Notificar a otros servidores (si hay múltiples instancias)
await this.pubsub.publish('config:invalidate', { key: CACHE_KEY });
return { success: true };
}
private validateSettings(settings: Partial<GamificationSettings>): void {
const errors: string[] = [];
// Validar XP
if (settings.xp_per_exercise_correct !== undefined) {
if (settings.xp_per_exercise_correct < 1 || settings.xp_per_exercise_correct > 100) {
errors.push('xp_per_exercise_correct debe estar entre 1 y 100');
}
}
if (settings.xp_per_exercise_excellent !== undefined && settings.xp_per_exercise_correct !== undefined) {
if (settings.xp_per_exercise_excellent < settings.xp_per_exercise_correct) {
errors.push('xp_excellent debe ser mayor o igual a xp_correct');
}
}
// Validar thresholds de rangos
if (settings.rank_thresholds) {
const thresholds = Object.values(settings.rank_thresholds);
for (let i = 1; i < thresholds.length; i++) {
if (thresholds[i] <= thresholds[i - 1]) {
errors.push('Los thresholds de rangos deben ser crecientes');
}
}
}
// Validar multiplicadores
if (settings.xp_multiplier_streak_3 !== undefined) {
if (settings.xp_multiplier_streak_3 < 1.0 || settings.xp_multiplier_streak_3 > 3.0) {
errors.push('Multiplicadores deben estar entre 1.0 y 3.0');
}
}
if (errors.length > 0) {
throw new BadRequestException(errors.join(', '));
}
}
async calculateImpact(updates: Partial<GamificationSettings>) {
// Consultar datos actuales de alumnos
const { data: students } = await this.supabase
.from('profiles')
.select('user_id, current_xp, current_rank')
.eq('role', 'student');
const currentSettings = await this.getSettings();
const newSettings = { ...currentSettings, ...updates };
// Simular redistribución de rangos
const currentDistribution = this.calculateRankDistribution(students, currentSettings);
const newDistribution = this.calculateRankDistribution(students, newSettings);
return {
current: currentDistribution,
projected: newDistribution,
changes: {
promotions: newDistribution.promotions,
demotions: newDistribution.demotions,
},
};
}
}
Controller: AdminConfigController
// apps/backend/src/modules/admin/controllers/admin-config.controller.ts
@Controller('admin/gamification')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('super_admin')
export class AdminConfigController {
constructor(private configService: GamificationConfigService) {}
@Get('settings')
async getSettings() {
return this.configService.getSettings();
}
@Put('settings')
async updateSettings(@Req() req, @Body() dto: UpdateGamificationSettingsDto) {
return this.configService.updateSettings(req.user.id, dto);
}
@Post('settings/preview')
async previewImpact(@Body() dto: UpdateGamificationSettingsDto) {
return this.configService.calculateImpact(dto);
}
@Post('settings/restore-defaults')
async restoreDefaults(@Req() req) {
const defaults = this.configService.getDefaultSettings();
return this.configService.updateSettings(req.user.id, defaults);
}
}
Frontend (React + TypeScript)
Componente: GamificationSettingsPanel
// apps/frontend/src/features/admin/components/GamificationSettingsPanel.tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { useGamificationSettings } from '../hooks/useGamificationSettings';
import { XpSettings } from './XpSettings';
import { CoinsSettings } from './CoinsSettings';
import { RankSettings } from './RankSettings';
import { AchievementSettings } from './AchievementSettings';
import { HelpSettings } from './HelpSettings';
import { PreviewModal } from './PreviewModal';
import { useState } from 'react';
export function GamificationSettingsPanel() {
const {
settings,
updateSettings,
restoreDefaults,
previewImpact,
hasChanges,
pendingChanges,
isLoading,
} = useGamificationSettings();
const [showPreview, setShowPreview] = useState(false);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Configuración de Gamificación</h1>
<p className="text-gray-600">
Ajusta los parámetros del sistema de puntos, niveles y recompensas
</p>
</div>
<div className="flex gap-3">
{hasChanges && (
<Button
variant="outline"
onClick={() => setShowPreview(true)}
>
Previsualizar impacto
</Button>
)}
<Button
variant="outline"
onClick={() => restoreDefaults()}
disabled={!hasChanges}
>
Restaurar defaults
</Button>
<Button
onClick={() => updateSettings(pendingChanges)}
disabled={!hasChanges}
isLoading={isLoading}
>
Guardar cambios
</Button>
</div>
</div>
<Tabs defaultValue="xp">
<TabsList>
<TabsTrigger value="xp">XP & Niveles</TabsTrigger>
<TabsTrigger value="coins">ML Coins</TabsTrigger>
<TabsTrigger value="ranks">Rangos Maya</TabsTrigger>
<TabsTrigger value="achievements">Insignias</TabsTrigger>
<TabsTrigger value="help">Ayudas</TabsTrigger>
</TabsList>
<TabsContent value="xp">
<XpSettings settings={settings} />
</TabsContent>
<TabsContent value="coins">
<CoinsSettings settings={settings} />
</TabsContent>
<TabsContent value="ranks">
<RankSettings settings={settings} />
</TabsContent>
<TabsContent value="achievements">
<AchievementSettings settings={settings} />
</TabsContent>
<TabsContent value="help">
<HelpSettings settings={settings} />
</TabsContent>
</Tabs>
<PreviewModal
open={showPreview}
onClose={() => setShowPreview(false)}
changes={pendingChanges}
/>
</div>
);
}
Hook: useGamificationSettings
// apps/frontend/src/features/admin/hooks/useGamificationSettings.ts
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { adminApi } from '@/api/admin';
import { toast } from 'sonner';
export function useGamificationSettings() {
const queryClient = useQueryClient();
const [pendingChanges, setPendingChanges] = useState({});
const { data: settings, isLoading } = useQuery({
queryKey: ['gamification-settings'],
queryFn: () => adminApi.getGamificationSettings(),
});
const updateMutation = useMutation({
mutationFn: (changes) => adminApi.updateGamificationSettings(changes),
onSuccess: () => {
queryClient.invalidateQueries(['gamification-settings']);
setPendingChanges({});
toast.success('Configuración actualizada exitosamente');
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Error al guardar configuración');
},
});
const previewMutation = useMutation({
mutationFn: (changes) => adminApi.previewGamificationImpact(changes),
});
const restoreDefaultsMutation = useMutation({
mutationFn: () => adminApi.restoreGamificationDefaults(),
onSuccess: () => {
queryClient.invalidateQueries(['gamification-settings']);
setPendingChanges({});
toast.success('Valores por defecto restaurados');
},
});
const updateSettings = (changes) => {
updateMutation.mutate(changes);
};
const restoreDefaults = () => {
if (confirm('¿Restaurar todos los valores por defecto?')) {
restoreDefaultsMutation.mutate();
}
};
const previewImpact = async (changes) => {
return previewMutation.mutateAsync(changes);
};
const changeSetting = (key, value) => {
setPendingChanges((prev) => ({ ...prev, [key]: value }));
};
const hasChanges = Object.keys(pendingChanges).length > 0;
return {
settings: { ...settings, ...pendingChanges },
updateSettings,
restoreDefaults,
previewImpact,
changeSetting,
hasChanges,
pendingChanges,
isLoading: isLoading || updateMutation.isPending,
};
}
🧪 Testing
Test Cases
TC-1: Validación de thresholds crecientes
it('should reject decreasing rank thresholds', async () => {
await expect(
configService.updateSettings(adminId, {
rank_thresholds: {
ix_chel_aprendiz: 0,
ah_puch_explorador: 500,
kukulkan_sabio: 400, // ERROR: menor que anterior
itzamna_maestro: 3500,
},
})
).rejects.toThrow('Los thresholds de rangos deben ser crecientes');
});
TC-2: Cache invalidation
it('should invalidate cache after update', async () => {
await configService.updateSettings(adminId, { xp_per_exercise_correct: 15 });
const cached = await cache.get(CACHE_KEY);
expect(cached).toBeNull();
const fresh = await configService.getSettings();
expect(fresh.xp_per_exercise_correct).toBe(15);
});
TC-3: Preview accuracy
it('should calculate impact correctly', async () => {
const impact = await configService.calculateImpact({
rank_thresholds: {
ix_chel_aprendiz: 0,
ah_puch_explorador: 1000, // Aumentado de 500
kukulkan_sabio: 1500,
itzamna_maestro: 3500,
},
});
expect(impact.changes.demotions).toBeGreaterThan(0);
});
Coverage Objetivo
- Backend: 95%+ (lógica de validación crítica)
- Frontend: 80%+ (UI components)
📊 Métricas
| Métrica | Valor Objetivo |
|---|---|
| Load time settings | < 300ms |
| Save changes | < 1s |
| Preview calculation | < 3s (con 10k alumnos) |
| Cache hit rate | > 95% |
🔐 Seguridad
Validaciones
- ✅ Solo
super_adminpuede acceder (RolesGuard) - ✅ Validación de rangos (min/max)
- ✅ Validación de coherencia (thresholds crecientes)
- ✅ Audit trail completo
- ✅ Preview antes de aplicar cambios drásticos
Rate Limiting
- Máximo 10 cambios por hora por admin (prevenir experimentos excesivos)
🚀 Deployment
Migraciones
- No requiere nuevas tablas (usa
system_settingsde EAI-006) - Requiere seed de valores por defecto
Seed de Defaults
INSERT INTO system_configuration.system_settings (category, key, value, value_type, description) VALUES
('gamification', 'xp_per_exercise_correct', '10', 'number', 'XP otorgado por ejercicio correcto'),
('gamification', 'xp_per_exercise_excellent', '20', 'number', 'XP otorgado por ejercicio excelente (>90%)'),
('gamification', 'xp_multiplier_streak_3', '1.2', 'number', 'Multiplicador por racha de 3 días'),
-- ... más settings
📚 Documentación para Usuario
Manual de Admin
Título: Configuración del Sistema de Gamificación
¿Qué puedes configurar?
- Puntos de experiencia (XP) por actividad
- Monedas lectoras (ML Coins) otorgadas
- Umbrales de XP para cada rango Maya
- Criterios de desbloqueo de insignias
- Costos de ayudas
Recomendaciones:
- Haz cambios pequeños e iterativos
- Usa "Previsualizar impacto" antes de guardar
- Monitorea métricas de engagement después de cambios
🔗 Dependencias
Épicas/US Relacionadas
- EAI-006: Infraestructura de
system_settings - EAI-003: Sistema de gamificación base
- RF-SYS-001: Sistema de configuración global
Bloqueantes
- ✅ Tabla
system_settingsdebe existir - ✅ Cache service operativo
- ✅ Audit service operativo
📅 Estimación
| Actividad | Esfuerzo |
|---|---|
| Backend (service, validations, cache) | 3.5 SP |
| Backend (preview/impact calculations) | 2 SP |
| Frontend (5 tabs + modales) | 4.5 SP |
| Testing (unit + integration) | 1.5 SP |
| Seed data + documentation | 0.5 SP |
| TOTAL | 12 SP |
Presupuesto: $4,800 MXN (12 SP × $400/SP)
Generado: 2025-11-08 Autor: Product Owner + Tech Lead Revisión: Pendiente Aprobación: Pendiente