workspace/projects/gamilit/docs/sistema-recompensas/03-API-ENDPOINTS.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

566 lines
13 KiB
Markdown

# 🌐 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:**
```http
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
```
**Query Parameters:** Ninguno
---
### Response
**Status:** `200 OK`
**Body:**
```json
[
{
"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
```typescript
@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
1. **Listar ejercicios en página de módulo**
- Frontend muestra badge "Completado" en ejercicios con `completed: true`
2. **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:**
```http
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
```
**URL Parameters:**
- `id` (UUID, required): ID del ejercicio
---
### Response
**Status:** `200 OK`
**Body:**
```json
{
"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
```typescript
@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**
```json
{
"statusCode": 404,
"message": "Exercise with ID {id} not found",
"error": "Not Found"
}
```
**401 Unauthorized**
```json
{
"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:**
```http
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
```
---
### Response
**Status:** `200 OK`
**Body:**
```json
[
{
"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
```typescript
@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
1. **Dashboard del estudiante**
- Mostrar progreso en todos los módulos
- Barras de progreso visuales (20%, 40%, etc.)
2. **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:**
```http
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
```
**Body:**
```json
{
"answers": {
"score": 100
},
"startedAt": 1731366000000,
"hintsUsed": 0,
"powerupsUsed": []
}
```
---
### Response
**Status:** `200 OK`
**Body:**
```json
{
"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:**
```http
Authorization: Bearer {JWT_TOKEN}
```
---
### Response
**Status:** `200 OK`
**Body:**
```json
{
"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:
```typescript
@UseGuards(JwtAuthGuard)
```
### Extracción de User ID
```typescript
@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:
```typescript
// ✅ 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
```sql
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