- 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>
13 KiB
🌐 API ENDPOINTS - SISTEMA DE RECOMPENSAS
Versión: v2.3.0 Fecha: 2025-11-12
📋 Índice de Endpoints
| Endpoint | Método | Auth | Modificado | Descripción |
|---|---|---|---|---|
/api/educational/exercises |
GET | ✅ | ✅ SÍ | Lista ejercicios con campo completed |
/api/educational/exercises/:id |
GET | ✅ | ✅ SÍ | Detalle de ejercicio con completed |
/api/educational/exercises/:id/submit |
POST | ✅ | ❌ NO | Submit ejercicio (ya existía) |
/api/educational/modules |
GET | ✅ | ✅ SÍ | Lista módulos con progreso |
/api/educational/modules/:id |
GET | ❌ | ❌ NO | Detalle de módulo (sin cambios) |
/api/gamification/users/:id/stats |
GET | ✅ | ❌ NO | Stats del usuario (sin cambios) |
📚 1. GET /api/educational/exercises
Descripción
Obtiene todos los ejercicios con el campo completed para el usuario autenticado.
Request
Endpoint:
GET /api/educational/exercises
Headers:
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
Query Parameters: Ninguno
Response
Status: 200 OK
Body:
[
{
"id": "5d682cbc-5875-4423-96a1-dad1b7dbfc5b",
"module_id": "896899ce-dd1d-4a36-a3ba-ab7f3531b517",
"title": "Mapa Conceptual: Descubrimientos de Marie Curie",
"subtitle": "Organiza los Conceptos Científicos",
"description": "Completa el mapa conceptual...",
"exercise_type": "mapa_conceptual",
"difficulty_level": "beginner",
"max_points": 100,
"xp_reward": 25,
"ml_coins_reward": 12,
"order_index": 4,
"completed": true, // ⬅️ NUEVO CAMPO
"is_active": true,
"created_at": "2025-11-12T02:56:52.392Z",
"updated_at": "2025-11-12T02:56:52.392Z"
},
{
"id": "1b439bd0-5988-4c58-b348-917662f0660a",
"module_id": "896899ce-dd1d-4a36-a3ba-ab7f3531b517",
"title": "Crucigrama: Infancia de Marie",
"completed": false, // ⬅️ NUEVO CAMPO
...
}
]
Lógica Implementada
@UseGuards(JwtAuthGuard)
@Get('exercises')
async findAll(@Request() req: any) {
const userId = req.user.id;
// 1. Obtener todos los ejercicios
const exercises = await this.exercisesService.findAll();
// 2. Obtener submissions del usuario
const allSubmissions = await this.exerciseSubmissionService.findByUserId(userId);
// 3. Crear Map de ejercicios completados (graded)
const completedExercisesMap = new Map<string, boolean>();
allSubmissions.forEach((submission) => {
if (submission.status === 'graded') {
completedExercisesMap.set(submission.exercise_id, true);
}
});
// 4. Agregar campo 'completed' a cada ejercicio
return exercises.map((exercise) => ({
...exercise,
completed: completedExercisesMap.get(exercise.id) || false,
}));
}
Casos de Uso
-
Listar ejercicios en página de módulo
- Frontend muestra badge "Completado" en ejercicios con
completed: true
- Frontend muestra badge "Completado" en ejercicios con
-
Verificar progreso general
- Admin dashboard puede ver todos los ejercicios disponibles
📖 2. GET /api/educational/exercises/:id
Descripción
Obtiene el detalle de un ejercicio específico con el campo completed para el usuario autenticado.
Request
Endpoint:
GET /api/educational/exercises/5d682cbc-5875-4423-96a1-dad1b7dbfc5b
Headers:
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
URL Parameters:
id(UUID, required): ID del ejercicio
Response
Status: 200 OK
Body:
{
"id": "5d682cbc-5875-4423-96a1-dad1b7dbfc5b",
"module_id": "896899ce-dd1d-4a36-a3ba-ab7f3531b517",
"title": "Mapa Conceptual: Descubrimientos de Marie Curie",
"subtitle": "Organiza los Conceptos Científicos",
"description": "Completa el mapa conceptual arrastrando los conceptos correctos a sus posiciones correspondientes.",
"instructions": "Arrastra cada concepto a su lugar correcto en el mapa.",
"order_index": 4,
"exercise_type": "mapa_conceptual",
"config": {
"layout": "hierarchical",
"autoConnect": true,
"dragAndDrop": true,
"allowConnections": true
},
"content": {
"nodes": [...],
"connections": [...],
"centralConcept": {...}
},
"solution": {...},
"difficulty_level": "beginner",
"max_points": 100,
"passing_score": 70,
"estimated_time_minutes": 15,
"time_limit_minutes": 25,
"max_attempts": 3,
"allow_retry": true,
"hints": [...],
"enable_hints": true,
"hint_cost_ml_coins": 15,
"xp_reward": 25,
"ml_coins_reward": 12,
"bonus_multiplier": "1.00",
"is_active": true,
"completed": true, // ⬅️ NUEVO CAMPO
"created_at": "2025-11-12T02:56:52.392Z",
"updated_at": "2025-11-12T02:56:52.392Z"
}
Lógica Implementada
@UseGuards(JwtAuthGuard)
@Get('exercises/:id')
async findOne(@Param('id') id: string, @Request() req: any) {
const userId = req.user.id;
// 1. Obtener ejercicio
const exercise = await this.exercisesService.findById(id);
if (!exercise) {
throw new NotFoundException(`Exercise with ID ${id} not found`);
}
// 2. Verificar si el usuario ha completado este ejercicio
const submission = await this.exerciseSubmissionService.findByUserAndExercise(
userId,
id
);
const completed = submission ? submission.status === 'graded' : false;
// 3. Retornar ejercicio con campo 'completed'
return {
...exercise,
completed,
};
}
Errores Posibles
404 Not Found
{
"statusCode": 404,
"message": "Exercise with ID {id} not found",
"error": "Not Found"
}
401 Unauthorized
{
"statusCode": 401,
"message": "Unauthorized"
}
📊 3. GET /api/educational/modules
Descripción
Obtiene todos los módulos educativos con progreso calculado para el usuario autenticado.
Request
Endpoint:
GET /api/educational/modules
Headers:
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
Response
Status: 200 OK
Body:
[
{
"id": "896899ce-dd1d-4a36-a3ba-ab7f3531b517",
"tenant_id": null,
"title": "Módulo 1: Comprensión Literal",
"subtitle": null,
"description": "Identifica información explícita en textos sobre la vida de Marie Curie",
"summary": null,
"order_index": 1,
"module_code": "MOD-01-LITERAL",
"difficulty_level": "beginner",
"grade_levels": ["6", "7", "8"],
"subjects": ["Literatura", "Ciencias"],
"estimated_duration_minutes": 120,
"estimated_sessions": 4,
"learning_objectives": [
"Identificar datos explícitos",
"Comprender hechos históricos",
"Reconocer personajes y lugares"
],
"xp_reward": 100,
"ml_coins_reward": 50,
"status": "published",
"is_published": true,
"is_featured": false,
"is_free": true,
"created_at": "2025-11-12T02:56:52.349Z",
"updated_at": "2025-11-12T02:56:52.392Z",
"total_exercises": 5, // ⬅️ NUEVO CAMPO
"completed_exercises": 1, // ⬅️ NUEVO CAMPO
"progress": 20, // ⬅️ NUEVO CAMPO (porcentaje 0-100)
"completed": false // ⬅️ NUEVO CAMPO (boolean)
},
{
"id": "1a0a7b7e-a5ee-4723-8536-0ae03fd030ed",
"title": "Módulo 2: Comprensión Inferencial",
"total_exercises": 5,
"completed_exercises": 0,
"progress": 0,
"completed": false,
...
}
]
Lógica Implementada
@UseGuards(JwtAuthGuard)
@Get('modules')
async findAll(@Request() req: any) {
const userId = req.user.id;
// 1. Obtener todos los módulos
const modules = await this.modulesService.findAll();
// 2. Obtener todas las submissions del usuario
const allSubmissions = await this.exerciseSubmissionService.findByUserId(userId);
// 3. Crear Map de ejercicios completados
const completedExercisesMap = new Map<string, boolean>();
allSubmissions.forEach((submission) => {
if (submission.status === 'graded') {
completedExercisesMap.set(submission.exercise_id, true);
}
});
// 4. Obtener todos los ejercicios
const allExercises = await this.exercisesService.findAll();
// 5. Agrupar ejercicios por módulo
const exercisesByModule = new Map<string, any[]>();
allExercises.forEach((exercise) => {
if (!exercisesByModule.has(exercise.module_id)) {
exercisesByModule.set(exercise.module_id, []);
}
exercisesByModule.get(exercise.module_id)!.push(exercise);
});
// 6. Calcular progreso para cada módulo
return modules.map((module) => {
const moduleExercises = exercisesByModule.get(module.id) || [];
const totalExercises = moduleExercises.length;
const completedExercises = moduleExercises.filter((ex) =>
completedExercisesMap.has(ex.id),
).length;
const progress = totalExercises > 0
? Math.round((completedExercises / totalExercises) * 100)
: 0;
const completed = totalExercises > 0 && completedExercises === totalExercises;
return {
...module,
total_exercises: totalExercises,
completed_exercises: completedExercises,
progress,
completed,
};
});
}
Casos de Uso
-
Dashboard del estudiante
- Mostrar progreso en todos los módulos
- Barras de progreso visuales (20%, 40%, etc.)
-
Módulo seleccionado
- Mostrar "Completado 2/5 ejercicios (40%)"
🎮 4. POST /api/educational/exercises/:id/submit
Descripción
Envía la solución de un ejercicio y obtiene recompensas. Este endpoint no fue modificado, pero es crítico para el flujo.
Request
Endpoint:
POST /api/educational/exercises/5d682cbc-5875-4423-96a1-dad1b7dbfc5b/submit
Headers:
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
Body:
{
"answers": {
"score": 100
},
"startedAt": 1731366000000,
"hintsUsed": 0,
"powerupsUsed": []
}
Response
Status: 200 OK
Body:
{
"score": 100,
"isPerfect": true,
"rewards": {
"xp": 200,
"mlCoins": 50,
"bonuses": []
},
"rankUp": null
}
Flujo Interno (Lo que sucede en Backend + BD)
1. ExerciseAttemptService.createAttempt()
├─▶ Calcular score
├─▶ Calcular xp_earned
├─▶ Calcular ml_coins_earned
└─▶ INSERT INTO exercise_attempts
2. Trigger automático: trg_update_user_stats_on_exercise
└─▶ gamilit.update_user_stats_on_exercise_complete()
└─▶ UPDATE gamification_system.user_stats
3. Response con rewards calculados
📊 5. GET /api/gamification/users/:userId/stats
Descripción
Obtiene las estadísticas del usuario. No fue modificado, pero muestra los valores actualizados por el trigger.
Request
Endpoint:
GET /api/gamification/users/cccccccc-cccc-cccc-cccc-cccccccccccc/stats
Headers:
Authorization: Bearer {JWT_TOKEN}
Response
Status: 200 OK
Body:
{
"id": "12862fee-8887-4ca5-8221-e504f516144a",
"user_id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
"tenant_id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"level": 2,
"total_xp": 200,
"xp_to_next_level": 100,
"current_rank": "Ajaw",
"rank_progress": "0.00",
"ml_coins": 150,
"ml_coins_earned_total": 50,
"ml_coins_spent_total": 0,
"exercises_completed": 1,
"modules_completed": 0,
"perfect_scores": 0,
"achievements_earned": 0,
"last_activity_at": "2025-11-12T05:58:57.132Z",
"created_at": "2025-11-12T02:56:52.133Z",
"updated_at": "2025-11-12T05:58:57.132Z"
}
🔐 Autenticación y Seguridad
JWT Authentication
Todos los endpoints modificados requieren autenticación JWT:
@UseGuards(JwtAuthGuard)
Extracción de User ID
@Get('exercises')
async findAll(@Request() req: any) {
const userId = req.user.id; // Extraído del JWT token
// ...
}
RLS (Row Level Security)
Las políticas de RLS en PostgreSQL aseguran que:
- Users solo ven sus propias submissions
- Queries automáticamente filtran por tenant_id
📈 Performance y Optimización
Batch Queries
Todos los endpoints modificados usan batch fetch para evitar N+1 queries:
// ✅ UNA query para todas las submissions
const allSubmissions = await findByUserId(userId);
// ❌ EVITADO: N queries (una por cada ejercicio)
for (const exercise of exercises) {
const submission = await findSubmission(userId, exercise.id);
}
Índices de BD
CREATE INDEX idx_exercise_submissions_user ON exercise_submissions(user_id);
CREATE INDEX idx_exercise_submissions_exercise ON exercise_submissions(exercise_id);
Última actualización: 2025-11-12 Autor: Sistema Gamilit Versión: 1.0