- 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>
493 lines
22 KiB
Markdown
493 lines
22 KiB
Markdown
# 🏗️ ARQUITECTURA DEL SISTEMA DE RECOMPENSAS Y PROGRESO
|
|
|
|
**Versión:** v2.8.0
|
|
**Fecha:** 2025-11-29
|
|
**Estado:** ✅ IMPLEMENTADO Y VERIFICADO
|
|
|
|
---
|
|
|
|
## 📋 Índice
|
|
|
|
1. [Visión General](#visión-general)
|
|
2. [Arquitectura de Componentes](#arquitectura-de-componentes)
|
|
3. [Dual-Table Pattern](#dual-table-pattern)
|
|
4. [Flujo de Datos](#flujo-de-datos)
|
|
5. [Patrones de Diseño](#patrones-de-diseño)
|
|
6. [Seguridad y Rendimiento](#seguridad-y-rendimiento)
|
|
|
|
---
|
|
|
|
## 🎯 1. Visión General
|
|
|
|
### Objetivo del Sistema
|
|
|
|
Implementar un sistema completo de **gamificación educativa** que:
|
|
- ✅ Otorga **XP y ML Coins** por completar ejercicios
|
|
- ✅ Actualiza **estadísticas del usuario** automáticamente
|
|
- ✅ Rastrea **progreso de módulos** en tiempo real
|
|
- ✅ Previene **duplicación de recompensas**
|
|
- ✅ Mantiene **integridad de datos** con triggers
|
|
|
|
---
|
|
|
|
## 🏛️ 2. Arquitectura de Componentes
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ FRONTEND │
|
|
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐│
|
|
│ │ ModuleDetailPage │──│ useModuleDetail │──│ ExerciseCard ││
|
|
│ │ (Component) │ │ (Hook) │ │ (Component) ││
|
|
│ └──────────────────┘ └──────────────────┘ └───────────────┘│
|
|
│ │ │ │ │
|
|
└───────────┼─────────────────────┼──────────────────────┼────────┘
|
|
│ │ │
|
|
│ GET /modules/:id │ GET /exercises │
|
|
│ GET /exercises │ (with completed) │
|
|
▼ ▼ ▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ BACKEND (NestJS) │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ API LAYER (Controllers + Guards) │ │
|
|
│ │ ┌────────────────────┐ ┌──────────────────────────┐ │ │
|
|
│ │ │ ModulesController │ │ ExercisesController │ │ │
|
|
│ │ │ + JwtAuthGuard │ │ + JwtAuthGuard │ │ │
|
|
│ │ └────────────────────┘ └──────────────────────────┘ │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ SERVICE LAYER │ │
|
|
│ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
|
|
│ │ │ ModulesService │ │ ExerciseAttemptService │ │ │
|
|
│ │ │ ExercisesService│ │ ExerciseSubmissionService │ │ │
|
|
│ │ └─────────────────┘ └─────────────────────────────┘ │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
│ │ │ │
|
|
└───────────┼────────────────────────────┼────────────────────────┘
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ DATABASE (PostgreSQL) │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ educational_content (schema) │ │
|
|
│ │ ┌──────────┐ ┌──────────┐ │ │
|
|
│ │ │ modules │ │exercises │ │ │
|
|
│ │ └──────────┘ └──────────┘ │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ progress_tracking (schema) │ │
|
|
│ │ ┌──────────────────┐ ┌──────────────────────┐ │ │
|
|
│ │ │ exercise_ │ │ exercise_attempts │ │ │
|
|
│ │ │ submissions │ │ (rewards table) │ │ │
|
|
│ │ │ (workflow) │ │ ▲ INSERT │ │ │
|
|
│ │ └──────────────────┘ └───┼──────────────────┘ │ │
|
|
│ └──────────────────────────────┼──────────────────────────┘ │
|
|
│ │ TRIGGER │
|
|
│ │ │
|
|
│ ┌──────────────────────────────┼──────────────────────────┐ │
|
|
│ │ gamilit (schema) │ │ │
|
|
│ │ ┌───────────────────────────▼────────────────────────┐ │ │
|
|
│ │ │ update_user_stats_on_exercise_complete() │ │ │
|
|
│ │ │ (TRIGGER FUNCTION) │ │ │
|
|
│ │ └───────────────────────────┬────────────────────────┘ │ │
|
|
│ └──────────────────────────────┼──────────────────────────┘ │
|
|
│ │ UPDATE/INSERT │
|
|
│ ▼ │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ gamification_system (schema) │ │
|
|
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
|
│ │ │ user_stats (table) │ │ │
|
|
│ │ │ - total_xp │ │ │
|
|
│ │ │ - ml_coins │ │ │
|
|
│ │ │ - ml_coins_earned_total │ │ │
|
|
│ │ │ - exercises_completed │ │ │
|
|
│ │ └──────────────────────────────────────────────────┘ │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🔄 3. Dual-Table Pattern
|
|
|
|
### Problema Resuelto
|
|
|
|
**Separación de Responsabilidades**: Necesitamos distinguir entre:
|
|
1. **Workflow de evaluación** (draft → submitted → graded)
|
|
2. **Sistema de recompensas** (XP, ML Coins, achievements)
|
|
|
|
### Solución: Dos Tablas Complementarias
|
|
|
|
#### 📝 `exercise_submissions` - Tabla de Workflow y Revisión Manual
|
|
|
|
**Propósito:** Gestión del ciclo de vida de submissions que requieren calificación manual
|
|
|
|
```sql
|
|
CREATE TABLE progress_tracking.exercise_submissions (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID NOT NULL,
|
|
exercise_id UUID NOT NULL,
|
|
status VARCHAR(20), -- 'draft', 'submitted', 'graded', 'reviewed'
|
|
answers JSONB,
|
|
answer_data JSONB,
|
|
is_correct BOOLEAN DEFAULT false,
|
|
xp_earned INTEGER DEFAULT 0,
|
|
ml_coins_earned INTEGER DEFAULT 0,
|
|
graded_at TIMESTAMP,
|
|
feedback TEXT,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
```
|
|
|
|
**Flujo:**
|
|
```
|
|
DRAFT → SUBMITTED → GRADED → REVIEWED
|
|
↓ ↓ ↓ ↓
|
|
Save Submit Teacher Final
|
|
```
|
|
|
|
**Trigger Asociado (v2.8.0):**
|
|
```sql
|
|
CREATE TRIGGER trg_update_user_stats_on_submission
|
|
AFTER UPDATE ON exercise_submissions
|
|
FOR EACH ROW
|
|
WHEN (status IN ('graded','reviewed') AND is_correct = true)
|
|
EXECUTE FUNCTION gamilit.update_user_stats_on_submission_graded();
|
|
```
|
|
|
|
---
|
|
|
|
#### 🏆 `exercise_attempts` - Tabla de Recompensas
|
|
|
|
**Propósito:** Tracking de intentos y cálculo de recompensas
|
|
|
|
```sql
|
|
CREATE TABLE progress_tracking.exercise_attempts (
|
|
id UUID PRIMARY KEY,
|
|
user_id UUID NOT NULL,
|
|
exercise_id UUID NOT NULL,
|
|
submission_id UUID REFERENCES exercise_submissions(id),
|
|
|
|
-- SCORING
|
|
score INTEGER NOT NULL,
|
|
is_correct BOOLEAN,
|
|
attempt_number INTEGER,
|
|
|
|
-- REWARDS (calculados por ExerciseAttemptService)
|
|
xp_earned INTEGER NOT NULL,
|
|
ml_coins_earned INTEGER NOT NULL,
|
|
|
|
-- GAMEPLAY
|
|
hints_used INTEGER DEFAULT 0,
|
|
powerups_used JSONB DEFAULT '[]'::JSONB,
|
|
time_spent INTEGER,
|
|
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
**Trigger Asociado:**
|
|
```sql
|
|
CREATE TRIGGER trg_update_user_stats_on_exercise
|
|
AFTER INSERT ON exercise_attempts
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION gamilit.update_user_stats_on_exercise_complete();
|
|
```
|
|
|
|
---
|
|
|
|
### Relación Entre Tablas (v2.8.0 - Dual Trigger)
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ ARQUITECTURA BD-FIRST (Triggers = Verdad) │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
|
|
┌──────────────────────┐
|
|
│ exercise_submissions │
|
|
│ (Revisión manual) │
|
|
└──────────┬───────────┘
|
|
│ UPDATE (status='graded', is_correct=true)
|
|
│
|
|
▼
|
|
TRIGGER 31 ─────────────────────────────────────────┐
|
|
trg_update_user_stats_on_submission │
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ update_user_stats_on_ │
|
|
│ submission_graded() │
|
|
└─────────────────────┬───────────────────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────┐ ┌──────────────────────────────────┐
|
|
│ exercise_attempts │ │ user_stats │
|
|
│ (Autocorregibles) │ │ total_xp, ml_coins, ... │
|
|
└──────────┬───────────┘ └──────────────┬───────────────────┘
|
|
│ INSERT │
|
|
│ │ UPDATE total_xp
|
|
▼ ▼
|
|
TRIGGER 21 ────────────┐ TRIGGER 27
|
|
trg_update_user_stats_ │ trg_update_missions_on_earn_xp
|
|
on_exercise │ │
|
|
│ │ ▼
|
|
▼ │ ┌────────────────────────┐
|
|
┌─────────────────┐ │ │ update_missions_on_ │
|
|
│ update_user_ │────┘ │ earn_xp() │
|
|
│ stats_on_ │ │ → Actualiza misiones │
|
|
│ exercise_ │ │ de tipo earn_xp │
|
|
│ complete() │ └────────────────────────┘
|
|
└─────────────────┘
|
|
```
|
|
|
|
**Ventajas:**
|
|
- ✅ **BD-first Architecture**: Triggers como fuente de verdad
|
|
- ✅ **Dual Path**: Ambos flujos actualizan user_stats y misiones
|
|
- ✅ **Separation of Concerns**: Cada tabla tiene una responsabilidad clara
|
|
- ✅ **Atomic Rewards**: Inserción/Actualización = recompensas otorgadas automáticamente
|
|
- ✅ **History Tracking**: Se mantiene historial completo de attempts/submissions
|
|
- ✅ **No Duplicate Rewards**: Anti-refire en triggers previene duplicados
|
|
- ✅ **Cascade to Missions**: Actualización de XP dispara automáticamente misiones earn_xp
|
|
|
|
---
|
|
|
|
## 🌊 4. Flujo de Datos
|
|
|
|
### 4.1 Submit de Ejercicio (Escritura)
|
|
|
|
```
|
|
1. Frontend
|
|
└─▶ POST /api/educational/exercises/:id/submit
|
|
Body: { answers, startedAt, hintsUsed, powerupsUsed }
|
|
|
|
2. ExercisesController.submit()
|
|
└─▶ ExerciseAttemptService.createAttempt()
|
|
│
|
|
├─▶ Calcular score (0-100)
|
|
├─▶ Calcular XP earned (con penalties)
|
|
├─▶ Calcular ML Coins earned (con penalties)
|
|
│
|
|
└─▶ INSERT INTO exercise_attempts
|
|
{ xp_earned, ml_coins_earned, is_correct, score }
|
|
|
|
3. Database Trigger (AUTOMÁTICO)
|
|
└─▶ gamilit.update_user_stats_on_exercise_complete()
|
|
│
|
|
├─▶ READ NEW.xp_earned, NEW.ml_coins_earned
|
|
│
|
|
└─▶ UPDATE gamification_system.user_stats
|
|
SET total_xp = total_xp + NEW.xp_earned
|
|
SET ml_coins = ml_coins + NEW.ml_coins_earned
|
|
SET exercises_completed = exercises_completed + 1
|
|
|
|
4. Respuesta al Frontend
|
|
└─▶ { score, isPerfect, rewards: { xp, mlCoins }, rankUp }
|
|
```
|
|
|
|
---
|
|
|
|
### 4.2 Consulta de Progreso (Lectura)
|
|
|
|
```
|
|
1. Frontend
|
|
└─▶ GET /api/educational/modules
|
|
Header: Authorization: Bearer {JWT}
|
|
|
|
2. ModulesController.findAll()
|
|
├─▶ Obtener todos los módulos
|
|
├─▶ Obtener submissions del usuario (status='graded')
|
|
├─▶ Obtener todos los ejercicios
|
|
│
|
|
└─▶ Para cada módulo:
|
|
├─▶ Filtrar ejercicios del módulo
|
|
├─▶ Contar completed (en completedExercisesMap)
|
|
├─▶ Calcular progress = (completed / total) * 100
|
|
└─▶ Agregar campos: { total_exercises, completed_exercises, progress, completed }
|
|
|
|
3. Respuesta al Frontend
|
|
└─▶ [
|
|
{ id, title, total_exercises: 5, completed_exercises: 2, progress: 40, completed: false },
|
|
...
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 5. Patrones de Diseño Implementados
|
|
|
|
### 5.1 UPSERT Pattern (Base de Datos)
|
|
|
|
**Ubicación:** `update_user_stats_on_exercise_complete()`
|
|
|
|
```sql
|
|
-- Intenta UPDATE
|
|
UPDATE user_stats SET ... WHERE user_id = NEW.user_id;
|
|
|
|
-- Si no existe (NOT FOUND), hace INSERT
|
|
IF NOT FOUND THEN
|
|
INSERT INTO user_stats (user_id, ...) VALUES (...);
|
|
END IF;
|
|
```
|
|
|
|
**Ventaja:** Evita errores por falta de registro inicial de user_stats
|
|
|
|
---
|
|
|
|
### 5.2 Map-based Lookup (Backend)
|
|
|
|
**Ubicación:** `ModulesController.findAll()`, `ExercisesController.findAll()`
|
|
|
|
```typescript
|
|
// Crear Map para O(1) lookup
|
|
const completedExercisesMap = new Map<string, boolean>();
|
|
allSubmissions.forEach((submission) => {
|
|
if (submission.status === 'graded') {
|
|
completedExercisesMap.set(submission.exercise_id, true);
|
|
}
|
|
});
|
|
|
|
// Usar Map para agregar campo 'completed'
|
|
exercises.map((exercise) => ({
|
|
...exercise,
|
|
completed: completedExercisesMap.get(exercise.id) || false
|
|
}));
|
|
```
|
|
|
|
**Ventaja:** Evita N+1 queries, eficiencia O(n) en lugar de O(n²)
|
|
|
|
---
|
|
|
|
### 5.3 Trigger-based Automation (Base de Datos)
|
|
|
|
**Patrón:** Event-driven updates
|
|
|
|
```sql
|
|
CREATE TRIGGER trg_update_user_stats_on_exercise
|
|
AFTER INSERT ON exercise_attempts
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_user_stats_on_exercise_complete();
|
|
```
|
|
|
|
**Ventajas:**
|
|
- ✅ Actualización automática (no requiere código de aplicación)
|
|
- ✅ Atomic (dentro de la misma transacción)
|
|
- ✅ No se puede olvidar actualizar stats
|
|
|
|
---
|
|
|
|
### 5.4 Dependency Injection (Backend)
|
|
|
|
**Ubicación:** `ModulesController`
|
|
|
|
```typescript
|
|
constructor(
|
|
private readonly modulesService: ModulesService,
|
|
private readonly exercisesService: ExercisesService,
|
|
private readonly exerciseSubmissionService: ExerciseSubmissionService,
|
|
) {}
|
|
```
|
|
|
|
**Ventaja:** Testeable, desacoplado, siguiendo principios SOLID
|
|
|
|
---
|
|
|
|
### 5.5 Custom Hooks (Frontend)
|
|
|
|
**Ubicación:** `useModuleDetail`
|
|
|
|
```typescript
|
|
export function useModuleDetail(moduleId: string) {
|
|
const [module, setModule] = useState<Module | null>(null);
|
|
const [exercises, setExercises] = useState<Exercise[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Fetch logic
|
|
}, [moduleId]);
|
|
|
|
return { module, exercises, loading, error };
|
|
}
|
|
```
|
|
|
|
**Ventaja:** Reusable, encapsula lógica de fetch, separation of concerns
|
|
|
|
---
|
|
|
|
## 🔒 6. Seguridad y Rendimiento
|
|
|
|
### 6.1 Seguridad
|
|
|
|
#### Autenticación JWT
|
|
```typescript
|
|
@UseGuards(JwtAuthGuard)
|
|
@Get('modules')
|
|
async findAll(@Request() req: any) {
|
|
const userId = req.user.id; // Extraído del JWT
|
|
// ...
|
|
}
|
|
```
|
|
|
|
#### RLS (Row Level Security)
|
|
```sql
|
|
-- Políticas en BD aseguran que users solo vean sus datos
|
|
ALTER TABLE progress_tracking.exercise_submissions ENABLE ROW LEVEL SECURITY;
|
|
```
|
|
|
|
#### SECURITY DEFINER
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION update_user_stats_on_exercise_complete()
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER -- Ejecuta con permisos del owner de la función
|
|
```
|
|
|
|
---
|
|
|
|
### 6.2 Rendimiento
|
|
|
|
#### Índices en BD
|
|
```sql
|
|
-- Índices para queries frecuentes
|
|
CREATE INDEX idx_exercise_submissions_user ON exercise_submissions(user_id);
|
|
CREATE INDEX idx_exercise_attempts_user ON exercise_attempts(user_id);
|
|
CREATE INDEX idx_user_stats_user ON user_stats(user_id);
|
|
```
|
|
|
|
#### Batch Fetch
|
|
```typescript
|
|
// En lugar de N queries, una sola
|
|
const allSubmissions = await this.exerciseSubmissionService.findByUserId(userId);
|
|
```
|
|
|
|
#### Caching Potencial
|
|
```typescript
|
|
// TODO: Implementar cache en Redis para submissions frecuentes
|
|
@Cacheable('user-submissions', ttl: 300)
|
|
async findByUserId(userId: string) { ... }
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 7. Métricas y Monitoreo
|
|
|
|
### Puntos de Observabilidad
|
|
|
|
1. **Trigger Execution Time**
|
|
- Medir tiempo de `update_user_stats_on_exercise_complete()`
|
|
- Alert si > 100ms
|
|
|
|
2. **API Response Time**
|
|
- GET /modules: objetivo < 200ms
|
|
- GET /exercises: objetivo < 150ms
|
|
|
|
3. **Error Rates**
|
|
- Trigger warnings en logs
|
|
- Failed submissions
|
|
- Token expiration
|
|
|
|
---
|
|
|
|
**Última actualización:** 2025-11-29
|
|
**Autor:** Sistema Gamilit
|
|
**Versión:** 2.8.0
|