workspace/projects/gamilit/docs/90-transversal/restructuracion-v2/US-AE-005-parametrizacion-gamificacion.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

766 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
```sql
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:**
```typescript
// 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:
1. **XP & Niveles** (xp_per_exercise, multiplicadores)
2. **ML Coins** (recompensas por logros)
3. **Rangos Maya** (thresholds de XP)
4. **Insignias** (criterios de desbloqueo)
5. **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"
- ✅ 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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**
```typescript
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**
```typescript
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**
```typescript
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_admin` puede 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_settings` de EAI-006)
- Requiere seed de valores por defecto
### Seed de Defaults
```sql
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_settings` debe 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