trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/ET-EDU-002-api.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
ML Engine Updates:
- Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records
- Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence)
- Backtest results: +176.71R profit with aggressive_filter strategy

Documentation Consolidation:
- Created docs/99-analisis/_MAP.md index with 13 new analysis documents
- Consolidated inventories: removed duplicates from orchestration/inventarios/
- Updated ML_INVENTORY.yml with BTCUSD metrics and training results
- Added execution reports: FASE11-BTCUSD, correction issues, alignment validation

Architecture & Integration:
- Updated all module documentation with NEXUS v3.4 frontmatter
- Fixed _MAP.md indexes across all folders
- Updated orchestration plans and traces

Files: 229 changed, 5064 insertions(+), 1872 deletions(-)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:31:29 -06:00

1899 lines
40 KiB
Markdown

---
id: "ET-EDU-002"
title: "API REST Education Module"
type: "Specification"
status: "Done"
rf_parent: "RF-EDU-002"
epic: "OQI-002"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-EDU-002: API REST - Endpoints del Módulo Educativo
**Versión:** 1.0.0
**Fecha:** 2025-12-05
**Épica:** OQI-002 - Módulo Educativo
**Componente:** Backend
---
## Descripción
Define todos los endpoints REST del módulo educativo de Trading Platform, implementados en Express.js con TypeScript. Incluye autenticación JWT, validación de datos, paginación, filtros y manejo de errores.
---
## Arquitectura
```
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway │
│ (Express.js + TypeScript) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Middleware Stack │ │
│ │ - JWT Authentication │ │
│ │ - Rate Limiting │ │
│ │ - Request Validation (Zod) │ │
│ │ - Error Handling │ │
│ │ - CORS │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Courses │ │ Enrollments │ │ Progress │ │
│ │ Controller │ │ Controller │ │ Controller │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ v v v │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Courses │ │ Enrollments │ │ Progress │ │
│ │ Service │ │ Service │ │ Service │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ v v v │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL (education schema) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Quizzes │ │Certificates │ │Achievements │ │
│ │ Controller │ │ Controller │ │ Controller │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## Especificación Detallada
### Base URL
```
Production: https://api.trading.ai/v1/education
Development: http://localhost:3000/v1/education
```
### Convenciones
- **Formato**: JSON
- **Encoding**: UTF-8
- **Versionado**: Prefijo `/v1`
- **Autenticación**: Bearer Token (JWT)
- **Rate Limit**: 100 req/min por IP, 1000 req/hour por usuario autenticado
---
## Endpoints
### 1. CATEGORIES
#### GET /categories
Obtener todas las categorías activas.
**Auth:** No requerido
**Query Parameters:**
```typescript
{
include_inactive?: boolean; // Admin only, default: false
parent_id?: string; // Filtrar por categoría padre
}
```
**Response 200:**
```typescript
{
success: true,
data: Array<{
id: string;
name: string;
slug: string;
description: string | null;
parent_id: string | null;
display_order: number;
icon_url: string | null;
color: string | null;
course_count: number; // Número de cursos en la categoría
}>,
metadata: {
total: number;
timestamp: string;
}
}
```
#### GET /categories/:slug
Obtener categoría por slug con sus cursos.
**Auth:** No requerido
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
name: string;
slug: string;
description: string | null;
parent_id: string | null;
icon_url: string | null;
color: string | null;
courses: Array<{
id: string;
title: string;
slug: string;
short_description: string;
thumbnail_url: string;
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
duration_minutes: number;
total_lessons: number;
avg_rating: number;
is_free: boolean;
price_usd: number | null;
}>;
}
}
```
---
### 2. COURSES
#### GET /courses
Listar cursos con filtros y paginación.
**Auth:** No requerido
**Query Parameters:**
```typescript
{
page?: number; // default: 1
limit?: number; // default: 20, max: 100
category?: string; // slug de categoría
difficulty?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
is_free?: boolean;
search?: string; // Búsqueda en título y descripción
sort_by?: 'newest' | 'popular' | 'rating' | 'title';
instructor_id?: string;
}
```
**Response 200:**
```typescript
{
success: true,
data: Array<{
id: string;
title: string;
slug: string;
short_description: string;
thumbnail_url: string;
category: {
id: string;
name: string;
slug: string;
};
difficulty_level: string;
duration_minutes: number;
total_modules: number;
total_lessons: number;
total_enrollments: number;
avg_rating: number;
total_reviews: number;
instructor_name: string;
is_free: boolean;
price_usd: number | null;
xp_reward: number;
published_at: string;
}>,
pagination: {
page: number;
limit: number;
total: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
}
}
```
#### GET /courses/:slug
Obtener detalle completo de un curso.
**Auth:** No requerido
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
title: string;
slug: string;
short_description: string;
full_description: string;
thumbnail_url: string;
trailer_url: string | null;
category: {
id: string;
name: string;
slug: string;
};
difficulty_level: string;
duration_minutes: number;
prerequisites: string[];
learning_objectives: string[];
instructor: {
id: string;
name: string;
avatar_url: string | null;
bio: string | null;
};
is_free: boolean;
price_usd: number | null;
xp_reward: number;
published_at: string;
// Estadísticas
total_modules: number;
total_lessons: number;
total_enrollments: number;
avg_rating: number;
total_reviews: number;
// Módulos con lecciones
modules: Array<{
id: string;
title: string;
description: string;
display_order: number;
duration_minutes: number;
is_locked: boolean;
lessons: Array<{
id: string;
title: string;
description: string;
content_type: 'video' | 'article' | 'interactive' | 'quiz';
video_duration_seconds: number | null;
display_order: number;
is_preview: boolean;
xp_reward: number;
}>;
}>;
// Estado del usuario (si está autenticado)
user_enrollment?: {
is_enrolled: boolean;
progress_percentage: number;
status: 'active' | 'completed' | 'expired' | 'cancelled';
enrolled_at: string;
};
}
}
```
#### POST /courses
Crear nuevo curso (solo instructores/admin).
**Auth:** Required (instructor/admin)
**Request Body:**
```typescript
{
title: string; // min: 10, max: 200
short_description: string; // min: 50, max: 500
full_description: string; // min: 100
category_id: string; // UUID
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
thumbnail_url?: string;
trailer_url?: string;
prerequisites?: string[];
learning_objectives: string[]; // min: 3
is_free?: boolean;
price_usd?: number;
xp_reward?: number;
}
```
**Response 201:**
```typescript
{
success: true,
data: {
id: string;
slug: string;
status: 'draft';
// ... resto de campos del curso
}
}
```
#### PATCH /courses/:id
Actualizar curso (solo instructor owner/admin).
**Auth:** Required (owner/admin)
**Request Body:** Partial de POST /courses
**Response 200:** Same as GET /courses/:slug
#### DELETE /courses/:id
Eliminar curso (solo admin).
**Auth:** Required (admin)
**Response 204:** No Content
#### PATCH /courses/:id/publish
Publicar curso (cambiar status a published).
**Auth:** Required (owner/admin)
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
status: 'published';
published_at: string;
}
}
```
---
### 3. MODULES
#### POST /courses/:courseId/modules
Crear módulo en un curso.
**Auth:** Required (course owner/admin)
**Request Body:**
```typescript
{
title: string;
description: string;
display_order: number;
duration_minutes?: number;
is_locked?: boolean;
unlock_after_module_id?: string;
}
```
**Response 201:**
```typescript
{
success: true,
data: {
id: string;
course_id: string;
title: string;
description: string;
display_order: number;
is_locked: boolean;
created_at: string;
}
}
```
#### PATCH /modules/:id
Actualizar módulo.
**Auth:** Required (course owner/admin)
**Request Body:** Partial de POST
**Response 200:** Same as POST response
#### DELETE /modules/:id
Eliminar módulo (cascade elimina lecciones).
**Auth:** Required (course owner/admin)
**Response 204:** No Content
#### PATCH /modules/:id/reorder
Reordenar módulos de un curso.
**Auth:** Required (course owner/admin)
**Request Body:**
```typescript
{
new_order: number;
}
```
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
display_order: number;
}
}
```
---
### 4. LESSONS
#### POST /modules/:moduleId/lessons
Crear lección en un módulo.
**Auth:** Required (course owner/admin)
**Request Body:**
```typescript
{
title: string;
description: string;
content_type: 'video' | 'article' | 'interactive' | 'quiz';
// Para video
video_url?: string;
video_duration_seconds?: number;
video_provider?: 'vimeo' | 's3';
video_id?: string;
// Para article
article_content?: string;
// Recursos
attachments?: Array<{
name: string;
url: string;
type: string;
size: number;
}>;
display_order: number;
is_preview?: boolean;
is_mandatory?: boolean;
xp_reward?: number;
}
```
**Response 201:**
```typescript
{
success: true,
data: {
id: string;
module_id: string;
title: string;
content_type: string;
display_order: number;
xp_reward: number;
created_at: string;
}
}
```
#### GET /lessons/:id
Obtener detalle de lección.
**Auth:** Required (enrolled users o lecciones preview)
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
module_id: string;
title: string;
description: string;
content_type: string;
// Video
video_url?: string;
video_duration_seconds?: number;
video_provider?: string;
video_id?: string;
// Article
article_content?: string;
// Recursos
attachments?: Array<{
name: string;
url: string;
type: string;
size: number;
}>;
is_preview: boolean;
xp_reward: number;
// Progreso del usuario
user_progress?: {
is_completed: boolean;
last_position_seconds: number;
watch_percentage: number;
last_viewed_at: string;
};
// Navegación
previous_lesson?: {
id: string;
title: string;
};
next_lesson?: {
id: string;
title: string;
};
}
}
```
#### PATCH /lessons/:id
Actualizar lección.
**Auth:** Required (course owner/admin)
**Request Body:** Partial de POST
**Response 200:** Same as GET /lessons/:id
#### DELETE /lessons/:id
Eliminar lección.
**Auth:** Required (course owner/admin)
**Response 204:** No Content
---
### 5. ENROLLMENTS
#### POST /enrollments
Enrollarse en un curso.
**Auth:** Required
**Request Body:**
```typescript
{
course_id: string;
}
```
**Response 201:**
```typescript
{
success: true,
data: {
id: string;
user_id: string;
course_id: string;
status: 'active';
progress_percentage: 0;
enrolled_at: string;
total_lessons: number;
}
}
```
**Error 409:** Ya está enrollado en el curso
#### GET /enrollments
Obtener enrollments del usuario autenticado.
**Auth:** Required
**Query Parameters:**
```typescript
{
status?: 'active' | 'completed' | 'expired' | 'cancelled';
page?: number;
limit?: number;
}
```
**Response 200:**
```typescript
{
success: true,
data: Array<{
id: string;
course: {
id: string;
title: string;
slug: string;
thumbnail_url: string;
instructor_name: string;
};
status: string;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
enrolled_at: string;
started_at: string | null;
completed_at: string | null;
total_xp_earned: number;
}>,
pagination: {
page: number;
limit: number;
total: number;
}
}
```
#### GET /enrollments/:id
Obtener detalle de enrollment con progreso completo.
**Auth:** Required (owner/admin)
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
course: {
id: string;
title: string;
slug: string;
thumbnail_url: string;
};
status: string;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
enrolled_at: string;
started_at: string | null;
completed_at: string | null;
total_xp_earned: number;
// Progreso por módulo
modules_progress: Array<{
module_id: string;
module_title: string;
total_lessons: number;
completed_lessons: number;
progress_percentage: number;
lessons_progress: Array<{
lesson_id: string;
lesson_title: string;
is_completed: boolean;
watch_percentage: number;
last_viewed_at: string | null;
}>;
}>;
}
}
```
#### DELETE /enrollments/:id
Cancelar enrollment.
**Auth:** Required (owner/admin)
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
status: 'cancelled';
}
}
```
---
### 6. PROGRESS
#### POST /progress
Actualizar progreso en una lección.
**Auth:** Required
**Request Body:**
```typescript
{
lesson_id: string;
enrollment_id: string;
last_position_seconds?: number; // Para videos
is_completed?: boolean;
watch_percentage?: number;
}
```
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
lesson_id: string;
is_completed: boolean;
last_position_seconds: number;
watch_percentage: number;
xp_earned: number; // Si se completó la lección
updated_at: string;
}
}
```
#### GET /progress/course/:courseId
Obtener progreso del usuario en un curso.
**Auth:** Required
**Response 200:**
```typescript
{
success: true,
data: {
enrollment_id: string;
course_id: string;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
total_xp_earned: number;
lessons: Array<{
lesson_id: string;
lesson_title: string;
module_title: string;
is_completed: boolean;
watch_percentage: number;
last_viewed_at: string | null;
completed_at: string | null;
}>;
}
}
```
---
### 7. QUIZZES
#### GET /quizzes/:id
Obtener quiz (sin respuestas correctas).
**Auth:** Required (enrolled)
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
module_id?: string;
lesson_id?: string;
title: string;
description: string;
passing_score_percentage: number;
max_attempts: number | null;
time_limit_minutes: number | null;
xp_reward: number;
xp_perfect_score_bonus: number;
// Estadísticas del usuario
user_attempts: Array<{
id: string;
score_percentage: number;
is_passed: boolean;
completed_at: string;
}>;
remaining_attempts: number | null;
best_score: number | null;
// Preguntas (sin respuestas correctas)
questions: Array<{
id: string;
question_text: string;
question_type: 'multiple_choice' | 'true_false' | 'multiple_select' | 'fill_blank' | 'code_challenge';
options?: Array<{
id: string;
text: string;
}>;
image_url?: string;
code_snippet?: string;
points: number;
display_order: number;
}>;
}
}
```
#### POST /quizzes/:id/attempts
Iniciar intento de quiz.
**Auth:** Required (enrolled)
**Response 201:**
```typescript
{
success: true,
data: {
id: string;
quiz_id: string;
started_at: string;
time_limit_expires_at: string | null;
questions: Array<{
id: string;
question_text: string;
question_type: string;
options?: Array<{ id: string; text: string }>;
points: number;
}>;
}
}
```
**Error 403:** No quedan intentos disponibles
#### POST /quizzes/attempts/:attemptId/submit
Enviar respuestas del quiz.
**Auth:** Required (owner)
**Request Body:**
```typescript
{
answers: Array<{
question_id: string;
answer: string | string[]; // string[] para multiple_select
}>;
}
```
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
is_completed: true;
is_passed: boolean;
score_points: number;
max_points: number;
score_percentage: number;
time_taken_seconds: number;
xp_earned: number;
completed_at: string;
// Desglose de respuestas
results: Array<{
question_id: string;
question_text: string;
user_answer: string | string[];
is_correct: boolean;
points_earned: number;
max_points: number;
explanation: string;
}>;
// Nuevo logro si aplica
achievement_earned?: {
id: string;
title: string;
description: string;
badge_icon_url: string;
};
}
}
```
#### GET /quizzes/attempts/:attemptId
Obtener resultado de un intento completado.
**Auth:** Required (owner/admin)
**Response 200:** Same as POST submit response
---
### 8. CERTIFICATES
#### GET /certificates
Obtener certificados del usuario.
**Auth:** Required
**Response 200:**
```typescript
{
success: true,
data: Array<{
id: string;
certificate_number: string;
course_title: string;
completion_date: string;
final_score: number;
certificate_url: string;
verification_code: string;
issued_at: string;
}>
}
```
#### GET /certificates/:id
Obtener detalle de certificado.
**Auth:** Required (owner/admin) o público con verification_code
**Query Parameters:**
```typescript
{
verification_code?: string; // Para acceso público
}
```
**Response 200:**
```typescript
{
success: true,
data: {
id: string;
certificate_number: string;
user_name: string;
course_title: string;
instructor_name: string;
completion_date: string;
final_score: number;
total_xp_earned: number;
certificate_url: string;
verification_code: string;
is_verified: boolean;
issued_at: string;
}
}
```
#### GET /certificates/verify/:certificateNumber
Verificar autenticidad de un certificado (público).
**Auth:** No requerido
**Response 200:**
```typescript
{
success: true,
data: {
is_valid: boolean;
certificate_number: string;
user_name: string;
course_title: string;
completion_date: string;
issued_at: string;
}
}
```
---
### 9. ACHIEVEMENTS
#### GET /achievements
Obtener logros del usuario autenticado.
**Auth:** Required
**Query Parameters:**
```typescript
{
achievement_type?: 'course_completion' | 'quiz_perfect_score' | 'streak_milestone' | 'level_up' | 'special_event';
}
```
**Response 200:**
```typescript
{
success: true,
data: Array<{
id: string;
achievement_type: string;
title: string;
description: string;
badge_icon_url: string;
xp_bonus: number;
earned_at: string;
metadata: {
course_title?: string;
quiz_score?: number;
streak_days?: number;
new_level?: number;
};
}>,
summary: {
total_achievements: number;
total_xp_from_achievements: number;
by_type: {
course_completion: number;
quiz_perfect_score: number;
streak_milestone: number;
level_up: number;
special_event: number;
};
}
}
```
#### GET /achievements/available
Obtener logros disponibles para desbloquear.
**Auth:** Required
**Response 200:**
```typescript
{
success: true,
data: Array<{
title: string;
description: string;
badge_icon_url: string;
xp_bonus: number;
requirements: string;
progress_percentage: number;
}>
}
```
---
### 10. GAMIFICATION
#### GET /gamification/profile
Obtener perfil de gamificación del usuario.
**Auth:** Required
**Response 200:**
```typescript
{
success: true,
data: {
user_id: string;
total_xp: number;
current_level: number;
xp_for_next_level: number;
xp_progress_percentage: number;
// Estadísticas
courses_completed: number;
courses_in_progress: number;
total_learning_time_minutes: number;
// Racha
current_streak_days: number;
longest_streak_days: number;
last_activity_date: string;
// Logros
total_achievements: number;
recent_achievements: Array<{
id: string;
title: string;
badge_icon_url: string;
earned_at: string;
}>;
// Ranking
global_rank: number | null;
percentile: number;
}
}
```
#### GET /gamification/leaderboard
Obtener tabla de clasificación.
**Auth:** Required
**Query Parameters:**
```typescript
{
period?: 'all_time' | 'month' | 'week';
limit?: number; // default: 100
}
```
**Response 200:**
```typescript
{
success: true,
data: Array<{
rank: number;
user_id: string;
user_name: string;
avatar_url: string | null;
total_xp: number;
current_level: number;
courses_completed: number;
achievements_count: number;
}>,
user_position: {
rank: number;
total_xp: number;
}
}
```
---
## Interfaces/Tipos
### Request/Response Types
```typescript
// ==================== COMMON ====================
export interface ApiResponse<T> {
success: boolean;
data: T;
error?: {
code: string;
message: string;
details?: any;
};
}
export interface PaginatedResponse<T> {
success: boolean;
data: T[];
pagination: {
page: number;
limit: number;
total: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
};
}
// ==================== COURSES ====================
export interface CourseListItem {
id: string;
title: string;
slug: string;
short_description: string;
thumbnail_url: string;
category: {
id: string;
name: string;
slug: string;
};
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
duration_minutes: number;
total_modules: number;
total_lessons: number;
total_enrollments: number;
avg_rating: number;
total_reviews: number;
instructor_name: string;
is_free: boolean;
price_usd: number | null;
xp_reward: number;
published_at: string;
}
export interface CourseDetail extends CourseListItem {
full_description: string;
trailer_url: string | null;
prerequisites: string[];
learning_objectives: string[];
instructor: {
id: string;
name: string;
avatar_url: string | null;
bio: string | null;
};
modules: ModuleWithLessons[];
user_enrollment?: {
is_enrolled: boolean;
progress_percentage: number;
status: EnrollmentStatus;
enrolled_at: string;
};
}
export interface CreateCourseDto {
title: string;
short_description: string;
full_description: string;
category_id: string;
difficulty_level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
thumbnail_url?: string;
trailer_url?: string;
prerequisites?: string[];
learning_objectives: string[];
is_free?: boolean;
price_usd?: number;
xp_reward?: number;
}
// ==================== MODULES ====================
export interface Module {
id: string;
course_id: string;
title: string;
description: string;
display_order: number;
duration_minutes: number;
is_locked: boolean;
unlock_after_module_id: string | null;
}
export interface ModuleWithLessons extends Module {
lessons: Lesson[];
}
export interface CreateModuleDto {
title: string;
description: string;
display_order: number;
duration_minutes?: number;
is_locked?: boolean;
unlock_after_module_id?: string;
}
// ==================== LESSONS ====================
export type LessonContentType = 'video' | 'article' | 'interactive' | 'quiz';
export interface Lesson {
id: string;
module_id: string;
title: string;
description: string;
content_type: LessonContentType;
video_url?: string;
video_duration_seconds?: number;
video_provider?: string;
video_id?: string;
article_content?: string;
attachments?: Attachment[];
display_order: number;
is_preview: boolean;
is_mandatory: boolean;
xp_reward: number;
}
export interface LessonDetail extends Lesson {
user_progress?: {
is_completed: boolean;
last_position_seconds: number;
watch_percentage: number;
last_viewed_at: string;
};
previous_lesson?: {
id: string;
title: string;
};
next_lesson?: {
id: string;
title: string;
};
}
export interface Attachment {
name: string;
url: string;
type: string;
size: number;
}
export interface CreateLessonDto {
title: string;
description: string;
content_type: LessonContentType;
video_url?: string;
video_duration_seconds?: number;
video_provider?: 'vimeo' | 's3';
video_id?: string;
article_content?: string;
attachments?: Attachment[];
display_order: number;
is_preview?: boolean;
is_mandatory?: boolean;
xp_reward?: number;
}
// ==================== ENROLLMENTS ====================
export type EnrollmentStatus = 'active' | 'completed' | 'expired' | 'cancelled';
export interface Enrollment {
id: string;
user_id: string;
course_id: string;
status: EnrollmentStatus;
progress_percentage: number;
completed_lessons: number;
total_lessons: number;
enrolled_at: string;
started_at: string | null;
completed_at: string | null;
expires_at: string | null;
total_xp_earned: number;
}
export interface EnrollmentWithCourse extends Enrollment {
course: {
id: string;
title: string;
slug: string;
thumbnail_url: string;
instructor_name: string;
};
}
// ==================== PROGRESS ====================
export interface Progress {
id: string;
user_id: string;
lesson_id: string;
enrollment_id: string;
is_completed: boolean;
last_position_seconds: number;
total_watch_time_seconds: number;
watch_percentage: number;
first_viewed_at: string | null;
last_viewed_at: string | null;
completed_at: string | null;
}
export interface UpdateProgressDto {
lesson_id: string;
enrollment_id: string;
last_position_seconds?: number;
is_completed?: boolean;
watch_percentage?: number;
}
// ==================== QUIZZES ====================
export type QuestionType = 'multiple_choice' | 'true_false' | 'multiple_select' | 'fill_blank' | 'code_challenge';
export interface Quiz {
id: string;
module_id?: string;
lesson_id?: string;
title: string;
description: string;
passing_score_percentage: number;
max_attempts: number | null;
time_limit_minutes: number | null;
shuffle_questions: boolean;
shuffle_answers: boolean;
show_correct_answers: boolean;
xp_reward: number;
xp_perfect_score_bonus: number;
}
export interface QuizQuestion {
id: string;
quiz_id: string;
question_text: string;
question_type: QuestionType;
options?: Array<{
id: string;
text: string;
isCorrect?: boolean; // Solo para admin
}>;
correct_answer?: string;
explanation: string;
image_url?: string;
code_snippet?: string;
points: number;
display_order: number;
}
export interface QuizAttempt {
id: string;
user_id: string;
quiz_id: string;
enrollment_id: string | null;
is_completed: boolean;
is_passed: boolean;
user_answers: any;
score_points: number;
max_points: number;
score_percentage: number;
started_at: string;
completed_at: string | null;
time_taken_seconds: number | null;
xp_earned: number;
}
export interface SubmitQuizDto {
answers: Array<{
question_id: string;
answer: string | string[];
}>;
}
// ==================== CERTIFICATES ====================
export interface Certificate {
id: string;
certificate_number: string;
user_name: string;
course_title: string;
instructor_name: string;
completion_date: string;
final_score: number;
total_xp_earned: number;
certificate_url: string;
verification_code: string;
is_verified: boolean;
issued_at: string;
}
// ==================== ACHIEVEMENTS ====================
export type AchievementType = 'course_completion' | 'quiz_perfect_score' | 'streak_milestone' | 'level_up' | 'special_event';
export interface Achievement {
id: string;
user_id: string;
achievement_type: AchievementType;
title: string;
description: string;
badge_icon_url: string;
metadata: any;
course_id?: string;
quiz_id?: string;
xp_bonus: number;
earned_at: string;
}
// ==================== GAMIFICATION ====================
export interface GamificationProfile {
user_id: string;
total_xp: number;
current_level: number;
xp_for_next_level: number;
xp_progress_percentage: number;
courses_completed: number;
courses_in_progress: number;
total_learning_time_minutes: number;
current_streak_days: number;
longest_streak_days: number;
last_activity_date: string;
total_achievements: number;
recent_achievements: Achievement[];
global_rank: number | null;
percentile: number;
}
export interface LeaderboardEntry {
rank: number;
user_id: string;
user_name: string;
avatar_url: string | null;
total_xp: number;
current_level: number;
courses_completed: number;
achievements_count: number;
}
```
---
## Configuración
### Variables de Entorno
```bash
# Server
PORT=3000
NODE_ENV=production
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/trading
DATABASE_SCHEMA=education
# JWT
JWT_SECRET=your-super-secret-key
JWT_EXPIRES_IN=7d
# Rate Limiting
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_AUTHENTICATED_MAX=1000
# CORS
CORS_ORIGIN=https://trading.ai,https://app.trading.ai
CORS_CREDENTIALS=true
# External Services
VIMEO_API_KEY=xxx
AWS_S3_BUCKET=trading-videos
AWS_CLOUDFRONT_URL=https://cdn.trading.ai
# Email (para certificados)
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=xxx
# Redis (para caching)
REDIS_URL=redis://localhost:6379
REDIS_TTL=3600
```
---
## Dependencias
```json
{
"dependencies": {
"express": "^4.18.2",
"typescript": "^5.3.3",
"@types/express": "^4.17.21",
"zod": "^3.22.4",
"pg": "^8.11.3",
"jsonwebtoken": "^9.0.2",
"bcrypt": "^5.1.1",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"winston": "^3.11.0",
"redis": "^4.6.12",
"axios": "^1.6.5"
},
"devDependencies": {
"@types/node": "^20.10.6",
"@types/jsonwebtoken": "^9.0.5",
"@types/bcrypt": "^5.0.2",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"jest": "^29.7.0",
"@types/jest": "^29.5.11",
"supertest": "^6.3.3",
"@types/supertest": "^6.0.2"
}
}
```
---
## Consideraciones de Seguridad
### 1. Autenticación
```typescript
// Middleware de autenticación JWT
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
error: {
code: 'UNAUTHORIZED',
message: 'Token no proporcionado'
}
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({
success: false,
error: {
code: 'INVALID_TOKEN',
message: 'Token inválido o expirado'
}
});
}
};
```
### 2. Autorización
```typescript
// Middleware de autorización por rol
export const authorize = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
error: {
code: 'FORBIDDEN',
message: 'No tienes permisos para esta acción'
}
});
}
next();
};
};
// Verificar ownership del recurso
export const verifyOwnership = async (
req: Request,
res: Response,
next: NextFunction
) => {
const resourceId = req.params.id;
const userId = req.user.id;
// Verificar que el recurso pertenece al usuario
const resource = await db.query(
'SELECT user_id FROM education.enrollments WHERE id = $1',
[resourceId]
);
if (resource.rows[0]?.user_id !== userId && req.user.role !== 'admin') {
return res.status(403).json({
success: false,
error: {
code: 'FORBIDDEN',
message: 'No tienes acceso a este recurso'
}
});
}
next();
};
```
### 3. Validación de Input
```typescript
import { z } from 'zod';
// Schema de validación para crear curso
export const CreateCourseSchema = z.object({
title: z.string().min(10).max(200),
short_description: z.string().min(50).max(500),
full_description: z.string().min(100),
category_id: z.string().uuid(),
difficulty_level: z.enum(['beginner', 'intermediate', 'advanced', 'expert']),
thumbnail_url: z.string().url().optional(),
trailer_url: z.string().url().optional(),
prerequisites: z.array(z.string().uuid()).optional(),
learning_objectives: z.array(z.string()).min(3),
is_free: z.boolean().optional(),
price_usd: z.number().min(0).optional(),
xp_reward: z.number().min(0).optional()
});
// Middleware de validación
export const validate = (schema: z.ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Datos de entrada inválidos',
details: error.errors
}
});
}
next(error);
}
};
};
```
### 4. Rate Limiting
```typescript
import rateLimit from 'express-rate-limit';
// Rate limiter general
export const generalLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 100,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Demasiadas solicitudes, intenta más tarde'
}
}
});
// Rate limiter para autenticados
export const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora
max: 1000,
skip: (req) => !req.user,
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Límite de solicitudes excedido'
}
}
});
```
### 5. SQL Injection Prevention
- Usar **prepared statements** siempre
- Usar ORM (Prisma, TypeORM) con queries parametrizadas
- Nunca concatenar strings para queries SQL
### 6. XSS Prevention
- Sanitizar todo input del usuario
- Usar headers de seguridad (Helmet.js)
- Content Security Policy
### 7. CORS
```typescript
import cors from 'cors';
app.use(cors({
origin: process.env.CORS_ORIGIN?.split(','),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
```
---
## Testing
### Estrategia
1. **Unit Tests**: Servicios y controladores
2. **Integration Tests**: Endpoints completos
3. **E2E Tests**: Flujos de usuario completos
### Ejemplo de Test
```typescript
import request from 'supertest';
import { app } from '../app';
import { generateAuthToken } from '../utils/auth';
describe('POST /enrollments', () => {
let authToken: string;
let courseId: string;
beforeAll(async () => {
// Setup test data
authToken = generateAuthToken({ id: 'test-user-id', role: 'student' });
courseId = await createTestCourse();
});
it('should enroll user in course successfully', async () => {
const response = await request(app)
.post('/v1/education/enrollments')
.set('Authorization', `Bearer ${authToken}`)
.send({ course_id: courseId })
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.course_id).toBe(courseId);
expect(response.body.data.status).toBe('active');
});
it('should return 401 without authentication', async () => {
await request(app)
.post('/v1/education/enrollments')
.send({ course_id: courseId })
.expect(401);
});
it('should return 409 if already enrolled', async () => {
// Enroll first time
await request(app)
.post('/v1/education/enrollments')
.set('Authorization', `Bearer ${authToken}`)
.send({ course_id: courseId });
// Try to enroll again
const response = await request(app)
.post('/v1/education/enrollments')
.set('Authorization', `Bearer ${authToken}`)
.send({ course_id: courseId })
.expect(409);
expect(response.body.error.code).toBe('ALREADY_ENROLLED');
});
});
```
---
## Códigos de Error
| Código | HTTP | Descripción |
|--------|------|-------------|
| UNAUTHORIZED | 401 | Token no proporcionado o inválido |
| FORBIDDEN | 403 | Sin permisos para esta acción |
| NOT_FOUND | 404 | Recurso no encontrado |
| ALREADY_ENROLLED | 409 | Ya enrollado en el curso |
| VALIDATION_ERROR | 400 | Datos de entrada inválidos |
| RATE_LIMIT_EXCEEDED | 429 | Límite de solicitudes excedido |
| NO_ATTEMPTS_REMAINING | 403 | Sin intentos disponibles para quiz |
| QUIZ_TIME_EXPIRED | 400 | Tiempo límite del quiz expirado |
| COURSE_NOT_PUBLISHED | 403 | Curso no está publicado |
| LESSON_LOCKED | 403 | Lección bloqueada (completar anteriores) |
| INTERNAL_SERVER_ERROR | 500 | Error interno del servidor |
---
**Fin de Especificación ET-EDU-002**