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

40 KiB

id title type status rf_parent epic version created_date updated_date
ET-EDU-002 API REST Education Module Specification Done RF-EDU-002 OQI-002 1.0 2025-12-05 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:

{
  include_inactive?: boolean; // Admin only, default: false
  parent_id?: string;         // Filtrar por categoría padre
}

Response 200:

{
  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:

{
  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:

{
  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:

{
  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:

{
  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:

{
  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:

{
  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:

{
  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:

{
  title: string;
  description: string;
  display_order: number;
  duration_minutes?: number;
  is_locked?: boolean;
  unlock_after_module_id?: string;
}

Response 201:

{
  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:

{
  new_order: number;
}

Response 200:

{
  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:

{
  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:

{
  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:

{
  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:

{
  course_id: string;
}

Response 201:

{
  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:

{
  status?: 'active' | 'completed' | 'expired' | 'cancelled';
  page?: number;
  limit?: number;
}

Response 200:

{
  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:

{
  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:

{
  success: true,
  data: {
    id: string;
    status: 'cancelled';
  }
}

6. PROGRESS

POST /progress

Actualizar progreso en una lección.

Auth: Required

Request Body:

{
  lesson_id: string;
  enrollment_id: string;
  last_position_seconds?: number;  // Para videos
  is_completed?: boolean;
  watch_percentage?: number;
}

Response 200:

{
  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:

{
  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:

{
  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:

{
  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:

{
  answers: Array<{
    question_id: string;
    answer: string | string[];  // string[] para multiple_select
  }>;
}

Response 200:

{
  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:

{
  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:

{
  verification_code?: string;  // Para acceso público
}

Response 200:

{
  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:

{
  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:

{
  achievement_type?: 'course_completion' | 'quiz_perfect_score' | 'streak_milestone' | 'level_up' | 'special_event';
}

Response 200:

{
  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:

{
  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:

{
  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:

{
  period?: 'all_time' | 'month' | 'week';
  limit?: number;  // default: 100
}

Response 200:

{
  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

// ==================== 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

# 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

{
  "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

// 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

// 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

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

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

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

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