- 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>
566 lines
13 KiB
Markdown
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
|