feat: Major platform documentation and architecture updates
Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9422e3d3a9
commit
a7cca885f0
339
AGENTS.md
Normal file
339
AGENTS.md
Normal file
@ -0,0 +1,339 @@
|
||||
# Guia para Agentes de IA - Trading Platform (OrbiQuant)
|
||||
|
||||
**Version:** 1.0
|
||||
**Ultima actualizacion:** 2026-01-04
|
||||
**Basado en:** Estandar-SCRUM.md (Principio SIMCO)
|
||||
|
||||
---
|
||||
|
||||
## 1. Estructura del Proyecto
|
||||
|
||||
### Ubicaciones Clave
|
||||
|
||||
| Tipo | Ubicacion |
|
||||
|------|-----------|
|
||||
| Documentacion general | `/docs/` |
|
||||
| Planificacion | `/docs/planning/` |
|
||||
| Definicion de Modulos | `/docs/02-definicion-modulos/` |
|
||||
| Backlog | `/docs/04-fase-backlog/` |
|
||||
| Transversal | `/docs/90-transversal/` |
|
||||
| Guias de Desarrollo | `/docs/95-guias-desarrollo/` |
|
||||
| Quick Reference | `/docs/96-quick-reference/` |
|
||||
| ADRs | `/docs/97-adr/` |
|
||||
| Tareas | `/docs/planning/tasks/` |
|
||||
| Bugs | `/docs/planning/bugs/` |
|
||||
| Tablero Kanban | `/docs/planning/Board.md` |
|
||||
| Orquestacion | `/orchestration/` |
|
||||
|
||||
### Estructura de un Modulo (Epica)
|
||||
|
||||
```
|
||||
docs/02-definicion-modulos/OQI-XXX-{nombre}/
|
||||
├── _MAP.md # Indice del modulo
|
||||
├── README.md # Descripcion del modulo
|
||||
├── historias-usuario/ # User Stories (US-*.md)
|
||||
├── requerimientos/ # Requerimientos Funcionales (RF-*.md)
|
||||
├── especificaciones/ # Especificaciones Tecnicas (ET-*.md)
|
||||
└── implementacion/ # Trazabilidad de implementacion
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Prefijos de Nomenclatura
|
||||
|
||||
| Prefijo | Tipo | Ejemplo | Descripcion |
|
||||
|---------|------|---------|-------------|
|
||||
| OQI- | Epica/Modulo | OQI-001-fundamentos-auth | Modulos principales |
|
||||
| US- | Historia de Usuario | US-AUTH-001 | User Stories |
|
||||
| TASK- | Tarea | TASK-001 | Tareas ejecutables |
|
||||
| BUG- | Bug | BUG-001 | Defectos/errores |
|
||||
| RF- | Requerimiento Funcional | RF-AUTH-001 | Requerimientos |
|
||||
| ET- | Especificacion Tecnica | ET-AUTH-001 | Especificaciones |
|
||||
| ADR- | Decision Record | ADR-001 | Decisiones arquitectonicas |
|
||||
|
||||
### Categorias de User Stories
|
||||
|
||||
| Sufijo | Modulo | Epica |
|
||||
|--------|--------|-------|
|
||||
| AUTH | Autenticacion | OQI-001 |
|
||||
| EDU | Educacion | OQI-002 |
|
||||
| TRD | Trading Charts | OQI-003 |
|
||||
| INV | Investment Accounts | OQI-004 |
|
||||
| PAY | Payments/Stripe | OQI-005 |
|
||||
| ML | ML Signals | OQI-006 |
|
||||
| LLM | LLM Agent | OQI-007 |
|
||||
| PFM | Portfolio Manager | OQI-008 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Como Trabajar con Tareas
|
||||
|
||||
### Tomar una Tarea
|
||||
|
||||
1. **Identificar tarea** en `/docs/planning/Board.md` (columna "Por Hacer")
|
||||
2. **Leer archivo** `TASK-XXX.md` correspondiente
|
||||
3. **Editar YAML front-matter**:
|
||||
```yaml
|
||||
status: "In Progress"
|
||||
assignee: "@NombreAgente"
|
||||
started_date: "YYYY-MM-DD"
|
||||
```
|
||||
4. **Mover tarea** a columna "En Progreso" en Board.md
|
||||
5. **Commit**: `git commit -m "Start TASK-XXX: [descripcion breve]"`
|
||||
|
||||
### Completar una Tarea
|
||||
|
||||
1. **Verificar** TODOS los criterios de aceptacion cumplidos
|
||||
2. **Editar YAML front-matter**:
|
||||
```yaml
|
||||
status: "Done"
|
||||
completed_date: "YYYY-MM-DD"
|
||||
actual_hours: X
|
||||
```
|
||||
3. **Agregar seccion** "## Notas de Implementacion" con detalles
|
||||
4. **Mover tarea** a columna "Hecho" en Board.md
|
||||
5. **Commit**: `git commit -m "Fixes TASK-XXX: [descripcion breve]"`
|
||||
|
||||
### Reportar Bloqueo
|
||||
|
||||
1. Cambiar `status: "Blocked"`
|
||||
2. Agregar seccion "## Bloqueo" con:
|
||||
- Descripcion del bloqueo
|
||||
- Dependencias faltantes
|
||||
- Accion requerida
|
||||
3. Notificar en Board.md
|
||||
|
||||
---
|
||||
|
||||
## 4. Como Trabajar con Bugs
|
||||
|
||||
### Reportar un Bug
|
||||
|
||||
1. **Crear archivo** `/docs/planning/bugs/BUG-XXX-descripcion.md`
|
||||
2. **Usar plantilla YAML**:
|
||||
```yaml
|
||||
---
|
||||
id: "BUG-XXX"
|
||||
title: "Descripcion del bug"
|
||||
type: "Bug"
|
||||
status: "Open"
|
||||
severity: "P0|P1|P2|P3"
|
||||
priority: "Critica|Alta|Media|Baja"
|
||||
assignee: ""
|
||||
affected_module: "Backend|Frontend|Database"
|
||||
steps_to_reproduce:
|
||||
- "Paso 1"
|
||||
- "Paso 2"
|
||||
expected_behavior: "Lo que deberia pasar"
|
||||
actual_behavior: "Lo que pasa realmente"
|
||||
created_date: "YYYY-MM-DD"
|
||||
---
|
||||
```
|
||||
3. **Incluir secciones**: Descripcion, Contexto, Impacto
|
||||
4. **Commit**: `git commit -m "Report BUG-XXX: [descripcion]"`
|
||||
|
||||
### Resolver un Bug
|
||||
|
||||
1. Editar YAML: `status: "Done"`, agregar `resolved_date`
|
||||
2. Documentar solucion en seccion "## Solucion Implementada"
|
||||
3. Agregar referencia al commit: `fix_commit: "abc123"`
|
||||
4. **Commit**: `git commit -m "Fix BUG-XXX: [descripcion]"`
|
||||
|
||||
---
|
||||
|
||||
## 5. Formato YAML Front-Matter
|
||||
|
||||
### Historia de Usuario (US)
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: "US-AUTH-001"
|
||||
title: "Registro con Email"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
assignee: "@Backend-Agent"
|
||||
epic: "OQI-001"
|
||||
story_points: 5
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
```
|
||||
|
||||
### Tarea (TASK)
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: "TASK-001"
|
||||
title: "Implementar endpoint POST /auth/register"
|
||||
type: "Task"
|
||||
status: "Done"
|
||||
priority: "P1"
|
||||
assignee: "@Backend-Agent"
|
||||
parent_us: "US-AUTH-001"
|
||||
epic: "OQI-001"
|
||||
estimated_hours: 4
|
||||
actual_hours: 4.5
|
||||
created_date: "2025-12-05"
|
||||
completed_date: "2025-12-05"
|
||||
---
|
||||
```
|
||||
|
||||
### Requerimiento Funcional (RF)
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: "RF-AUTH-001"
|
||||
title: "OAuth Multi-proveedor"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "auth"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
```
|
||||
|
||||
### Especificacion Tecnica (ET)
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: "ET-AUTH-001"
|
||||
title: "OAuth Providers Implementation"
|
||||
type: "Specification"
|
||||
status: "Done"
|
||||
rf_parent: "RF-AUTH-001"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Convenciones de Commit
|
||||
|
||||
```
|
||||
<tipo>(<scope>): <descripcion>
|
||||
|
||||
Tipos:
|
||||
- feat: Nueva funcionalidad
|
||||
- fix: Correccion de bug
|
||||
- docs: Documentacion
|
||||
- refactor: Refactoring
|
||||
- test: Tests
|
||||
- chore: Mantenimiento
|
||||
|
||||
Scopes comunes:
|
||||
- auth, education, trading, investment, payments, ml, llm, portfolio
|
||||
- database, backend, frontend (capas)
|
||||
- US-XXX, TASK-XXX, BUG-XXX (referencias)
|
||||
|
||||
Ejemplos:
|
||||
- feat(auth): Implement OAuth 2.0 with Google
|
||||
- fix(BUG-001): Resolve login redirect issue
|
||||
- docs(US-AUTH-001): Add acceptance criteria
|
||||
- Start TASK-XXX: Begin implementation
|
||||
- Fixes TASK-XXX: Complete implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Estados Validos
|
||||
|
||||
### Para Tareas y User Stories
|
||||
|
||||
| Estado | Descripcion |
|
||||
|--------|-------------|
|
||||
| Backlog | En cola, no planificado |
|
||||
| To Do | Planificado para sprint actual |
|
||||
| In Progress | En desarrollo activo |
|
||||
| Blocked | Bloqueado por dependencia |
|
||||
| In Review | En revision/testing |
|
||||
| Done | Completado y validado |
|
||||
|
||||
### Para Bugs
|
||||
|
||||
| Estado | Descripcion |
|
||||
|--------|-------------|
|
||||
| Open | Reportado, pendiente |
|
||||
| In Progress | En investigacion/correccion |
|
||||
| Fixed | Corregido, pendiente validacion |
|
||||
| Done | Corregido y validado |
|
||||
| Won't Fix | No se corregira (documentar razon) |
|
||||
|
||||
---
|
||||
|
||||
## 8. Archivos Importantes
|
||||
|
||||
| Archivo | Proposito |
|
||||
|---------|-----------|
|
||||
| `/docs/planning/Board.md` | Tablero Kanban actual |
|
||||
| `/docs/planning/config.yml` | Configuracion del proyecto |
|
||||
| `/docs/04-fase-backlog/DEFINITION-OF-READY.md` | Criterios para Ready |
|
||||
| `/docs/04-fase-backlog/DEFINITION-OF-DONE.md` | Criterios para Done |
|
||||
| `/docs/_MAP.md` | Mapa de navegacion principal |
|
||||
| `/docs/02-definicion-modulos/_MAP.md` | Indice de modulos |
|
||||
|
||||
---
|
||||
|
||||
## 9. Validaciones Antes de Commit
|
||||
|
||||
- [ ] YAML front-matter valido (sin errores de sintaxis)
|
||||
- [ ] Campo `id` presente y unico
|
||||
- [ ] Campo `status` actualizado correctamente
|
||||
- [ ] Board.md actualizado si cambio estado
|
||||
- [ ] Referencias cruzadas verificadas
|
||||
- [ ] Criterios de aceptacion actualizados (si aplica)
|
||||
- [ ] _MAP.md actualizado si se agrego/elimino archivo
|
||||
|
||||
---
|
||||
|
||||
## 10. Flujo de Trabajo Recomendado
|
||||
|
||||
```
|
||||
1. Consultar Board.md para ver tareas disponibles
|
||||
2. Seleccionar tarea de "Por Hacer"
|
||||
3. Leer archivo TASK-XXX.md completo
|
||||
4. Verificar dependencias resueltas
|
||||
5. Cambiar status a "In Progress"
|
||||
6. Ejecutar trabajo
|
||||
7. Documentar notas de implementacion
|
||||
8. Verificar criterios de aceptacion
|
||||
9. Cambiar status a "Done"
|
||||
10. Actualizar Board.md
|
||||
11. Commit con mensaje apropiado
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Modulos del Proyecto
|
||||
|
||||
| Epica | Nombre | Descripcion | Estado |
|
||||
|-------|--------|-------------|--------|
|
||||
| OQI-001 | Fundamentos Auth | Autenticacion multi-proveedor | Implementado |
|
||||
| OQI-002 | Education | Cursos y lecciones de trading | Implementado |
|
||||
| OQI-003 | Trading Charts | Charts con indicadores tecnicos | Implementado |
|
||||
| OQI-004 | Investment Accounts | Cuentas de inversion | Implementado |
|
||||
| OQI-005 | Payments Stripe | Pagos y suscripciones | Implementado |
|
||||
| OQI-006 | ML Signals | Senales con Machine Learning | Implementado |
|
||||
| OQI-007 | LLM Agent | Agente conversacional | Implementado |
|
||||
| OQI-008 | Portfolio Manager | Gestion de portafolio | Implementado |
|
||||
|
||||
---
|
||||
|
||||
## 12. Contacto y Soporte
|
||||
|
||||
Para dudas sobre el proceso:
|
||||
- Revisar `/docs/README.md` para vision general
|
||||
- Consultar `/orchestration/directivas/` para directivas
|
||||
- Ver ejemplos en modulos completados (OQI-001 a OQI-008)
|
||||
|
||||
---
|
||||
|
||||
**Creado:** 2026-01-04
|
||||
**Mantenido por:** Architecture Team
|
||||
**Version:** 1.0
|
||||
52
apps/mcp-binance-connector/.env.example
Normal file
52
apps/mcp-binance-connector/.env.example
Normal file
@ -0,0 +1,52 @@
|
||||
# MCP Binance Connector Configuration
|
||||
# Copy this file to .env and configure values
|
||||
|
||||
# ==========================================
|
||||
# Server Configuration
|
||||
# ==========================================
|
||||
PORT=3606
|
||||
NODE_ENV=development
|
||||
|
||||
# ==========================================
|
||||
# MCP Authentication
|
||||
# ==========================================
|
||||
MCP_API_KEY=your_mcp_api_key_here
|
||||
|
||||
# ==========================================
|
||||
# Binance API Configuration
|
||||
# ==========================================
|
||||
BINANCE_API_KEY=your_binance_api_key
|
||||
BINANCE_API_SECRET=your_binance_api_secret
|
||||
|
||||
# ==========================================
|
||||
# Network Configuration
|
||||
# ==========================================
|
||||
# Use testnet by default (set to false for production)
|
||||
BINANCE_TESTNET=true
|
||||
BINANCE_FUTURES_TESTNET=true
|
||||
|
||||
# ==========================================
|
||||
# Risk Limits
|
||||
# ==========================================
|
||||
# Maximum value for a single order in USDT
|
||||
MAX_ORDER_VALUE_USDT=1000
|
||||
# Maximum daily trading volume in USDT
|
||||
MAX_DAILY_VOLUME_USDT=10000
|
||||
# Maximum allowed leverage
|
||||
MAX_LEVERAGE=20
|
||||
# Maximum position size as percentage of equity
|
||||
MAX_POSITION_SIZE_PCT=5
|
||||
|
||||
# ==========================================
|
||||
# Request Configuration
|
||||
# ==========================================
|
||||
# Timeout for requests to Binance (ms)
|
||||
REQUEST_TIMEOUT=10000
|
||||
# Maximum retries for failed requests
|
||||
MAX_RETRIES=3
|
||||
|
||||
# ==========================================
|
||||
# Logging
|
||||
# ==========================================
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=logs/mcp-binance.log
|
||||
31
apps/mcp-binance-connector/.gitignore
vendored
Normal file
31
apps/mcp-binance-connector/.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Misc
|
||||
*.tsbuildinfo
|
||||
57
apps/mcp-binance-connector/Dockerfile
Normal file
57
apps/mcp-binance-connector/Dockerfile
Normal file
@ -0,0 +1,57 @@
|
||||
# MCP Binance Connector Dockerfile
|
||||
# OrbiQuant Trading Platform
|
||||
# Version: 1.0.0
|
||||
|
||||
# ==========================================
|
||||
# Build Stage
|
||||
# ==========================================
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source and build
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
|
||||
# ==========================================
|
||||
# Production Stage
|
||||
# ==========================================
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install production dependencies only
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production && npm cache clean --force
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Create non-root user for security
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S mcpuser -u 1001 -G nodejs
|
||||
|
||||
# Create logs directory
|
||||
RUN mkdir -p logs && chown -R mcpuser:nodejs logs
|
||||
|
||||
# Switch to non-root user
|
||||
USER mcpuser
|
||||
|
||||
# Environment configuration
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3606
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3606
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3606/health || exit 1
|
||||
|
||||
# Start application
|
||||
CMD ["node", "dist/index.js"]
|
||||
345
apps/mcp-binance-connector/README.md
Normal file
345
apps/mcp-binance-connector/README.md
Normal file
@ -0,0 +1,345 @@
|
||||
# MCP Binance Connector
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Date:** 2026-01-04
|
||||
**System:** OrbiQuant Trading Platform + NEXUS v3.4 + SIMCO
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
MCP Server that exposes Binance cryptocurrency exchange capabilities as tools for AI agents. This service enables AI agents to:
|
||||
|
||||
- Query market data (prices, order books, candles)
|
||||
- Monitor account balances
|
||||
- View and manage open orders
|
||||
- Execute trades (buy/sell with market, limit, stop orders)
|
||||
|
||||
Uses [CCXT](https://github.com/ccxt/ccxt) library for Binance API integration.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Navigate to the project
|
||||
cd /home/isem/workspace-v1/projects/trading-platform/apps/mcp-binance-connector
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with your Binance API credentials
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | MCP Server port | 3606 |
|
||||
| `MCP_API_KEY` | API key for MCP authentication | - |
|
||||
| `BINANCE_API_KEY` | Binance API key | - |
|
||||
| `BINANCE_API_SECRET` | Binance API secret | - |
|
||||
| `BINANCE_TESTNET` | Use Binance testnet | true |
|
||||
| `MAX_ORDER_VALUE_USDT` | Max order value limit | 1000 |
|
||||
| `MAX_DAILY_VOLUME_USDT` | Max daily trading volume | 10000 |
|
||||
| `MAX_LEVERAGE` | Max leverage allowed | 20 |
|
||||
| `LOG_LEVEL` | Logging level | info |
|
||||
|
||||
### Example .env
|
||||
|
||||
```env
|
||||
PORT=3606
|
||||
BINANCE_API_KEY=your_api_key_here
|
||||
BINANCE_API_SECRET=your_api_secret_here
|
||||
BINANCE_TESTNET=true
|
||||
MAX_ORDER_VALUE_USDT=1000
|
||||
MAX_DAILY_VOLUME_USDT=10000
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Start Server
|
||||
|
||||
```bash
|
||||
# Development (with hot reload)
|
||||
npm run dev
|
||||
|
||||
# Production
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3606/health
|
||||
```
|
||||
|
||||
### List Available Tools
|
||||
|
||||
```bash
|
||||
curl http://localhost:3606/tools
|
||||
```
|
||||
|
||||
### Execute a Tool
|
||||
|
||||
```bash
|
||||
# Get BTC price
|
||||
curl -X POST http://localhost:3606/tools/binance_get_ticker \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "BTCUSDT"}}'
|
||||
|
||||
# Get order book
|
||||
curl -X POST http://localhost:3606/tools/binance_get_orderbook \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "ETHUSDT", "limit": 10}}'
|
||||
|
||||
# Get candlestick data
|
||||
curl -X POST http://localhost:3606/tools/binance_get_klines \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "BTCUSDT", "interval": "1h", "limit": 24}}'
|
||||
|
||||
# Get account balance (requires API keys)
|
||||
curl -X POST http://localhost:3606/tools/binance_get_account \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {}}'
|
||||
|
||||
# Create order (requires API keys) - HIGH RISK
|
||||
curl -X POST http://localhost:3606/tools/binance_create_order \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "BTCUSDT", "side": "buy", "type": "market", "amount": 0.001}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Tools Available
|
||||
|
||||
| Tool | Description | Risk Level |
|
||||
|------|-------------|------------|
|
||||
| `binance_get_ticker` | Get current price and 24h stats | LOW |
|
||||
| `binance_get_orderbook` | Get order book depth | LOW |
|
||||
| `binance_get_klines` | Get OHLCV candles | LOW |
|
||||
| `binance_get_account` | Get account balances | MEDIUM |
|
||||
| `binance_get_open_orders` | List open orders | MEDIUM |
|
||||
| `binance_create_order` | Create buy/sell order | HIGH (*) |
|
||||
| `binance_cancel_order` | Cancel pending order | MEDIUM |
|
||||
|
||||
(*) Tools marked with HIGH risk require explicit confirmation and pass through risk checks.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mcp-binance-connector/
|
||||
├── README.md # This file
|
||||
├── package.json # Dependencies
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── .env.example # Environment template
|
||||
├── Dockerfile # Container configuration
|
||||
└── src/
|
||||
├── index.ts # Server entry point
|
||||
├── config.ts # Configuration management
|
||||
├── utils/
|
||||
│ └── logger.ts # Winston logger
|
||||
├── services/
|
||||
│ └── binance-client.ts # CCXT wrapper
|
||||
├── middleware/
|
||||
│ └── risk-check.ts # Pre-trade risk validation
|
||||
└── tools/
|
||||
├── index.ts # Tool registry
|
||||
├── market.ts # Market data tools
|
||||
├── account.ts # Account tools
|
||||
└── orders.ts # Order management tools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Type Check
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
### Build Image
|
||||
|
||||
```bash
|
||||
docker build -t mcp-binance-connector:1.0.0 .
|
||||
```
|
||||
|
||||
### Run Container
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mcp-binance-connector \
|
||||
-p 3606:3606 \
|
||||
-e BINANCE_API_KEY=your_key \
|
||||
-e BINANCE_API_SECRET=your_secret \
|
||||
-e BINANCE_TESTNET=true \
|
||||
mcp-binance-connector:1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Claude
|
||||
|
||||
### MCP Configuration
|
||||
|
||||
Add to your Claude/MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"binance": {
|
||||
"url": "http://localhost:3606",
|
||||
"transport": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Agent Prompts
|
||||
|
||||
```
|
||||
"What's the current Bitcoin price?"
|
||||
-> Uses binance_get_ticker({ symbol: "BTCUSDT" })
|
||||
|
||||
"Show me the ETH order book"
|
||||
-> Uses binance_get_orderbook({ symbol: "ETHUSDT" })
|
||||
|
||||
"Get the last 50 hourly candles for BTC"
|
||||
-> Uses binance_get_klines({ symbol: "BTCUSDT", interval: "1h", limit: 50 })
|
||||
|
||||
"Check my Binance balance"
|
||||
-> Uses binance_get_account()
|
||||
|
||||
"Buy 0.01 BTC at market price"
|
||||
-> Uses binance_create_order({ symbol: "BTCUSDT", side: "buy", type: "market", amount: 0.01 })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risk Management
|
||||
|
||||
The connector includes built-in risk checks:
|
||||
|
||||
1. **Maximum Order Value**: Orders exceeding `MAX_ORDER_VALUE_USDT` are rejected
|
||||
2. **Daily Volume Limit**: Trading stops when `MAX_DAILY_VOLUME_USDT` is reached
|
||||
3. **Balance Check**: Buy orders verify sufficient balance
|
||||
4. **Testnet Default**: Testnet is enabled by default for safety
|
||||
5. **High-Risk Confirmation**: Orders require explicit confirmation flag
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
- `express` - HTTP server
|
||||
- `ccxt` - Cryptocurrency exchange library
|
||||
- `zod` - Input validation
|
||||
- `winston` - Logging
|
||||
- `dotenv` - Environment configuration
|
||||
- `@modelcontextprotocol/sdk` - MCP protocol
|
||||
|
||||
### Development
|
||||
- `typescript` - Type safety
|
||||
- `ts-node-dev` - Development server
|
||||
- `jest` - Testing framework
|
||||
- `eslint` - Code linting
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Binance Account** with API keys (optional for public data)
|
||||
2. **Testnet API Keys** for testing (recommended)
|
||||
3. **Node.js** >= 20.0.0
|
||||
|
||||
### Getting Binance API Keys
|
||||
|
||||
1. Log into [Binance](https://www.binance.com)
|
||||
2. Go to API Management
|
||||
3. Create a new API key
|
||||
4. Enable Spot Trading permissions
|
||||
5. (Optional) For testnet: [Binance Testnet](https://testnet.binance.vision/)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot connect to Binance
|
||||
|
||||
```bash
|
||||
# Check connectivity
|
||||
curl https://api.binance.com/api/v3/ping
|
||||
|
||||
# If using testnet, check testnet connectivity
|
||||
curl https://testnet.binance.vision/api/v3/ping
|
||||
```
|
||||
|
||||
### Authentication errors
|
||||
|
||||
```bash
|
||||
# Verify API keys are set
|
||||
cat .env | grep BINANCE
|
||||
|
||||
# Check health endpoint for config status
|
||||
curl http://localhost:3606/health
|
||||
```
|
||||
|
||||
### Order rejected by risk check
|
||||
|
||||
The order may exceed configured limits. Check:
|
||||
- `MAX_ORDER_VALUE_USDT` - single order limit
|
||||
- `MAX_DAILY_VOLUME_USDT` - daily trading limit
|
||||
- Available balance for buy orders
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [MCP Protocol](https://modelcontextprotocol.io)
|
||||
- [CCXT Documentation](https://docs.ccxt.com)
|
||||
- [Binance API](https://binance-docs.github.io/apidocs/)
|
||||
- Architecture: `/docs/01-arquitectura/MCP-BINANCE-CONNECTOR-SPEC.md`
|
||||
- MT4 Connector: `/apps/mcp-mt4-connector/` (reference implementation)
|
||||
|
||||
---
|
||||
|
||||
**Maintained by:** @PERFIL_MCP_DEVELOPER
|
||||
**Project:** OrbiQuant Trading Platform
|
||||
54
apps/mcp-binance-connector/package.json
Normal file
54
apps/mcp-binance-connector/package.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "mcp-binance-connector",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP Server for Binance trading operations via CCXT",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev --respawn src/index.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"health-check": "curl -s http://localhost:${PORT:-3606}/health || echo 'Server not running'"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"anthropic",
|
||||
"claude",
|
||||
"binance",
|
||||
"crypto",
|
||||
"trading",
|
||||
"ccxt"
|
||||
],
|
||||
"author": "OrbiQuant Trading Platform",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"ccxt": "^4.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"eslint": "^8.54.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
159
apps/mcp-binance-connector/src/config.ts
Normal file
159
apps/mcp-binance-connector/src/config.ts
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Configuration Module
|
||||
*
|
||||
* Manages environment variables and creates Binance clients via CCXT.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import ccxt from 'ccxt';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// ==========================================
|
||||
// Configuration Interface
|
||||
// ==========================================
|
||||
|
||||
export interface BinanceConfig {
|
||||
apiKey: string;
|
||||
apiSecret: string;
|
||||
testnet: boolean;
|
||||
futuresTestnet: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export interface RiskConfig {
|
||||
maxOrderValueUsdt: number;
|
||||
maxDailyVolumeUsdt: number;
|
||||
maxLeverage: number;
|
||||
maxPositionSizePct: number;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
port: number;
|
||||
nodeEnv: string;
|
||||
mcpApiKey: string;
|
||||
logLevel: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Configuration Loading
|
||||
// ==========================================
|
||||
|
||||
export const binanceConfig: BinanceConfig = {
|
||||
apiKey: process.env.BINANCE_API_KEY || '',
|
||||
apiSecret: process.env.BINANCE_API_SECRET || '',
|
||||
testnet: process.env.BINANCE_TESTNET === 'true',
|
||||
futuresTestnet: process.env.BINANCE_FUTURES_TESTNET === 'true',
|
||||
timeout: parseInt(process.env.REQUEST_TIMEOUT || '10000', 10),
|
||||
};
|
||||
|
||||
export const riskConfig: RiskConfig = {
|
||||
maxOrderValueUsdt: parseFloat(process.env.MAX_ORDER_VALUE_USDT || '1000'),
|
||||
maxDailyVolumeUsdt: parseFloat(process.env.MAX_DAILY_VOLUME_USDT || '10000'),
|
||||
maxLeverage: parseInt(process.env.MAX_LEVERAGE || '20', 10),
|
||||
maxPositionSizePct: parseFloat(process.env.MAX_POSITION_SIZE_PCT || '5'),
|
||||
};
|
||||
|
||||
export const serverConfig: ServerConfig = {
|
||||
port: parseInt(process.env.PORT || '3606', 10),
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
mcpApiKey: process.env.MCP_API_KEY || '',
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// Binance Client Factory
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Create a Binance Spot client
|
||||
*/
|
||||
export function createBinanceSpotClient(): ccxt.binance {
|
||||
const isTestnet = binanceConfig.testnet;
|
||||
|
||||
const client = new ccxt.binance({
|
||||
apiKey: binanceConfig.apiKey,
|
||||
secret: binanceConfig.apiSecret,
|
||||
sandbox: isTestnet,
|
||||
options: {
|
||||
defaultType: 'spot',
|
||||
adjustForTimeDifference: true,
|
||||
},
|
||||
enableRateLimit: true,
|
||||
rateLimit: 100,
|
||||
timeout: binanceConfig.timeout,
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Binance Futures client
|
||||
*/
|
||||
export function createBinanceFuturesClient(): ccxt.binance {
|
||||
const isTestnet = binanceConfig.futuresTestnet;
|
||||
|
||||
const client = new ccxt.binance({
|
||||
apiKey: binanceConfig.apiKey,
|
||||
secret: binanceConfig.apiSecret,
|
||||
sandbox: isTestnet,
|
||||
options: {
|
||||
defaultType: 'future',
|
||||
adjustForTimeDifference: true,
|
||||
},
|
||||
enableRateLimit: true,
|
||||
rateLimit: 100,
|
||||
timeout: binanceConfig.timeout,
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Configuration Validation
|
||||
// ==========================================
|
||||
|
||||
export function validateConfig(): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Binance API keys are optional for public endpoints
|
||||
// but required for account/trading operations
|
||||
if (!binanceConfig.apiKey && serverConfig.nodeEnv === 'production') {
|
||||
errors.push('BINANCE_API_KEY is required in production');
|
||||
}
|
||||
|
||||
if (!binanceConfig.apiSecret && serverConfig.nodeEnv === 'production') {
|
||||
errors.push('BINANCE_API_SECRET is required in production');
|
||||
}
|
||||
|
||||
// Validate risk limits
|
||||
if (riskConfig.maxOrderValueUsdt <= 0) {
|
||||
errors.push('MAX_ORDER_VALUE_USDT must be positive');
|
||||
}
|
||||
|
||||
if (riskConfig.maxLeverage < 1 || riskConfig.maxLeverage > 125) {
|
||||
errors.push('MAX_LEVERAGE must be between 1 and 125');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Exports
|
||||
// ==========================================
|
||||
|
||||
export default {
|
||||
binance: binanceConfig,
|
||||
risk: riskConfig,
|
||||
server: serverConfig,
|
||||
createBinanceSpotClient,
|
||||
createBinanceFuturesClient,
|
||||
validateConfig,
|
||||
};
|
||||
332
apps/mcp-binance-connector/src/index.ts
Normal file
332
apps/mcp-binance-connector/src/index.ts
Normal file
@ -0,0 +1,332 @@
|
||||
/**
|
||||
* MCP Server: Binance Connector
|
||||
*
|
||||
* Exposes Binance trading capabilities as MCP tools for AI agents.
|
||||
* Uses CCXT library to communicate with Binance API.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import dotenv from 'dotenv';
|
||||
import { mcpToolSchemas, toolHandlers, getAllToolDefinitions, toolRequiresConfirmation, getToolRiskLevel } from './tools';
|
||||
import { getBinanceClient } from './services/binance-client';
|
||||
import { serverConfig, binanceConfig, validateConfig } from './config';
|
||||
import { logger } from './utils/logger';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = serverConfig.port;
|
||||
const SERVICE_NAME = 'mcp-binance-connector';
|
||||
const VERSION = '1.0.0';
|
||||
|
||||
// ==========================================
|
||||
// Middleware
|
||||
// ==========================================
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, _res: Response, next: NextFunction) => {
|
||||
logger.info(`${req.method} ${req.path}`, {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// MCP API Key authentication (optional, for protected endpoints)
|
||||
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
const mcpKey = req.headers['x-mcp-api-key'];
|
||||
|
||||
// Skip auth if MCP_API_KEY is not configured
|
||||
if (!serverConfig.mcpApiKey) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mcpKey !== serverConfig.mcpApiKey) {
|
||||
res.status(401).json({ error: 'Invalid MCP API key' });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// Health & Status Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Health check endpoint
|
||||
*/
|
||||
app.get('/health', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const binanceConnected = await client.isConnected();
|
||||
const binanceConfigured = client.isConfigured();
|
||||
|
||||
res.json({
|
||||
status: binanceConnected ? 'healthy' : 'degraded',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
testnet: binanceConfig.testnet,
|
||||
dependencies: {
|
||||
binance: binanceConnected ? 'connected' : 'disconnected',
|
||||
binanceApiConfigured: binanceConfigured,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.json({
|
||||
status: 'unhealthy',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
testnet: binanceConfig.testnet,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List available MCP tools
|
||||
*/
|
||||
app.get('/tools', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: mcpToolSchemas.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
riskLevel: (tool as { riskLevel?: string }).riskLevel,
|
||||
requiresConfirmation: (tool as { requiresConfirmation?: boolean }).requiresConfirmation,
|
||||
})),
|
||||
count: mcpToolSchemas.length,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get specific tool schema
|
||||
*/
|
||||
app.get('/tools/:toolName', (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const tool = mcpToolSchemas.find((t) => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
res.status(404).json({
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: mcpToolSchemas.map((t) => t.name),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(tool);
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Execution Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Execute an MCP tool
|
||||
* POST /tools/:toolName
|
||||
* Body: { parameters: {...} }
|
||||
*/
|
||||
app.post('/tools/:toolName', authMiddleware, async (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const { parameters = {} } = req.body;
|
||||
|
||||
// Validate tool exists
|
||||
const handler = toolHandlers[toolName];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: Object.keys(toolHandlers),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Executing tool: ${toolName}`, {
|
||||
parameters,
|
||||
riskLevel: getToolRiskLevel(toolName),
|
||||
requiresConfirmation: toolRequiresConfirmation(toolName),
|
||||
});
|
||||
|
||||
const result = await handler(parameters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
tool: toolName,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Tool execution failed: ${toolName}`, { error, parameters });
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: (error as { issues: unknown[] }).issues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Protocol Endpoints (Standard)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* MCP Initialize
|
||||
* Returns server capabilities
|
||||
*/
|
||||
app.post('/mcp/initialize', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP List Tools
|
||||
* Returns all available tools in MCP format
|
||||
*/
|
||||
app.post('/mcp/tools/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: getAllToolDefinitions(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP Call Tool
|
||||
* Execute a tool with parameters
|
||||
*/
|
||||
app.post('/mcp/tools/call', authMiddleware, async (req: Request, res: Response) => {
|
||||
const { name, arguments: args = {} } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_request',
|
||||
message: 'Tool name is required',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = toolHandlers[name];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
code: 'unknown_tool',
|
||||
message: `Tool '${name}' not found`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(args);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_params',
|
||||
message: 'Invalid tool parameters',
|
||||
data: (error as { issues: unknown[] }).issues,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'internal_error',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Error Handler
|
||||
// ==========================================
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
logger.error('Unhandled error', { error: err });
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Start Server
|
||||
// ==========================================
|
||||
|
||||
// Validate configuration before starting
|
||||
const configValidation = validateConfig();
|
||||
if (!configValidation.valid) {
|
||||
logger.warn('Configuration warnings', { errors: configValidation.errors });
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('');
|
||||
console.log('================================================================');
|
||||
console.log(' MCP Binance Connector - OrbiQuant Trading Platform ');
|
||||
console.log('================================================================');
|
||||
console.log(` Service: ${SERVICE_NAME}`);
|
||||
console.log(` Version: ${VERSION}`);
|
||||
console.log(` Port: ${PORT}`);
|
||||
console.log(` Environment: ${serverConfig.nodeEnv}`);
|
||||
console.log(` Testnet Mode: ${binanceConfig.testnet ? 'ENABLED' : 'DISABLED'}`);
|
||||
console.log(` API Configured: ${binanceConfig.apiKey ? 'Yes' : 'No'}`);
|
||||
console.log('----------------------------------------------------------------');
|
||||
console.log(' Endpoints:');
|
||||
console.log(` - Health: http://localhost:${PORT}/health`);
|
||||
console.log(` - Tools: http://localhost:${PORT}/tools`);
|
||||
console.log('----------------------------------------------------------------');
|
||||
console.log(' MCP Tools Available:');
|
||||
mcpToolSchemas.forEach((tool) => {
|
||||
const risk = (tool as { riskLevel?: string }).riskLevel || 'N/A';
|
||||
const confirm = (tool as { requiresConfirmation?: boolean }).requiresConfirmation ? ' (!)' : '';
|
||||
console.log(` - ${tool.name} [${risk}]${confirm}`);
|
||||
});
|
||||
console.log('================================================================');
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Graceful Shutdown
|
||||
// ==========================================
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('Received SIGTERM, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('Received SIGINT, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
export default app;
|
||||
209
apps/mcp-binance-connector/src/middleware/risk-check.ts
Normal file
209
apps/mcp-binance-connector/src/middleware/risk-check.ts
Normal file
@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Risk Check Middleware
|
||||
*
|
||||
* Pre-trade risk validation to ensure orders comply with risk limits.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import { riskConfig } from '../config';
|
||||
import { getBinanceClient } from '../services/binance-client';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ==========================================
|
||||
// Types
|
||||
// ==========================================
|
||||
|
||||
export interface RiskCheckParams {
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
amount: number;
|
||||
price?: number;
|
||||
}
|
||||
|
||||
export interface RiskCheckResult {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
warnings?: string[];
|
||||
orderValue?: number;
|
||||
}
|
||||
|
||||
// Daily volume tracking (in-memory, resets on restart)
|
||||
let dailyVolume = 0;
|
||||
let lastVolumeResetDate = new Date().toDateString();
|
||||
|
||||
// ==========================================
|
||||
// Risk Check Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Reset daily volume counter at midnight
|
||||
*/
|
||||
function checkAndResetDailyVolume(): void {
|
||||
const today = new Date().toDateString();
|
||||
if (today !== lastVolumeResetDate) {
|
||||
dailyVolume = 0;
|
||||
lastVolumeResetDate = today;
|
||||
logger.info('Daily volume counter reset');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the quote asset from a symbol (e.g., USDT from BTCUSDT)
|
||||
*/
|
||||
function getQuoteAsset(symbol: string): string {
|
||||
const stablecoins = ['USDT', 'BUSD', 'USDC', 'TUSD', 'DAI'];
|
||||
for (const stable of stablecoins) {
|
||||
if (symbol.endsWith(stable)) {
|
||||
return stable;
|
||||
}
|
||||
}
|
||||
return 'USDT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform comprehensive risk check before order execution
|
||||
*/
|
||||
export async function performRiskCheck(params: RiskCheckParams): Promise<RiskCheckResult> {
|
||||
const { symbol, side, amount, price } = params;
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
checkAndResetDailyVolume();
|
||||
|
||||
const client = getBinanceClient();
|
||||
|
||||
// 1. Get current price if not provided
|
||||
let orderPrice = price;
|
||||
if (!orderPrice) {
|
||||
try {
|
||||
orderPrice = await client.getCurrentPrice(symbol);
|
||||
} catch (error) {
|
||||
logger.warn(`Could not fetch current price for ${symbol}, using amount as value estimate`);
|
||||
orderPrice = 1; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Calculate order value in quote currency (usually USDT)
|
||||
const orderValue = amount * orderPrice;
|
||||
|
||||
// 3. Check maximum order value
|
||||
if (orderValue > riskConfig.maxOrderValueUsdt) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Order value ${orderValue.toFixed(2)} USDT exceeds maximum ${riskConfig.maxOrderValueUsdt} USDT`,
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Check daily volume limit
|
||||
if (dailyVolume + orderValue > riskConfig.maxDailyVolumeUsdt) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Daily volume limit reached. Current: ${dailyVolume.toFixed(2)} USDT, Limit: ${riskConfig.maxDailyVolumeUsdt} USDT`,
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Check if API keys are configured for trading
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Binance API keys are not configured. Cannot execute trades.',
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 6. Verify we can connect to Binance
|
||||
const connected = await client.isConnected();
|
||||
if (!connected) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Cannot connect to Binance. Please check your network and API configuration.',
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// 7. Check balance for buy orders (if we have account access)
|
||||
if (side === 'buy') {
|
||||
try {
|
||||
const account = await client.getAccount();
|
||||
const quoteAsset = getQuoteAsset(symbol);
|
||||
const quoteBalance = account.balances.find(b => b.asset === quoteAsset);
|
||||
const available = quoteBalance?.free ?? 0;
|
||||
|
||||
if (available < orderValue) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Insufficient ${quoteAsset} balance. Required: ${orderValue.toFixed(2)}, Available: ${available.toFixed(2)}`,
|
||||
orderValue,
|
||||
};
|
||||
}
|
||||
|
||||
// Warning if using more than 50% of available balance
|
||||
if (orderValue > available * 0.5) {
|
||||
warnings.push(`This order uses ${((orderValue / available) * 100).toFixed(1)}% of your available ${quoteAsset}`);
|
||||
}
|
||||
} catch (error) {
|
||||
warnings.push('Could not verify account balance');
|
||||
logger.warn('Balance check failed', { error });
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Check for large order warning
|
||||
if (orderValue > riskConfig.maxOrderValueUsdt * 0.5) {
|
||||
warnings.push(`Large order: ${orderValue.toFixed(2)} USDT (${((orderValue / riskConfig.maxOrderValueUsdt) * 100).toFixed(0)}% of max)`);
|
||||
}
|
||||
|
||||
// All checks passed
|
||||
return {
|
||||
allowed: true,
|
||||
orderValue,
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Risk check failed', { error, params });
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Risk check error: ${message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record executed trade volume
|
||||
*/
|
||||
export function recordTradeVolume(orderValue: number): void {
|
||||
checkAndResetDailyVolume();
|
||||
dailyVolume += orderValue;
|
||||
logger.info(`Trade recorded. Daily volume: ${dailyVolume.toFixed(2)} USDT`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current daily volume
|
||||
*/
|
||||
export function getDailyVolume(): number {
|
||||
checkAndResetDailyVolume();
|
||||
return dailyVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining daily volume allowance
|
||||
*/
|
||||
export function getRemainingDailyVolume(): number {
|
||||
checkAndResetDailyVolume();
|
||||
return Math.max(0, riskConfig.maxDailyVolumeUsdt - dailyVolume);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Exports
|
||||
// ==========================================
|
||||
|
||||
export default {
|
||||
performRiskCheck,
|
||||
recordTradeVolume,
|
||||
getDailyVolume,
|
||||
getRemainingDailyVolume,
|
||||
};
|
||||
471
apps/mcp-binance-connector/src/services/binance-client.ts
Normal file
471
apps/mcp-binance-connector/src/services/binance-client.ts
Normal file
@ -0,0 +1,471 @@
|
||||
/**
|
||||
* Binance Client Service
|
||||
*
|
||||
* CCXT wrapper for Binance operations.
|
||||
* Provides a unified interface for both Spot and Futures trading.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import ccxt, { Ticker, OrderBook, OHLCV, Balance, Order, Trade } from 'ccxt';
|
||||
import { createBinanceSpotClient, createBinanceFuturesClient, binanceConfig } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ==========================================
|
||||
// Types
|
||||
// ==========================================
|
||||
|
||||
export interface BinanceTicker {
|
||||
symbol: string;
|
||||
price: number;
|
||||
bid: number;
|
||||
ask: number;
|
||||
high24h: number;
|
||||
low24h: number;
|
||||
volume24h: number;
|
||||
change24h: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface BinanceOrderBook {
|
||||
symbol: string;
|
||||
bids: [number, number][];
|
||||
asks: [number, number][];
|
||||
spread: number;
|
||||
spreadPercentage: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface BinanceKline {
|
||||
timestamp: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export interface BinanceAccountBalance {
|
||||
asset: string;
|
||||
free: number;
|
||||
locked: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface BinanceAccount {
|
||||
accountType: string;
|
||||
balances: BinanceAccountBalance[];
|
||||
canTrade: boolean;
|
||||
canWithdraw: boolean;
|
||||
updateTime: number;
|
||||
}
|
||||
|
||||
export interface BinanceOrder {
|
||||
id: string;
|
||||
symbol: string;
|
||||
side: string;
|
||||
type: string;
|
||||
price: number | null;
|
||||
amount: number;
|
||||
filled: number;
|
||||
remaining: number;
|
||||
status: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface CreateOrderParams {
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
type: 'market' | 'limit' | 'stop_loss' | 'take_profit';
|
||||
amount: number;
|
||||
price?: number;
|
||||
stopPrice?: number;
|
||||
}
|
||||
|
||||
export interface OrderResult {
|
||||
success: boolean;
|
||||
order?: BinanceOrder;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Binance Client Class
|
||||
// ==========================================
|
||||
|
||||
export class BinanceClient {
|
||||
private spotClient: ccxt.binance;
|
||||
private futuresClient: ccxt.binance;
|
||||
private marketsLoaded: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.spotClient = createBinanceSpotClient();
|
||||
this.futuresClient = createBinanceFuturesClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if client is properly configured
|
||||
*/
|
||||
isConfigured(): boolean {
|
||||
return binanceConfig.apiKey !== '' && binanceConfig.apiSecret !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connectivity to Binance
|
||||
*/
|
||||
async isConnected(): Promise<boolean> {
|
||||
try {
|
||||
await this.spotClient.fetchTime();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load markets if not already loaded
|
||||
*/
|
||||
private async ensureMarketsLoaded(): Promise<void> {
|
||||
if (!this.marketsLoaded) {
|
||||
await this.spotClient.loadMarkets();
|
||||
this.marketsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Market Data Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get ticker for a symbol
|
||||
*/
|
||||
async getTicker(symbol: string): Promise<BinanceTicker> {
|
||||
try {
|
||||
await this.ensureMarketsLoaded();
|
||||
const ticker: Ticker = await this.spotClient.fetchTicker(symbol);
|
||||
|
||||
return {
|
||||
symbol: ticker.symbol,
|
||||
price: ticker.last ?? 0,
|
||||
bid: ticker.bid ?? 0,
|
||||
ask: ticker.ask ?? 0,
|
||||
high24h: ticker.high ?? 0,
|
||||
low24h: ticker.low ?? 0,
|
||||
volume24h: ticker.baseVolume ?? 0,
|
||||
change24h: ticker.percentage ?? 0,
|
||||
timestamp: ticker.timestamp ?? Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get ticker for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order book for a symbol
|
||||
*/
|
||||
async getOrderBook(symbol: string, limit: number = 20): Promise<BinanceOrderBook> {
|
||||
try {
|
||||
await this.ensureMarketsLoaded();
|
||||
const orderbook: OrderBook = await this.spotClient.fetchOrderBook(symbol, limit);
|
||||
|
||||
const topBid = orderbook.bids[0]?.[0] ?? 0;
|
||||
const topAsk = orderbook.asks[0]?.[0] ?? 0;
|
||||
const spread = topAsk - topBid;
|
||||
const spreadPercentage = topBid > 0 ? (spread / topBid) * 100 : 0;
|
||||
|
||||
return {
|
||||
symbol,
|
||||
bids: orderbook.bids.slice(0, limit) as [number, number][],
|
||||
asks: orderbook.asks.slice(0, limit) as [number, number][],
|
||||
spread,
|
||||
spreadPercentage,
|
||||
timestamp: orderbook.timestamp ?? Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get order book for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OHLCV (klines/candles) for a symbol
|
||||
*/
|
||||
async getKlines(
|
||||
symbol: string,
|
||||
interval: string = '5m',
|
||||
limit: number = 100
|
||||
): Promise<BinanceKline[]> {
|
||||
try {
|
||||
await this.ensureMarketsLoaded();
|
||||
const ohlcv: OHLCV[] = await this.spotClient.fetchOHLCV(symbol, interval, undefined, limit);
|
||||
|
||||
return ohlcv.map((candle) => ({
|
||||
timestamp: candle[0] as number,
|
||||
open: candle[1] as number,
|
||||
high: candle[2] as number,
|
||||
low: candle[3] as number,
|
||||
close: candle[4] as number,
|
||||
volume: candle[5] as number,
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get klines for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Account Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get account balance
|
||||
*/
|
||||
async getAccount(): Promise<BinanceAccount> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const balance: Balance = await this.spotClient.fetchBalance();
|
||||
|
||||
// Filter non-zero balances
|
||||
const balances: BinanceAccountBalance[] = Object.entries(balance.total)
|
||||
.filter(([_, amount]) => (amount as number) > 0)
|
||||
.map(([asset, total]) => ({
|
||||
asset,
|
||||
free: (balance.free[asset] as number) ?? 0,
|
||||
locked: (balance.used[asset] as number) ?? 0,
|
||||
total: total as number,
|
||||
}));
|
||||
|
||||
return {
|
||||
accountType: 'SPOT',
|
||||
balances,
|
||||
canTrade: true,
|
||||
canWithdraw: true,
|
||||
updateTime: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to get account info', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get open orders
|
||||
*/
|
||||
async getOpenOrders(symbol?: string): Promise<BinanceOrder[]> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const orders: Order[] = await this.spotClient.fetchOpenOrders(symbol);
|
||||
|
||||
return orders.map((order) => ({
|
||||
id: order.id,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
type: order.type,
|
||||
price: order.price,
|
||||
amount: order.amount,
|
||||
filled: order.filled,
|
||||
remaining: order.remaining,
|
||||
status: order.status,
|
||||
createdAt: order.timestamp ?? Date.now(),
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Failed to get open orders', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trade history
|
||||
*/
|
||||
async getTradeHistory(symbol: string, limit: number = 50): Promise<Trade[]> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
return await this.spotClient.fetchMyTrades(symbol, undefined, limit);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get trade history for ${symbol}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Order Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Create a new order
|
||||
*/
|
||||
async createOrder(params: CreateOrderParams): Promise<OrderResult> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
await this.ensureMarketsLoaded();
|
||||
|
||||
let order: Order;
|
||||
|
||||
switch (params.type) {
|
||||
case 'market':
|
||||
order = await this.spotClient.createMarketOrder(
|
||||
params.symbol,
|
||||
params.side,
|
||||
params.amount
|
||||
);
|
||||
break;
|
||||
|
||||
case 'limit':
|
||||
if (!params.price) {
|
||||
return { success: false, error: 'Price is required for limit orders' };
|
||||
}
|
||||
order = await this.spotClient.createLimitOrder(
|
||||
params.symbol,
|
||||
params.side,
|
||||
params.amount,
|
||||
params.price
|
||||
);
|
||||
break;
|
||||
|
||||
case 'stop_loss':
|
||||
if (!params.stopPrice) {
|
||||
return { success: false, error: 'Stop price is required for stop loss orders' };
|
||||
}
|
||||
order = await this.spotClient.createOrder(
|
||||
params.symbol,
|
||||
'stop_loss',
|
||||
params.side,
|
||||
params.amount,
|
||||
undefined,
|
||||
{ stopPrice: params.stopPrice }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'take_profit':
|
||||
if (!params.stopPrice) {
|
||||
return { success: false, error: 'Stop price is required for take profit orders' };
|
||||
}
|
||||
order = await this.spotClient.createOrder(
|
||||
params.symbol,
|
||||
'take_profit',
|
||||
params.side,
|
||||
params.amount,
|
||||
undefined,
|
||||
{ stopPrice: params.stopPrice }
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
return { success: false, error: `Unsupported order type: ${params.type}` };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order: {
|
||||
id: order.id,
|
||||
symbol: order.symbol,
|
||||
side: order.side,
|
||||
type: order.type,
|
||||
price: order.price ?? order.average ?? null,
|
||||
amount: order.amount,
|
||||
filled: order.filled,
|
||||
remaining: order.remaining,
|
||||
status: order.status,
|
||||
createdAt: order.timestamp ?? Date.now(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Failed to create order', { error, params });
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an order
|
||||
*/
|
||||
async cancelOrder(orderId: string, symbol: string): Promise<OrderResult> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const result = await this.spotClient.cancelOrder(orderId, symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order: {
|
||||
id: result.id,
|
||||
symbol: result.symbol,
|
||||
side: result.side,
|
||||
type: result.type,
|
||||
price: result.price,
|
||||
amount: result.amount,
|
||||
filled: result.filled,
|
||||
remaining: result.remaining,
|
||||
status: 'CANCELLED',
|
||||
createdAt: result.timestamp ?? Date.now(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Failed to cancel order', { error, orderId, symbol });
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all orders for a symbol
|
||||
*/
|
||||
async cancelAllOrders(symbol: string): Promise<{ success: boolean; cancelledCount: number; error?: string }> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('Binance API keys not configured');
|
||||
}
|
||||
|
||||
const result = await this.spotClient.cancelAllOrders(symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cancelledCount: Array.isArray(result) ? result.length : 0,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('Failed to cancel all orders', { error, symbol });
|
||||
return { success: false, cancelledCount: 0, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current price for a symbol (helper method)
|
||||
*/
|
||||
async getCurrentPrice(symbol: string): Promise<number> {
|
||||
const ticker = await this.getTicker(symbol);
|
||||
return ticker.price;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Singleton Instance
|
||||
// ==========================================
|
||||
|
||||
let clientInstance: BinanceClient | null = null;
|
||||
|
||||
export function getBinanceClient(): BinanceClient {
|
||||
if (!clientInstance) {
|
||||
clientInstance = new BinanceClient();
|
||||
}
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
export function resetBinanceClient(): void {
|
||||
clientInstance = null;
|
||||
}
|
||||
265
apps/mcp-binance-connector/src/tools/account.ts
Normal file
265
apps/mcp-binance-connector/src/tools/account.ts
Normal file
@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Binance Account Tools
|
||||
*
|
||||
* - binance_get_account: Get account balance and status
|
||||
* - binance_get_open_orders: Get all open orders
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getBinanceClient, BinanceAccount, BinanceOrder } from '../services/binance-client';
|
||||
|
||||
// ==========================================
|
||||
// binance_get_account
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_account
|
||||
* Get account balance and status
|
||||
*/
|
||||
export const binanceGetAccountSchema = {
|
||||
name: 'binance_get_account',
|
||||
description: 'Get Binance account balance and status. Shows all assets with non-zero balance.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetAccountInputSchema = z.object({});
|
||||
|
||||
export type BinanceGetAccountInput = z.infer<typeof BinanceGetAccountInputSchema>;
|
||||
|
||||
export interface BinanceGetAccountResult {
|
||||
success: boolean;
|
||||
data?: BinanceAccount & { totalUsdtEstimate?: number };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_account(
|
||||
_params: BinanceGetAccountInput
|
||||
): Promise<BinanceGetAccountResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
const connected = await client.isConnected();
|
||||
if (!connected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cannot connect to Binance. Please check your network.',
|
||||
};
|
||||
}
|
||||
|
||||
const account = await client.getAccount();
|
||||
|
||||
// Estimate total value in USDT
|
||||
let totalUsdtEstimate = 0;
|
||||
for (const balance of account.balances) {
|
||||
if (balance.asset === 'USDT' || balance.asset === 'BUSD' || balance.asset === 'USDC') {
|
||||
totalUsdtEstimate += balance.total;
|
||||
} else if (balance.total > 0) {
|
||||
try {
|
||||
const price = await client.getCurrentPrice(`${balance.asset}USDT`);
|
||||
totalUsdtEstimate += balance.total * price;
|
||||
} catch {
|
||||
// Skip if no USDT pair exists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...account,
|
||||
totalUsdtEstimate,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetAccount(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetAccountInputSchema.parse(params);
|
||||
const result = await binance_get_account(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
|
||||
// Sort balances by total value
|
||||
const sortedBalances = [...d.balances].sort((a, b) => {
|
||||
// USDT first, then by total
|
||||
if (a.asset === 'USDT') return -1;
|
||||
if (b.asset === 'USDT') return 1;
|
||||
return b.total - a.total;
|
||||
});
|
||||
|
||||
let balancesStr = sortedBalances
|
||||
.slice(0, 20) // Top 20 assets
|
||||
.map((b) => {
|
||||
const lockedStr = b.locked > 0 ? ` (Locked: ${b.locked.toFixed(8)})` : '';
|
||||
return ` ${b.asset.padEnd(8)} Free: ${b.free.toFixed(8)}${lockedStr}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const formattedOutput = `
|
||||
Binance Account Information
|
||||
${'='.repeat(35)}
|
||||
Account Type: ${d.accountType}
|
||||
Can Trade: ${d.canTrade ? 'Yes' : 'No'}
|
||||
Can Withdraw: ${d.canWithdraw ? 'Yes' : 'No'}
|
||||
|
||||
Estimated Total Value
|
||||
---------------------
|
||||
~$${d.totalUsdtEstimate?.toFixed(2) ?? 'N/A'} USDT
|
||||
|
||||
Asset Balances (${d.balances.length} with balance)
|
||||
${'='.repeat(35)}
|
||||
${balancesStr}
|
||||
${d.balances.length > 20 ? `\n ... and ${d.balances.length - 20} more assets` : ''}
|
||||
|
||||
Last Update: ${new Date(d.updateTime).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_get_open_orders
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_open_orders
|
||||
* Get all open (pending) orders
|
||||
*/
|
||||
export const binanceGetOpenOrdersSchema = {
|
||||
name: 'binance_get_open_orders',
|
||||
description: 'Get all open (pending) orders. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter by trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetOpenOrdersInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()).optional(),
|
||||
});
|
||||
|
||||
export type BinanceGetOpenOrdersInput = z.infer<typeof BinanceGetOpenOrdersInputSchema>;
|
||||
|
||||
export interface BinanceGetOpenOrdersResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
orders: BinanceOrder[];
|
||||
count: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_open_orders(
|
||||
params: BinanceGetOpenOrdersInput
|
||||
): Promise<BinanceGetOpenOrdersResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
const orders = await client.getOpenOrders(params.symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
orders,
|
||||
count: orders.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetOpenOrders(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetOpenOrdersInputSchema.parse(params);
|
||||
const result = await binance_get_open_orders(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
|
||||
if (d.count === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No open orders${validatedParams.symbol ? ` for ${validatedParams.symbol}` : ''}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let ordersStr = d.orders
|
||||
.map((o) => {
|
||||
const priceStr = o.price ? `$${o.price.toFixed(8)}` : 'MARKET';
|
||||
const filledPct = o.amount > 0 ? ((o.filled / o.amount) * 100).toFixed(1) : '0';
|
||||
return ` #${o.id}
|
||||
Symbol: ${o.symbol} | ${o.side.toUpperCase()} | ${o.type.toUpperCase()}
|
||||
Price: ${priceStr} | Amount: ${o.amount.toFixed(8)}
|
||||
Filled: ${o.filled.toFixed(8)} (${filledPct}%) | Remaining: ${o.remaining.toFixed(8)}
|
||||
Status: ${o.status} | Created: ${new Date(o.createdAt).toISOString()}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
const formattedOutput = `
|
||||
Open Orders${validatedParams.symbol ? ` - ${validatedParams.symbol}` : ''}
|
||||
${'='.repeat(35)}
|
||||
Total Orders: ${d.count}
|
||||
|
||||
${ordersStr}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
288
apps/mcp-binance-connector/src/tools/index.ts
Normal file
288
apps/mcp-binance-connector/src/tools/index.ts
Normal file
@ -0,0 +1,288 @@
|
||||
/**
|
||||
* MCP Tools Index
|
||||
*
|
||||
* Exports all Binance MCP tools and their schemas for registration
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
// Import handlers for use in toolHandlers map
|
||||
import { handleBinanceGetTicker, handleBinanceGetOrderbook, handleBinanceGetKlines } from './market';
|
||||
import { handleBinanceGetAccount, handleBinanceGetOpenOrders } from './account';
|
||||
import { handleBinanceCreateOrder, handleBinanceCancelOrder } from './orders';
|
||||
|
||||
// ==========================================
|
||||
// Market Tools Exports
|
||||
// ==========================================
|
||||
|
||||
export {
|
||||
binanceGetTickerSchema,
|
||||
binance_get_ticker,
|
||||
handleBinanceGetTicker,
|
||||
BinanceGetTickerInputSchema,
|
||||
type BinanceGetTickerInput,
|
||||
type BinanceGetTickerResult,
|
||||
binanceGetOrderbookSchema,
|
||||
binance_get_orderbook,
|
||||
handleBinanceGetOrderbook,
|
||||
BinanceGetOrderbookInputSchema,
|
||||
type BinanceGetOrderbookInput,
|
||||
type BinanceGetOrderbookResult,
|
||||
binanceGetKlinesSchema,
|
||||
binance_get_klines,
|
||||
handleBinanceGetKlines,
|
||||
BinanceGetKlinesInputSchema,
|
||||
type BinanceGetKlinesInput,
|
||||
type BinanceGetKlinesResult,
|
||||
} from './market';
|
||||
|
||||
// ==========================================
|
||||
// Account Tools Exports
|
||||
// ==========================================
|
||||
|
||||
export {
|
||||
binanceGetAccountSchema,
|
||||
binance_get_account,
|
||||
handleBinanceGetAccount,
|
||||
BinanceGetAccountInputSchema,
|
||||
type BinanceGetAccountInput,
|
||||
type BinanceGetAccountResult,
|
||||
binanceGetOpenOrdersSchema,
|
||||
binance_get_open_orders,
|
||||
handleBinanceGetOpenOrders,
|
||||
BinanceGetOpenOrdersInputSchema,
|
||||
type BinanceGetOpenOrdersInput,
|
||||
type BinanceGetOpenOrdersResult,
|
||||
} from './account';
|
||||
|
||||
// ==========================================
|
||||
// Order Tools Exports
|
||||
// ==========================================
|
||||
|
||||
export {
|
||||
binanceCreateOrderSchema,
|
||||
binance_create_order,
|
||||
handleBinanceCreateOrder,
|
||||
BinanceCreateOrderInputSchema,
|
||||
type BinanceCreateOrderInput,
|
||||
type BinanceCreateOrderResult,
|
||||
binanceCancelOrderSchema,
|
||||
binance_cancel_order,
|
||||
handleBinanceCancelOrder,
|
||||
BinanceCancelOrderInputSchema,
|
||||
type BinanceCancelOrderInput,
|
||||
type BinanceCancelOrderResult,
|
||||
} from './orders';
|
||||
|
||||
// ==========================================
|
||||
// Tool Registry
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* All available MCP tools with their schemas
|
||||
* Follows MCP protocol format
|
||||
*/
|
||||
export const mcpToolSchemas = [
|
||||
// Market Data Tools (Low Risk)
|
||||
{
|
||||
name: 'binance_get_ticker',
|
||||
description: 'Get the current price and 24-hour statistics for a Binance trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT, BNBUSDT)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
{
|
||||
name: 'binance_get_orderbook',
|
||||
description: 'Get the order book (bids and asks) with the specified depth for a trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Order book depth (5, 10, 20, 50, or 100). Default: 20',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
{
|
||||
name: 'binance_get_klines',
|
||||
description: 'Get historical candlestick (OHLCV) data for technical analysis',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
interval: {
|
||||
type: 'string',
|
||||
description: 'Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 5m',
|
||||
enum: ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'],
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of candles to retrieve (max 500). Default: 100',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
// Account Tools (Medium Risk)
|
||||
{
|
||||
name: 'binance_get_account',
|
||||
description: 'Get Binance account balance and status. Shows all assets with non-zero balance.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
{
|
||||
name: 'binance_get_open_orders',
|
||||
description: 'Get all open (pending) orders. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter by trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
|
||||
// Order Tools (High Risk)
|
||||
{
|
||||
name: 'binance_create_order',
|
||||
description: 'Create a new buy or sell order on Binance. HIGH RISK - Ensure you validate with the user before executing.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT)',
|
||||
},
|
||||
side: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Order direction: buy or sell',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['market', 'limit', 'stop_loss', 'take_profit'],
|
||||
description: 'Order type. Default: market',
|
||||
},
|
||||
amount: {
|
||||
type: 'number',
|
||||
description: 'Amount of the base asset to buy/sell',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'Price per unit (required for limit orders)',
|
||||
},
|
||||
stopPrice: {
|
||||
type: 'number',
|
||||
description: 'Stop price (required for stop_loss and take_profit orders)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'side', 'amount'] as string[],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
{
|
||||
name: 'binance_cancel_order',
|
||||
description: 'Cancel a pending order by order ID and symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'Order ID to cancel',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'orderId'] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Tool handler routing map
|
||||
* Maps tool names to their handler functions
|
||||
*/
|
||||
export const toolHandlers: Record<
|
||||
string,
|
||||
(params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>
|
||||
> = {
|
||||
// Market tools
|
||||
binance_get_ticker: handleBinanceGetTicker,
|
||||
binance_get_orderbook: handleBinanceGetOrderbook,
|
||||
binance_get_klines: handleBinanceGetKlines,
|
||||
|
||||
// Account tools
|
||||
binance_get_account: handleBinanceGetAccount,
|
||||
binance_get_open_orders: handleBinanceGetOpenOrders,
|
||||
|
||||
// Order tools
|
||||
binance_create_order: handleBinanceCreateOrder,
|
||||
binance_cancel_order: handleBinanceCancelOrder,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all tool definitions for MCP protocol
|
||||
*/
|
||||
export function getAllToolDefinitions() {
|
||||
return mcpToolSchemas.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool by name
|
||||
*/
|
||||
export function getToolByName(name: string) {
|
||||
return mcpToolSchemas.find((tool) => tool.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool requires confirmation
|
||||
*/
|
||||
export function toolRequiresConfirmation(name: string): boolean {
|
||||
const tool = mcpToolSchemas.find((t) => t.name === name);
|
||||
return (tool as { requiresConfirmation?: boolean })?.requiresConfirmation === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool risk level
|
||||
*/
|
||||
export function getToolRiskLevel(name: string): string {
|
||||
const tool = mcpToolSchemas.find((t) => t.name === name);
|
||||
return (tool as { riskLevel?: string })?.riskLevel ?? 'UNKNOWN';
|
||||
}
|
||||
392
apps/mcp-binance-connector/src/tools/market.ts
Normal file
392
apps/mcp-binance-connector/src/tools/market.ts
Normal file
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Binance Market Data Tools
|
||||
*
|
||||
* - binance_get_ticker: Get current price and 24h stats
|
||||
* - binance_get_orderbook: Get order book depth
|
||||
* - binance_get_klines: Get OHLCV candles
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getBinanceClient, BinanceTicker, BinanceOrderBook, BinanceKline } from '../services/binance-client';
|
||||
|
||||
// ==========================================
|
||||
// binance_get_ticker
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_ticker
|
||||
* Get current price and 24h statistics for a trading pair
|
||||
*/
|
||||
export const binanceGetTickerSchema = {
|
||||
name: 'binance_get_ticker',
|
||||
description: 'Get the current price and 24-hour statistics for a Binance trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT, BNBUSDT)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetTickerInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
});
|
||||
|
||||
export type BinanceGetTickerInput = z.infer<typeof BinanceGetTickerInputSchema>;
|
||||
|
||||
export interface BinanceGetTickerResult {
|
||||
success: boolean;
|
||||
data?: BinanceTicker;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_ticker(
|
||||
params: BinanceGetTickerInput
|
||||
): Promise<BinanceGetTickerResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const ticker = await client.getTicker(params.symbol);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: ticker,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetTicker(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetTickerInputSchema.parse(params);
|
||||
const result = await binance_get_ticker(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
const changeSymbol = d.change24h >= 0 ? '+' : '';
|
||||
|
||||
const formattedOutput = `
|
||||
Binance Ticker: ${d.symbol}
|
||||
${'='.repeat(35)}
|
||||
Current Price: $${d.price.toFixed(getPriceDecimals(d.symbol))}
|
||||
Bid: $${d.bid.toFixed(getPriceDecimals(d.symbol))}
|
||||
Ask: $${d.ask.toFixed(getPriceDecimals(d.symbol))}
|
||||
|
||||
24h Statistics
|
||||
--------------
|
||||
High: $${d.high24h.toFixed(getPriceDecimals(d.symbol))}
|
||||
Low: $${d.low24h.toFixed(getPriceDecimals(d.symbol))}
|
||||
Volume: ${formatVolume(d.volume24h)}
|
||||
Change: ${changeSymbol}${d.change24h.toFixed(2)}%
|
||||
|
||||
Last Update: ${new Date(d.timestamp).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_get_orderbook
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_orderbook
|
||||
* Get order book (bids and asks) with specified depth
|
||||
*/
|
||||
export const binanceGetOrderbookSchema = {
|
||||
name: 'binance_get_orderbook',
|
||||
description: 'Get the order book (bids and asks) with the specified depth for a trading pair',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Order book depth (5, 10, 20, 50, or 100). Default: 20',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetOrderbookInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
limit: z.number().int().min(5).max(100).default(20),
|
||||
});
|
||||
|
||||
export type BinanceGetOrderbookInput = z.infer<typeof BinanceGetOrderbookInputSchema>;
|
||||
|
||||
export interface BinanceGetOrderbookResult {
|
||||
success: boolean;
|
||||
data?: BinanceOrderBook;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_orderbook(
|
||||
params: BinanceGetOrderbookInput
|
||||
): Promise<BinanceGetOrderbookResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const orderbook = await client.getOrderBook(params.symbol, params.limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: orderbook,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetOrderbook(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetOrderbookInputSchema.parse(params);
|
||||
const result = await binance_get_orderbook(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
const decimals = getPriceDecimals(d.symbol);
|
||||
|
||||
// Format top 10 levels
|
||||
const topBids = d.bids.slice(0, 10);
|
||||
const topAsks = d.asks.slice(0, 10);
|
||||
|
||||
let bidsStr = topBids
|
||||
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
|
||||
.join('\n');
|
||||
|
||||
let asksStr = topAsks
|
||||
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
|
||||
.join('\n');
|
||||
|
||||
const formattedOutput = `
|
||||
Order Book: ${d.symbol}
|
||||
${'='.repeat(35)}
|
||||
Spread: $${d.spread.toFixed(decimals)} (${d.spreadPercentage.toFixed(4)}%)
|
||||
|
||||
Top ${topAsks.length} Asks (Sell Orders)
|
||||
${'-'.repeat(25)}
|
||||
${asksStr}
|
||||
|
||||
Top ${topBids.length} Bids (Buy Orders)
|
||||
${'-'.repeat(25)}
|
||||
${bidsStr}
|
||||
|
||||
Timestamp: ${new Date(d.timestamp).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_get_klines
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_get_klines
|
||||
* Get historical OHLCV candles for technical analysis
|
||||
*/
|
||||
export const binanceGetKlinesSchema = {
|
||||
name: 'binance_get_klines',
|
||||
description: 'Get historical candlestick (OHLCV) data for technical analysis',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
interval: {
|
||||
type: 'string',
|
||||
description: 'Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 5m',
|
||||
enum: ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'],
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of candles to retrieve (max 500). Default: 100',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const BinanceGetKlinesInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
interval: z.enum(['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w']).default('5m'),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
});
|
||||
|
||||
export type BinanceGetKlinesInput = z.infer<typeof BinanceGetKlinesInputSchema>;
|
||||
|
||||
export interface BinanceGetKlinesResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
symbol: string;
|
||||
interval: string;
|
||||
candles: BinanceKline[];
|
||||
count: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_get_klines(
|
||||
params: BinanceGetKlinesInput
|
||||
): Promise<BinanceGetKlinesResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
const klines = await client.getKlines(params.symbol, params.interval, params.limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
symbol: params.symbol,
|
||||
interval: params.interval,
|
||||
candles: klines,
|
||||
count: klines.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceGetKlines(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceGetKlinesInputSchema.parse(params);
|
||||
const result = await binance_get_klines(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const d = result.data;
|
||||
const decimals = getPriceDecimals(d.symbol);
|
||||
|
||||
// Get last 5 candles for display
|
||||
const recentCandles = d.candles.slice(-5);
|
||||
|
||||
let candlesStr = recentCandles
|
||||
.map((c) => {
|
||||
const time = new Date(c.timestamp).toISOString().slice(0, 16).replace('T', ' ');
|
||||
const direction = c.close >= c.open ? 'UP' : 'DOWN';
|
||||
return ` ${time} | O:${c.open.toFixed(decimals)} H:${c.high.toFixed(decimals)} L:${c.low.toFixed(decimals)} C:${c.close.toFixed(decimals)} | V:${formatVolume(c.volume)} | ${direction}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// Calculate basic stats
|
||||
const closes = d.candles.map((c) => c.close);
|
||||
const high = Math.max(...d.candles.map((c) => c.high));
|
||||
const low = Math.min(...d.candles.map((c) => c.low));
|
||||
const avgVolume = d.candles.reduce((sum, c) => sum + c.volume, 0) / d.candles.length;
|
||||
|
||||
const formattedOutput = `
|
||||
Klines: ${d.symbol} (${d.interval})
|
||||
${'='.repeat(45)}
|
||||
Retrieved: ${d.count} candles
|
||||
|
||||
Period Statistics
|
||||
-----------------
|
||||
Highest High: $${high.toFixed(decimals)}
|
||||
Lowest Low: $${low.toFixed(decimals)}
|
||||
Avg Volume: ${formatVolume(avgVolume)}
|
||||
|
||||
Recent Candles (last 5)
|
||||
-----------------------
|
||||
${candlesStr}
|
||||
|
||||
First Candle: ${new Date(d.candles[0].timestamp).toISOString()}
|
||||
Last Candle: ${new Date(d.candles[d.candles.length - 1].timestamp).toISOString()}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Helper Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get appropriate decimal places for price display
|
||||
*/
|
||||
function getPriceDecimals(symbol: string): number {
|
||||
const upper = symbol.toUpperCase();
|
||||
|
||||
// Stablecoins and fiat pairs
|
||||
if (upper.includes('USD') && !upper.startsWith('BTC') && !upper.startsWith('ETH')) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
// BTC pairs
|
||||
if (upper === 'BTCUSDT' || upper === 'BTCBUSD') {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ETH pairs
|
||||
if (upper === 'ETHUSDT' || upper === 'ETHBUSD') {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Small value coins
|
||||
if (upper.includes('SHIB') || upper.includes('DOGE') || upper.includes('PEPE')) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
// Default
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format large volume numbers
|
||||
*/
|
||||
function formatVolume(volume: number): string {
|
||||
if (volume >= 1_000_000_000) {
|
||||
return `${(volume / 1_000_000_000).toFixed(2)}B`;
|
||||
}
|
||||
if (volume >= 1_000_000) {
|
||||
return `${(volume / 1_000_000).toFixed(2)}M`;
|
||||
}
|
||||
if (volume >= 1_000) {
|
||||
return `${(volume / 1_000).toFixed(2)}K`;
|
||||
}
|
||||
return volume.toFixed(4);
|
||||
}
|
||||
334
apps/mcp-binance-connector/src/tools/orders.ts
Normal file
334
apps/mcp-binance-connector/src/tools/orders.ts
Normal file
@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Binance Order Management Tools
|
||||
*
|
||||
* - binance_create_order: Create a new order (HIGH RISK)
|
||||
* - binance_cancel_order: Cancel a pending order
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getBinanceClient, BinanceOrder, CreateOrderParams } from '../services/binance-client';
|
||||
import { performRiskCheck, recordTradeVolume } from '../middleware/risk-check';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ==========================================
|
||||
// binance_create_order
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_create_order
|
||||
* Create a new buy or sell order
|
||||
* HIGH RISK - Requires confirmation
|
||||
*/
|
||||
export const binanceCreateOrderSchema = {
|
||||
name: 'binance_create_order',
|
||||
description: 'Create a new buy or sell order on Binance. HIGH RISK - Ensure you validate with the user before executing.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT)',
|
||||
},
|
||||
side: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Order direction: buy or sell',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['market', 'limit', 'stop_loss', 'take_profit'],
|
||||
description: 'Order type. Default: market',
|
||||
},
|
||||
amount: {
|
||||
type: 'number',
|
||||
description: 'Amount of the base asset to buy/sell',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'Price per unit (required for limit orders)',
|
||||
},
|
||||
stopPrice: {
|
||||
type: 'number',
|
||||
description: 'Stop price (required for stop_loss and take_profit orders)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'side', 'amount'] as string[],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
requiresConfirmation: true,
|
||||
};
|
||||
|
||||
export const BinanceCreateOrderInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
side: z.enum(['buy', 'sell']),
|
||||
type: z.enum(['market', 'limit', 'stop_loss', 'take_profit']).default('market'),
|
||||
amount: z.number().positive(),
|
||||
price: z.number().positive().optional(),
|
||||
stopPrice: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
export type BinanceCreateOrderInput = z.infer<typeof BinanceCreateOrderInputSchema>;
|
||||
|
||||
export interface BinanceCreateOrderResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
order: BinanceOrder;
|
||||
riskWarnings?: string[];
|
||||
};
|
||||
error?: string;
|
||||
riskCheckFailed?: boolean;
|
||||
}
|
||||
|
||||
export async function binance_create_order(
|
||||
params: BinanceCreateOrderInput
|
||||
): Promise<BinanceCreateOrderResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Perform risk check
|
||||
const riskCheck = await performRiskCheck({
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
amount: params.amount,
|
||||
price: params.price,
|
||||
});
|
||||
|
||||
if (!riskCheck.allowed) {
|
||||
logger.warn('Order rejected by risk check', {
|
||||
params,
|
||||
reason: riskCheck.reason,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: riskCheck.reason,
|
||||
riskCheckFailed: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Validate order parameters
|
||||
if (params.type === 'limit' && !params.price) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Price is required for limit orders',
|
||||
};
|
||||
}
|
||||
|
||||
if ((params.type === 'stop_loss' || params.type === 'take_profit') && !params.stopPrice) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Stop price is required for ${params.type} orders`,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Create the order
|
||||
const orderParams: CreateOrderParams = {
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
type: params.type,
|
||||
amount: params.amount,
|
||||
price: params.price,
|
||||
stopPrice: params.stopPrice,
|
||||
};
|
||||
|
||||
const result = await client.createOrder(orderParams);
|
||||
|
||||
if (result.success && result.order) {
|
||||
// Record trade volume for daily limit tracking
|
||||
if (riskCheck.orderValue) {
|
||||
recordTradeVolume(riskCheck.orderValue);
|
||||
}
|
||||
|
||||
logger.info('Order created successfully', {
|
||||
orderId: result.order.id,
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
amount: params.amount,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
order: result.order,
|
||||
riskWarnings: riskCheck.warnings,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Failed to create order',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Order creation failed', { error, params });
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceCreateOrder(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceCreateOrderInputSchema.parse(params);
|
||||
const result = await binance_create_order(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const o = result.data.order;
|
||||
const priceStr = o.price ? `$${o.price.toFixed(8)}` : 'MARKET';
|
||||
|
||||
let warningsStr = '';
|
||||
if (result.data.riskWarnings && result.data.riskWarnings.length > 0) {
|
||||
warningsStr = `\n\nWarnings:\n${result.data.riskWarnings.map((w) => ` - ${w}`).join('\n')}`;
|
||||
}
|
||||
|
||||
const formattedOutput = `
|
||||
Order Created Successfully
|
||||
${'='.repeat(35)}
|
||||
Order ID: ${o.id}
|
||||
Symbol: ${o.symbol}
|
||||
Side: ${o.side.toUpperCase()}
|
||||
Type: ${o.type.toUpperCase()}
|
||||
Price: ${priceStr}
|
||||
Amount: ${o.amount.toFixed(8)}
|
||||
Filled: ${o.filled.toFixed(8)}
|
||||
Status: ${o.status}
|
||||
Created: ${new Date(o.createdAt).toISOString()}${warningsStr}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
const errorPrefix = result.riskCheckFailed ? 'Risk Check Failed: ' : 'Error: ';
|
||||
return {
|
||||
content: [{ type: 'text', text: `${errorPrefix}${result.error}` }],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// binance_cancel_order
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Tool: binance_cancel_order
|
||||
* Cancel a pending order
|
||||
*/
|
||||
export const binanceCancelOrderSchema = {
|
||||
name: 'binance_cancel_order',
|
||||
description: 'Cancel a pending order by order ID and symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading pair symbol (e.g., BTCUSDT)',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'Order ID to cancel',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'orderId'] as string[],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
};
|
||||
|
||||
export const BinanceCancelOrderInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
|
||||
orderId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type BinanceCancelOrderInput = z.infer<typeof BinanceCancelOrderInputSchema>;
|
||||
|
||||
export interface BinanceCancelOrderResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
cancelledOrder: BinanceOrder;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function binance_cancel_order(
|
||||
params: BinanceCancelOrderInput
|
||||
): Promise<BinanceCancelOrderResult> {
|
||||
try {
|
||||
const client = getBinanceClient();
|
||||
|
||||
if (!client.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Binance API keys are not configured',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await client.cancelOrder(params.orderId, params.symbol);
|
||||
|
||||
if (result.success && result.order) {
|
||||
logger.info('Order cancelled successfully', {
|
||||
orderId: params.orderId,
|
||||
symbol: params.symbol,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
cancelledOrder: result.order,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Failed to cancel order',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Order cancellation failed', { error, params });
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBinanceCancelOrder(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = BinanceCancelOrderInputSchema.parse(params);
|
||||
const result = await binance_cancel_order(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const o = result.data.cancelledOrder;
|
||||
|
||||
const formattedOutput = `
|
||||
Order Cancelled Successfully
|
||||
${'='.repeat(35)}
|
||||
Order ID: ${o.id}
|
||||
Symbol: ${o.symbol}
|
||||
Side: ${o.side.toUpperCase()}
|
||||
Type: ${o.type.toUpperCase()}
|
||||
Original Amount: ${o.amount.toFixed(8)}
|
||||
Filled Before Cancel: ${o.filled.toFixed(8)}
|
||||
Status: ${o.status}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedOutput }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${result.error}` }],
|
||||
};
|
||||
}
|
||||
67
apps/mcp-binance-connector/src/utils/logger.ts
Normal file
67
apps/mcp-binance-connector/src/utils/logger.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Logger Utility
|
||||
*
|
||||
* Winston-based logging for the MCP Binance Connector.
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import winston from 'winston';
|
||||
import { serverConfig } from '../config';
|
||||
|
||||
const { combine, timestamp, printf, colorize, errors } = winston.format;
|
||||
|
||||
// Custom log format
|
||||
const logFormat = printf(({ level, message, timestamp, ...metadata }) => {
|
||||
let msg = `${timestamp} [${level}]: ${message}`;
|
||||
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
msg += ` ${JSON.stringify(metadata)}`;
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
|
||||
// Create logger instance
|
||||
export const logger = winston.createLogger({
|
||||
level: serverConfig.logLevel,
|
||||
format: combine(
|
||||
errors({ stack: true }),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
logFormat
|
||||
),
|
||||
defaultMeta: { service: 'mcp-binance-connector' },
|
||||
transports: [
|
||||
// Console transport
|
||||
new winston.transports.Console({
|
||||
format: combine(
|
||||
colorize(),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
logFormat
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Add file transport in production
|
||||
if (serverConfig.nodeEnv === 'production') {
|
||||
logger.add(
|
||||
new winston.transports.File({
|
||||
filename: process.env.LOG_FILE || 'logs/mcp-binance.log',
|
||||
maxsize: 10 * 1024 * 1024, // 10MB
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
|
||||
logger.add(
|
||||
new winston.transports.File({
|
||||
filename: 'logs/mcp-binance-error.log',
|
||||
level: 'error',
|
||||
maxsize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default logger;
|
||||
23
apps/mcp-binance-connector/tsconfig.json
Normal file
23
apps/mcp-binance-connector/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
31
apps/mcp-mt4-connector/.env.example
Normal file
31
apps/mcp-mt4-connector/.env.example
Normal file
@ -0,0 +1,31 @@
|
||||
# MCP MT4 Connector Configuration
|
||||
# Copy this file to .env and configure values
|
||||
|
||||
# ==========================================
|
||||
# Server Configuration
|
||||
# ==========================================
|
||||
PORT=3605
|
||||
NODE_ENV=development
|
||||
|
||||
# ==========================================
|
||||
# MT4 Gateway Connection
|
||||
# ==========================================
|
||||
# Host where mt4-gateway is running
|
||||
MT4_GATEWAY_HOST=localhost
|
||||
# Port of the mt4-gateway service
|
||||
MT4_GATEWAY_PORT=8081
|
||||
# Authentication token for mt4-gateway
|
||||
MT4_GATEWAY_AUTH_TOKEN=your-secret-token-here
|
||||
|
||||
# ==========================================
|
||||
# Request Configuration
|
||||
# ==========================================
|
||||
# Timeout for requests to MT4 Gateway (ms)
|
||||
REQUEST_TIMEOUT=10000
|
||||
# Maximum retries for failed requests
|
||||
MAX_RETRIES=3
|
||||
|
||||
# ==========================================
|
||||
# Logging
|
||||
# ==========================================
|
||||
LOG_LEVEL=info
|
||||
31
apps/mcp-mt4-connector/.gitignore
vendored
Normal file
31
apps/mcp-mt4-connector/.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
277
apps/mcp-mt4-connector/README.md
Normal file
277
apps/mcp-mt4-connector/README.md
Normal file
@ -0,0 +1,277 @@
|
||||
# MCP MT4 Connector
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Date:** 2026-01-04
|
||||
**System:** OrbiQuant Trading Platform + NEXUS v3.4 + SIMCO
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
MCP Server that exposes MetaTrader 4 (MT4) trading capabilities as tools for AI agents. This service enables AI agents to:
|
||||
- Query account information
|
||||
- Monitor open positions
|
||||
- Execute trades (BUY/SELL)
|
||||
- Manage positions (modify SL/TP, close)
|
||||
- Get real-time price quotes
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Navigate to the project
|
||||
cd /home/isem/workspace-v1/projects/trading-platform/apps/mcp-mt4-connector
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with your MT4 Gateway credentials
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | MCP Server port | 3605 |
|
||||
| `MT4_GATEWAY_HOST` | MT4 Gateway hostname | localhost |
|
||||
| `MT4_GATEWAY_PORT` | MT4 Gateway port | 8081 |
|
||||
| `MT4_GATEWAY_AUTH_TOKEN` | Authentication token | secret |
|
||||
| `REQUEST_TIMEOUT` | Request timeout (ms) | 10000 |
|
||||
| `LOG_LEVEL` | Logging level | info |
|
||||
|
||||
### Example .env
|
||||
```env
|
||||
PORT=3605
|
||||
MT4_GATEWAY_HOST=localhost
|
||||
MT4_GATEWAY_PORT=8081
|
||||
MT4_GATEWAY_AUTH_TOKEN=your-secure-token
|
||||
REQUEST_TIMEOUT=10000
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Start Server
|
||||
|
||||
```bash
|
||||
# Development (with hot reload)
|
||||
npm run dev
|
||||
|
||||
# Production
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3605/health
|
||||
```
|
||||
|
||||
### List Available Tools
|
||||
|
||||
```bash
|
||||
curl http://localhost:3605/tools
|
||||
```
|
||||
|
||||
### Execute a Tool
|
||||
|
||||
```bash
|
||||
# Get account info
|
||||
curl -X POST http://localhost:3605/tools/mt4_get_account \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {}}'
|
||||
|
||||
# Get price quote
|
||||
curl -X POST http://localhost:3605/tools/mt4_get_quote \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "XAUUSD"}}'
|
||||
|
||||
# Execute trade
|
||||
curl -X POST http://localhost:3605/tools/mt4_execute_trade \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameters": {"symbol": "XAUUSD", "action": "buy", "lots": 0.1}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Tools Available
|
||||
|
||||
| Tool | Description | Risk |
|
||||
|------|-------------|------|
|
||||
| `mt4_get_account` | Get account balance, equity, margin | Low |
|
||||
| `mt4_get_positions` | List open positions | Low |
|
||||
| `mt4_get_quote` | Get current bid/ask price | Low |
|
||||
| `mt4_execute_trade` | Execute BUY/SELL order | HIGH |
|
||||
| `mt4_close_position` | Close a position | HIGH |
|
||||
| `mt4_modify_position` | Modify SL/TP | Medium |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mcp-mt4-connector/
|
||||
├── README.md # This file
|
||||
├── package.json # Dependencies
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── .env.example # Environment template
|
||||
├── .gitignore # Git ignore rules
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md # Architecture documentation
|
||||
│ └── MCP-TOOLS-SPEC.md # Detailed tool specifications
|
||||
└── src/
|
||||
├── index.ts # Server entry point
|
||||
├── tools/
|
||||
│ ├── index.ts # Tool exports
|
||||
│ ├── account.ts # mt4_get_account
|
||||
│ ├── positions.ts # mt4_get_positions, mt4_close_position
|
||||
│ ├── trading.ts # mt4_execute_trade, mt4_modify_position
|
||||
│ └── quotes.ts # mt4_get_quote
|
||||
└── services/
|
||||
└── mt4-client.ts # MT4 Gateway HTTP client
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Type Check
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with Claude
|
||||
|
||||
### MCP Configuration
|
||||
|
||||
Add to your Claude/MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mt4": {
|
||||
"url": "http://localhost:3605",
|
||||
"transport": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Agent Prompts
|
||||
|
||||
```
|
||||
"Check my MT4 account balance"
|
||||
→ Uses mt4_get_account
|
||||
|
||||
"What's the current gold price?"
|
||||
→ Uses mt4_get_quote({ symbol: "XAUUSD" })
|
||||
|
||||
"Buy 0.1 lots of XAUUSD with stop loss at 2640"
|
||||
→ Uses mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1, stopLoss: 2640 })
|
||||
|
||||
"Close my profitable gold positions"
|
||||
→ Uses mt4_get_positions({ symbol: "XAUUSD" }) + mt4_close_position for each
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
- `express` - HTTP server
|
||||
- `axios` - HTTP client
|
||||
- `zod` - Input validation
|
||||
- `dotenv` - Environment configuration
|
||||
- `@modelcontextprotocol/sdk` - MCP protocol
|
||||
|
||||
### Development
|
||||
- `typescript` - Type safety
|
||||
- `ts-node-dev` - Development server
|
||||
- `jest` - Testing framework
|
||||
- `eslint` - Code linting
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **MT4 Gateway** running on configured host:port
|
||||
2. **MT4 Terminal** connected with EA Bridge active
|
||||
3. **Node.js** >= 18.0.0
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot connect to MT4 Gateway
|
||||
```bash
|
||||
# Check if mt4-gateway is running
|
||||
curl http://localhost:8081/status
|
||||
|
||||
# Verify environment variables
|
||||
cat .env | grep MT4
|
||||
```
|
||||
|
||||
### Tool execution fails
|
||||
```bash
|
||||
# Check health endpoint for dependency status
|
||||
curl http://localhost:3605/health
|
||||
|
||||
# Check server logs
|
||||
npm run dev # Logs will show in console
|
||||
```
|
||||
|
||||
### Invalid parameters error
|
||||
```bash
|
||||
# Verify tool schema
|
||||
curl http://localhost:3605/tools/mt4_execute_trade
|
||||
|
||||
# Check parameter names match schema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [MCP Protocol](https://modelcontextprotocol.io)
|
||||
- MT4 Gateway: `apps/mt4-gateway/`
|
||||
- SIMCO-MCP: `orchestration/directivas/simco/SIMCO-MCP.md`
|
||||
- Architecture: `docs/ARCHITECTURE.md`
|
||||
- Tool Specs: `docs/MCP-TOOLS-SPEC.md`
|
||||
|
||||
---
|
||||
|
||||
**Maintained by:** @PERFIL_MCP_DEVELOPER
|
||||
**Project:** OrbiQuant Trading Platform
|
||||
272
apps/mcp-mt4-connector/docs/ARCHITECTURE.md
Normal file
272
apps/mcp-mt4-connector/docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,272 @@
|
||||
# MCP MT4 Connector - Architecture
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Date:** 2026-01-04
|
||||
**System:** OrbiQuant Trading Platform
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP MT4 Connector is a Model Context Protocol (MCP) server that exposes MetaTrader 4 trading capabilities as tools that AI agents can use. It acts as a bridge between MCP-compatible AI systems (like Claude) and the MT4 trading terminal.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ AI Agent (Claude) │
|
||||
│ │
|
||||
│ "Execute a buy order for 0.1 lots of XAUUSD with SL at 2640" │
|
||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
||||
│ MCP Protocol
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ MCP MT4 Connector (Port 3605) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Express Server │ │
|
||||
│ │ │ │
|
||||
│ │ /health - Health check endpoint │ │
|
||||
│ │ /tools - List available tools │ │
|
||||
│ │ /tools/:name - Execute specific tool │ │
|
||||
│ │ /mcp/* - MCP protocol endpoints │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Tool Handlers │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
|
||||
│ │ │ account.ts │ │positions.ts │ │ trading.ts │ │ quotes.ts │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ MT4 Client Service │ │
|
||||
│ │ │ │
|
||||
│ │ - HTTP client wrapper for mt4-gateway │ │
|
||||
│ │ - Request/response handling │ │
|
||||
│ │ - Error management │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
||||
│ HTTP/REST
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ MT4 Gateway (Port 8081) │
|
||||
│ │
|
||||
│ Python service that communicates with MT4 EA Bridge │
|
||||
└─────────────────────────────────────┬───────────────────────────────────┘
|
||||
│ Local Socket/HTTP
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ MT4 Terminal + EA Bridge │
|
||||
│ │
|
||||
│ Windows MT4 with Expert Advisor running │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. Express Server (`src/index.ts`)
|
||||
|
||||
The main entry point that:
|
||||
- Hosts the MCP server on port 3605
|
||||
- Provides REST endpoints for tool execution
|
||||
- Implements MCP protocol endpoints
|
||||
- Handles health checks and service discovery
|
||||
|
||||
**Endpoints:**
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/health` | GET | Health check with MT4 connection status |
|
||||
| `/tools` | GET | List all available MCP tools |
|
||||
| `/tools/:name` | GET | Get specific tool schema |
|
||||
| `/tools/:name` | POST | Execute tool with parameters |
|
||||
| `/mcp/initialize` | POST | MCP initialization handshake |
|
||||
| `/mcp/tools/list` | POST | MCP tool listing |
|
||||
| `/mcp/tools/call` | POST | MCP tool execution |
|
||||
|
||||
### 2. Tool Handlers (`src/tools/`)
|
||||
|
||||
Individual tool implementations following the MCP tool pattern:
|
||||
|
||||
| File | Tools | Description |
|
||||
|------|-------|-------------|
|
||||
| `account.ts` | `mt4_get_account` | Account information retrieval |
|
||||
| `positions.ts` | `mt4_get_positions`, `mt4_close_position` | Position management |
|
||||
| `trading.ts` | `mt4_execute_trade`, `mt4_modify_position` | Trade execution |
|
||||
| `quotes.ts` | `mt4_get_quote` | Price data retrieval |
|
||||
|
||||
Each tool handler:
|
||||
- Defines Zod validation schemas
|
||||
- Implements the core logic
|
||||
- Formats responses for MCP protocol
|
||||
- Handles errors gracefully
|
||||
|
||||
### 3. MT4 Client Service (`src/services/mt4-client.ts`)
|
||||
|
||||
HTTP client wrapper that:
|
||||
- Manages connection to mt4-gateway
|
||||
- Handles authentication (Bearer token)
|
||||
- Provides typed interfaces for all operations
|
||||
- Manages request timeouts and retries
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Example: Execute Trade
|
||||
|
||||
```
|
||||
1. Agent Request
|
||||
POST /mcp/tools/call
|
||||
{
|
||||
"name": "mt4_execute_trade",
|
||||
"arguments": {
|
||||
"symbol": "XAUUSD",
|
||||
"action": "buy",
|
||||
"lots": 0.1,
|
||||
"stopLoss": 2640,
|
||||
"takeProfit": 2680
|
||||
}
|
||||
}
|
||||
|
||||
2. Tool Handler (trading.ts)
|
||||
- Validates input with Zod schema
|
||||
- Checks MT4 connection status
|
||||
- Validates SL/TP logic
|
||||
- Calls MT4Client.executeTrade()
|
||||
|
||||
3. MT4 Client Service
|
||||
- Formats request payload
|
||||
- Sends HTTP POST to mt4-gateway
|
||||
- Receives and parses response
|
||||
|
||||
4. MT4 Gateway
|
||||
- Forwards to EA Bridge
|
||||
- EA executes trade on MT4
|
||||
- Returns result
|
||||
|
||||
5. Response to Agent
|
||||
{
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "Trade Executed Successfully\n..."
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | 3605 | MCP server port |
|
||||
| `MT4_GATEWAY_HOST` | localhost | MT4 Gateway host |
|
||||
| `MT4_GATEWAY_PORT` | 8081 | MT4 Gateway port |
|
||||
| `MT4_GATEWAY_AUTH_TOKEN` | secret | Authentication token |
|
||||
| `REQUEST_TIMEOUT` | 10000 | Request timeout (ms) |
|
||||
| `LOG_LEVEL` | info | Logging level |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Types
|
||||
|
||||
1. **Connection Errors**
|
||||
- MT4 Gateway unreachable
|
||||
- MT4 Terminal disconnected
|
||||
|
||||
2. **Validation Errors**
|
||||
- Invalid parameters (Zod)
|
||||
- Invalid SL/TP configuration
|
||||
|
||||
3. **Trading Errors**
|
||||
- Insufficient margin
|
||||
- Market closed
|
||||
- Invalid symbol
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message description"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Authentication**
|
||||
- Bearer token for mt4-gateway communication
|
||||
- No external network exposure by default
|
||||
|
||||
2. **Validation**
|
||||
- All inputs validated with Zod schemas
|
||||
- Type-safe throughout the codebase
|
||||
|
||||
3. **Rate Limiting**
|
||||
- Consider adding rate limiting for production
|
||||
- Respect MT4 order frequency limits
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `express` | HTTP server |
|
||||
| `axios` | HTTP client |
|
||||
| `zod` | Input validation |
|
||||
| `dotenv` | Environment configuration |
|
||||
| `@modelcontextprotocol/sdk` | MCP protocol types |
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Docker (Future)
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY dist ./dist
|
||||
EXPOSE 3605
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- MCP Protocol: https://modelcontextprotocol.io
|
||||
- MT4 Bridge Client: `apps/mt4-gateway/src/providers/mt4_bridge_client.py`
|
||||
- Trading Platform: `projects/trading-platform/`
|
||||
- SIMCO-MCP Directive: `orchestration/directivas/simco/SIMCO-MCP.md`
|
||||
428
apps/mcp-mt4-connector/docs/MCP-TOOLS-SPEC.md
Normal file
428
apps/mcp-mt4-connector/docs/MCP-TOOLS-SPEC.md
Normal file
@ -0,0 +1,428 @@
|
||||
# MCP MT4 Connector - Tools Specification
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Date:** 2026-01-04
|
||||
**Total Tools:** 6
|
||||
|
||||
---
|
||||
|
||||
## Tool Overview
|
||||
|
||||
| Tool Name | Description | Risk Level |
|
||||
|-----------|-------------|------------|
|
||||
| `mt4_get_account` | Get account information | Low |
|
||||
| `mt4_get_positions` | List open positions | Low |
|
||||
| `mt4_get_quote` | Get current price quote | Low |
|
||||
| `mt4_execute_trade` | Execute market order | HIGH |
|
||||
| `mt4_close_position` | Close a position | HIGH |
|
||||
| `mt4_modify_position` | Modify SL/TP | Medium |
|
||||
|
||||
---
|
||||
|
||||
## mt4_get_account
|
||||
|
||||
### Description
|
||||
Retrieves comprehensive account information from the connected MT4 terminal including balance, equity, margin, leverage, and broker details.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| - | - | - | No parameters required |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"balance": 10000.00,
|
||||
"equity": 10250.50,
|
||||
"margin": 500.00,
|
||||
"freeMargin": 9750.50,
|
||||
"marginLevel": 2050.10,
|
||||
"profit": 250.50,
|
||||
"currency": "USD",
|
||||
"leverage": 100,
|
||||
"name": "Demo Account",
|
||||
"server": "ICMarkets-Demo",
|
||||
"company": "IC Markets"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Get account info
|
||||
const result = await mt4_get_account({});
|
||||
|
||||
// Response content:
|
||||
// MT4 Account Information
|
||||
// =======================
|
||||
// Account Name: Demo Account
|
||||
// Server: ICMarkets-Demo
|
||||
// Broker: IC Markets
|
||||
// Leverage: 1:100
|
||||
//
|
||||
// Financial Summary
|
||||
// -----------------
|
||||
// Balance: 10000.00 USD
|
||||
// Equity: 10250.50 USD
|
||||
// Profit/Loss: +250.50 USD
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
|
||||
|
||||
---
|
||||
|
||||
## mt4_get_positions
|
||||
|
||||
### Description
|
||||
Lists all currently open trading positions from the MT4 terminal. Can optionally filter by symbol.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `symbol` | string | No | Filter positions by symbol (e.g., XAUUSD) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"positions": [
|
||||
{
|
||||
"ticket": 123456,
|
||||
"symbol": "XAUUSD",
|
||||
"type": "buy",
|
||||
"lots": 0.10,
|
||||
"openPrice": 2650.50,
|
||||
"currentPrice": 2655.00,
|
||||
"stopLoss": 2640.00,
|
||||
"takeProfit": 2680.00,
|
||||
"profit": 45.00,
|
||||
"swap": -1.20,
|
||||
"openTime": "2026-01-04T10:30:00Z",
|
||||
"magic": 12345,
|
||||
"comment": "AI Signal"
|
||||
}
|
||||
],
|
||||
"totalProfit": 45.00,
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Get all positions
|
||||
const result = await mt4_get_positions({});
|
||||
|
||||
// Get only XAUUSD positions
|
||||
const goldPositions = await mt4_get_positions({ symbol: "XAUUSD" });
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
|
||||
|
||||
---
|
||||
|
||||
## mt4_get_quote
|
||||
|
||||
### Description
|
||||
Retrieves the current bid/ask prices for a trading symbol. Also calculates the spread in points/pips.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD, EURUSD) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"symbol": "XAUUSD",
|
||||
"bid": 2650.50,
|
||||
"ask": 2650.80,
|
||||
"spread": 0.30,
|
||||
"timestamp": "2026-01-04T12:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Get gold price
|
||||
const quote = await mt4_get_quote({ symbol: "XAUUSD" });
|
||||
|
||||
// Response content:
|
||||
// Price Quote: XAUUSD
|
||||
// =========================
|
||||
// Bid: 2650.50
|
||||
// Ask: 2650.80
|
||||
// Spread: 0.30 (3.0 pips)
|
||||
// Time: 2026-01-04T12:00:00.000Z
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | No quote data available for {symbol} | Verify symbol is available on broker |
|
||||
|
||||
---
|
||||
|
||||
## mt4_execute_trade
|
||||
|
||||
### Description
|
||||
Opens a new trading position with a market order. Supports BUY and SELL orders with optional stop loss and take profit levels.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD) |
|
||||
| `action` | string | Yes | Trade direction: "buy" or "sell" |
|
||||
| `lots` | number | Yes | Volume in lots (e.g., 0.01, 0.1, 1.0) |
|
||||
| `stopLoss` | number | No | Stop loss price level |
|
||||
| `takeProfit` | number | No | Take profit price level |
|
||||
| `slippage` | number | No | Maximum slippage in points (default: 3) |
|
||||
| `magic` | number | No | Magic number for EA identification (default: 12345) |
|
||||
| `comment` | string | No | Order comment (max 31 chars) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"ticket": 123456,
|
||||
"message": "Order placed successfully",
|
||||
"symbol": "XAUUSD",
|
||||
"action": "buy",
|
||||
"lots": 0.1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Simple buy order
|
||||
const result = await mt4_execute_trade({
|
||||
symbol: "XAUUSD",
|
||||
action: "buy",
|
||||
lots: 0.1
|
||||
});
|
||||
|
||||
// Buy with risk management
|
||||
const result = await mt4_execute_trade({
|
||||
symbol: "XAUUSD",
|
||||
action: "buy",
|
||||
lots: 0.1,
|
||||
stopLoss: 2640.00,
|
||||
takeProfit: 2680.00,
|
||||
comment: "AI Signal - Gold Long"
|
||||
});
|
||||
|
||||
// Sell order
|
||||
const result = await mt4_execute_trade({
|
||||
symbol: "EURUSD",
|
||||
action: "sell",
|
||||
lots: 0.5,
|
||||
stopLoss: 1.1050,
|
||||
takeProfit: 1.0900
|
||||
});
|
||||
```
|
||||
|
||||
### Validation Rules
|
||||
- For BUY orders: stopLoss must be below takeProfit
|
||||
- For SELL orders: stopLoss must be above takeProfit
|
||||
- Lots must be positive and reasonable (max 100)
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | For BUY orders, stop loss must be below take profit | Fix SL/TP levels |
|
||||
| - | For SELL orders, stop loss must be above take profit | Fix SL/TP levels |
|
||||
| - | Trade execution failed | Check margin, market hours |
|
||||
|
||||
---
|
||||
|
||||
## mt4_close_position
|
||||
|
||||
### Description
|
||||
Closes an open position by ticket number. Can optionally close only a partial volume.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `ticket` | number | Yes | Position ticket number to close |
|
||||
| `lots` | number | No | Partial volume to close (default: close all) |
|
||||
| `slippage` | number | No | Maximum slippage in points (default: 3) |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"ticket": 123456,
|
||||
"message": "Position closed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Close entire position
|
||||
const result = await mt4_close_position({
|
||||
ticket: 123456
|
||||
});
|
||||
|
||||
// Close partial position (0.5 of 1.0 lots)
|
||||
const result = await mt4_close_position({
|
||||
ticket: 123456,
|
||||
lots: 0.5
|
||||
});
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | Position with ticket {x} not found | Verify ticket number |
|
||||
| - | Requested lots exceeds position size | Reduce lots parameter |
|
||||
|
||||
---
|
||||
|
||||
## mt4_modify_position
|
||||
|
||||
### Description
|
||||
Modifies the stop loss and/or take profit levels of an existing open position.
|
||||
|
||||
### Parameters
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `ticket` | number | Yes | Position ticket number to modify |
|
||||
| `stopLoss` | number | No | New stop loss price level |
|
||||
| `takeProfit` | number | No | New take profit price level |
|
||||
|
||||
### Return Value
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"ticket": 123456,
|
||||
"message": "Position modified successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
```typescript
|
||||
// Set both SL and TP
|
||||
const result = await mt4_modify_position({
|
||||
ticket: 123456,
|
||||
stopLoss: 2640.00,
|
||||
takeProfit: 2680.00
|
||||
});
|
||||
|
||||
// Update only take profit (trailing)
|
||||
const result = await mt4_modify_position({
|
||||
ticket: 123456,
|
||||
takeProfit: 2700.00
|
||||
});
|
||||
|
||||
// Set only stop loss (risk management)
|
||||
const result = await mt4_modify_position({
|
||||
ticket: 123456,
|
||||
stopLoss: 2650.00
|
||||
});
|
||||
```
|
||||
|
||||
### Validation Rules
|
||||
- At least one of stopLoss or takeProfit must be provided
|
||||
- For BUY positions: stopLoss must be below takeProfit
|
||||
- For SELL positions: stopLoss must be above takeProfit
|
||||
|
||||
### Errors
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| - | At least one of stopLoss or takeProfit must be provided | Add SL or TP |
|
||||
| - | Position with ticket {x} not found | Verify ticket number |
|
||||
| - | For BUY positions, stop loss must be below take profit | Fix SL/TP levels |
|
||||
|
||||
---
|
||||
|
||||
## Common Error Responses
|
||||
|
||||
### Connection Error
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "MT4 terminal is not connected"
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Error
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Validation error",
|
||||
"details": [
|
||||
{
|
||||
"path": ["symbol"],
|
||||
"message": "Required"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Trading Error
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Trade execution failed: Insufficient margin"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples with AI Agent
|
||||
|
||||
### Scenario 1: Check Account and Open Trade
|
||||
```
|
||||
Agent: "Check my account balance and if equity is above 10000, buy 0.1 lots of XAUUSD"
|
||||
|
||||
1. Call mt4_get_account({})
|
||||
2. Parse response, check equity > 10000
|
||||
3. Call mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1 })
|
||||
```
|
||||
|
||||
### Scenario 2: Risk Management
|
||||
```
|
||||
Agent: "Set stop loss at 2640 and take profit at 2680 for my gold position"
|
||||
|
||||
1. Call mt4_get_positions({ symbol: "XAUUSD" })
|
||||
2. Get ticket number from response
|
||||
3. Call mt4_modify_position({ ticket: 123456, stopLoss: 2640, takeProfit: 2680 })
|
||||
```
|
||||
|
||||
### Scenario 3: Close Profitable Trades
|
||||
```
|
||||
Agent: "Close all profitable gold positions"
|
||||
|
||||
1. Call mt4_get_positions({ symbol: "XAUUSD" })
|
||||
2. Filter positions where profit > 0
|
||||
3. For each: Call mt4_close_position({ ticket: ticketNumber })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 0.1.0 | 2026-01-04 | Initial release with 6 core tools |
|
||||
7170
apps/mcp-mt4-connector/package-lock.json
generated
Normal file
7170
apps/mcp-mt4-connector/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
apps/mcp-mt4-connector/package.json
Normal file
53
apps/mcp-mt4-connector/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "mcp-mt4-connector",
|
||||
"version": "0.1.0",
|
||||
"description": "MCP Server for MT4 trading operations via EA Bridge",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev --respawn src/index.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"health-check": "curl -s http://localhost:${PORT:-3605}/health || echo 'Server not running'"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"anthropic",
|
||||
"claude",
|
||||
"mt4",
|
||||
"metatrader",
|
||||
"trading",
|
||||
"forex"
|
||||
],
|
||||
"author": "OrbiQuant Trading Platform",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"axios": "^1.6.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||
"@typescript-eslint/parser": "^6.13.0",
|
||||
"eslint": "^8.54.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
291
apps/mcp-mt4-connector/src/index.ts
Normal file
291
apps/mcp-mt4-connector/src/index.ts
Normal file
@ -0,0 +1,291 @@
|
||||
/**
|
||||
* MCP Server: MT4 Connector
|
||||
*
|
||||
* Exposes MT4 trading capabilities as MCP tools for AI agents.
|
||||
* Communicates with mt4-gateway service to execute trading operations.
|
||||
*
|
||||
* @version 0.1.0
|
||||
* @author OrbiQuant Trading Platform
|
||||
*/
|
||||
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import dotenv from 'dotenv';
|
||||
import { mcpToolSchemas, toolHandlers } from './tools';
|
||||
import { getMT4Client } from './services/mt4-client';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3605;
|
||||
const SERVICE_NAME = 'mcp-mt4-connector';
|
||||
const VERSION = '0.1.0';
|
||||
|
||||
// ==========================================
|
||||
// Middleware
|
||||
// ==========================================
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, _res: Response, next: NextFunction) => {
|
||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Health & Status Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Health check endpoint
|
||||
*/
|
||||
app.get('/health', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
const mt4Connected = await client.isConnected();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
dependencies: {
|
||||
mt4Gateway: mt4Connected ? 'connected' : 'disconnected',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.json({
|
||||
status: 'degraded',
|
||||
service: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
timestamp: new Date().toISOString(),
|
||||
dependencies: {
|
||||
mt4Gateway: 'error',
|
||||
},
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List available MCP tools
|
||||
*/
|
||||
app.get('/tools', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: mcpToolSchemas,
|
||||
count: mcpToolSchemas.length,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get specific tool schema
|
||||
*/
|
||||
app.get('/tools/:toolName', (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const tool = mcpToolSchemas.find(t => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
res.status(404).json({
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: mcpToolSchemas.map(t => t.name),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(tool);
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Execution Endpoints
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Execute an MCP tool
|
||||
* POST /tools/:toolName
|
||||
* Body: { parameters: {...} }
|
||||
*/
|
||||
app.post('/tools/:toolName', async (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const { parameters = {} } = req.body;
|
||||
|
||||
// Validate tool exists
|
||||
const handler = toolHandlers[toolName];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Tool '${toolName}' not found`,
|
||||
availableTools: Object.keys(toolHandlers),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[${new Date().toISOString()}] Executing tool: ${toolName}`);
|
||||
console.log(`Parameters: ${JSON.stringify(parameters)}`);
|
||||
|
||||
const result = await handler(parameters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
tool: toolName,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Tool error: ${toolName}`, error);
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: (error as { issues: unknown[] }).issues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// MCP Protocol Endpoints (Standard)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* MCP Initialize
|
||||
* Returns server capabilities
|
||||
*/
|
||||
app.post('/mcp/initialize', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: SERVICE_NAME,
|
||||
version: VERSION,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP List Tools
|
||||
* Returns all available tools in MCP format
|
||||
*/
|
||||
app.post('/mcp/tools/list', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
tools: mcpToolSchemas.map(tool => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCP Call Tool
|
||||
* Execute a tool with parameters
|
||||
*/
|
||||
app.post('/mcp/tools/call', async (req: Request, res: Response) => {
|
||||
const { name, arguments: args = {} } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_request',
|
||||
message: 'Tool name is required',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = toolHandlers[name];
|
||||
if (!handler) {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
code: 'unknown_tool',
|
||||
message: `Tool '${name}' not found`,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(args);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
// Handle Zod validation errors
|
||||
if (error && typeof error === 'object' && 'issues' in error) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'invalid_params',
|
||||
message: 'Invalid tool parameters',
|
||||
data: (error as { issues: unknown[] }).issues,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'internal_error',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Error Handler
|
||||
// ==========================================
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
console.error(`[${new Date().toISOString()}] Unhandled error:`, err);
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Start Server
|
||||
// ==========================================
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('');
|
||||
console.log('╔══════════════════════════════════════════════════════════╗');
|
||||
console.log('║ MCP MT4 Connector - Trading Platform ║');
|
||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
||||
console.log(`║ Service: ${SERVICE_NAME.padEnd(45)}║`);
|
||||
console.log(`║ Version: ${VERSION.padEnd(45)}║`);
|
||||
console.log(`║ Port: ${String(PORT).padEnd(48)}║`);
|
||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
||||
console.log('║ Endpoints: ║');
|
||||
console.log(`║ - Health: http://localhost:${PORT}/health`.padEnd(63) + '║');
|
||||
console.log(`║ - Tools: http://localhost:${PORT}/tools`.padEnd(63) + '║');
|
||||
console.log('╠══════════════════════════════════════════════════════════╣');
|
||||
console.log('║ MCP Tools Available: ║');
|
||||
mcpToolSchemas.forEach(tool => {
|
||||
console.log(`║ - ${tool.name.padEnd(54)}║`);
|
||||
});
|
||||
console.log('╚══════════════════════════════════════════════════════════╝');
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Graceful Shutdown
|
||||
// ==========================================
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Received SIGTERM, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Received SIGINT, shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
375
apps/mcp-mt4-connector/src/services/mt4-client.ts
Normal file
375
apps/mcp-mt4-connector/src/services/mt4-client.ts
Normal file
@ -0,0 +1,375 @@
|
||||
/**
|
||||
* MT4 Client Service
|
||||
*
|
||||
* HTTP client wrapper for communicating with mt4-gateway.
|
||||
* This service mirrors the functionality of mt4_bridge_client.py
|
||||
* but is written in TypeScript for the MCP Server.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
|
||||
// ==========================================
|
||||
// Types
|
||||
// ==========================================
|
||||
|
||||
export interface MT4AccountInfo {
|
||||
balance: number;
|
||||
equity: number;
|
||||
margin: number;
|
||||
freeMargin: number;
|
||||
marginLevel: number | null;
|
||||
profit: number;
|
||||
currency: string;
|
||||
leverage: number;
|
||||
name: string;
|
||||
server: string;
|
||||
company: string;
|
||||
}
|
||||
|
||||
export interface MT4Position {
|
||||
ticket: number;
|
||||
symbol: string;
|
||||
type: 'buy' | 'sell';
|
||||
lots: number;
|
||||
openPrice: number;
|
||||
currentPrice: number;
|
||||
stopLoss: number | null;
|
||||
takeProfit: number | null;
|
||||
profit: number;
|
||||
swap: number;
|
||||
openTime: string;
|
||||
magic: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export interface MT4Tick {
|
||||
symbol: string;
|
||||
bid: number;
|
||||
ask: number;
|
||||
timestamp: string;
|
||||
spread: number;
|
||||
}
|
||||
|
||||
export interface TradeResult {
|
||||
success: boolean;
|
||||
ticket?: number;
|
||||
message: string;
|
||||
errorCode?: number;
|
||||
}
|
||||
|
||||
export interface TradeRequest {
|
||||
action: 'buy' | 'sell';
|
||||
symbol: string;
|
||||
lots: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
price?: number;
|
||||
slippage?: number;
|
||||
magic?: number;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface ClosePositionRequest {
|
||||
ticket: number;
|
||||
lots?: number;
|
||||
slippage?: number;
|
||||
}
|
||||
|
||||
export interface ModifyPositionRequest {
|
||||
ticket: number;
|
||||
stopLoss?: number;
|
||||
takeProfit?: number;
|
||||
}
|
||||
|
||||
export interface MT4ClientConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
authToken: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MT4 Client Class
|
||||
// ==========================================
|
||||
|
||||
export class MT4Client {
|
||||
private client: AxiosInstance;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(config: MT4ClientConfig) {
|
||||
this.baseUrl = `http://${config.host}:${config.port}`;
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
timeout: config.timeout || 10000,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.authToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the MT4 terminal is connected
|
||||
*/
|
||||
async isConnected(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.client.get('/status');
|
||||
return response.data?.connected ?? false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MT4 account information
|
||||
*/
|
||||
async getAccountInfo(): Promise<MT4AccountInfo> {
|
||||
try {
|
||||
const response = await this.client.get('/account');
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
balance: data.balance ?? 0,
|
||||
equity: data.equity ?? 0,
|
||||
margin: data.margin ?? 0,
|
||||
freeMargin: data.freeMargin ?? 0,
|
||||
marginLevel: data.marginLevel ?? null,
|
||||
profit: data.profit ?? 0,
|
||||
currency: data.currency ?? 'USD',
|
||||
leverage: data.leverage ?? 100,
|
||||
name: data.name ?? '',
|
||||
server: data.server ?? '',
|
||||
company: data.company ?? '',
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.handleError(error, 'Failed to get account info');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current tick (quote) for a symbol
|
||||
*/
|
||||
async getTick(symbol: string): Promise<MT4Tick> {
|
||||
try {
|
||||
const response = await this.client.get(`/tick/${symbol}`);
|
||||
const data = response.data;
|
||||
|
||||
const bid = data.bid ?? 0;
|
||||
const ask = data.ask ?? 0;
|
||||
|
||||
return {
|
||||
symbol,
|
||||
bid,
|
||||
ask,
|
||||
timestamp: data.time ?? new Date().toISOString(),
|
||||
spread: Math.round((ask - bid) * 100000) / 100000,
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.handleError(error, `Failed to get tick for ${symbol}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all open positions
|
||||
*/
|
||||
async getPositions(): Promise<MT4Position[]> {
|
||||
try {
|
||||
const response = await this.client.get('/positions');
|
||||
const data = response.data;
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.map((p: Record<string, unknown>) => ({
|
||||
ticket: (p.ticket as number) ?? 0,
|
||||
symbol: (p.symbol as string) ?? '',
|
||||
type: (p.type as 'buy' | 'sell') ?? 'buy',
|
||||
lots: (p.lots as number) ?? 0,
|
||||
openPrice: (p.openPrice as number) ?? 0,
|
||||
currentPrice: (p.currentPrice as number) ?? 0,
|
||||
stopLoss: (p.stopLoss as number | null) ?? null,
|
||||
takeProfit: (p.takeProfit as number | null) ?? null,
|
||||
profit: (p.profit as number) ?? 0,
|
||||
swap: (p.swap as number) ?? 0,
|
||||
openTime: (p.openTime as string) ?? new Date().toISOString(),
|
||||
magic: (p.magic as number) ?? 0,
|
||||
comment: (p.comment as string) ?? '',
|
||||
}));
|
||||
} catch (error) {
|
||||
throw this.handleError(error, 'Failed to get positions');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific position by ticket
|
||||
*/
|
||||
async getPosition(ticket: number): Promise<MT4Position | null> {
|
||||
const positions = await this.getPositions();
|
||||
return positions.find(p => p.ticket === ticket) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a trade (buy or sell)
|
||||
*/
|
||||
async executeTrade(request: TradeRequest): Promise<TradeResult> {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
action: request.action,
|
||||
symbol: request.symbol,
|
||||
lots: request.lots,
|
||||
slippage: request.slippage ?? 3,
|
||||
magic: request.magic ?? 12345,
|
||||
comment: request.comment ?? 'MCP-MT4',
|
||||
};
|
||||
|
||||
if (request.stopLoss !== undefined) {
|
||||
payload.stopLoss = request.stopLoss;
|
||||
}
|
||||
if (request.takeProfit !== undefined) {
|
||||
payload.takeProfit = request.takeProfit;
|
||||
}
|
||||
if (request.price !== undefined) {
|
||||
payload.price = request.price;
|
||||
}
|
||||
|
||||
const response = await this.client.post('/trade', payload);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
success: data.success ?? false,
|
||||
ticket: data.ticket,
|
||||
message: data.message ?? '',
|
||||
errorCode: data.errorCode,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: this.getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a position
|
||||
*/
|
||||
async closePosition(request: ClosePositionRequest): Promise<TradeResult> {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
action: 'close',
|
||||
ticket: request.ticket,
|
||||
slippage: request.slippage ?? 3,
|
||||
};
|
||||
|
||||
if (request.lots !== undefined) {
|
||||
payload.lots = request.lots;
|
||||
}
|
||||
|
||||
const response = await this.client.post('/trade', payload);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
success: data.success ?? false,
|
||||
ticket: request.ticket,
|
||||
message: data.message ?? '',
|
||||
errorCode: data.errorCode,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
ticket: request.ticket,
|
||||
message: this.getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a position (SL/TP)
|
||||
*/
|
||||
async modifyPosition(request: ModifyPositionRequest): Promise<TradeResult> {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
action: 'modify',
|
||||
ticket: request.ticket,
|
||||
};
|
||||
|
||||
if (request.stopLoss !== undefined) {
|
||||
payload.stopLoss = request.stopLoss;
|
||||
}
|
||||
if (request.takeProfit !== undefined) {
|
||||
payload.takeProfit = request.takeProfit;
|
||||
}
|
||||
|
||||
const response = await this.client.post('/trade', payload);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
success: data.success ?? false,
|
||||
ticket: request.ticket,
|
||||
message: data.message ?? '',
|
||||
errorCode: data.errorCode,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
ticket: request.ticket,
|
||||
message: this.getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle axios errors and convert to meaningful messages
|
||||
*/
|
||||
private handleError(error: unknown, context: string): Error {
|
||||
const message = this.getErrorMessage(error);
|
||||
return new Error(`${context}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message from various error types
|
||||
*/
|
||||
private getErrorMessage(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosError = error as AxiosError;
|
||||
if (axiosError.response) {
|
||||
return `HTTP ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data)}`;
|
||||
}
|
||||
if (axiosError.code === 'ECONNREFUSED') {
|
||||
return 'Connection refused - MT4 Gateway is not running';
|
||||
}
|
||||
if (axiosError.code === 'ETIMEDOUT') {
|
||||
return 'Connection timeout - MT4 Gateway is not responding';
|
||||
}
|
||||
return axiosError.message;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Singleton Instance
|
||||
// ==========================================
|
||||
|
||||
let clientInstance: MT4Client | null = null;
|
||||
|
||||
export function getMT4Client(): MT4Client {
|
||||
if (!clientInstance) {
|
||||
const config: MT4ClientConfig = {
|
||||
host: process.env.MT4_GATEWAY_HOST || 'localhost',
|
||||
port: parseInt(process.env.MT4_GATEWAY_PORT || '8081', 10),
|
||||
authToken: process.env.MT4_GATEWAY_AUTH_TOKEN || 'secret',
|
||||
timeout: parseInt(process.env.REQUEST_TIMEOUT || '10000', 10),
|
||||
};
|
||||
clientInstance = new MT4Client(config);
|
||||
}
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
export function resetMT4Client(): void {
|
||||
clientInstance = null;
|
||||
}
|
||||
143
apps/mcp-mt4-connector/src/tools/account.ts
Normal file
143
apps/mcp-mt4-connector/src/tools/account.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* mt4_get_account - Get MT4 account information
|
||||
*
|
||||
* @description Retrieves comprehensive account information from the connected MT4 terminal
|
||||
* including balance, equity, margin, leverage, and broker details.
|
||||
*
|
||||
* @returns Account information object with balance, equity, margin details
|
||||
*
|
||||
* @example
|
||||
* const result = await mt4_get_account({});
|
||||
* // Returns:
|
||||
* // {
|
||||
* // balance: 10000.00,
|
||||
* // equity: 10250.50,
|
||||
* // margin: 500.00,
|
||||
* // freeMargin: 9750.50,
|
||||
* // marginLevel: 2050.10,
|
||||
* // profit: 250.50,
|
||||
* // currency: "USD",
|
||||
* // leverage: 100,
|
||||
* // name: "Demo Account",
|
||||
* // server: "ICMarkets-Demo",
|
||||
* // company: "IC Markets"
|
||||
* // }
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, MT4AccountInfo } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// Schema Definition
|
||||
// ==========================================
|
||||
|
||||
export const mt4GetAccountSchema = {
|
||||
name: 'mt4_get_account',
|
||||
description: 'Get MT4 account information including balance, equity, margin, and broker details',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
// Input validation schema (no params required)
|
||||
export const Mt4GetAccountInputSchema = z.object({});
|
||||
|
||||
export type Mt4GetAccountInput = z.infer<typeof Mt4GetAccountInputSchema>;
|
||||
|
||||
// ==========================================
|
||||
// Tool Implementation
|
||||
// ==========================================
|
||||
|
||||
export interface Mt4GetAccountResult {
|
||||
success: boolean;
|
||||
data?: MT4AccountInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_get_account(
|
||||
_params: Mt4GetAccountInput
|
||||
): Promise<Mt4GetAccountResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection first
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Get account info
|
||||
const accountInfo = await client.getAccountInfo();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accountInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Handler
|
||||
// ==========================================
|
||||
|
||||
export async function handleMt4GetAccount(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
// Validate input
|
||||
const validatedParams = Mt4GetAccountInputSchema.parse(params);
|
||||
|
||||
// Execute tool
|
||||
const result = await mt4_get_account(validatedParams);
|
||||
|
||||
// Format response for MCP
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
MT4 Account Information
|
||||
=======================
|
||||
Account Name: ${result.data.name}
|
||||
Server: ${result.data.server}
|
||||
Broker: ${result.data.company}
|
||||
Leverage: 1:${result.data.leverage}
|
||||
|
||||
Financial Summary
|
||||
-----------------
|
||||
Balance: ${result.data.balance.toFixed(2)} ${result.data.currency}
|
||||
Equity: ${result.data.equity.toFixed(2)} ${result.data.currency}
|
||||
Profit/Loss: ${result.data.profit >= 0 ? '+' : ''}${result.data.profit.toFixed(2)} ${result.data.currency}
|
||||
|
||||
Margin Details
|
||||
--------------
|
||||
Used Margin: ${result.data.margin.toFixed(2)} ${result.data.currency}
|
||||
Free Margin: ${result.data.freeMargin.toFixed(2)} ${result.data.currency}
|
||||
Margin Level: ${result.data.marginLevel !== null ? result.data.marginLevel.toFixed(2) + '%' : 'N/A'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
212
apps/mcp-mt4-connector/src/tools/index.ts
Normal file
212
apps/mcp-mt4-connector/src/tools/index.ts
Normal file
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* MCP Tools Index
|
||||
*
|
||||
* Exports all MT4 MCP tools and their schemas for registration
|
||||
*/
|
||||
|
||||
// Import handlers for use in toolHandlers map
|
||||
import { handleMt4GetAccount } from './account';
|
||||
import { handleMt4GetPositions, handleMt4ClosePosition } from './positions';
|
||||
import { handleMt4ExecuteTrade, handleMt4ModifyPosition } from './trading';
|
||||
import { handleMt4GetQuote } from './quotes';
|
||||
|
||||
// Account tools
|
||||
export {
|
||||
mt4GetAccountSchema,
|
||||
mt4_get_account,
|
||||
handleMt4GetAccount,
|
||||
Mt4GetAccountInputSchema,
|
||||
type Mt4GetAccountInput,
|
||||
type Mt4GetAccountResult,
|
||||
} from './account';
|
||||
|
||||
// Position tools
|
||||
export {
|
||||
mt4GetPositionsSchema,
|
||||
mt4_get_positions,
|
||||
handleMt4GetPositions,
|
||||
Mt4GetPositionsInputSchema,
|
||||
mt4ClosePositionSchema,
|
||||
mt4_close_position,
|
||||
handleMt4ClosePosition,
|
||||
Mt4ClosePositionInputSchema,
|
||||
type Mt4GetPositionsInput,
|
||||
type Mt4GetPositionsResult,
|
||||
type Mt4ClosePositionInput,
|
||||
type Mt4ClosePositionResult,
|
||||
} from './positions';
|
||||
|
||||
// Trading tools
|
||||
export {
|
||||
mt4ExecuteTradeSchema,
|
||||
mt4_execute_trade,
|
||||
handleMt4ExecuteTrade,
|
||||
Mt4ExecuteTradeInputSchema,
|
||||
mt4ModifyPositionSchema,
|
||||
mt4_modify_position,
|
||||
handleMt4ModifyPosition,
|
||||
Mt4ModifyPositionInputSchema,
|
||||
type Mt4ExecuteTradeInput,
|
||||
type Mt4ExecuteTradeResult,
|
||||
type Mt4ModifyPositionInput,
|
||||
type Mt4ModifyPositionResult,
|
||||
} from './trading';
|
||||
|
||||
// Quote tools
|
||||
export {
|
||||
mt4GetQuoteSchema,
|
||||
mt4_get_quote,
|
||||
handleMt4GetQuote,
|
||||
Mt4GetQuoteInputSchema,
|
||||
type Mt4GetQuoteInput,
|
||||
type Mt4GetQuoteResult,
|
||||
} from './quotes';
|
||||
|
||||
// ==========================================
|
||||
// Tool Registry
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* All available MCP tools with their schemas
|
||||
*/
|
||||
export const mcpToolSchemas = [
|
||||
{
|
||||
name: 'mt4_get_account',
|
||||
description: 'Get MT4 account information including balance, equity, margin, and broker details',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {},
|
||||
required: [] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_get_positions',
|
||||
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_execute_trade',
|
||||
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Trade direction: buy or sell',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'Optional: Stop loss price level',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'Optional: Take profit price level',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
magic: {
|
||||
type: 'number',
|
||||
description: 'Optional: Magic number for EA identification (default: 12345)',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Optional: Order comment (max 31 chars)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'action', 'lots'] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_close_position',
|
||||
description: 'Close an open trading position by ticket number. Can close partially.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to close',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_modify_position',
|
||||
description: 'Modify stop loss and/or take profit of an existing position',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to modify',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'New stop loss price level (optional)',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'New take profit price level (optional)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mt4_get_quote',
|
||||
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Tool handler routing map
|
||||
*/
|
||||
export const toolHandlers: Record<
|
||||
string,
|
||||
(params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>
|
||||
> = {
|
||||
mt4_get_account: handleMt4GetAccount,
|
||||
mt4_get_positions: handleMt4GetPositions,
|
||||
mt4_execute_trade: handleMt4ExecuteTrade,
|
||||
mt4_close_position: handleMt4ClosePosition,
|
||||
mt4_modify_position: handleMt4ModifyPosition,
|
||||
mt4_get_quote: handleMt4GetQuote,
|
||||
};
|
||||
315
apps/mcp-mt4-connector/src/tools/positions.ts
Normal file
315
apps/mcp-mt4-connector/src/tools/positions.ts
Normal file
@ -0,0 +1,315 @@
|
||||
/**
|
||||
* MT4 Position Tools
|
||||
*
|
||||
* - mt4_get_positions: List all open positions
|
||||
* - mt4_close_position: Close a specific position
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, MT4Position, TradeResult } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// mt4_get_positions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_get_positions - List all open positions
|
||||
*
|
||||
* @description Retrieves all currently open positions from MT4 terminal.
|
||||
* Can optionally filter by symbol.
|
||||
*
|
||||
* @param symbol - Optional symbol to filter positions (e.g., "XAUUSD")
|
||||
* @returns Array of open positions with details
|
||||
*
|
||||
* @example
|
||||
* const result = await mt4_get_positions({});
|
||||
* // Returns all positions
|
||||
*
|
||||
* const result = await mt4_get_positions({ symbol: "XAUUSD" });
|
||||
* // Returns only XAUUSD positions
|
||||
*/
|
||||
|
||||
export const mt4GetPositionsSchema = {
|
||||
name: 'mt4_get_positions',
|
||||
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
|
||||
},
|
||||
},
|
||||
required: [] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4GetPositionsInputSchema = z.object({
|
||||
symbol: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Mt4GetPositionsInput = z.infer<typeof Mt4GetPositionsInputSchema>;
|
||||
|
||||
export interface Mt4GetPositionsResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
positions: MT4Position[];
|
||||
totalProfit: number;
|
||||
count: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_get_positions(
|
||||
params: Mt4GetPositionsInput
|
||||
): Promise<Mt4GetPositionsResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Get all positions
|
||||
let positions = await client.getPositions();
|
||||
|
||||
// Filter by symbol if specified
|
||||
if (params.symbol) {
|
||||
positions = positions.filter(
|
||||
p => p.symbol.toUpperCase() === params.symbol!.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate total profit
|
||||
const totalProfit = positions.reduce((sum, p) => sum + p.profit, 0);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
positions,
|
||||
totalProfit,
|
||||
count: positions.length,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4GetPositions(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4GetPositionsInputSchema.parse(params);
|
||||
const result = await mt4_get_positions(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
if (result.data.count === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: params && (params as Mt4GetPositionsInput).symbol
|
||||
? `No open positions found for ${(params as Mt4GetPositionsInput).symbol}`
|
||||
: 'No open positions found',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const positionLines = result.data.positions.map(p => {
|
||||
const direction = p.type.toUpperCase();
|
||||
const profitSign = p.profit >= 0 ? '+' : '';
|
||||
const slInfo = p.stopLoss !== null ? `SL: ${p.stopLoss}` : 'SL: None';
|
||||
const tpInfo = p.takeProfit !== null ? `TP: ${p.takeProfit}` : 'TP: None';
|
||||
|
||||
return `
|
||||
#${p.ticket} | ${p.symbol} | ${direction} ${p.lots} lots
|
||||
Open: ${p.openPrice} | Current: ${p.currentPrice}
|
||||
${slInfo} | ${tpInfo}
|
||||
P/L: ${profitSign}${p.profit.toFixed(2)} | Swap: ${p.swap.toFixed(2)}
|
||||
Opened: ${p.openTime}
|
||||
Magic: ${p.magic} | Comment: ${p.comment || 'None'}`;
|
||||
});
|
||||
|
||||
const formattedOutput = `
|
||||
Open Positions (${result.data.count})
|
||||
${'='.repeat(30)}
|
||||
${positionLines.join('\n---\n')}
|
||||
|
||||
Total P/L: ${result.data.totalProfit >= 0 ? '+' : ''}${result.data.totalProfit.toFixed(2)}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// mt4_close_position
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_close_position - Close a trading position
|
||||
*
|
||||
* @description Closes an open position by ticket number.
|
||||
* Can optionally close partial volume.
|
||||
*
|
||||
* @param ticket - Position ticket number to close
|
||||
* @param lots - Optional: Partial volume to close (default: close all)
|
||||
* @param slippage - Optional: Maximum slippage in points (default: 3)
|
||||
* @returns Trade result with success status
|
||||
*
|
||||
* @example
|
||||
* // Close entire position
|
||||
* const result = await mt4_close_position({ ticket: 123456 });
|
||||
*
|
||||
* // Close partial position
|
||||
* const result = await mt4_close_position({ ticket: 123456, lots: 0.5 });
|
||||
*/
|
||||
|
||||
export const mt4ClosePositionSchema = {
|
||||
name: 'mt4_close_position',
|
||||
description: 'Close an open trading position by ticket number. Can close partially.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to close',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4ClosePositionInputSchema = z.object({
|
||||
ticket: z.number().int().positive(),
|
||||
lots: z.number().positive().optional(),
|
||||
slippage: z.number().int().min(0).max(100).optional(),
|
||||
});
|
||||
|
||||
export type Mt4ClosePositionInput = z.infer<typeof Mt4ClosePositionInputSchema>;
|
||||
|
||||
export interface Mt4ClosePositionResult {
|
||||
success: boolean;
|
||||
data?: TradeResult;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_close_position(
|
||||
params: Mt4ClosePositionInput
|
||||
): Promise<Mt4ClosePositionResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Verify position exists
|
||||
const position = await client.getPosition(params.ticket);
|
||||
if (!position) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Position with ticket ${params.ticket} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate lots if specified
|
||||
if (params.lots !== undefined && params.lots > position.lots) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Requested lots (${params.lots}) exceeds position size (${position.lots})`,
|
||||
};
|
||||
}
|
||||
|
||||
// Close position
|
||||
const result = await client.closePosition({
|
||||
ticket: params.ticket,
|
||||
lots: params.lots,
|
||||
slippage: params.slippage,
|
||||
});
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
data: result,
|
||||
error: result.success ? undefined : result.message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4ClosePosition(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4ClosePositionInputSchema.parse(params);
|
||||
const result = await mt4_close_position(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
Position Closed Successfully
|
||||
============================
|
||||
Ticket: ${validatedParams.ticket}
|
||||
${validatedParams.lots ? `Closed Volume: ${validatedParams.lots} lots` : 'Closed: Entire position'}
|
||||
Message: ${result.data.message || 'Position closed'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error closing position: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
193
apps/mcp-mt4-connector/src/tools/quotes.ts
Normal file
193
apps/mcp-mt4-connector/src/tools/quotes.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/**
|
||||
* mt4_get_quote - Get current price quote for a symbol
|
||||
*
|
||||
* @description Retrieves the current bid/ask prices for a trading symbol.
|
||||
* Also calculates the spread in points.
|
||||
*
|
||||
* @param symbol - Trading symbol to get quote for (e.g., "XAUUSD")
|
||||
* @returns Current bid, ask, spread, and timestamp
|
||||
*
|
||||
* @example
|
||||
* const result = await mt4_get_quote({ symbol: "XAUUSD" });
|
||||
* // Returns:
|
||||
* // {
|
||||
* // symbol: "XAUUSD",
|
||||
* // bid: 2650.50,
|
||||
* // ask: 2650.80,
|
||||
* // spread: 0.30,
|
||||
* // timestamp: "2026-01-04T12:00:00.000Z"
|
||||
* // }
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, MT4Tick } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// Schema Definition
|
||||
// ==========================================
|
||||
|
||||
export const mt4GetQuoteSchema = {
|
||||
name: 'mt4_get_quote',
|
||||
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
},
|
||||
required: ['symbol'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4GetQuoteInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20),
|
||||
});
|
||||
|
||||
export type Mt4GetQuoteInput = z.infer<typeof Mt4GetQuoteInputSchema>;
|
||||
|
||||
// ==========================================
|
||||
// Tool Implementation
|
||||
// ==========================================
|
||||
|
||||
export interface Mt4GetQuoteResult {
|
||||
success: boolean;
|
||||
data?: MT4Tick;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_get_quote(
|
||||
params: Mt4GetQuoteInput
|
||||
): Promise<Mt4GetQuoteResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Get tick data
|
||||
const tick = await client.getTick(params.symbol.toUpperCase());
|
||||
|
||||
// Validate we got valid data
|
||||
if (tick.bid === 0 && tick.ask === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No quote data available for ${params.symbol}. Symbol may not be available on this broker.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: tick,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MCP Tool Handler
|
||||
// ==========================================
|
||||
|
||||
export async function handleMt4GetQuote(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4GetQuoteInputSchema.parse(params);
|
||||
const result = await mt4_get_quote(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
// Determine decimal places based on symbol
|
||||
const decimals = getDecimalPlaces(result.data.symbol);
|
||||
const spreadPips = calculateSpreadPips(result.data.spread, result.data.symbol);
|
||||
|
||||
const formattedOutput = `
|
||||
Price Quote: ${result.data.symbol}
|
||||
${'='.repeat(25)}
|
||||
Bid: ${result.data.bid.toFixed(decimals)}
|
||||
Ask: ${result.data.ask.toFixed(decimals)}
|
||||
Spread: ${result.data.spread.toFixed(decimals)} (${spreadPips.toFixed(1)} pips)
|
||||
Time: ${result.data.timestamp}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Helper Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Determine decimal places based on symbol type
|
||||
*/
|
||||
function getDecimalPlaces(symbol: string): number {
|
||||
const upperSymbol = symbol.toUpperCase();
|
||||
|
||||
// JPY pairs have 3 decimals, most forex 5 decimals
|
||||
if (upperSymbol.includes('JPY')) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
// Gold and metals typically 2 decimals
|
||||
if (upperSymbol.startsWith('XAU') || upperSymbol.startsWith('XAG')) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Indices vary, use 2 as default
|
||||
if (
|
||||
upperSymbol.includes('US30') ||
|
||||
upperSymbol.includes('US500') ||
|
||||
upperSymbol.includes('NAS')
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Default forex pairs
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate spread in pips based on symbol
|
||||
*/
|
||||
function calculateSpreadPips(spread: number, symbol: string): number {
|
||||
const upperSymbol = symbol.toUpperCase();
|
||||
|
||||
// JPY pairs: 1 pip = 0.01
|
||||
if (upperSymbol.includes('JPY')) {
|
||||
return spread * 100;
|
||||
}
|
||||
|
||||
// Gold: 1 pip = 0.10
|
||||
if (upperSymbol.startsWith('XAU')) {
|
||||
return spread * 10;
|
||||
}
|
||||
|
||||
// Default forex: 1 pip = 0.0001
|
||||
return spread * 10000;
|
||||
}
|
||||
402
apps/mcp-mt4-connector/src/tools/trading.ts
Normal file
402
apps/mcp-mt4-connector/src/tools/trading.ts
Normal file
@ -0,0 +1,402 @@
|
||||
/**
|
||||
* MT4 Trading Tools
|
||||
*
|
||||
* - mt4_execute_trade: Execute BUY/SELL market orders
|
||||
* - mt4_modify_position: Modify SL/TP of existing positions
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getMT4Client, TradeResult } from '../services/mt4-client';
|
||||
|
||||
// ==========================================
|
||||
// mt4_execute_trade
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_execute_trade - Execute a market order (BUY or SELL)
|
||||
*
|
||||
* @description Opens a new trading position with optional SL/TP levels.
|
||||
* Supports market orders only (pending orders not implemented).
|
||||
*
|
||||
* @param symbol - Trading symbol (e.g., "XAUUSD", "EURUSD")
|
||||
* @param action - Trade direction: "buy" or "sell"
|
||||
* @param lots - Volume in lots (e.g., 0.01, 0.1, 1.0)
|
||||
* @param stopLoss - Optional stop loss price
|
||||
* @param takeProfit - Optional take profit price
|
||||
* @param slippage - Optional max slippage in points (default: 3)
|
||||
* @param magic - Optional magic number for EA identification
|
||||
* @param comment - Optional order comment
|
||||
* @returns Trade result with ticket number
|
||||
*
|
||||
* @example
|
||||
* // Simple buy order
|
||||
* const result = await mt4_execute_trade({
|
||||
* symbol: "XAUUSD",
|
||||
* action: "buy",
|
||||
* lots: 0.1
|
||||
* });
|
||||
*
|
||||
* // Buy with SL/TP
|
||||
* const result = await mt4_execute_trade({
|
||||
* symbol: "XAUUSD",
|
||||
* action: "buy",
|
||||
* lots: 0.1,
|
||||
* stopLoss: 2640.00,
|
||||
* takeProfit: 2680.00,
|
||||
* comment: "AI Signal"
|
||||
* });
|
||||
*/
|
||||
|
||||
export const mt4ExecuteTradeSchema = {
|
||||
name: 'mt4_execute_trade',
|
||||
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
symbol: {
|
||||
type: 'string',
|
||||
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['buy', 'sell'],
|
||||
description: 'Trade direction: buy or sell',
|
||||
},
|
||||
lots: {
|
||||
type: 'number',
|
||||
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'Optional: Stop loss price level',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'Optional: Take profit price level',
|
||||
},
|
||||
slippage: {
|
||||
type: 'number',
|
||||
description: 'Optional: Maximum slippage in points (default: 3)',
|
||||
},
|
||||
magic: {
|
||||
type: 'number',
|
||||
description: 'Optional: Magic number for EA identification (default: 12345)',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Optional: Order comment (max 31 chars)',
|
||||
},
|
||||
},
|
||||
required: ['symbol', 'action', 'lots'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4ExecuteTradeInputSchema = z.object({
|
||||
symbol: z.string().min(1).max(20),
|
||||
action: z.enum(['buy', 'sell']),
|
||||
lots: z.number().positive().max(100),
|
||||
stopLoss: z.number().positive().optional(),
|
||||
takeProfit: z.number().positive().optional(),
|
||||
slippage: z.number().int().min(0).max(100).optional(),
|
||||
magic: z.number().int().optional(),
|
||||
comment: z.string().max(31).optional(),
|
||||
});
|
||||
|
||||
export type Mt4ExecuteTradeInput = z.infer<typeof Mt4ExecuteTradeInputSchema>;
|
||||
|
||||
export interface Mt4ExecuteTradeResult {
|
||||
success: boolean;
|
||||
data?: TradeResult & {
|
||||
symbol: string;
|
||||
action: string;
|
||||
lots: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_execute_trade(
|
||||
params: Mt4ExecuteTradeInput
|
||||
): Promise<Mt4ExecuteTradeResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Validate SL/TP logic (basic check)
|
||||
if (params.stopLoss !== undefined && params.takeProfit !== undefined) {
|
||||
if (params.action === 'buy') {
|
||||
// For buy: SL should be below current price, TP above
|
||||
if (params.stopLoss >= params.takeProfit) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For BUY orders, stop loss must be below take profit',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// For sell: SL should be above current price, TP below
|
||||
if (params.stopLoss <= params.takeProfit) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For SELL orders, stop loss must be above take profit',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute trade
|
||||
const result = await client.executeTrade({
|
||||
symbol: params.symbol.toUpperCase(),
|
||||
action: params.action,
|
||||
lots: params.lots,
|
||||
stopLoss: params.stopLoss,
|
||||
takeProfit: params.takeProfit,
|
||||
slippage: params.slippage,
|
||||
magic: params.magic,
|
||||
comment: params.comment,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...result,
|
||||
symbol: params.symbol.toUpperCase(),
|
||||
action: params.action,
|
||||
lots: params.lots,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || 'Trade execution failed',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4ExecuteTrade(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4ExecuteTradeInputSchema.parse(params);
|
||||
const result = await mt4_execute_trade(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
Trade Executed Successfully
|
||||
===========================
|
||||
Ticket: #${result.data.ticket}
|
||||
Symbol: ${result.data.symbol}
|
||||
Direction: ${result.data.action.toUpperCase()}
|
||||
Volume: ${result.data.lots} lots
|
||||
${validatedParams.stopLoss ? `Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Not set'}
|
||||
${validatedParams.takeProfit ? `Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Not set'}
|
||||
Message: ${result.data.message || 'Order placed successfully'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error executing trade: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// mt4_modify_position
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* mt4_modify_position - Modify SL/TP of an existing position
|
||||
*
|
||||
* @description Updates the stop loss and/or take profit levels of an open position.
|
||||
*
|
||||
* @param ticket - Position ticket number to modify
|
||||
* @param stopLoss - New stop loss price (null to remove)
|
||||
* @param takeProfit - New take profit price (null to remove)
|
||||
* @returns Modification result
|
||||
*
|
||||
* @example
|
||||
* // Set both SL and TP
|
||||
* const result = await mt4_modify_position({
|
||||
* ticket: 123456,
|
||||
* stopLoss: 2640.00,
|
||||
* takeProfit: 2680.00
|
||||
* });
|
||||
*
|
||||
* // Update only TP
|
||||
* const result = await mt4_modify_position({
|
||||
* ticket: 123456,
|
||||
* takeProfit: 2700.00
|
||||
* });
|
||||
*/
|
||||
|
||||
export const mt4ModifyPositionSchema = {
|
||||
name: 'mt4_modify_position',
|
||||
description: 'Modify stop loss and/or take profit of an existing position',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
ticket: {
|
||||
type: 'number',
|
||||
description: 'Position ticket number to modify',
|
||||
},
|
||||
stopLoss: {
|
||||
type: 'number',
|
||||
description: 'New stop loss price level (optional)',
|
||||
},
|
||||
takeProfit: {
|
||||
type: 'number',
|
||||
description: 'New take profit price level (optional)',
|
||||
},
|
||||
},
|
||||
required: ['ticket'] as string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mt4ModifyPositionInputSchema = z.object({
|
||||
ticket: z.number().int().positive(),
|
||||
stopLoss: z.number().positive().optional(),
|
||||
takeProfit: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
export type Mt4ModifyPositionInput = z.infer<typeof Mt4ModifyPositionInputSchema>;
|
||||
|
||||
export interface Mt4ModifyPositionResult {
|
||||
success: boolean;
|
||||
data?: TradeResult;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function mt4_modify_position(
|
||||
params: Mt4ModifyPositionInput
|
||||
): Promise<Mt4ModifyPositionResult> {
|
||||
try {
|
||||
const client = getMT4Client();
|
||||
|
||||
// Check connection
|
||||
const isConnected = await client.isConnected();
|
||||
if (!isConnected) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'MT4 terminal is not connected',
|
||||
};
|
||||
}
|
||||
|
||||
// Validate at least one parameter is provided
|
||||
if (params.stopLoss === undefined && params.takeProfit === undefined) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'At least one of stopLoss or takeProfit must be provided',
|
||||
};
|
||||
}
|
||||
|
||||
// Verify position exists
|
||||
const position = await client.getPosition(params.ticket);
|
||||
if (!position) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Position with ticket ${params.ticket} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate SL/TP based on position type
|
||||
const sl = params.stopLoss;
|
||||
const tp = params.takeProfit;
|
||||
|
||||
if (sl !== undefined && tp !== undefined) {
|
||||
if (position.type === 'buy') {
|
||||
if (sl >= tp) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For BUY positions, stop loss must be below take profit',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (sl <= tp) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'For SELL positions, stop loss must be above take profit',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modify position
|
||||
const result = await client.modifyPosition({
|
||||
ticket: params.ticket,
|
||||
stopLoss: params.stopLoss,
|
||||
takeProfit: params.takeProfit,
|
||||
});
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
data: result,
|
||||
error: result.success ? undefined : result.message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMt4ModifyPosition(
|
||||
params: unknown
|
||||
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
||||
const validatedParams = Mt4ModifyPositionInputSchema.parse(params);
|
||||
const result = await mt4_modify_position(validatedParams);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const formattedOutput = `
|
||||
Position Modified Successfully
|
||||
==============================
|
||||
Ticket: #${validatedParams.ticket}
|
||||
${validatedParams.stopLoss !== undefined ? `New Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Unchanged'}
|
||||
${validatedParams.takeProfit !== undefined ? `New Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Unchanged'}
|
||||
Message: ${result.data.message || 'Position modified successfully'}
|
||||
`.trim();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error modifying position: ${result.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
23
apps/mcp-mt4-connector/tsconfig.json
Normal file
23
apps/mcp-mt4-connector/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
@ -13,23 +13,23 @@ services:
|
||||
# ===========================================================================
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: orbiquant-postgres
|
||||
image: postgres:16-alpine
|
||||
container_name: orbiquantia-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: orbiquant_trading
|
||||
POSTGRES_USER: orbiquant_user
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
|
||||
POSTGRES_DB: orbiquantia_platform
|
||||
POSTGRES_USER: orbiquantia
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025}
|
||||
POSTGRES_INITDB_ARGS: "-E UTF8"
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
- "${POSTGRES_PORT:-5433}:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./apps/database/schemas:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
- orbiquant-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U orbiquant_user -d orbiquant_trading"]
|
||||
test: ["CMD-SHELL", "pg_isready -U orbiquantia -d orbiquantia_platform"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@ -66,9 +66,9 @@ services:
|
||||
PORT: ${BACKEND_API_PORT:-3081}
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: orbiquant_trading
|
||||
DB_USER: orbiquant_user
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
|
||||
DB_NAME: orbiquantia_platform
|
||||
DB_USER: orbiquantia
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025}
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
ML_ENGINE_URL: http://ml-engine:3083
|
||||
@ -131,9 +131,9 @@ services:
|
||||
PYTHONUNBUFFERED: 1
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: orbiquant_trading
|
||||
DB_USER: orbiquant_user
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
|
||||
DB_NAME: orbiquantia_platform
|
||||
DB_USER: orbiquantia
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025}
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
PORT: ${ML_ENGINE_PORT:-3083}
|
||||
@ -163,9 +163,9 @@ services:
|
||||
PYTHONUNBUFFERED: 1
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: orbiquant_trading
|
||||
DB_USER: orbiquant_user
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
|
||||
DB_NAME: orbiquantia_platform
|
||||
DB_USER: orbiquantia
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025}
|
||||
POLYGON_API_KEY: ${POLYGON_API_KEY}
|
||||
METAAPI_TOKEN: ${METAAPI_TOKEN}
|
||||
METAAPI_ACCOUNT_ID: ${METAAPI_ACCOUNT_ID}
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "NOTA-DISCREPANCIA-PUERTOS-2025-12-08"
|
||||
title: "Discrepancia de Puertos Detectada"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# NOTA: Discrepancia de Puertos Detectada
|
||||
|
||||
**Fecha:** 2025-12-08
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "ARQUITECTURA-GENERAL"
|
||||
title: "Arquitectura General - OrbiQuant IA"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Arquitectura General - OrbiQuant IA
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
414
docs/00-vision-general/Curso_Basico.md
Normal file
414
docs/00-vision-general/Curso_Basico.md
Normal file
@ -0,0 +1,414 @@
|
||||
Modulo 1
|
||||
Elementos básicos
|
||||
Mercado de derivados y OTC
|
||||
Derivados
|
||||
El mercado de derivados es donde se negocian instrumentos financieros cuyo valor depende (o deriva) del precio de otro activo, como acciones, divisas, materias primas, índices, etc.
|
||||
Algunos ejemplos de derivados son:
|
||||
• Futuros
|
||||
• Opciones
|
||||
• CFDs (Contratos por diferencia)
|
||||
• Swaps
|
||||
OTC
|
||||
OTC significa "fuera de mercado" o "sobre el mostrador". Es decir, son operaciones que no pasan por una bolsa formal como la de Nueva York o Chicago.
|
||||
En el mercado OTC:
|
||||
• Las operaciones se hacen directamente entre dos partes, como un trader y un broker.
|
||||
• Hay más flexibilidad en los contratos (pueden ser a medida).
|
||||
• Es común en productos como CFDs, divisas (forex), swaps, etc.
|
||||
Tipos de gráficos
|
||||
1.- Gráfico de líneas
|
||||
• Qué muestra: Solo el precio de cierre de cada periodo (por ejemplo, cada día, hora o minuto).
|
||||
• Cómo se ve: Una línea continua que conecta los precios de cierre.
|
||||
• Ventajas:
|
||||
o Simple y limpio, ideal para ver la tendencia general.
|
||||
o Útil para principiantes que no quieren sobrecargarse de información.
|
||||
• Desventaja: No muestra el rango completo de precios (apertura, máximo y mínimo).
|
||||
• Ejemplo 1:
|
||||
|
||||
2.- Gráfico de barras
|
||||
• Qué muestra: También OHLC, como las velas, pero de forma diferente.
|
||||
• Cómo se ve:
|
||||
o Una línea vertical representa el rango (mínimo a máximo).
|
||||
o Una línea corta a la izquierda marca la apertura.
|
||||
o Una línea corta a la derecha marca el cierre.
|
||||
• Ventajas:
|
||||
o Más compacto que las velas, puede mostrar más datos en menos espacio.
|
||||
• Desventaja: Menos visual y atractivo que las velas japonesas.
|
||||
|
||||
3.- Gráfico de velas japonesas
|
||||
• Qué muestra: Apertura, máximo, mínimo y cierre (OHLC) de cada periodo.
|
||||
• Cómo se ve: Cada vela tiene un cuerpo (de color verde o rojo) y "mechas" arriba y abajo.
|
||||
o Cuerpo: distancia entre el precio de apertura y cierre.
|
||||
o Mechas: muestran los precios más altos y bajos alcanzados.
|
||||
• Ventajas:
|
||||
o Muy visual, permite detectar patrones de comportamiento del mercado.
|
||||
o Ideal para análisis técnico.
|
||||
• Desventaja: Puede parecer complejo al principio por la cantidad de información.
|
||||
|
||||
Siendo este último gráfico el más relevante para nosotros, pues será el principal que observaremos al estar en la operativa.
|
||||
Velas japonesas
|
||||
Material de apoyo, ejemplos y su uso:
|
||||
Modulo 2
|
||||
2.- Factores y herramientas
|
||||
Tipos de broker
|
||||
ECN (Electronic Communication Network)
|
||||
• Cómo funciona: Te conecta directamente con una red de participantes del mercado (otros traders, bancos, instituciones). Tú operas con precios reales del mercado.
|
||||
• Ventaja: Spreads ultra bajos, alta transparencia, sin mesa de negociación.
|
||||
• Desventaja: Comisiones por operación, puede requerir mayor capital mínimo.
|
||||
• Ideal para: Traders profesionales, scalpers, o quienes quieren máxima precisión.
|
||||
STP (Straight Through Processing)
|
||||
• Cómo funciona: El broker no opera contra ti, sino que pasa tus órdenes directamente a sus proveedores de liquidez (como bancos o instituciones grandes).
|
||||
• Ventaja: Menos conflicto de interés, spreads más realistas.
|
||||
• Desventaja: Puede haber comisiones por operación, y en momentos de alta volatilidad la ejecución puede no ser tan instantánea.
|
||||
• Ideal para: Traders intermedios o con enfoque más técnico.
|
||||
Market Maker (creador de mercado)
|
||||
• Cómo funciona: El broker crea el mercado interno. Es decir, toma el otro lado de tu operación. Si tú compras, ellos venden, y viceversa.
|
||||
• Ventaja: Spreads bajos, ejecución rápida, acceso con poco capital.
|
||||
• Desventaja: Puede haber conflicto de interés (porque ganan cuando tú pierdes, aunque muchos están regulados para evitar abusos).
|
||||
• Ideal para: Traders principiantes o con capital pequeño.
|
||||
Híbrido (STP + Market Maker)
|
||||
• Muchos brokers usan un modelo mixto. Por ejemplo, pueden ejecutar operaciones pequeñas como Market Maker y operaciones grandes pasarlas a proveedores de liquidez (STP).
|
||||
• Buscan ofrecer lo mejor de ambos mundos: costos bajos y buena ejecución.
|
||||
Regulaciones perspectiva global
|
||||
# Regulador País / Región Nivel de Protección Comentarios clave
|
||||
1 FCA (Financial Conduct Authority) Reino Unido 🇬🇧 Muy alto 🟢🟢🟢 Supervisión estricta, protección de fondos, política clara contra conflictos.
|
||||
2 ASIC (Australian Securities & Investments Commission) Australia 🇦🇺 Muy alto 🟢🟢🟢 Requiere transparencia, separación de fondos, protección al retail.
|
||||
3 CySEC (Cyprus Securities and Exchange Commission) Chipre 🇨🇾 (UE) Alto 🟢🟢 Muy común en brokers europeos, cumple con normativa MiFID II.
|
||||
4 BaFin (Federal Financial Supervisory Authority) Alemania 🇩🇪 Alto 🟢🟢 Muy sólida, regulaciones conservadoras, parte de la UE.
|
||||
5 CNMV (Comisión Nacional del Mercado de Valores) España 🇪🇸 Alto 🟢🟢 Buen nivel de supervisión, dentro del marco europeo.
|
||||
6 FINMA (Swiss Financial Market Supervisory Authority) Suiza 🇨🇭 Alto 🟢🟢 Muy confiable, aunque pocos brokers CFDs están bajo esta licencia.
|
||||
7 CFTC / NFA (Commodity Futures Trading Commission / National Futures Association) EE.UU. 🇺🇸 Muy alto 🟢🟢🟢 pero... Muy estricta, pero no permite CFDs para retail en EE.UU.
|
||||
8 DFSA (Dubai Financial Services Authority) Dubái 🇦🇪 Medio 🟡 Creciendo en prestigio, aún en desarrollo.
|
||||
9 FSA (Financial Services Authority) Seychelles / Mauricio / Belice 🌴 Bajo 🔴 Regulación suave, común en brokers offshore. Cuidado.
|
||||
10 No regulado / Offshore sin supervisión Ninguno Muy bajo 🔴🔴🔴 Riesgo muy alto. No recomendable.
|
||||
Terminal operativa (Metatrader 4 y 5)
|
||||
¿Qué es MetaTrader (MT4 y MT5)?
|
||||
MetaTrader es una plataforma de trading muy popular que permite operar en mercados como forex, CFDs, acciones, índices, criptos, etc.
|
||||
• MT4: Lanzado en 2005. Muy usado en forex y CFDs.
|
||||
• MT5: Lanzado en 2010. Más moderno, con más funciones y acceso a más mercados (acciones reales, futuros, etc.).
|
||||
Funciones Básicas (tanto en MT4 como en MT5)
|
||||
Función Descripción
|
||||
📊 Gráficos en tiempo real Puedes analizar el movimiento del precio con velas, líneas o barras.
|
||||
🧰 Indicadores técnicos Vienen con indicadores como RSI, MACD, Medias Móviles, etc.
|
||||
🎯 Órdenes de compra/venta Ejecuta operaciones con distintos tipos de órdenes (market, stop, limit).
|
||||
🧱 Trading automático (EA) Puedes usar o crear Expert Advisors para operar de forma automática.
|
||||
🗂️ Gestión de múltiples activos Puedes operar varios pares o instrumentos al mismo tiempo.
|
||||
📝 Historial y análisis Ver resultados de tus operaciones pasadas y analizar tu rendimiento.
|
||||
Diferencias clave entre MT4 y MT5
|
||||
Característica MT4 MT5
|
||||
🛠️ Año de lanzamiento 2005 2010
|
||||
🎯 Mercados disponibles Principalmente Forex y CFDs Forex, CFDs, acciones, futuros, criptos
|
||||
🧠 Tipo de arquitectura 32 bits 64 bits (más rápido y eficiente)
|
||||
📅 Calendario económico ❌ No integrado ✅ Integrado
|
||||
🧮 Tipos de órdenes 4 tipos 6 tipos (más flexibilidad)
|
||||
📊 Timeframes de gráficos 9 21 (más opciones para análisis)
|
||||
💻 Lenguaje de programación MQL4 (más limitado) MQL5 (más potente, tipo C++)
|
||||
🔄 Compatibilidad con EAs Solo EAs de MT4 Solo EAs de MT5 (no son intercambiables)
|
||||
Terminal de análisis (Tradingview)
|
||||
¿Qué es TradingView?
|
||||
TradingView es una plataforma en línea para análisis técnico y social. Permite ver gráficos en tiempo real de acciones, criptomonedas, forex, índices y más, desde cualquier navegador o app.
|
||||
Es muy popular entre traders por su interfaz intuitiva, herramientas visuales y comunidad activa.
|
||||
Funciones básicas e importantes
|
||||
Función Descripción
|
||||
📈 Gráficos interactivos Personalizables, con múltiples tipos: velas, líneas, barras, Heikin Ashi…
|
||||
🧰 Indicadores técnicos Vienen más de 100 por defecto (RSI, MACD, EMA, etc.)
|
||||
✏️ Herramientas de dibujo Líneas, retrocesos de Fibonacci, canales, patrones armónicos, etc.
|
||||
⏱️ Múltiples marcos de tiempo Desde segundos hasta meses
|
||||
💬 Ideas de la comunidad Miles de análisis públicos de otros traders para inspirarte o aprender
|
||||
🧠 Pine Script (avanzado) Lenguaje para crear tus propios indicadores o estrategias automáticas
|
||||
🔔 Alertas Te avisa cuando el precio cumple una condición que tú defines
|
||||
🖥️ Multipantalla y diseño Puedes ver varios activos a la vez, en la misma pantalla
|
||||
Nota: Existe una versión gratuita y otra de paga, difieren en su potencia y accesibilidad a herramientas, etc.
|
||||
Para nuestro uso inicial usaremos la versión gratuita que nos permite operar perfectamente funcional.
|
||||
Tipos de cuenta
|
||||
ECN, Standard, Cent, Zero, FixAPI y Social Trading (PAMM y Copy)
|
||||
|
||||
|
||||
Creación de cuentas y conexión a terminal operativa
|
||||
Crea una cuenta en un broker 100% B2B, hiper regulado, con las mejores condiciones de mercado y retiros instantáneos.
|
||||
CXM una excelente opción para operar con confianza y precisión.
|
||||
|
||||
|
||||
|
||||
|
||||
link de apertura ⬇⬇⬇
|
||||
CXM
|
||||
QR de apertura ⬇⬇⬇
|
||||
|
||||
Modulo 3
|
||||
Introducción al IPDA
|
||||
Interbank Price Delivery Algorithmic, estructura y narrativa del precio
|
||||
Comencemos con la pregunta inicial base ⬇️⬇️⬇️
|
||||
🧠 ¿Qué es el IPDA (Interbank Price Delivery Algorithmic).pdf
|
||||
¿Cómo es visualmente?
|
||||
El IPDA crea fractales en formación A+B+C
|
||||
Formación de fractal alcista
|
||||
|
||||
Formación de fractal bajista
|
||||
|
||||
Durante la creación de un fractal completo, cuando los puntos ABC están listos para el siguiente movimiento del precio, sucede una actualización en la estructura del IPDA para el siguiente fractal que se forme (fractal 2), donde el punto C se transforma en un Nuevo punto A (Ai).
|
||||
Al ser superado el anterior punto B, iniciamos la creación de un nuevo fractal Ai + Bi + Ci
|
||||
Ejemplo:
|
||||
Fractal alcista 1 completado, fractal alcista 2 en formación
|
||||
|
||||
Fractal bajista 1 completado, fractal bajista 2 en formación
|
||||
|
||||
Es así como llegamos a la siguiente nomenclatura que nos ayudará a crear una narrativa inicial del precio y entender la tendencia.
|
||||
Modificamos A+B+C por HH, HL, LH, LL
|
||||
Tabla descriptiva:
|
||||
|
||||
Ejemplo visual comparativa de HH y HL (tendencia alcista)
|
||||
|
||||
Ejemplo visual de LL y LH (tendencia bajista)
|
||||
|
||||
🎯 En resumen:
|
||||
• HH + HL = Mercado subiendo con fuerza (tendencia alcista).
|
||||
• LH + LL = Mercado cayendo (tendencia bajista).
|
||||
• Mezcla de ambos = Rango o posible cambio de tendencia.
|
||||
Swing High y Swing Low
|
||||
Durante la creación de la estructura del precio, tenemos nuevos máximos y nuevos mínimos respectivamente, pero para determinar si dicho punto es o no valido para la estructura debemos incluir nueva información, información disponible en el grafico de velas japonesas.
|
||||
¿Qué es un Swing High y un Swing Low?
|
||||
Término Definición simple
|
||||
Swing High Es un pico o punto más alto, rodeado por dos velas a su izquierda y derecha con máximos más bajos.
|
||||
Swing Low Es un valle o punto más bajo, rodeado por dos velas a su izquierda y derecha con mínimos más altos.
|
||||
Swing High
|
||||
Es una vela alcista seguida de 2 velas bajistas
|
||||
|
||||
Swing Low
|
||||
Es una vela bajista seguida de 2 velas alcistas
|
||||
|
||||
Existe un aumento de relevancia Segun el time frame, mientras más alto sea el time frame en el que se observan las velas, más contundente podría ser nuestro Swing High/Low respectivamente.
|
||||
Switch Market (Shift Market), BOS y BIS
|
||||
🧠 GLOSARIO ICT
|
||||
Concepto Significado básico
|
||||
BOS (Break of Structure) El mercado confirma la continuidad de la tendencia actual.
|
||||
SHIFT (Market Structure Shift) El mercado cambia su dirección: de alcista a bajista o viceversa.
|
||||
BIOS (Bias) Es la dirección más probable en que se moverá el mercado (tu “norte”).
|
||||
Switch Market Es cuando el mercado hace un SHIFT, cambia de tendencia (ej. de bullish a bearish), y te da señales claras para entrar en sentido contrario.
|
||||
🔄 ¿Qué es un Market Structure Shift (o "Switch Market")?
|
||||
Un SHIFT es cuando el mercado rompe la estructura contraria.
|
||||
Ejemplo:
|
||||
• El mercado iba haciendo HH y HL (alcista).
|
||||
• Pero rompe un HL anterior → esto no es BOS, es un cambio: ahora hay intención bajista.
|
||||
📌 El SHIFT es una señal de que algo cambió, y puede indicar una reversión.
|
||||
Ejemplo visual Switch Market bajista
|
||||
|
||||
Ejemplo visual Switch Market Alcista
|
||||
|
||||
¿Qué es un BOS (Break of Structure)?
|
||||
Imagina que el mercado está haciendo Higher Highs y Higher Lows (tendencia alcista).
|
||||
Cuando rompe el último Higher High (HH) y hace un nuevo HH más alto → eso es un BOS alcista.
|
||||
Es una confirmación de que el precio sigue su rumbo.
|
||||
▶️ ¿Qué confirma un BOS?
|
||||
• Que la tendencia continúa.
|
||||
• Que el smart money sigue en control en esa dirección.
|
||||
Ejemplo Visual BOS bajista creado por un SW Market y seguido de BIS
|
||||
|
||||
Modulo 4
|
||||
1.- Tendencia
|
||||
Se dice que la tendencia es tu amiga o aliada, se tiene una tendencia cuando un conjunto de máximos y mínimos tienen una misma orientación.
|
||||
¿Qué es la tendencia en los mercados financieros?
|
||||
La tendencia es la dirección general en la que se mueve el precio de un activo durante un periodo de tiempo. Hay tres tipos principales:
|
||||
1. Tendencia alcista (bullish): el precio sube y forma mínimos y máximos cada vez más altos.
|
||||
o Ejemplo: El precio pasa de 100 → 105 → 110 → 115.
|
||||
2. Tendencia bajista (bearish): el precio baja y forma mínimos y máximos cada vez más bajos.
|
||||
o Ejemplo: El precio cae de 100 → 95 → 90 → 85.
|
||||
3. Tendencia lateral (rango o consolidación): el precio no tiene una dirección clara, se mueve entre un soporte y una resistencia.
|
||||
o Ejemplo: El precio oscila entre 98 y 102 por varios días.
|
||||
Otros detalles importantes:
|
||||
• Una tendencia no significa que el precio suba o baje siempre en línea recta. Hay retrocesos (correcciones) normales dentro de una tendencia.
|
||||
• La temporalidad importa: algo puede estar bajista en 5 minutos y alcista en 1 hora.
|
||||
• Las tendencias fuertes suelen respetar zonas clave de soporte o resistencia.
|
||||
• Nunca operes en contra de la tendencia, a menos que tengas una estrategia avanzada (no recomendada para principiantes).
|
||||
2.- Key levels (KL, KLi, KLii)
|
||||
Es definido como un precio especifico en el que los agentes institucionales inyectan liquidez a la estructura del precio, suelen ser atractores del precio, y también detonadores de nuevas tendencias. Son precios que suelen tener terminación en 00 y hasta 000.
|
||||
¿Como se verían visualmente?
|
||||
aquí hay un ejemplo de cómo se ven los KL en el índice Nasdaq, grafico semanal.
|
||||
|
||||
Una vez detectado un KL, es posible crear una proyección interna sobre otros precios relevante en la estructura macro, tomando como referencia los KL ya negociados buscaremos el 50% de esa área, y una vez identificado el punto medio es importante destacar un nuevo KL interno (KLi) mismo precio que tendrá un uso similar a su sucesor macro, ya que son atractores del precio e inyectores de liquidez.
|
||||
¿Como se vería visualmente?
|
||||
Aquí un ejemplo de cómo se ven los KLi en el índice Nasdaq, conservando los KL previos
|
||||
|
||||
Como punto de acceso adicional, existen precios usados como confluencia adicional, para el uso de la estrategia los definimos como Key Level interno de grado inferior (KLii), y se encuentran en el 50% del area disponible entre KL y KLi.
|
||||
¿Cómo se verían visualmente?
|
||||
Aquí un ejemplo de cómo se ven los KLii en el índice Nasdaq, conservando los KL y KLi previos
|
||||
|
||||
3.- Power of three
|
||||
¿Qué es el Power of Three (POT)?
|
||||
El Power of Three es un patrón de comportamiento del precio durante una sesión o ciclo de mercado, que refleja la manipulación institucional intencional del precio antes de su verdadero movimiento. Según la teoría del IPDA, los grandes participantes (instituciones, bancos, algoritmos interbancarios) manipulan el mercado para maximizar liquidez y tomar el lado opuesto del público minorista.
|
||||
Las 3 Fases del Power of Three (AMD)
|
||||
1. Acumulación (Accumulation):
|
||||
o El precio se mueve de forma lateral o con pequeñas trampas.
|
||||
o Objetivo: Crear una zona de liquidez o engañar al trader minorista.
|
||||
o Se acumulan órdenes de compra/venta en zonas clave (como zonas de liquidez, equal highs/lows, etc.).
|
||||
2. Manipulación
|
||||
o Aquí el precio se mueve hacia la dirección institucional real, rompiendo estructuras o zonas manipuladas.
|
||||
o Este movimiento busca capturar la liquidez que se generó en la fase de acumulación.
|
||||
3. Distribución - Expansión (Expansión / Real Move; Distribution / Reversal or Take Profit):
|
||||
o Movimiento impulsivo fuerte en una dirección (el "true move").
|
||||
o El precio desacelera o se revierte.
|
||||
o Objetivo: Cerrar operaciones institucionales, tomar beneficios, o preparar una nueva trampa.
|
||||
o Suele terminar el ciclo diario o la sesión, y puede dar inicio a otro patrón POT.
|
||||
Esquema visual de líneas POT/AMD, en un proceso de creación de un fractal con orientación alcista.
|
||||
|
||||
Esquema visual de líneas POT/AMD, en un proceso de creación de un fractal con orientación bajista.
|
||||
|
||||
Ejemplo X: AMD/POT alcista con volumen
|
||||
|
||||
Detalles clave desde la teoría IPDA:
|
||||
• El precio es entregado a través de algoritmos interbancarios diseñados para buscar liquidez y ejecutar órdenes institucionales.
|
||||
• El POT refleja la lógica detrás de este algoritmo: crear desequilibrio (imbalance), inducir a los trades a cometer un error (liquidez inducida), ejecutar el movimiento real (expansión), y luego estabilizar.
|
||||
• IPDA y POT van de la mano en el análisis de cómo se mueve el precio en función del "Smart Money" y nuestro método/lenguaje de estudio ICT.
|
||||
4.- Matrices IPDA
|
||||
Aqui es donde comienza a ponerse interesante, pues una vez que tengamos claro como leer una Matriz IPDA, es posible que la idea de que esto es un mercado aleatorio comienza a no tener tanto sentido y por el contrario comienza un entendimiento aún más profundo de la estructura.
|
||||
Una matriz está compuesta por distintos factores, los principales son:
|
||||
1.- Fractal en formación
|
||||
2.- Un punto máximo, un punto mínimo y un punto de equilibrio
|
||||
3.- Temporalidad
|
||||
4.- Zona Premium y zona Discount
|
||||
¿Como se ve una matriz en el grafico?
|
||||
Aquí un ejemplo visual de una matriz que ya ha completado su fractal y adicional a ello a culminado con el movimiento expansivo.
|
||||
Al ser una matriz alcista una vez completado el fractal e ingresado en zona de Discount es posible determinar que el siguiente movimiento será alcista
|
||||
|
||||
Mismo ejemplo usando POT/AMD, durante la creación de dicho fractal alcista.
|
||||
|
||||
4.1.- Uniendo las primeras piezas del juego
|
||||
Tomando en consideración las confluencias que conocemos hasta este momento, podríamos ingresar en un trade alcista (una compra; BUY), usando nuevamente el Ejemplo X:
|
||||
|
||||
1.- Ubicamos la zona de acumulación con los LL iguales creados al cierre del día anterior.
|
||||
2.- Una vez ha sido superado ese nivel, trazamos los LH y LL de dicho movimiento (manipulación).
|
||||
3.- El Switch market es nuestra referencia y nuestra ventaja estadística para la operativa, cuando lo observamos trazamos la matriz con una caja de GANN sin vertices o un fibonacci (indicadores disponibles en todas las terminales)
|
||||
|
||||
4.- Tomaremos el trade una vez haya creado un descuento o retroceso a la zona Discount IPDA, en la matriz creada por el ultimo LL y el switch market.
|
||||
5.- EL Stop Loss (SL) irá por debajo del último LL, creado en la manipulación y el Take Profit (TP) ira exactamente a 2 veces el SL, es decir un trade donde arriesgamos 1 y buscamos ganar 2.
|
||||
Trade 1 a 2
|
||||
|
||||
Módulo 5
|
||||
Killer zone y momentum
|
||||
1.- Killer zone:
|
||||
Son los horarios que competen a la apertura y cierre de las principales bolsas de valores del mundo.
|
||||
Bolsa de Valores Apertura Cierre Zona horaria
|
||||
Asia 17:00 20:59 (UTC -6)
|
||||
London 23:00 02:59 (UTC -6)
|
||||
New York 07:30 12:00 (UTC -6)
|
||||
La Killer zone es una de las confluencias más importantes para la toma de Trades de alta probabilidad, pues es el horario cuando las “manos más grandes están dentro del juego” buscando oportunidades.
|
||||
Durante la creación de la estructura, el precio tiene horarios específicos de terminaos por el IPDA, y son importantes ya que suelen ser puntos de atracción del precio y puntos de reacción del precio.
|
||||
Aquí los enlistamos e iremos añadiendo a un gráfico de timeframe de 1 minuto, para una mejor comprensión sobre su uso, y visualización.
|
||||
1.- New York Open (07:30 UTC -6).
|
||||
Una vela relevante pues a partir del OPEN de la primera vela del primer minuto en este horario, inicia también el mercado “real”, en el CME inician las cotizaciones.
|
||||
|
||||
2.- Inicio IPDA, Real inicio del mercado, NY (22:00 UTC -6).
|
||||
Una vela relevante, en el OPEN de dicha vela representa el reinicio de los algoritmos bancarios implícitos en el IPDA.
|
||||
|
||||
3.- High y Low de la Sesión Asiática (17:00 - 20:59 UTC-6) y Sesión de Londres (23:00 - 02:59 UTC -6)
|
||||
Representa la liquidez creada durante la sesión de trading 100% electrónico, y nos da el previo a la Kill zone de New york, mismas que tendrán distinta relevancia según el activo que estés operando, y destacamos New York, ya que es la principal sesión, con mayor liquidez (según el activo).
|
||||
|
||||
4.- Liquidez adicional: High y Low de Lunch NY (09:30 - 11:29 UTC -6)
|
||||
Un rango donde suelen haber continuaciones, regresos y expansiones, un rango realmente importante para el desarrollo del día y la sesión.
|
||||
|
||||
5.- Last Cumulation LAC (13:15 - 13:44 UTC -6)
|
||||
El precio se prepara para el cierre, y suele crear liquidez o tomarla.
|
||||
|
||||
6.- Movement complete MOC (13:50 - 13:59 UTC -6)
|
||||
Durante este breve “rango final”, suele tener las últimas extensiones o momento de volatilidad antes del cierre de bolsa
|
||||
|
||||
Módulo 6
|
||||
Order Blocks
|
||||
Son zonas del gráfico donde las manos grandes (bancos, fondos de inversión, etc.) han colocado órdenes importantes, ya sea de compra o de venta, tiene distintos timeframes y distintas interpretaciones.
|
||||
¿Qué caracteriza a un Order Block?
|
||||
Un Order Block típicamente es:
|
||||
• La última vela contraria antes de un movimiento fuerte del precio.
|
||||
o Por ejemplo, una vela bajista antes de una gran subida (order block de compra).
|
||||
o O una vela alcista antes de una gran caída (order block de venta).
|
||||
• Zona de consolidación previa al rompimiento.
|
||||
• Lugar donde probablemente se activaron muchas órdenes institucionales.
|
||||
¿Por qué son útiles?
|
||||
• Pueden funcionar como zonas de entrada precisas, con buen riesgo-beneficio.
|
||||
• Marcan niveles de interés institucional, donde podría haber reversión o continuación.
|
||||
• Ayudan a evitar entrar en zonas manipuladas.
|
||||
Tipos de Order Blocks
|
||||
• Order Block High Timeframe (OB)
|
||||
• Order Block Low Timeframe (OB)
|
||||
• Order Block en Quiebre (OBQ)
|
||||
• Killer Block (KOB)
|
||||
• Ghost Block (GOB)
|
||||
• Propulsion Block (POB)
|
||||
Todos los anteriores los tendrás disponibles en cada Timeframe, su interpretación depende de su tamaño, volumen, estructura, tiempo de creación, ETC.
|
||||
Hablemos de los Order Blocks de High time frame, los encontraremos en el grafico semanal (W), Diario (D), y 4 horas (4H).
|
||||
Estos bloques a pesar de ser enormes y para nada recomendables para buscar una entrada por el tamaño que tendría tu prospecto Stop Loss (en una estrategia Intraday), si te pueden ayudar a tener una primera orientación sobre la posible dirección del día, tomando en cuenta el activo que estemos analizando.
|
||||
Crucial Timeframe, o mejor dicho los timeframe que usaremos para considerar tomar una posición en el mercado (1H, 15m). en este caso los OB que encontremos, sí tendrán oportunidad de darnos acceso a un trade de probabilidad y de riesgo bien gestionado usando la estructura del precio a nuestro favor, y fungen excelente en todo tipo de estrategias.
|
||||
Ejemplo visual, estructura actual Nasdaq (01/06/25) podemos observar en este grafico de Timeframe Diario, que tenemos actualmente 2 Order blocks (OB) alcistas y 2 Order blocks En quiebre (OBQ) siendo 1 alcista y 1 bajista.
|
||||
|
||||
La estructura se va creando entorno a estos bloques, mismos que delimitan e impulsan el precio en una dirección según sea su interpretación
|
||||
Ejemplo visual mismo activo Nasdaq (01/06/25) en un time frame de 4H podemos observar la estructura creándose internamente con los OB ahora disponibles.
|
||||
|
||||
Cada time frame tendrá acceso a distintos OB dentro de los OB del time frame superior, dándote una mayor perspectiva de cual podría ser el siguiente movimiento del precio, y también dándote acceso a nuevos objetivos y zonas de posible entrada.
|
||||
Order Block en Quiebre (OBQ)
|
||||
El quiebre de estructura o de OB, hace referencia a momentos del mercado donde la tendencia actual es cambiada por el inicio de un proceso de giro o cambio de tendencia tras una toma de liquidez relevante (proceso de absorción; profundizaremos en el tema más adelante en los siguientes módulos), es decir se crea un Shift Market, y para tener una confluencia adicional que confirme el giro, este debe ser acompañado por un OBQ, para tener la primera referencia visual.
|
||||
Ejemplo visual, donde únicamente conservare los OBQ del anterior ejemplo con Nasdaq
|
||||
|
||||
Un OBQ es válido si una vela de su mismo time frame tiene un Close por debajo o por encima (respectivamente) del Open de la vela en cuestión OBQ, adicional al cierre como ya habíamos mencionado, mientras más alto sea el timeframe en el que estamos observando el OBQ en cuestión, será de mayor probabilidad para una posible entrada y mientras menor sea el timeframe habrá una mayor probabilidad de que el precio NO respete la zona.
|
||||
Ejemplo visual OBQ en timeframe de 1H, tras un proceso de absorción, donde el precio supera los anteriores máximos, para posteriormente hacer el quiebre del Bloque que inició el movimiento, Nasdaq (01/06/25)
|
||||
|
||||
Killer Block (KOB)
|
||||
Es un bloque especial, importante ya que suele ser muy protagonista en el movimiento actual y en un futuro suele ser testeado en distintas ocasiones, ya que posee liquidez prácticamente garantizada.
|
||||
Su característica principal, es que suele crear un HH o un LL, y marca el final de una tendencia.
|
||||
Ejemplo visual usando el anterior con Nasdaq time frame 1H, podemos observar en el máximo nuevo creado tras el proceso de absorción o toma de liquidez de máximos anteriores, se crea en la cima un KOB.
|
||||
|
||||
Ghost Block (GOB)
|
||||
Caracterizado por ser difícil de detectar, únicamente dejando rastro de mechas y su contundencia y relevancia para toma de decisiones es de grado menor, conocerlos como una confluencia más, pienso que puede ser positivo.
|
||||
Ejemplo visual usando el anterior ejemplo, donde se presentó un GOB en 1H, Nasdaq (01/06/25), y aunque no fue el principal factor del movimiento alcista, si formo parte del protagonismo del movimiento.
|
||||
|
||||
Propulsion Block (POB)
|
||||
Ejemplo visual de POB, timeframe 4H, el precio crea su estructura y presenta distintos tipos de bloques, pero el re-testeo del POB, crea la oportunidad de entrar en un fuerte movimiento bajista. Se caracteriza por ser una vela contraria al movimiento expansivo actual, y su re-testeo suele indicar trade de alta probabilidad.
|
||||
|
||||
Módulo 7
|
||||
Imbalances
|
||||
Fair Value Gap
|
||||
Se refiere a un desequilibrio o ineficiencia en el precio que ocurre cuando el mercado se mueve bruscamente en una dirección, dejando un vacío entre velas consecutivas sin que se hayan ejecutado transacciones en esa zona. Este hueco representa una zona donde no hubo oferta y demanda equilibradas.
|
||||
Este mismo será la base para comprender el resto de los tipos de imbalances disponibles en el chart (grafico), ya que su interacción, una vez que han sido creados, puede tener distintas interpretaciones, lo que se vuelve un tema profundo.
|
||||
Un FVG está representado por 3 velas, donde la vela 2 es la creadora del movimiento expansivo y las velas 1 y 3 con sus respectivas mechas, en confluencia con el cuerpo de la vela 2, forman el high y el low de nuestro FVG.
|
||||
Ejemplo visual, FVG bajista.
|
||||
|
||||
|
||||
Ejemplo visual, FVG alcista.
|
||||
|
||||
|
||||
El FVG es una buena confluencia para la creación de una estrategia, ya que suelen ser importantes zonas de reacción y atractores del precio. el IPDA está programado para balancear las órdenes. y crear momentum berish o bullish (bajista o alcista). donde no hubo oportunidad de tenerlo.
|
||||
El FVG está presente en todas las temporalidades, y su interpretación dependerá de:
|
||||
• La estructura actual del precio.
|
||||
• Tendencia macro (1h y 4h).
|
||||
iFVG (inversión Fair Value Gap)
|
||||
Es una transformación del FVG cotidiano, la característica principal de este imbalance es que el precio a cruzado delante del FVG inicial, no respetando la interpretación inicial y usándolo precisamente al revés.
|
||||
Es decir, un FVG alcista al convertirse en iFVG podríamos considerar que su interpretación ahora será bajista.
|
||||
Y lo mismo si el FVG inicial fuera bajista al transformarse en iFVG ahora su interpretación deberá ser alcista
|
||||
Ejemplo visual iFVG bajista
|
||||
|
||||
|
||||
Ejemplo visual iFVG alcista
|
||||
|
||||
|
||||
BPR (Balance Price Range)
|
||||
Al igual que el iFVG su interpretación deberá modificarse según el testeo de este imbalance, la diferencia con el iFVG únicamente se centra en que el BPR es “respetado” en esencia de imbalance, a pesar de ser cruzado por completo por enfrente por parte del precio
|
||||
Ejemplo visual BPR
|
||||
|
||||
|
||||
|
||||
|
||||
VFVG (Volume Fair Value Gap)
|
||||
Representa un momento en el gráfico donde hubo un cierre y una apertura de 2 velas consecutivas, con una diferencia de puntos que crea este imbalance, pero con la cualidad de que las mechas de dichas velas coinciden dentro del imbalance y el cierre y apertura de las velas creadoras, son el máximo y el mínimo respectivamente para este imbalance.
|
||||
Suelen ser muy esporádicos, y suelen aparecer en momentos de mucha volatilidad, representando así un atractor y zona de reacción para el precio.
|
||||
Ejemplo visual de VFVG
|
||||
|
||||
NWOG (New Weekly Open Gap)
|
||||
Su aparición tiene en esencia una importante participación durante el desarrollo de la vela semanal y de la estructura semanal. Suele ser un catalizador y también uno de los atractores del precio con mayor relevancia.
|
||||
Durante la apertura de la bolsa Asiática el día Domingo, podemos observar el NWOG, es muy importante tenerlo en cuenta si se requiere tener un BIAS semanal con claridad.
|
||||
Ejemplo visual de NWOG
|
||||
|
||||
NWOG creado de viernes a domingo.
|
||||
NDOG (New Daily Open Gap)
|
||||
Suele aparecer cuando la sesión americana termina con un movimiento expansivo o se tiene un proceso de alta volatilidad o se requiere tomar un punto de liquidez sin oportunidad de nuevos trades.
|
||||
Al reiniciar la sesión asiática, sin haber cambio de semana, el NDOG suele ser importante, pero con menor nivel de relevancia que el NWOG.
|
||||
Ejemplo visual de NDOG
|
||||
|
||||
NDOG creado de jueves para viernes durante el desarrollo semanal
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "STACK-TECNOLOGICO"
|
||||
title: "Stack Tecnologico - OrbiQuant IA"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Stack Tecnologico - OrbiQuant IA
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "VISION-PRODUCTO"
|
||||
title: "Vision del Producto - OrbiQuant IA"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Vision del Producto - OrbiQuant IA
|
||||
|
||||
**Version:** 1.0.0
|
||||
@ -115,6 +124,87 @@ Empoderar a personas de todos los niveles de experiencia para que puedan inverti
|
||||
|
||||
**Objetivo:** Aprender de la comunidad y validar estrategias.
|
||||
|
||||
### 5. Wallet Completo
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ SISTEMA DE WALLET │
|
||||
├─────────────────────────────────────────┤
|
||||
│ DEPOSITOS │
|
||||
│ ├── Tarjeta credito/debito (Stripe) │
|
||||
│ ├── SPEI (transferencia Mexico) │
|
||||
│ └── Crypto (BTC, ETH, USDT) │
|
||||
│ │
|
||||
│ RETIROS │
|
||||
│ ├── Cuenta bancaria (KYC requerido) │
|
||||
│ ├── Wallet crypto │
|
||||
│ └── Instantaneo a wallet interno │
|
||||
│ │
|
||||
│ TRANSFERENCIAS │
|
||||
│ ├── P2P entre usuarios (0% comision) │
|
||||
│ ├── Fondear Money Managers │
|
||||
│ └── Comprar en Marketplace │
|
||||
│ │
|
||||
│ RENDIMIENTOS │
|
||||
│ ├── Deposito automatico de ganancias │
|
||||
│ ├── Historial detallado │
|
||||
│ └── Reportes fiscales │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Objetivo:** Un wallet universal para todas las operaciones de la plataforma.
|
||||
|
||||
**Limites:**
|
||||
|
||||
| Operacion | Minimo | Maximo |
|
||||
|-----------|--------|--------|
|
||||
| Deposito tarjeta | $10 USD | $10,000 USD |
|
||||
| Deposito SPEI | $100 MXN | $500,000 MXN |
|
||||
| Deposito crypto | $10 equiv | $50,000/dia |
|
||||
| Retiro banco | $50 USD | $25,000 USD |
|
||||
| Retiro crypto | $50 equiv | $25,000/dia |
|
||||
| Transferencia P2P | $1 USD | $5,000 USD |
|
||||
|
||||
### 6. Marketplace de Productos
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ MARKETPLACE │
|
||||
├─────────────────────────────────────────┤
|
||||
│ SENALES ML PREMIUM │
|
||||
│ ├── Basic Pack: 50 senales ($9) │
|
||||
│ ├── Pro Pack: 200 senales ($29) │
|
||||
│ └── Unlimited: ilimitadas ($49/mes) │
|
||||
│ │
|
||||
│ ASESORIA FINANCIERA │
|
||||
│ ├── 30 minutos ($49) │
|
||||
│ ├── 60 minutos ($89) │
|
||||
│ └── 90 minutos ($119) │
|
||||
│ │
|
||||
│ VISUALIZACION PREMIUM │
|
||||
│ ├── Indicadores ML avanzados │
|
||||
│ ├── Predictor de Rango │
|
||||
│ ├── AMD Detector │
|
||||
│ └── Signal Overlay ($19/mes) │
|
||||
│ │
|
||||
│ CURSOS PREMIUM │
|
||||
│ ├── Trading avanzado ($29-199) │
|
||||
│ ├── Masterclasses │
|
||||
│ └── Certificaciones especiales │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Objetivo:** Monetizacion adicional con productos de alto valor.
|
||||
|
||||
**Modelo de Ingresos Marketplace:**
|
||||
|
||||
| Producto | Precio | Margen |
|
||||
|----------|--------|--------|
|
||||
| Senales Premium | $9-49/mes | 90% |
|
||||
| Asesoria | $49-119/sesion | 70% (30% asesor) |
|
||||
| Visualizacion | $19/mes | 95% |
|
||||
| Cursos | $29-199 | 85% |
|
||||
|
||||
---
|
||||
|
||||
## Modelo de Suscripcion
|
||||
@ -229,3 +319,6 @@ Empoderar a personas de todos los niveles de experiencia para que puedan inverti
|
||||
- [Arquitectura General](./ARQUITECTURA-GENERAL.md)
|
||||
- [Modelo de Negocio](./MODELO-NEGOCIO.md)
|
||||
- [Stack Tecnologico](./STACK-TECNOLOGICO.md)
|
||||
- [Modulo Payments](../02-definicion-modulos/OQI-005-payments-stripe/README.md)
|
||||
- [Modulo Marketplace](../02-definicion-modulos/OQI-009-marketplace/README.md)
|
||||
- [Analisis Wallet y Marketplace](../99-analisis/ANALISIS-SAAS-WALLET-MARKETPLACE.md)
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
---
|
||||
id: "MAP-00-vision-general"
|
||||
title: "Mapa de 00-vision-general"
|
||||
type: "Index"
|
||||
project: "trading-platform"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# _MAP: Vision General
|
||||
|
||||
**Ultima actualizacion:** 2025-12-05
|
||||
|
||||
252
docs/01-arquitectura/ARQUITECTURA-INTEGRACION-MT4-MCP-LLM.md
Normal file
252
docs/01-arquitectura/ARQUITECTURA-INTEGRACION-MT4-MCP-LLM.md
Normal file
@ -0,0 +1,252 @@
|
||||
---
|
||||
id: "ARQUITECTURA-INTEGRACION-MT4-MCP-LLM"
|
||||
title: "Arquitectura de Integracion MT4-MCP-LLM"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
created_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Arquitectura de Integracion MT4-MCP-LLM
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2026-01-04
|
||||
**Autor:** Tech-Leader Agent
|
||||
**Estado:** En Desarrollo
|
||||
|
||||
---
|
||||
|
||||
## Vision General
|
||||
|
||||
Este documento describe la arquitectura de integracion entre:
|
||||
- **MetaTrader4 (MT4)** - Plataforma de trading forex
|
||||
- **MCP Server** - Model Context Protocol para herramientas
|
||||
- **LLM Agent** - Copiloto de trading con IA
|
||||
- **ML Engine** - Modelos de prediccion de mercado
|
||||
|
||||
---
|
||||
|
||||
## Diagrama de Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ORBIQUANT IA - TRADING PLATFORM ARCHITECTURE │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ USER INTERFACES │ │
|
||||
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
|
||||
│ │ │ Web App │ │ Chat Widget │ │ Telegram Bot │ │ CLI │ │ │
|
||||
│ │ │ (React UI) │ │ (LLM UI) │ │ (Future) │ │ (Future) │ │ │
|
||||
│ │ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │ │
|
||||
│ └──────────┼──────────────────┼──────────────────┼──────────────────┼─────────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └──────────────────┼──────────────────┴──────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ API GATEWAY (Backend Express) │ │
|
||||
│ │ :3000 │ │
|
||||
│ └────────────────────────────────────┬────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────┼────────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ LLM AGENT │ │ ML ENGINE │ │ TRADING AGENTS │ │
|
||||
│ │ :8003 │ │ :8001 │ │ :8004 │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ Ollama LLM │ │ │ │ AMD Detector │ │ │ │ Atlas │ │ Orion │ │ │
|
||||
│ │ │ Llama 3 8B │ │ │ │ RangePredict │ │ │ │ (Cons) │ │ (Mod) │ │ │
|
||||
│ │ │ (GPU) │ │ │ │ TPSLClass │ │ │ └────────┘ └────────┘ │ │
|
||||
│ │ └──────────────┘ │ │ └──────────────┘ │ │ │ │
|
||||
│ │ │ │ │ │ ┌────────┐ │ │
|
||||
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │ Nova │ │ │
|
||||
│ │ │Trading Tools │◄┼─────┼─│ Signal API │ │ │ │ (Aggr) │ │ │
|
||||
│ │ │ - MT4 Tools │ │ │ └──────────────┘ │ │ └────────┘ │ │
|
||||
│ │ │ - ML Tools │ │ │ │ │ │ │
|
||||
│ │ │ - Strategy │ │ │ │ │ │ │
|
||||
│ │ └──────────────┘ │ │ │ │ │ │
|
||||
│ └────────┬─────────┘ └──────────────────┘ └──────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ MCP SERVER MT4 CONNECTOR │ │
|
||||
│ │ :3605 │ │
|
||||
│ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ MCP Tools: │ │ │
|
||||
│ │ │ - mt4_get_account - mt4_execute_trade - mt4_get_quote │ │ │
|
||||
│ │ │ - mt4_get_positions - mt4_close_position - mt4_modify_position │ │ │
|
||||
│ │ └────────────────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────┬───────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ MT4 GATEWAY SERVICE │ │
|
||||
│ │ :8005 │ │
|
||||
│ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ FastAPI Endpoints: │ │ │
|
||||
│ │ │ - GET /api/mt4/account - GET /api/mt4/positions │ │ │
|
||||
│ │ │ - POST /api/mt4/trade - DELETE /api/mt4/positions/{id} │ │ │
|
||||
│ │ │ - GET /api/mt4/tick/{symbol} - PUT /api/mt4/positions/{id} │ │ │
|
||||
│ │ └─────────────────────────────────────┬──────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────────────────────────────────────▼──────────────────────────────────┐ │ │
|
||||
│ │ │ MT4 Bridge Client (aiohttp) │ │ │
|
||||
│ │ │ - Comunicacion con EA Bridge │ │ │
|
||||
│ │ └────────────────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────┬───────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ HTTP/WebSocket │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ METATRADER 4 TERMINALS │ │
|
||||
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
|
||||
│ │ │ EA Bridge #1 │ │ EA Bridge #2 │ │ EA Bridge #3 │ │ │
|
||||
│ │ │ (IC Markets) │ │ (Pepperstone) │ │ (XM) │ │ │
|
||||
│ │ │ :8081 │ │ :8082 │ │ :8083 │ │ │
|
||||
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ DATA LAYER │ │
|
||||
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
|
||||
│ │ │ PostgreSQL │ │ Redis │ │ Binance API │ │ Market Data │ │ │
|
||||
│ │ │ :5432 │ │ :6379 │ │ (external) │ │ (external) │ │ │
|
||||
│ │ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flujo de Operaciones
|
||||
|
||||
### 1. Flujo de Chat con LLM
|
||||
|
||||
```
|
||||
Usuario (Chat)
|
||||
│
|
||||
▼
|
||||
LLM Agent (:8003)
|
||||
│
|
||||
├── Procesa mensaje
|
||||
├── Detecta intencion
|
||||
│
|
||||
▼
|
||||
¿Requiere Tool?
|
||||
│
|
||||
├─[SI]─► Ejecuta Tool
|
||||
│ │
|
||||
│ ├── MT4 Tool ──► MCP Server ──► MT4 Gateway ──► MT4
|
||||
│ ├── ML Tool ──► ML Engine
|
||||
│ └── Strategy Tool ──► Trading Agents
|
||||
│ │
|
||||
│ ▼
|
||||
│ Procesa resultado
|
||||
│
|
||||
└─[NO]─► Genera respuesta directa
|
||||
│
|
||||
▼
|
||||
Respuesta al Usuario
|
||||
```
|
||||
|
||||
### 2. Flujo de Ejecucion de Trade
|
||||
|
||||
```
|
||||
LLM Agent (Tool: execute_trade)
|
||||
│
|
||||
▼
|
||||
MCP Server MT4 (:3605)
|
||||
│ (Valida request, formatea)
|
||||
▼
|
||||
MT4 Gateway (:8005)
|
||||
│ (Prepara orden, risk check)
|
||||
▼
|
||||
MT4 Bridge Client
|
||||
│ (HTTP POST /trade)
|
||||
▼
|
||||
EA Bridge (MT4 Terminal)
|
||||
│ (Ejecuta orden)
|
||||
▼
|
||||
Broker Server
|
||||
│ (Confirma ejecucion)
|
||||
▼
|
||||
Response ──► MT4 Gateway ──► MCP Server ──► LLM Agent ──► Usuario
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Puertos de Servicios
|
||||
|
||||
| Servicio | Puerto | Descripcion |
|
||||
|----------|--------|-------------|
|
||||
| Frontend | 5173 | React UI (Vite) |
|
||||
| Backend API | 3000 | Express.js Gateway |
|
||||
| ML Engine | 8001 | FastAPI - Modelos ML |
|
||||
| Data Service | 8002 | FastAPI - Market Data |
|
||||
| LLM Agent | 8003 | FastAPI - Copiloto AI |
|
||||
| Trading Agents | 8004 | FastAPI - Atlas/Orion/Nova |
|
||||
| MT4 Gateway | 8005 | FastAPI - Bridge MT4 |
|
||||
| MCP Server MT4 | 3605 | MCP Tools MT4 |
|
||||
| PostgreSQL | 5432 | Base de datos |
|
||||
| Redis | 6379 | Cache y sesiones |
|
||||
| Ollama | 11434 | LLM Server (GPU) |
|
||||
| EA Bridge #1 | 8081 | MT4 Terminal 1 |
|
||||
| EA Bridge #2 | 8082 | MT4 Terminal 2 |
|
||||
| EA Bridge #3 | 8083 | MT4 Terminal 3 |
|
||||
|
||||
---
|
||||
|
||||
## Componentes Implementados
|
||||
|
||||
### Completados
|
||||
|
||||
| Componente | Ubicacion | Estado |
|
||||
|------------|-----------|--------|
|
||||
| MT4 Bridge Client | `apps/mt4-gateway/src/providers/mt4_bridge_client.py` | OK |
|
||||
| LLM Tools MT4 | `apps/llm-agent/src/tools/mt4_tools.py` | OK |
|
||||
| LLM Tools ML | `apps/llm-agent/src/tools/ml_tools.py` | OK |
|
||||
| ML Engine | `apps/ml-engine/` | OK |
|
||||
| Trading Agents | `apps/trading-agents/` | OK |
|
||||
|
||||
### En Desarrollo
|
||||
|
||||
| Componente | Ubicacion | Estado |
|
||||
|------------|-----------|--------|
|
||||
| MT4 Gateway API | `apps/mt4-gateway/src/api/` | En progreso |
|
||||
| MCP Server MT4 | `apps/mcp-mt4-connector/` | En progreso |
|
||||
| Fine-tuning Pipeline | `apps/llm-agent/fine-tuning/` | En progreso |
|
||||
| Strategy Analysis Tools | `apps/llm-agent/src/tools/strategy_analysis.py` | En progreso |
|
||||
|
||||
---
|
||||
|
||||
## Seguridad
|
||||
|
||||
### Autenticacion
|
||||
|
||||
- JWT tokens para API Gateway
|
||||
- API keys para servicios internos
|
||||
- Auth token para EA Bridge
|
||||
|
||||
### Validaciones
|
||||
|
||||
- Risk checks antes de ejecutar trades
|
||||
- Validacion de volumenes y simbolos
|
||||
- Rate limiting en endpoints criticos
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- `docs/01-arquitectura/INTEGRACION-METATRADER4.md`
|
||||
- `docs/01-arquitectura/INTEGRACION-LLM-LOCAL.md`
|
||||
- `orchestration/PROXIMA-ACCION.md`
|
||||
- `core/mcp-servers/README.md`
|
||||
|
||||
---
|
||||
|
||||
*Documento generado por Tech-Leader Agent*
|
||||
*OrbiQuant IA Trading Platform*
|
||||
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "ARQUITECTURA-MULTI-AGENTE-MT4"
|
||||
title: "Arquitectura Multi-Agente MT4"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Arquitectura Multi-Agente MT4
|
||||
|
||||
**Fecha:** 2025-12-12
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "INTEGRACION-API-MASSIVE"
|
||||
title: "Integracion API Massive - Pipeline de Datos"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Integracion API Massive - Pipeline de Datos
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
1699
docs/01-arquitectura/INTEGRACION-LLM-FINE-TUNING.md
Normal file
1699
docs/01-arquitectura/INTEGRACION-LLM-FINE-TUNING.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "INTEGRACION-LLM-LOCAL"
|
||||
title: "Integracion LLM Local - chatgpt-oss 16GB"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Integracion LLM Local - chatgpt-oss 16GB
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "INTEGRACION-METATRADER4"
|
||||
title: "Integracion MetaTrader4 via MetaAPI"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Integracion MetaTrader4 via MetaAPI
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1255
docs/01-arquitectura/MCP-BINANCE-CONNECTOR-SPEC.md
Normal file
1255
docs/01-arquitectura/MCP-BINANCE-CONNECTOR-SPEC.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "README"
|
||||
title: "Fundamentos y Autenticación"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# OQI-001: Fundamentos y Autenticación
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
---
|
||||
id: "MAP-OQI-001-fundamentos-auth"
|
||||
title: "Mapa de OQI-001-fundamentos-auth"
|
||||
type: "Index"
|
||||
project: "trading-platform"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# _MAP: OQI-001 - Fundamentos y Autenticación
|
||||
|
||||
**Ultima actualizacion:** 2025-12-05
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "ET-AUTH-001"
|
||||
title: "OAuth Providers Implementation"
|
||||
type: "Specification"
|
||||
status: "Done"
|
||||
rf_parent: "RF-AUTH-001"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# ET-AUTH-001: Especificación Técnica - OAuth Providers
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "ET-AUTH-002"
|
||||
title: "JWT Tokens Implementation"
|
||||
type: "Specification"
|
||||
status: "Done"
|
||||
rf_parent: "RF-AUTH-002"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# ET-AUTH-002: Especificación Técnica - JWT Tokens
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "ET-AUTH-003"
|
||||
title: "Database Schema for Auth"
|
||||
type: "Specification"
|
||||
status: "Done"
|
||||
rf_parent: "RF-AUTH-002"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# ET-AUTH-003: Especificación Técnica - Esquema de Base de Datos
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "ET-AUTH-004"
|
||||
title: "API Endpoints for Auth"
|
||||
type: "Specification"
|
||||
status: "Done"
|
||||
rf_parent: "RF-AUTH-002"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# ET-AUTH-004: Especificación Técnica - API Endpoints
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "ET-AUTH-005"
|
||||
title: "Security Implementation"
|
||||
type: "Specification"
|
||||
status: "Done"
|
||||
rf_parent: "RF-AUTH-005"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# ET-AUTH-005: Especificación Técnica - Seguridad
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "US-AUTH-001"
|
||||
title: "Registro con Email"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 5
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-001: Registro con Email
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,259 +1,271 @@
|
||||
# US-AUTH-002: Login con Email
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P0 (Crítica)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario registrado de OrbiQuant
|
||||
**Quiero** iniciar sesión con mi email y contraseña
|
||||
**Para** acceder a mi cuenta y utilizar la plataforma
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Formulario de login
|
||||
|
||||
**Dado** que soy un usuario registrado
|
||||
**Cuando** accedo a la página de login
|
||||
**Entonces** debería ver un formulario con:
|
||||
- Campo de email
|
||||
- Campo de contraseña
|
||||
- Checkbox "Recordarme"
|
||||
- Botón "Iniciar sesión"
|
||||
- Link "¿Olvidaste tu contraseña?"
|
||||
- Opciones de OAuth
|
||||
|
||||
### AC-002: Validación de campos
|
||||
|
||||
**Dado** que estoy en el formulario de login
|
||||
**Cuando** intento enviar el formulario con campos vacíos
|
||||
**Entonces** debería ver mensajes de error:
|
||||
- "El email es requerido"
|
||||
- "La contraseña es requerida"
|
||||
|
||||
### AC-003: Login exitoso
|
||||
|
||||
**Dado** que ingresé credenciales válidas
|
||||
**Cuando** hago click en "Iniciar sesión"
|
||||
**Entonces** debería:
|
||||
1. Recibir un JWT token
|
||||
2. Ser redirigido al dashboard
|
||||
3. Ver mi nombre en el header
|
||||
4. Tener la sesión activa
|
||||
|
||||
### AC-004: Credenciales incorrectas
|
||||
|
||||
**Dado** que ingreso credenciales incorrectas
|
||||
**Cuando** hago click en "Iniciar sesión"
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Email o contraseña incorrectos"
|
||||
**Y** los campos no deberían limpiarse
|
||||
**Y** debería poder intentar de nuevo
|
||||
|
||||
### AC-005: Email no verificado
|
||||
|
||||
**Dado** que mi cuenta no ha verificado el email
|
||||
**Cuando** intento hacer login
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Por favor verifica tu email antes de iniciar sesión"
|
||||
**Y** debería ver un botón "Reenviar email de verificación"
|
||||
|
||||
### AC-006: Cuenta bloqueada
|
||||
|
||||
**Dado** que mi cuenta ha sido bloqueada por seguridad
|
||||
**Cuando** intento hacer login
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Tu cuenta ha sido bloqueada. Contacta soporte."
|
||||
|
||||
### AC-007: Recordarme
|
||||
|
||||
**Dado** que marqué la opción "Recordarme"
|
||||
**Cuando** inicio sesión exitosamente
|
||||
**Entonces** el token debería tener una duración de 30 días
|
||||
**Y** cuando cierre el navegador y vuelva a abrir
|
||||
**Entonces** debería seguir con sesión activa
|
||||
|
||||
### AC-008: Rate limiting
|
||||
|
||||
**Dado** que he fallado 5 intentos de login
|
||||
**Cuando** intento iniciar sesión nuevamente
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Demasiados intentos. Intenta en 15 minutos"
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Inicia sesión en OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ usuario@example.com │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Contraseña │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ •••••••••••• 👁 │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ☐ Recordarme ¿Olvidaste tu contraseña? │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Iniciar sesión │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ─────────────────── O continúa con ─────────────────── │
|
||||
│ │
|
||||
│ [Google] [Facebook] [X] [Apple] [GitHub] │
|
||||
│ │
|
||||
│ ¿No tienes cuenta? Regístrate │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Crear tabla `login_attempts` para rate limiting
|
||||
```sql
|
||||
CREATE TABLE login_attempts (
|
||||
id UUID PRIMARY KEY,
|
||||
email VARCHAR(255),
|
||||
ip_address VARCHAR(45),
|
||||
attempt_time TIMESTAMP,
|
||||
success BOOLEAN,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
- [ ] Índice en `email` y `attempt_time`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Endpoint `POST /api/v1/auth/login`
|
||||
- Validación de entrada con Zod
|
||||
- Verificar email verificado
|
||||
- Verificar cuenta no bloqueada
|
||||
- Comparar hash con bcrypt
|
||||
- Generar JWT token
|
||||
- Rate limiting (5 intentos / 15 min)
|
||||
- Logging de intentos
|
||||
- [ ] Service `AuthService.login()`
|
||||
- [ ] Middleware de rate limiting
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración (5 escenarios)
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Componente `apps/frontend/src/modules/auth/pages/Login.tsx`
|
||||
- [ ] Form validation con React Hook Form + Zod
|
||||
- [ ] Estado de loading durante autenticación
|
||||
- [ ] Manejo de errores específicos
|
||||
- [ ] Almacenamiento de token en localStorage/sessionStorage
|
||||
- [ ] Redirección post-login
|
||||
- [ ] Tests con React Testing Library (6 casos)
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Login exitoso (Playwright)
|
||||
- [ ] E2E: Credenciales incorrectas
|
||||
- [ ] E2E: Email no verificado
|
||||
- [ ] E2E: Rate limiting
|
||||
- [ ] E2E: Recordarme funcionalidad
|
||||
- [ ] Test de seguridad: SQL injection
|
||||
- [ ] Test de seguridad: XSS
|
||||
- [ ] Performance: < 500ms response time
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- US-AUTH-001: Necesita usuarios registrados para poder hacer login
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-011: Para el flujo de "¿Olvidaste tu contraseña?"
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Mockups aprobados por UX
|
||||
- [ ] Esquema de base de datos revisado
|
||||
- [ ] API contract definido
|
||||
- [ ] Criterios de aceptación claros
|
||||
- [ ] Estimación acordada por el equipo
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado (code review)
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Documentación API actualizada
|
||||
- [ ] Rate limiting configurado
|
||||
- [ ] Logs implementados
|
||||
- [ ] Seguridad validada (OWASP)
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### JWT Token Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "user-id",
|
||||
"email": "user@example.com",
|
||||
"role": "user",
|
||||
"iat": 1234567890,
|
||||
"exp": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting Strategy
|
||||
|
||||
- 5 intentos fallidos por email
|
||||
- Ventana de 15 minutos
|
||||
- Reset después de login exitoso
|
||||
- Bloqueo temporal, no permanente
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- HTTPS obligatorio
|
||||
- Password no visible en logs
|
||||
- Tokens con expiración
|
||||
- CSRF protection
|
||||
- Content-Security-Policy headers
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-002: Autenticación por Email](../requerimientos/RF-AUTH-002-email.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-003: Database](../especificaciones/ET-AUTH-003-database.md)
|
||||
---
|
||||
id: "US-AUTH-002"
|
||||
title: "Login con Email"
|
||||
type: "User Story"
|
||||
status: "To Do"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-002: Login con Email
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P0 (Crítica)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario registrado de OrbiQuant
|
||||
**Quiero** iniciar sesión con mi email y contraseña
|
||||
**Para** acceder a mi cuenta y utilizar la plataforma
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Formulario de login
|
||||
|
||||
**Dado** que soy un usuario registrado
|
||||
**Cuando** accedo a la página de login
|
||||
**Entonces** debería ver un formulario con:
|
||||
- Campo de email
|
||||
- Campo de contraseña
|
||||
- Checkbox "Recordarme"
|
||||
- Botón "Iniciar sesión"
|
||||
- Link "¿Olvidaste tu contraseña?"
|
||||
- Opciones de OAuth
|
||||
|
||||
### AC-002: Validación de campos
|
||||
|
||||
**Dado** que estoy en el formulario de login
|
||||
**Cuando** intento enviar el formulario con campos vacíos
|
||||
**Entonces** debería ver mensajes de error:
|
||||
- "El email es requerido"
|
||||
- "La contraseña es requerida"
|
||||
|
||||
### AC-003: Login exitoso
|
||||
|
||||
**Dado** que ingresé credenciales válidas
|
||||
**Cuando** hago click en "Iniciar sesión"
|
||||
**Entonces** debería:
|
||||
1. Recibir un JWT token
|
||||
2. Ser redirigido al dashboard
|
||||
3. Ver mi nombre en el header
|
||||
4. Tener la sesión activa
|
||||
|
||||
### AC-004: Credenciales incorrectas
|
||||
|
||||
**Dado** que ingreso credenciales incorrectas
|
||||
**Cuando** hago click en "Iniciar sesión"
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Email o contraseña incorrectos"
|
||||
**Y** los campos no deberían limpiarse
|
||||
**Y** debería poder intentar de nuevo
|
||||
|
||||
### AC-005: Email no verificado
|
||||
|
||||
**Dado** que mi cuenta no ha verificado el email
|
||||
**Cuando** intento hacer login
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Por favor verifica tu email antes de iniciar sesión"
|
||||
**Y** debería ver un botón "Reenviar email de verificación"
|
||||
|
||||
### AC-006: Cuenta bloqueada
|
||||
|
||||
**Dado** que mi cuenta ha sido bloqueada por seguridad
|
||||
**Cuando** intento hacer login
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Tu cuenta ha sido bloqueada. Contacta soporte."
|
||||
|
||||
### AC-007: Recordarme
|
||||
|
||||
**Dado** que marqué la opción "Recordarme"
|
||||
**Cuando** inicio sesión exitosamente
|
||||
**Entonces** el token debería tener una duración de 30 días
|
||||
**Y** cuando cierre el navegador y vuelva a abrir
|
||||
**Entonces** debería seguir con sesión activa
|
||||
|
||||
### AC-008: Rate limiting
|
||||
|
||||
**Dado** que he fallado 5 intentos de login
|
||||
**Cuando** intento iniciar sesión nuevamente
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Demasiados intentos. Intenta en 15 minutos"
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Inicia sesión en OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ usuario@example.com │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Contraseña │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ •••••••••••• 👁 │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ☐ Recordarme ¿Olvidaste tu contraseña? │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Iniciar sesión │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ─────────────────── O continúa con ─────────────────── │
|
||||
│ │
|
||||
│ [Google] [Facebook] [X] [Apple] [GitHub] │
|
||||
│ │
|
||||
│ ¿No tienes cuenta? Regístrate │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Crear tabla `login_attempts` para rate limiting
|
||||
```sql
|
||||
CREATE TABLE login_attempts (
|
||||
id UUID PRIMARY KEY,
|
||||
email VARCHAR(255),
|
||||
ip_address VARCHAR(45),
|
||||
attempt_time TIMESTAMP,
|
||||
success BOOLEAN,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
- [ ] Índice en `email` y `attempt_time`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Endpoint `POST /api/v1/auth/login`
|
||||
- Validación de entrada con Zod
|
||||
- Verificar email verificado
|
||||
- Verificar cuenta no bloqueada
|
||||
- Comparar hash con bcrypt
|
||||
- Generar JWT token
|
||||
- Rate limiting (5 intentos / 15 min)
|
||||
- Logging de intentos
|
||||
- [ ] Service `AuthService.login()`
|
||||
- [ ] Middleware de rate limiting
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración (5 escenarios)
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Componente `apps/frontend/src/modules/auth/pages/Login.tsx`
|
||||
- [ ] Form validation con React Hook Form + Zod
|
||||
- [ ] Estado de loading durante autenticación
|
||||
- [ ] Manejo de errores específicos
|
||||
- [ ] Almacenamiento de token en localStorage/sessionStorage
|
||||
- [ ] Redirección post-login
|
||||
- [ ] Tests con React Testing Library (6 casos)
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Login exitoso (Playwright)
|
||||
- [ ] E2E: Credenciales incorrectas
|
||||
- [ ] E2E: Email no verificado
|
||||
- [ ] E2E: Rate limiting
|
||||
- [ ] E2E: Recordarme funcionalidad
|
||||
- [ ] Test de seguridad: SQL injection
|
||||
- [ ] Test de seguridad: XSS
|
||||
- [ ] Performance: < 500ms response time
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- US-AUTH-001: Necesita usuarios registrados para poder hacer login
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-011: Para el flujo de "¿Olvidaste tu contraseña?"
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Mockups aprobados por UX
|
||||
- [ ] Esquema de base de datos revisado
|
||||
- [ ] API contract definido
|
||||
- [ ] Criterios de aceptación claros
|
||||
- [ ] Estimación acordada por el equipo
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado (code review)
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Documentación API actualizada
|
||||
- [ ] Rate limiting configurado
|
||||
- [ ] Logs implementados
|
||||
- [ ] Seguridad validada (OWASP)
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### JWT Token Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "user-id",
|
||||
"email": "user@example.com",
|
||||
"role": "user",
|
||||
"iat": 1234567890,
|
||||
"exp": 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting Strategy
|
||||
|
||||
- 5 intentos fallidos por email
|
||||
- Ventana de 15 minutos
|
||||
- Reset después de login exitoso
|
||||
- Bloqueo temporal, no permanente
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- HTTPS obligatorio
|
||||
- Password no visible en logs
|
||||
- Tokens con expiración
|
||||
- CSRF protection
|
||||
- Content-Security-Policy headers
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-002: Autenticación por Email](../requerimientos/RF-AUTH-002-email.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-003: Database](../especificaciones/ET-AUTH-003-database.md)
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "US-AUTH-003"
|
||||
title: "Login con Google"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 5
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-003: Login con Google
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,293 +1,305 @@
|
||||
# US-AUTH-004: OAuth Facebook
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** visitante o usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de Facebook
|
||||
**Para** tener un acceso rápido y sencillo sin crear una nueva contraseña
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de Facebook visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con Facebook"
|
||||
**Y** debería tener el color y logo oficial de Facebook
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con Facebook"
|
||||
**Cuando** se abre la ventana de Facebook
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de Facebook
|
||||
2. Poder revisar los permisos solicitados
|
||||
3. Poder autorizar o cancelar
|
||||
|
||||
### AC-003: Permisos solicitados
|
||||
|
||||
**Dado** que estoy en la pantalla de autorización de Facebook
|
||||
**Cuando** reviso los permisos
|
||||
**Entonces** la app debería solicitar únicamente:
|
||||
- Email
|
||||
- Nombre público
|
||||
- Foto de perfil
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando Facebook OAuth
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre y foto de Facebook
|
||||
5. NO necesitar verificación de email
|
||||
|
||||
### AC-005: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con Facebook
|
||||
**Cuando** uso "Continuar con Facebook"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Ser redirigido al dashboard
|
||||
3. NO ver pantalla de registro
|
||||
|
||||
### AC-006: Email ya registrado con otro método
|
||||
|
||||
**Dado** que mi email de Facebook ya está registrado con email/password
|
||||
**Cuando** intento usar Facebook OAuth
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Este email ya está registrado. ¿Deseas vincular tu cuenta de Facebook?"
|
||||
**Y** debería poder vincular las cuentas
|
||||
|
||||
### AC-007: Cancelación del flujo
|
||||
|
||||
**Dado** que inicio el flujo de Facebook OAuth
|
||||
**Cuando** cancelo en la ventana de Facebook
|
||||
**Entonces** debería:
|
||||
1. Volver a la página de login/registro
|
||||
2. Ver un mensaje "Autenticación cancelada"
|
||||
3. Poder intentar con otro método
|
||||
|
||||
### AC-008: Error de Facebook
|
||||
|
||||
**Dado** que hay un error en el servicio de Facebook
|
||||
**Cuando** intento autenticarme
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Error al conectar con Facebook. Intenta más tarde"
|
||||
**Y** debería poder usar otro método de autenticación
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔵 Continuar con Facebook │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Twitter/X] [Apple] [GitHub] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de Facebook OAuth:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ facebook.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ OrbiQuant desea acceder a: │
|
||||
│ │
|
||||
│ ✓ Tu nombre y foto de perfil │
|
||||
│ ✓ Tu dirección de email │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Continuar como Juan Pérez │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancelar │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN facebook_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN avatar_url TEXT;
|
||||
```
|
||||
- [ ] Tabla `oauth_connections`:
|
||||
```sql
|
||||
CREATE TABLE oauth_connections (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
provider VARCHAR(50), -- 'facebook', 'google', etc
|
||||
provider_user_id VARCHAR(255),
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(provider, provider_user_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar Facebook App en Meta Developers
|
||||
- [ ] Obtener App ID y App Secret
|
||||
- [ ] Endpoint `GET /api/v1/auth/facebook`
|
||||
- Redirige a Facebook OAuth
|
||||
- [ ] Endpoint `GET /api/v1/auth/facebook/callback`
|
||||
- Recibe código de autorización
|
||||
- Intercambia por access token
|
||||
- Obtiene datos del usuario
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Service `FacebookOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `exchangeCodeForToken()`
|
||||
- `getUserProfile()`
|
||||
- `linkAccount()`
|
||||
- [ ] Manejo de refresh tokens
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock de Facebook API
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Continuar con Facebook"
|
||||
- [ ] Manejo de popup o redirect de OAuth
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading durante OAuth
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Modal de vinculación de cuentas
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro nuevo con Facebook
|
||||
- [ ] E2E: Login existente con Facebook
|
||||
- [ ] E2E: Vinculación de cuentas
|
||||
- [ ] E2E: Cancelación del flujo
|
||||
- [ ] E2E: Permisos rechazados
|
||||
- [ ] Test de seguridad: Validación de tokens
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de Facebook API para tests
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Cuenta de Facebook Developer
|
||||
- Configuración de dominio verificado
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Para mantener consistencia con Google OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Facebook App creada y configurada
|
||||
- [ ] Credenciales de desarrollo disponibles
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Política de privacidad publicada (requerido por Facebook)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Facebook App Review aprobado (para producción)
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Manejo de errores completo
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Facebook OAuth Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/facebook`
|
||||
2. Backend redirige a Facebook con:
|
||||
- `client_id`
|
||||
- `redirect_uri`
|
||||
- `scope=email,public_profile`
|
||||
- `state` (CSRF token)
|
||||
3. Usuario autoriza en Facebook
|
||||
4. Facebook redirige a `redirect_uri` con `code`
|
||||
5. Backend intercambia `code` por `access_token`
|
||||
6. Backend obtiene perfil del usuario
|
||||
7. Backend crea/actualiza usuario
|
||||
8. Backend genera JWT y redirige a frontend
|
||||
|
||||
### Facebook API Endpoints
|
||||
|
||||
- Authorization: `https://www.facebook.com/v18.0/dialog/oauth`
|
||||
- Token exchange: `https://graph.facebook.com/v18.0/oauth/access_token`
|
||||
- User info: `https://graph.facebook.com/v18.0/me?fields=id,name,email,picture`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
FACEBOOK_APP_ID=your_app_id
|
||||
FACEBOOK_APP_SECRET=your_app_secret
|
||||
FACEBOOK_CALLBACK_URL=https://orbiquant.com/api/v1/auth/facebook/callback
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Validar `state` parameter para prevenir CSRF
|
||||
- No almacenar access tokens en localStorage
|
||||
- Usar refresh tokens cuando sea posible
|
||||
- Validar que el email viene de Facebook
|
||||
- Rate limiting en endpoints de OAuth
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
---
|
||||
id: "US-AUTH-004"
|
||||
title: "OAuth Facebook"
|
||||
type: "User Story"
|
||||
status: "To Do"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-004: OAuth Facebook
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** visitante o usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de Facebook
|
||||
**Para** tener un acceso rápido y sencillo sin crear una nueva contraseña
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de Facebook visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con Facebook"
|
||||
**Y** debería tener el color y logo oficial de Facebook
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con Facebook"
|
||||
**Cuando** se abre la ventana de Facebook
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de Facebook
|
||||
2. Poder revisar los permisos solicitados
|
||||
3. Poder autorizar o cancelar
|
||||
|
||||
### AC-003: Permisos solicitados
|
||||
|
||||
**Dado** que estoy en la pantalla de autorización de Facebook
|
||||
**Cuando** reviso los permisos
|
||||
**Entonces** la app debería solicitar únicamente:
|
||||
- Email
|
||||
- Nombre público
|
||||
- Foto de perfil
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando Facebook OAuth
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre y foto de Facebook
|
||||
5. NO necesitar verificación de email
|
||||
|
||||
### AC-005: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con Facebook
|
||||
**Cuando** uso "Continuar con Facebook"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Ser redirigido al dashboard
|
||||
3. NO ver pantalla de registro
|
||||
|
||||
### AC-006: Email ya registrado con otro método
|
||||
|
||||
**Dado** que mi email de Facebook ya está registrado con email/password
|
||||
**Cuando** intento usar Facebook OAuth
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Este email ya está registrado. ¿Deseas vincular tu cuenta de Facebook?"
|
||||
**Y** debería poder vincular las cuentas
|
||||
|
||||
### AC-007: Cancelación del flujo
|
||||
|
||||
**Dado** que inicio el flujo de Facebook OAuth
|
||||
**Cuando** cancelo en la ventana de Facebook
|
||||
**Entonces** debería:
|
||||
1. Volver a la página de login/registro
|
||||
2. Ver un mensaje "Autenticación cancelada"
|
||||
3. Poder intentar con otro método
|
||||
|
||||
### AC-008: Error de Facebook
|
||||
|
||||
**Dado** que hay un error en el servicio de Facebook
|
||||
**Cuando** intento autenticarme
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Error al conectar con Facebook. Intenta más tarde"
|
||||
**Y** debería poder usar otro método de autenticación
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔵 Continuar con Facebook │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Twitter/X] [Apple] [GitHub] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de Facebook OAuth:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ facebook.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ OrbiQuant desea acceder a: │
|
||||
│ │
|
||||
│ ✓ Tu nombre y foto de perfil │
|
||||
│ ✓ Tu dirección de email │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Continuar como Juan Pérez │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancelar │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN facebook_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN avatar_url TEXT;
|
||||
```
|
||||
- [ ] Tabla `oauth_connections`:
|
||||
```sql
|
||||
CREATE TABLE oauth_connections (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
provider VARCHAR(50), -- 'facebook', 'google', etc
|
||||
provider_user_id VARCHAR(255),
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(provider, provider_user_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar Facebook App en Meta Developers
|
||||
- [ ] Obtener App ID y App Secret
|
||||
- [ ] Endpoint `GET /api/v1/auth/facebook`
|
||||
- Redirige a Facebook OAuth
|
||||
- [ ] Endpoint `GET /api/v1/auth/facebook/callback`
|
||||
- Recibe código de autorización
|
||||
- Intercambia por access token
|
||||
- Obtiene datos del usuario
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Service `FacebookOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `exchangeCodeForToken()`
|
||||
- `getUserProfile()`
|
||||
- `linkAccount()`
|
||||
- [ ] Manejo de refresh tokens
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock de Facebook API
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Continuar con Facebook"
|
||||
- [ ] Manejo de popup o redirect de OAuth
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading durante OAuth
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Modal de vinculación de cuentas
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro nuevo con Facebook
|
||||
- [ ] E2E: Login existente con Facebook
|
||||
- [ ] E2E: Vinculación de cuentas
|
||||
- [ ] E2E: Cancelación del flujo
|
||||
- [ ] E2E: Permisos rechazados
|
||||
- [ ] Test de seguridad: Validación de tokens
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de Facebook API para tests
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Cuenta de Facebook Developer
|
||||
- Configuración de dominio verificado
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Para mantener consistencia con Google OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Facebook App creada y configurada
|
||||
- [ ] Credenciales de desarrollo disponibles
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Política de privacidad publicada (requerido por Facebook)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Facebook App Review aprobado (para producción)
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Manejo de errores completo
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Facebook OAuth Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/facebook`
|
||||
2. Backend redirige a Facebook con:
|
||||
- `client_id`
|
||||
- `redirect_uri`
|
||||
- `scope=email,public_profile`
|
||||
- `state` (CSRF token)
|
||||
3. Usuario autoriza en Facebook
|
||||
4. Facebook redirige a `redirect_uri` con `code`
|
||||
5. Backend intercambia `code` por `access_token`
|
||||
6. Backend obtiene perfil del usuario
|
||||
7. Backend crea/actualiza usuario
|
||||
8. Backend genera JWT y redirige a frontend
|
||||
|
||||
### Facebook API Endpoints
|
||||
|
||||
- Authorization: `https://www.facebook.com/v18.0/dialog/oauth`
|
||||
- Token exchange: `https://graph.facebook.com/v18.0/oauth/access_token`
|
||||
- User info: `https://graph.facebook.com/v18.0/me?fields=id,name,email,picture`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
FACEBOOK_APP_ID=your_app_id
|
||||
FACEBOOK_APP_SECRET=your_app_secret
|
||||
FACEBOOK_CALLBACK_URL=https://orbiquant.com/api/v1/auth/facebook/callback
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Validar `state` parameter para prevenir CSRF
|
||||
- No almacenar access tokens en localStorage
|
||||
- Usar refresh tokens cuando sea posible
|
||||
- Validar que el email viene de Facebook
|
||||
- Rate limiting en endpoints de OAuth
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
|
||||
@ -1,316 +1,328 @@
|
||||
# US-AUTH-005: OAuth Twitter/X
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** visitante o usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de Twitter/X
|
||||
**Para** tener un acceso rápido sin crear una nueva contraseña
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de Twitter/X visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con X"
|
||||
**Y** debería tener el logo y estilo oficial de X (antes Twitter)
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con X"
|
||||
**Cuando** se abre la ventana de autorización
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de X
|
||||
2. Poder revisar los permisos solicitados
|
||||
3. Poder autorizar la aplicación
|
||||
|
||||
### AC-003: Permisos solicitados
|
||||
|
||||
**Dado** que estoy en la pantalla de autorización de X
|
||||
**Cuando** reviso los permisos
|
||||
**Entonces** la app debería solicitar:
|
||||
- Leer información de perfil
|
||||
- Email (si está disponible)
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando X OAuth
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre y foto de X
|
||||
5. Si X no proporciona email, solicitar email adicional
|
||||
|
||||
### AC-005: Solicitud de email adicional
|
||||
|
||||
**Dado** que X no proporcionó mi email
|
||||
**Cuando** completo la autorización
|
||||
**Entonces** debería ver un formulario que solicita:
|
||||
- "Completa tu registro: ingresa tu email"
|
||||
**Y** debería validar que el email no esté en uso
|
||||
**Y** debería enviar email de verificación
|
||||
|
||||
### AC-006: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con X
|
||||
**Cuando** uso "Continuar con X"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Ser redirigido al dashboard
|
||||
|
||||
### AC-007: Email ya registrado
|
||||
|
||||
**Dado** que mi email de X ya está registrado con otro método
|
||||
**Cuando** intento usar X OAuth
|
||||
**Entonces** debería ver opción de vincular cuentas
|
||||
**Y** debería poder vincular después de autenticarme
|
||||
|
||||
### AC-008: Cancelación del flujo
|
||||
|
||||
**Dado** que inicio el flujo de X OAuth
|
||||
**Cuando** cancelo en la ventana de X
|
||||
**Entonces** debería volver a login/registro
|
||||
**Y** ver mensaje "Autenticación cancelada"
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ ⚫ Continuar con X │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Facebook] [Apple] [GitHub] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de X OAuth:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ x.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Autorizar OrbiQuant │
|
||||
│ │
|
||||
│ Esta aplicación podrá: │
|
||||
│ │
|
||||
│ • Ver tu perfil y posts │
|
||||
│ • Ver los perfiles que sigues │
|
||||
│ • Actualizar tu perfil │
|
||||
│ │
|
||||
│ @juanperez │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Autorizar aplicación │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancelar │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Formulario adicional si falta email:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Un último paso para completar tu registro │
|
||||
│ │
|
||||
│ X no compartió tu email. Por favor ingrésalo: │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ tu@email.com │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Completar registro │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN twitter_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN twitter_username VARCHAR(255);
|
||||
```
|
||||
- [ ] Usar tabla `oauth_connections` existente
|
||||
- [ ] Índice en `twitter_id`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar X Developer App
|
||||
- [ ] Obtener API Key y API Secret
|
||||
- [ ] Endpoint `GET /api/v1/auth/twitter`
|
||||
- Redirige a X OAuth
|
||||
- [ ] Endpoint `GET /api/v1/auth/twitter/callback`
|
||||
- Recibe código de autorización
|
||||
- Intercambia por access token
|
||||
- Obtiene datos del usuario
|
||||
- Verifica si email está disponible
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Endpoint `POST /api/v1/auth/twitter/complete-email`
|
||||
- Para usuarios sin email de X
|
||||
- Valida email
|
||||
- Envía verificación
|
||||
- [ ] Service `TwitterOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `exchangeCodeForToken()`
|
||||
- `getUserProfile()`
|
||||
- `linkAccount()`
|
||||
- [ ] Tests unitarios (10 casos)
|
||||
- [ ] Tests de integración con mock de X API
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Continuar con X"
|
||||
- [ ] Manejo de popup/redirect OAuth
|
||||
- [ ] Componente `CompleteEmailForm.tsx`
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con X (con email)
|
||||
- [ ] E2E: Registro con X (sin email, completar manualmente)
|
||||
- [ ] E2E: Login existente con X
|
||||
- [ ] E2E: Vinculación de cuentas
|
||||
- [ ] E2E: Cancelación del flujo
|
||||
- [ ] Test de seguridad: Validación de tokens
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de X API para tests
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Cuenta de X Developer (con nivel Elevated o superior)
|
||||
- Configuración de OAuth 2.0 en X Developer Portal
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Consistencia con otros OAuth
|
||||
- US-AUTH-004: Patrón similar a Facebook OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] X Developer App creada
|
||||
- [ ] Credenciales OAuth 2.0 disponibles
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Flujo de email adicional diseñado
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Manejo de errores completo
|
||||
- [ ] Flujo de email adicional funcional
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Twitter/X OAuth 2.0 Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/twitter`
|
||||
2. Backend redirige a X con:
|
||||
- `client_id`
|
||||
- `redirect_uri`
|
||||
- `scope=tweet.read users.read offline.access`
|
||||
- `state` (CSRF token)
|
||||
- `code_challenge` (PKCE)
|
||||
3. Usuario autoriza en X
|
||||
4. X redirige a `redirect_uri` con `code`
|
||||
5. Backend intercambia `code` por `access_token`
|
||||
6. Backend obtiene perfil del usuario
|
||||
7. Si no hay email, redirige a formulario adicional
|
||||
8. Backend crea/actualiza usuario
|
||||
9. Backend genera JWT
|
||||
|
||||
### X API v2 Endpoints
|
||||
|
||||
- Authorization: `https://twitter.com/i/oauth2/authorize`
|
||||
- Token exchange: `https://api.twitter.com/2/oauth2/token`
|
||||
- User info: `https://api.twitter.com/2/users/me?user.fields=profile_image_url`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
TWITTER_CLIENT_ID=your_client_id
|
||||
TWITTER_CLIENT_SECRET=your_client_secret
|
||||
TWITTER_CALLBACK_URL=https://orbiquant.com/api/v1/auth/twitter/callback
|
||||
```
|
||||
|
||||
### Consideraciones Especiales de X
|
||||
|
||||
- X no siempre proporciona email del usuario
|
||||
- Requiere OAuth 2.0 con PKCE
|
||||
- Necesita scope `offline.access` para refresh tokens
|
||||
- Rate limits más estrictos que otros proveedores
|
||||
- Requiere X Developer Account con nivel "Elevated" mínimo
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Implementar PKCE (Proof Key for Code Exchange)
|
||||
- Validar `state` parameter (CSRF)
|
||||
- Validar email adicional si es requerido
|
||||
- Rate limiting en endpoints
|
||||
- Logs de intentos de autenticación
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
---
|
||||
id: "US-AUTH-005"
|
||||
title: "OAuth Twitter/X"
|
||||
type: "User Story"
|
||||
status: "To Do"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-005: OAuth Twitter/X
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** visitante o usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de Twitter/X
|
||||
**Para** tener un acceso rápido sin crear una nueva contraseña
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de Twitter/X visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con X"
|
||||
**Y** debería tener el logo y estilo oficial de X (antes Twitter)
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con X"
|
||||
**Cuando** se abre la ventana de autorización
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de X
|
||||
2. Poder revisar los permisos solicitados
|
||||
3. Poder autorizar la aplicación
|
||||
|
||||
### AC-003: Permisos solicitados
|
||||
|
||||
**Dado** que estoy en la pantalla de autorización de X
|
||||
**Cuando** reviso los permisos
|
||||
**Entonces** la app debería solicitar:
|
||||
- Leer información de perfil
|
||||
- Email (si está disponible)
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando X OAuth
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre y foto de X
|
||||
5. Si X no proporciona email, solicitar email adicional
|
||||
|
||||
### AC-005: Solicitud de email adicional
|
||||
|
||||
**Dado** que X no proporcionó mi email
|
||||
**Cuando** completo la autorización
|
||||
**Entonces** debería ver un formulario que solicita:
|
||||
- "Completa tu registro: ingresa tu email"
|
||||
**Y** debería validar que el email no esté en uso
|
||||
**Y** debería enviar email de verificación
|
||||
|
||||
### AC-006: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con X
|
||||
**Cuando** uso "Continuar con X"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Ser redirigido al dashboard
|
||||
|
||||
### AC-007: Email ya registrado
|
||||
|
||||
**Dado** que mi email de X ya está registrado con otro método
|
||||
**Cuando** intento usar X OAuth
|
||||
**Entonces** debería ver opción de vincular cuentas
|
||||
**Y** debería poder vincular después de autenticarme
|
||||
|
||||
### AC-008: Cancelación del flujo
|
||||
|
||||
**Dado** que inicio el flujo de X OAuth
|
||||
**Cuando** cancelo en la ventana de X
|
||||
**Entonces** debería volver a login/registro
|
||||
**Y** ver mensaje "Autenticación cancelada"
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ ⚫ Continuar con X │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Facebook] [Apple] [GitHub] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de X OAuth:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ x.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Autorizar OrbiQuant │
|
||||
│ │
|
||||
│ Esta aplicación podrá: │
|
||||
│ │
|
||||
│ • Ver tu perfil y posts │
|
||||
│ • Ver los perfiles que sigues │
|
||||
│ • Actualizar tu perfil │
|
||||
│ │
|
||||
│ @juanperez │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Autorizar aplicación │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancelar │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Formulario adicional si falta email:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Un último paso para completar tu registro │
|
||||
│ │
|
||||
│ X no compartió tu email. Por favor ingrésalo: │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ tu@email.com │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Completar registro │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN twitter_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN twitter_username VARCHAR(255);
|
||||
```
|
||||
- [ ] Usar tabla `oauth_connections` existente
|
||||
- [ ] Índice en `twitter_id`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar X Developer App
|
||||
- [ ] Obtener API Key y API Secret
|
||||
- [ ] Endpoint `GET /api/v1/auth/twitter`
|
||||
- Redirige a X OAuth
|
||||
- [ ] Endpoint `GET /api/v1/auth/twitter/callback`
|
||||
- Recibe código de autorización
|
||||
- Intercambia por access token
|
||||
- Obtiene datos del usuario
|
||||
- Verifica si email está disponible
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Endpoint `POST /api/v1/auth/twitter/complete-email`
|
||||
- Para usuarios sin email de X
|
||||
- Valida email
|
||||
- Envía verificación
|
||||
- [ ] Service `TwitterOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `exchangeCodeForToken()`
|
||||
- `getUserProfile()`
|
||||
- `linkAccount()`
|
||||
- [ ] Tests unitarios (10 casos)
|
||||
- [ ] Tests de integración con mock de X API
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Continuar con X"
|
||||
- [ ] Manejo de popup/redirect OAuth
|
||||
- [ ] Componente `CompleteEmailForm.tsx`
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con X (con email)
|
||||
- [ ] E2E: Registro con X (sin email, completar manualmente)
|
||||
- [ ] E2E: Login existente con X
|
||||
- [ ] E2E: Vinculación de cuentas
|
||||
- [ ] E2E: Cancelación del flujo
|
||||
- [ ] Test de seguridad: Validación de tokens
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de X API para tests
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Cuenta de X Developer (con nivel Elevated o superior)
|
||||
- Configuración de OAuth 2.0 en X Developer Portal
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Consistencia con otros OAuth
|
||||
- US-AUTH-004: Patrón similar a Facebook OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] X Developer App creada
|
||||
- [ ] Credenciales OAuth 2.0 disponibles
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Flujo de email adicional diseñado
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Manejo de errores completo
|
||||
- [ ] Flujo de email adicional funcional
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Twitter/X OAuth 2.0 Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/twitter`
|
||||
2. Backend redirige a X con:
|
||||
- `client_id`
|
||||
- `redirect_uri`
|
||||
- `scope=tweet.read users.read offline.access`
|
||||
- `state` (CSRF token)
|
||||
- `code_challenge` (PKCE)
|
||||
3. Usuario autoriza en X
|
||||
4. X redirige a `redirect_uri` con `code`
|
||||
5. Backend intercambia `code` por `access_token`
|
||||
6. Backend obtiene perfil del usuario
|
||||
7. Si no hay email, redirige a formulario adicional
|
||||
8. Backend crea/actualiza usuario
|
||||
9. Backend genera JWT
|
||||
|
||||
### X API v2 Endpoints
|
||||
|
||||
- Authorization: `https://twitter.com/i/oauth2/authorize`
|
||||
- Token exchange: `https://api.twitter.com/2/oauth2/token`
|
||||
- User info: `https://api.twitter.com/2/users/me?user.fields=profile_image_url`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
TWITTER_CLIENT_ID=your_client_id
|
||||
TWITTER_CLIENT_SECRET=your_client_secret
|
||||
TWITTER_CALLBACK_URL=https://orbiquant.com/api/v1/auth/twitter/callback
|
||||
```
|
||||
|
||||
### Consideraciones Especiales de X
|
||||
|
||||
- X no siempre proporciona email del usuario
|
||||
- Requiere OAuth 2.0 con PKCE
|
||||
- Necesita scope `offline.access` para refresh tokens
|
||||
- Rate limits más estrictos que otros proveedores
|
||||
- Requiere X Developer Account con nivel "Elevated" mínimo
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Implementar PKCE (Proof Key for Code Exchange)
|
||||
- Validar `state` parameter (CSRF)
|
||||
- Validar email adicional si es requerido
|
||||
- Rate limiting en endpoints
|
||||
- Logs de intentos de autenticación
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
|
||||
@ -1,337 +1,349 @@
|
||||
# US-AUTH-006: OAuth Apple Sign In
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** visitante o usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando Apple Sign In
|
||||
**Para** tener un acceso seguro y privado sin compartir mi email real
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de Apple visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con Apple"
|
||||
**Y** debería seguir las guías de diseño de Apple (botón negro con logo)
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con Apple"
|
||||
**Cuando** se abre la ventana de Apple
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de Apple
|
||||
2. Poder elegir compartir mi email real o ocultarlo
|
||||
3. Poder usar Touch ID / Face ID si está disponible
|
||||
4. Poder autorizar con mi Apple ID
|
||||
|
||||
### AC-003: Opción de ocultar email
|
||||
|
||||
**Dado** que estoy en la pantalla de Apple Sign In
|
||||
**Cuando** elijo ocultar mi email
|
||||
**Entonces** Apple debería generar un email relay privado
|
||||
**Y** ese email debería funcionar para comunicaciones
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando Apple Sign In
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre de Apple (si lo compartí)
|
||||
5. Email verificado automáticamente
|
||||
|
||||
### AC-005: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con Apple
|
||||
**Cuando** uso "Continuar con Apple"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Usar Touch ID / Face ID si está configurado
|
||||
3. Ser redirigido al dashboard
|
||||
|
||||
### AC-006: Email relay de Apple
|
||||
|
||||
**Dado** que usé la opción de ocultar mi email
|
||||
**Cuando** la aplicación envía emails
|
||||
**Entonces** debería enviarlos al email relay de Apple
|
||||
**Y** Apple debería reenviarlos a mi email real
|
||||
**Y** debería poder responder a través del relay
|
||||
|
||||
### AC-007: Datos mínimos recibidos
|
||||
|
||||
**Dado** que autorizo Apple Sign In
|
||||
**Cuando** completo el flujo
|
||||
**Entonces** la app debería recibir:
|
||||
- `user_id` único de Apple
|
||||
- Email (real o relay)
|
||||
- Nombre (opcional, solo primera vez)
|
||||
**Y** NO debería recibir otra información personal
|
||||
|
||||
### AC-008: Revocación de acceso
|
||||
|
||||
**Dado** que revoco el acceso desde configuración de Apple
|
||||
**Cuando** intento hacer login nuevamente
|
||||
**Entonces** debería ver un error de autorización
|
||||
**Y** debería poder re-autorizar la aplicación
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Continuar con Apple │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ (Botón negro con logo de Apple blanco) │
|
||||
│ │
|
||||
│ [Facebook] [Twitter/X] [GitHub] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de Apple Sign In:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ appleid.apple.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ "OrbiQuant" desea usar tu Apple ID para iniciar sesión │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Nombre │ │
|
||||
│ │ ○ Compartir mi nombre │ │
|
||||
│ │ ○ No compartir │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email │ │
|
||||
│ │ ○ Compartir mi email (juan@icloud.com) │ │
|
||||
│ │ ● Ocultar mi email │ │
|
||||
│ │ (se usará: xyz123@privaterelay.appleid.com) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Tu información será compartida según las políticas de │
|
||||
│ privacidad de OrbiQuant. │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Continuar con Touch ID │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancelar │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN apple_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN is_private_email BOOLEAN DEFAULT false;
|
||||
```
|
||||
- [ ] Usar tabla `oauth_connections` existente
|
||||
- [ ] Índice en `apple_id`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar Apple Developer Account
|
||||
- [ ] Crear App ID y Service ID
|
||||
- [ ] Generar y configurar private key (.p8)
|
||||
- [ ] Endpoint `GET /api/v1/auth/apple`
|
||||
- Redirige a Apple Sign In
|
||||
- [ ] Endpoint `POST /api/v1/auth/apple/callback`
|
||||
- Recibe ID Token (JWT)
|
||||
- Valida firma del token con Apple public key
|
||||
- Decodifica user info
|
||||
- Maneja primera autorización (recibe nombre)
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Service `AppleOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `validateIdToken()`
|
||||
- `decodeUserInfo()`
|
||||
- `linkAccount()`
|
||||
- [ ] Librería: `apple-signin-auth` o similar
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Sign in with Apple" (siguiendo guías de Apple)
|
||||
- [ ] Manejo de popup/redirect OAuth
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con Apple (email real)
|
||||
- [ ] E2E: Registro con Apple (email oculto)
|
||||
- [ ] E2E: Login existente
|
||||
- [ ] E2E: Revocación y re-autorización
|
||||
- [ ] Test de validación de ID Token
|
||||
- [ ] Test de seguridad: Firma del token
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de Apple ID Token
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Apple Developer Account ($99/año)
|
||||
- Dominio verificado en Apple
|
||||
- Configuración de Service ID
|
||||
- Private key (.p8) generada
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Consistencia con otros OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Apple Developer Account activa
|
||||
- [ ] Service ID configurado
|
||||
- [ ] Private key generada y segura
|
||||
- [ ] Mockups aprobados siguiendo Apple HIG
|
||||
- [ ] API contract definido
|
||||
- [ ] Dominio verificado en Apple
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Botón cumple con Apple guidelines
|
||||
- [ ] Validación de ID Token implementada
|
||||
- [ ] Manejo de email relay funcional
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Apple Sign In Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/apple`
|
||||
2. Backend redirige a Apple con:
|
||||
- `client_id` (Service ID)
|
||||
- `redirect_uri`
|
||||
- `response_type=code id_token`
|
||||
- `response_mode=form_post`
|
||||
- `scope=name email`
|
||||
- `state` (CSRF token)
|
||||
3. Usuario autoriza en Apple
|
||||
4. Apple envía POST a `redirect_uri` con:
|
||||
- `id_token` (JWT firmado)
|
||||
- `code` (authorization code)
|
||||
- `user` (solo primera vez, contiene nombre)
|
||||
5. Backend valida ID Token con Apple public key
|
||||
6. Backend decodifica user info
|
||||
7. Backend crea/actualiza usuario
|
||||
8. Backend genera JWT y redirige a frontend
|
||||
|
||||
### Apple ID Token Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"iss": "https://appleid.apple.com",
|
||||
"aud": "com.orbiquant.service",
|
||||
"exp": 1234567890,
|
||||
"iat": 1234567890,
|
||||
"sub": "001234.abc123...", // Apple User ID
|
||||
"email": "xyz@privaterelay.appleid.com",
|
||||
"email_verified": true,
|
||||
"is_private_email": true,
|
||||
"nonce_supported": true
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
APPLE_SERVICE_ID=com.orbiquant.service
|
||||
APPLE_TEAM_ID=ABC123XYZ
|
||||
APPLE_KEY_ID=KEY123
|
||||
APPLE_PRIVATE_KEY_PATH=/path/to/AuthKey_KEY123.p8
|
||||
APPLE_CALLBACK_URL=https://orbiquant.com/api/v1/auth/apple/callback
|
||||
```
|
||||
|
||||
### Consideraciones Especiales de Apple
|
||||
|
||||
- Apple solo envía nombre en la primera autorización
|
||||
- Email relay de Apple es permanente por app
|
||||
- ID Token está firmado con RS256
|
||||
- Requiere validar firma con Apple public keys
|
||||
- Response mode debe ser `form_post` (no query params)
|
||||
- Requiere HTTPS estricto
|
||||
- No hay refresh tokens en el flujo web
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Validar firma del ID Token con Apple public key
|
||||
- Verificar `aud` claim coincide con Service ID
|
||||
- Validar `iss` es `https://appleid.apple.com`
|
||||
- Verificar `exp` no está expirado
|
||||
- Validar `state` parameter (CSRF)
|
||||
- Guardar nombre solo en primera autorización
|
||||
- Logs de autenticación
|
||||
|
||||
### Apple Design Guidelines
|
||||
|
||||
- Usar botón oficial "Sign in with Apple"
|
||||
- Color negro en tema claro, blanco en tema oscuro
|
||||
- Logo de Apple siempre visible
|
||||
- Texto específico según contexto
|
||||
- Mismo tamaño que otros botones OAuth
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
---
|
||||
id: "US-AUTH-006"
|
||||
title: "OAuth Apple Sign In"
|
||||
type: "User Story"
|
||||
status: "To Do"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-006: OAuth Apple Sign In
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** visitante o usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando Apple Sign In
|
||||
**Para** tener un acceso seguro y privado sin compartir mi email real
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de Apple visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con Apple"
|
||||
**Y** debería seguir las guías de diseño de Apple (botón negro con logo)
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con Apple"
|
||||
**Cuando** se abre la ventana de Apple
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de Apple
|
||||
2. Poder elegir compartir mi email real o ocultarlo
|
||||
3. Poder usar Touch ID / Face ID si está disponible
|
||||
4. Poder autorizar con mi Apple ID
|
||||
|
||||
### AC-003: Opción de ocultar email
|
||||
|
||||
**Dado** que estoy en la pantalla de Apple Sign In
|
||||
**Cuando** elijo ocultar mi email
|
||||
**Entonces** Apple debería generar un email relay privado
|
||||
**Y** ese email debería funcionar para comunicaciones
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando Apple Sign In
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre de Apple (si lo compartí)
|
||||
5. Email verificado automáticamente
|
||||
|
||||
### AC-005: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con Apple
|
||||
**Cuando** uso "Continuar con Apple"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Usar Touch ID / Face ID si está configurado
|
||||
3. Ser redirigido al dashboard
|
||||
|
||||
### AC-006: Email relay de Apple
|
||||
|
||||
**Dado** que usé la opción de ocultar mi email
|
||||
**Cuando** la aplicación envía emails
|
||||
**Entonces** debería enviarlos al email relay de Apple
|
||||
**Y** Apple debería reenviarlos a mi email real
|
||||
**Y** debería poder responder a través del relay
|
||||
|
||||
### AC-007: Datos mínimos recibidos
|
||||
|
||||
**Dado** que autorizo Apple Sign In
|
||||
**Cuando** completo el flujo
|
||||
**Entonces** la app debería recibir:
|
||||
- `user_id` único de Apple
|
||||
- Email (real o relay)
|
||||
- Nombre (opcional, solo primera vez)
|
||||
**Y** NO debería recibir otra información personal
|
||||
|
||||
### AC-008: Revocación de acceso
|
||||
|
||||
**Dado** que revoco el acceso desde configuración de Apple
|
||||
**Cuando** intento hacer login nuevamente
|
||||
**Entonces** debería ver un error de autorización
|
||||
**Y** debería poder re-autorizar la aplicación
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Continuar con Apple │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ (Botón negro con logo de Apple blanco) │
|
||||
│ │
|
||||
│ [Facebook] [Twitter/X] [GitHub] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de Apple Sign In:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ appleid.apple.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ "OrbiQuant" desea usar tu Apple ID para iniciar sesión │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Nombre │ │
|
||||
│ │ ○ Compartir mi nombre │ │
|
||||
│ │ ○ No compartir │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email │ │
|
||||
│ │ ○ Compartir mi email (juan@icloud.com) │ │
|
||||
│ │ ● Ocultar mi email │ │
|
||||
│ │ (se usará: xyz123@privaterelay.appleid.com) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Tu información será compartida según las políticas de │
|
||||
│ privacidad de OrbiQuant. │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Continuar con Touch ID │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancelar │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN apple_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN is_private_email BOOLEAN DEFAULT false;
|
||||
```
|
||||
- [ ] Usar tabla `oauth_connections` existente
|
||||
- [ ] Índice en `apple_id`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar Apple Developer Account
|
||||
- [ ] Crear App ID y Service ID
|
||||
- [ ] Generar y configurar private key (.p8)
|
||||
- [ ] Endpoint `GET /api/v1/auth/apple`
|
||||
- Redirige a Apple Sign In
|
||||
- [ ] Endpoint `POST /api/v1/auth/apple/callback`
|
||||
- Recibe ID Token (JWT)
|
||||
- Valida firma del token con Apple public key
|
||||
- Decodifica user info
|
||||
- Maneja primera autorización (recibe nombre)
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Service `AppleOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `validateIdToken()`
|
||||
- `decodeUserInfo()`
|
||||
- `linkAccount()`
|
||||
- [ ] Librería: `apple-signin-auth` o similar
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Sign in with Apple" (siguiendo guías de Apple)
|
||||
- [ ] Manejo de popup/redirect OAuth
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con Apple (email real)
|
||||
- [ ] E2E: Registro con Apple (email oculto)
|
||||
- [ ] E2E: Login existente
|
||||
- [ ] E2E: Revocación y re-autorización
|
||||
- [ ] Test de validación de ID Token
|
||||
- [ ] Test de seguridad: Firma del token
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de Apple ID Token
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Apple Developer Account ($99/año)
|
||||
- Dominio verificado en Apple
|
||||
- Configuración de Service ID
|
||||
- Private key (.p8) generada
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Consistencia con otros OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Apple Developer Account activa
|
||||
- [ ] Service ID configurado
|
||||
- [ ] Private key generada y segura
|
||||
- [ ] Mockups aprobados siguiendo Apple HIG
|
||||
- [ ] API contract definido
|
||||
- [ ] Dominio verificado en Apple
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Botón cumple con Apple guidelines
|
||||
- [ ] Validación de ID Token implementada
|
||||
- [ ] Manejo de email relay funcional
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Apple Sign In Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/apple`
|
||||
2. Backend redirige a Apple con:
|
||||
- `client_id` (Service ID)
|
||||
- `redirect_uri`
|
||||
- `response_type=code id_token`
|
||||
- `response_mode=form_post`
|
||||
- `scope=name email`
|
||||
- `state` (CSRF token)
|
||||
3. Usuario autoriza en Apple
|
||||
4. Apple envía POST a `redirect_uri` con:
|
||||
- `id_token` (JWT firmado)
|
||||
- `code` (authorization code)
|
||||
- `user` (solo primera vez, contiene nombre)
|
||||
5. Backend valida ID Token con Apple public key
|
||||
6. Backend decodifica user info
|
||||
7. Backend crea/actualiza usuario
|
||||
8. Backend genera JWT y redirige a frontend
|
||||
|
||||
### Apple ID Token Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"iss": "https://appleid.apple.com",
|
||||
"aud": "com.orbiquant.service",
|
||||
"exp": 1234567890,
|
||||
"iat": 1234567890,
|
||||
"sub": "001234.abc123...", // Apple User ID
|
||||
"email": "xyz@privaterelay.appleid.com",
|
||||
"email_verified": true,
|
||||
"is_private_email": true,
|
||||
"nonce_supported": true
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
APPLE_SERVICE_ID=com.orbiquant.service
|
||||
APPLE_TEAM_ID=ABC123XYZ
|
||||
APPLE_KEY_ID=KEY123
|
||||
APPLE_PRIVATE_KEY_PATH=/path/to/AuthKey_KEY123.p8
|
||||
APPLE_CALLBACK_URL=https://orbiquant.com/api/v1/auth/apple/callback
|
||||
```
|
||||
|
||||
### Consideraciones Especiales de Apple
|
||||
|
||||
- Apple solo envía nombre en la primera autorización
|
||||
- Email relay de Apple es permanente por app
|
||||
- ID Token está firmado con RS256
|
||||
- Requiere validar firma con Apple public keys
|
||||
- Response mode debe ser `form_post` (no query params)
|
||||
- Requiere HTTPS estricto
|
||||
- No hay refresh tokens en el flujo web
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Validar firma del ID Token con Apple public key
|
||||
- Verificar `aud` claim coincide con Service ID
|
||||
- Validar `iss` es `https://appleid.apple.com`
|
||||
- Verificar `exp` no está expirado
|
||||
- Validar `state` parameter (CSRF)
|
||||
- Guardar nombre solo en primera autorización
|
||||
- Logs de autenticación
|
||||
|
||||
### Apple Design Guidelines
|
||||
|
||||
- Usar botón oficial "Sign in with Apple"
|
||||
- Color negro en tema claro, blanco en tema oscuro
|
||||
- Logo de Apple siempre visible
|
||||
- Texto específico según contexto
|
||||
- Mismo tamaño que otros botones OAuth
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
|
||||
@ -1,307 +1,319 @@
|
||||
# US-AUTH-007: OAuth GitHub
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P2 (Media)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** desarrollador o usuario técnico de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de GitHub
|
||||
**Para** tener un acceso rápido usando mis credenciales de desarrollador
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de GitHub visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con GitHub"
|
||||
**Y** debería tener el logo oficial de GitHub
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con GitHub"
|
||||
**Cuando** se abre la ventana de GitHub
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de GitHub
|
||||
2. Poder revisar los permisos solicitados
|
||||
3. Poder autorizar la aplicación
|
||||
|
||||
### AC-003: Permisos solicitados
|
||||
|
||||
**Dado** que estoy en la pantalla de autorización de GitHub
|
||||
**Cuando** reviso los permisos
|
||||
**Entonces** la app debería solicitar únicamente:
|
||||
- `user:email` (para leer email)
|
||||
- `read:user` (para leer perfil básico)
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando GitHub OAuth
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre y avatar de GitHub
|
||||
5. Usar mi email primario de GitHub
|
||||
|
||||
### AC-005: Email primario privado
|
||||
|
||||
**Dado** que tengo mi email configurado como privado en GitHub
|
||||
**Cuando** autorizo la aplicación
|
||||
**Entonces** debería usar mi email noreply de GitHub
|
||||
**O** solicitar un email alternativo
|
||||
|
||||
### AC-006: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con GitHub
|
||||
**Cuando** uso "Continuar con GitHub"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Ser redirigido al dashboard
|
||||
|
||||
### AC-007: Múltiples emails en GitHub
|
||||
|
||||
**Dado** que tengo múltiples emails en mi cuenta de GitHub
|
||||
**Cuando** autorizo la aplicación
|
||||
**Entonces** debería usar el email marcado como primario
|
||||
**Y** debería verificar que no esté ya registrado
|
||||
|
||||
### AC-008: Cancelación del flujo
|
||||
|
||||
**Dado** que inicio el flujo de GitHub OAuth
|
||||
**Cuando** cancelo en la ventana de GitHub
|
||||
**Entonces** debería volver a login/registro
|
||||
**Y** ver mensaje "Autenticación cancelada"
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Facebook] [Twitter/X] [Apple] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ ⚫ Continuar con GitHub │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de GitHub OAuth:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ github.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Authorize OrbiQuant │
|
||||
│ │
|
||||
│ OrbiQuant by OrbiQuant Team │
|
||||
│ wants to access your juanperez account │
|
||||
│ │
|
||||
│ This application will be able to: │
|
||||
│ │
|
||||
│ ✓ Verify your GitHub identity │
|
||||
│ ✓ Read your email addresses │
|
||||
│ ✓ Read your profile information │
|
||||
│ │
|
||||
│ Authorizing will redirect to: │
|
||||
│ https://orbiquant.com/api/v1/auth/github/callback │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Authorize OrbiQuant │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancel │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN github_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN github_username VARCHAR(255);
|
||||
```
|
||||
- [ ] Usar tabla `oauth_connections` existente
|
||||
- [ ] Índice en `github_id`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Crear GitHub OAuth App
|
||||
- [ ] Obtener Client ID y Client Secret
|
||||
- [ ] Endpoint `GET /api/v1/auth/github`
|
||||
- Redirige a GitHub OAuth
|
||||
- [ ] Endpoint `GET /api/v1/auth/github/callback`
|
||||
- Recibe código de autorización
|
||||
- Intercambia por access token
|
||||
- Obtiene perfil del usuario
|
||||
- Obtiene emails del usuario
|
||||
- Selecciona email primario
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Service `GitHubOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `exchangeCodeForToken()`
|
||||
- `getUserProfile()`
|
||||
- `getUserEmails()`
|
||||
- `selectPrimaryEmail()`
|
||||
- `linkAccount()`
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock de GitHub API
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Continuar con GitHub"
|
||||
- [ ] Manejo de popup/redirect OAuth
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con GitHub
|
||||
- [ ] E2E: Login existente
|
||||
- [ ] E2E: Email privado/noreply
|
||||
- [ ] E2E: Múltiples emails
|
||||
- [ ] E2E: Cancelación del flujo
|
||||
- [ ] Test de seguridad: Validación de tokens
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de GitHub API
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- GitHub OAuth App creada
|
||||
- Credenciales configuradas
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Consistencia con otros OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] GitHub OAuth App creada
|
||||
- [ ] Client ID y Secret disponibles
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Callback URL configurada
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Manejo de emails privados funcional
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### GitHub OAuth Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/github`
|
||||
2. Backend redirige a GitHub con:
|
||||
- `client_id`
|
||||
- `redirect_uri`
|
||||
- `scope=user:email read:user`
|
||||
- `state` (CSRF token)
|
||||
3. Usuario autoriza en GitHub
|
||||
4. GitHub redirige a `redirect_uri` con `code`
|
||||
5. Backend intercambia `code` por `access_token`
|
||||
6. Backend obtiene perfil: `GET /user`
|
||||
7. Backend obtiene emails: `GET /user/emails`
|
||||
8. Backend selecciona email primario y verificado
|
||||
9. Backend crea/actualiza usuario
|
||||
10. Backend genera JWT
|
||||
|
||||
### GitHub API Endpoints
|
||||
|
||||
- Authorization: `https://github.com/login/oauth/authorize`
|
||||
- Token exchange: `https://github.com/login/oauth/access_token`
|
||||
- User profile: `https://api.github.com/user`
|
||||
- User emails: `https://api.github.com/user/emails`
|
||||
|
||||
### Email Selection Logic
|
||||
|
||||
```typescript
|
||||
// Prioridad de selección de email:
|
||||
1. Email primario + verificado
|
||||
2. Email primario (aunque no esté verificado)
|
||||
3. Primer email verificado
|
||||
4. Solicitar email adicional al usuario
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
GITHUB_CLIENT_ID=your_client_id
|
||||
GITHUB_CLIENT_SECRET=your_client_secret
|
||||
GITHUB_CALLBACK_URL=https://orbiquant.com/api/v1/auth/github/callback
|
||||
```
|
||||
|
||||
### Consideraciones Especiales de GitHub
|
||||
|
||||
- GitHub permite múltiples emails por cuenta
|
||||
- Email puede ser privado (noreply@github.com)
|
||||
- Necesita dos llamadas: una para perfil, otra para emails
|
||||
- Access tokens no expiran (a menos que sean revocados)
|
||||
- Scopes mínimos: `user:email` y `read:user`
|
||||
- Rate limit: 5000 requests/hour para usuarios autenticados
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Validar `state` parameter (CSRF)
|
||||
- Usar HTTPS en callbacks
|
||||
- No almacenar access tokens sin encriptar
|
||||
- Rate limiting en endpoints
|
||||
- Logs de intentos de autenticación
|
||||
- Validar que el email sea verificado en GitHub
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
---
|
||||
id: "US-AUTH-007"
|
||||
title: "OAuth GitHub"
|
||||
type: "User Story"
|
||||
status: "To Do"
|
||||
priority: "Media"
|
||||
epic: "OQI-001"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-007: OAuth GitHub
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P2 (Media)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** desarrollador o usuario técnico de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de GitHub
|
||||
**Para** tener un acceso rápido usando mis credenciales de desarrollador
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Botón de GitHub visible
|
||||
|
||||
**Dado** que estoy en la página de registro o login
|
||||
**Cuando** veo las opciones de autenticación
|
||||
**Entonces** debería ver un botón "Continuar con GitHub"
|
||||
**Y** debería tener el logo oficial de GitHub
|
||||
|
||||
### AC-002: Flujo de OAuth
|
||||
|
||||
**Dado** que hago click en "Continuar con GitHub"
|
||||
**Cuando** se abre la ventana de GitHub
|
||||
**Entonces** debería:
|
||||
1. Ver la pantalla de autorización de GitHub
|
||||
2. Poder revisar los permisos solicitados
|
||||
3. Poder autorizar la aplicación
|
||||
|
||||
### AC-003: Permisos solicitados
|
||||
|
||||
**Dado** que estoy en la pantalla de autorización de GitHub
|
||||
**Cuando** reviso los permisos
|
||||
**Entonces** la app debería solicitar únicamente:
|
||||
- `user:email` (para leer email)
|
||||
- `read:user` (para leer perfil básico)
|
||||
|
||||
### AC-004: Primer registro exitoso
|
||||
|
||||
**Dado** que es mi primera vez usando GitHub OAuth
|
||||
**Cuando** autorizo los permisos
|
||||
**Entonces** debería:
|
||||
1. Crear mi cuenta automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
4. Ver mi nombre y avatar de GitHub
|
||||
5. Usar mi email primario de GitHub
|
||||
|
||||
### AC-005: Email primario privado
|
||||
|
||||
**Dado** que tengo mi email configurado como privado en GitHub
|
||||
**Cuando** autorizo la aplicación
|
||||
**Entonces** debería usar mi email noreply de GitHub
|
||||
**O** solicitar un email alternativo
|
||||
|
||||
### AC-006: Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta vinculada con GitHub
|
||||
**Cuando** uso "Continuar con GitHub"
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Ser redirigido al dashboard
|
||||
|
||||
### AC-007: Múltiples emails en GitHub
|
||||
|
||||
**Dado** que tengo múltiples emails en mi cuenta de GitHub
|
||||
**Cuando** autorizo la aplicación
|
||||
**Entonces** debería usar el email marcado como primario
|
||||
**Y** debería verificar que no esté ya registrado
|
||||
|
||||
### AC-008: Cancelación del flujo
|
||||
|
||||
**Dado** que inicio el flujo de GitHub OAuth
|
||||
**Cuando** cancelo en la ventana de GitHub
|
||||
**Entonces** debería volver a login/registro
|
||||
**Y** ver mensaje "Autenticación cancelada"
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Bienvenido a OrbiQuant │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 📧 Email │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔴 Continuar con Google │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Facebook] [Twitter/X] [Apple] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ ⚫ Continuar con GitHub │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Ventana de GitHub OAuth:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ github.com ✕ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Authorize OrbiQuant │
|
||||
│ │
|
||||
│ OrbiQuant by OrbiQuant Team │
|
||||
│ wants to access your juanperez account │
|
||||
│ │
|
||||
│ This application will be able to: │
|
||||
│ │
|
||||
│ ✓ Verify your GitHub identity │
|
||||
│ ✓ Read your email addresses │
|
||||
│ ✓ Read your profile information │
|
||||
│ │
|
||||
│ Authorizing will redirect to: │
|
||||
│ https://orbiquant.com/api/v1/auth/github/callback │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Authorize OrbiQuant │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cancel │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN github_id VARCHAR(255) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN github_username VARCHAR(255);
|
||||
```
|
||||
- [ ] Usar tabla `oauth_connections` existente
|
||||
- [ ] Índice en `github_id`
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Crear GitHub OAuth App
|
||||
- [ ] Obtener Client ID y Client Secret
|
||||
- [ ] Endpoint `GET /api/v1/auth/github`
|
||||
- Redirige a GitHub OAuth
|
||||
- [ ] Endpoint `GET /api/v1/auth/github/callback`
|
||||
- Recibe código de autorización
|
||||
- Intercambia por access token
|
||||
- Obtiene perfil del usuario
|
||||
- Obtiene emails del usuario
|
||||
- Selecciona email primario
|
||||
- Crea o actualiza usuario
|
||||
- Genera JWT token
|
||||
- [ ] Service `GitHubOAuthService`
|
||||
- `getAuthorizationUrl()`
|
||||
- `exchangeCodeForToken()`
|
||||
- `getUserProfile()`
|
||||
- `getUserEmails()`
|
||||
- `selectPrimaryEmail()`
|
||||
- `linkAccount()`
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock de GitHub API
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Botón "Continuar con GitHub"
|
||||
- [ ] Manejo de popup/redirect OAuth
|
||||
- [ ] Recepción de callback
|
||||
- [ ] Almacenamiento de token JWT
|
||||
- [ ] Estado de loading
|
||||
- [ ] Manejo de errores
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con GitHub
|
||||
- [ ] E2E: Login existente
|
||||
- [ ] E2E: Email privado/noreply
|
||||
- [ ] E2E: Múltiples emails
|
||||
- [ ] E2E: Cancelación del flujo
|
||||
- [ ] Test de seguridad: Validación de tokens
|
||||
- [ ] Test de seguridad: CSRF protection
|
||||
- [ ] Mock de GitHub API
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- GitHub OAuth App creada
|
||||
- Credenciales configuradas
|
||||
- SSL/HTTPS en producción
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-003: Consistencia con otros OAuth
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] GitHub OAuth App creada
|
||||
- [ ] Client ID y Secret disponibles
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Callback URL configurada
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Manejo de emails privados funcional
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Logs implementados
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### GitHub OAuth Flow
|
||||
|
||||
1. Frontend redirige a `/api/v1/auth/github`
|
||||
2. Backend redirige a GitHub con:
|
||||
- `client_id`
|
||||
- `redirect_uri`
|
||||
- `scope=user:email read:user`
|
||||
- `state` (CSRF token)
|
||||
3. Usuario autoriza en GitHub
|
||||
4. GitHub redirige a `redirect_uri` con `code`
|
||||
5. Backend intercambia `code` por `access_token`
|
||||
6. Backend obtiene perfil: `GET /user`
|
||||
7. Backend obtiene emails: `GET /user/emails`
|
||||
8. Backend selecciona email primario y verificado
|
||||
9. Backend crea/actualiza usuario
|
||||
10. Backend genera JWT
|
||||
|
||||
### GitHub API Endpoints
|
||||
|
||||
- Authorization: `https://github.com/login/oauth/authorize`
|
||||
- Token exchange: `https://github.com/login/oauth/access_token`
|
||||
- User profile: `https://api.github.com/user`
|
||||
- User emails: `https://api.github.com/user/emails`
|
||||
|
||||
### Email Selection Logic
|
||||
|
||||
```typescript
|
||||
// Prioridad de selección de email:
|
||||
1. Email primario + verificado
|
||||
2. Email primario (aunque no esté verificado)
|
||||
3. Primer email verificado
|
||||
4. Solicitar email adicional al usuario
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
GITHUB_CLIENT_ID=your_client_id
|
||||
GITHUB_CLIENT_SECRET=your_client_secret
|
||||
GITHUB_CALLBACK_URL=https://orbiquant.com/api/v1/auth/github/callback
|
||||
```
|
||||
|
||||
### Consideraciones Especiales de GitHub
|
||||
|
||||
- GitHub permite múltiples emails por cuenta
|
||||
- Email puede ser privado (noreply@github.com)
|
||||
- Necesita dos llamadas: una para perfil, otra para emails
|
||||
- Access tokens no expiran (a menos que sean revocados)
|
||||
- Scopes mínimos: `user:email` y `read:user`
|
||||
- Rate limit: 5000 requests/hour para usuarios autenticados
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Validar `state` parameter (CSRF)
|
||||
- Usar HTTPS en callbacks
|
||||
- No almacenar access tokens sin encriptar
|
||||
- Rate limiting en endpoints
|
||||
- Logs de intentos de autenticación
|
||||
- Validar que el email sea verificado en GitHub
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
|
||||
|
||||
@ -1,442 +1,454 @@
|
||||
# US-AUTH-008: Autenticación con SMS (Twilio)
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 5
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi número de teléfono y un código SMS
|
||||
**Para** tener un acceso rápido sin necesidad de recordar contraseñas
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Formulario de teléfono
|
||||
|
||||
**Dado** que estoy en la página de registro/login
|
||||
**Cuando** selecciono la opción de teléfono
|
||||
**Entonces** debería ver:
|
||||
- Selector de país con banderas (+1, +52, +34, etc.)
|
||||
- Campo para número de teléfono
|
||||
- Formato visual según el país seleccionado
|
||||
- Botón "Enviar código"
|
||||
|
||||
### AC-002: Validación de número
|
||||
|
||||
**Dado** que ingreso un número de teléfono
|
||||
**Cuando** el número no es válido para el país seleccionado
|
||||
**Entonces** debería ver un mensaje de error
|
||||
**Y** el botón "Enviar código" debería estar deshabilitado
|
||||
|
||||
### AC-003: Envío de código SMS
|
||||
|
||||
**Dado** que ingresé un número válido
|
||||
**Cuando** hago click en "Enviar código"
|
||||
**Entonces** debería:
|
||||
1. Ver un mensaje "Enviando código..."
|
||||
2. Recibir un SMS con un código de 6 dígitos
|
||||
3. Ver pantalla de verificación de código
|
||||
4. El código debería expirar en 10 minutos
|
||||
|
||||
### AC-004: Formato del SMS
|
||||
|
||||
**Dado** que solicité un código
|
||||
**Cuando** recibo el SMS
|
||||
**Entonces** debería tener el formato:
|
||||
```
|
||||
Tu código de OrbiQuant es: 123456
|
||||
|
||||
Válido por 10 minutos.
|
||||
No compartas este código.
|
||||
```
|
||||
|
||||
### AC-005: Ingreso de código
|
||||
|
||||
**Dado** que recibí el código por SMS
|
||||
**Cuando** ingreso el código en la app
|
||||
**Entonces** debería:
|
||||
- Autoformatear con espacios (123 456)
|
||||
- Auto-enviar al completar 6 dígitos
|
||||
- Validar el código en tiempo real
|
||||
|
||||
### AC-006: Código correcto - Primer registro
|
||||
|
||||
**Dado** que es mi primera vez usando este número
|
||||
**Cuando** ingreso el código correcto
|
||||
**Entonces** debería:
|
||||
1. Ver formulario para completar perfil (nombre, apellido, email opcional)
|
||||
2. Crear mi cuenta
|
||||
3. Recibir un JWT token
|
||||
4. Ser redirigido al dashboard
|
||||
|
||||
### AC-007: Código correcto - Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta con este número
|
||||
**Cuando** ingreso el código correcto
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
|
||||
### AC-008: Código incorrecto
|
||||
|
||||
**Dado** que ingreso un código incorrecto
|
||||
**Cuando** envío el código
|
||||
**Entonces** debería:
|
||||
- Ver mensaje "Código incorrecto"
|
||||
- Poder intentar nuevamente
|
||||
- Después de 3 intentos fallidos, invalidar el código
|
||||
- Poder solicitar un nuevo código
|
||||
|
||||
### AC-009: Código expirado
|
||||
|
||||
**Dado** que pasaron más de 10 minutos desde el envío
|
||||
**Cuando** intento usar el código
|
||||
**Entonces** debería ver mensaje "Código expirado"
|
||||
**Y** debería poder solicitar un nuevo código
|
||||
|
||||
### AC-010: Reenvío de código
|
||||
|
||||
**Dado** que no recibí el código o expiró
|
||||
**Cuando** hago click en "Reenviar código"
|
||||
**Entonces** debería:
|
||||
- Esperar 60 segundos antes de permitir reenvío
|
||||
- Ver contador regresivo "Reenviar en 59s..."
|
||||
- Recibir un nuevo código (el anterior se invalida)
|
||||
|
||||
### AC-011: Rate limiting
|
||||
|
||||
**Dado** que solicité 5 códigos en 1 hora
|
||||
**Cuando** intento solicitar otro
|
||||
**Entonces** debería ver mensaje:
|
||||
- "Demasiados intentos. Intenta en 1 hora"
|
||||
|
||||
### AC-012: Número ya registrado con email
|
||||
|
||||
**Dado** que mi número ya está vinculado a una cuenta de email
|
||||
**Cuando** completo la verificación SMS
|
||||
**Entonces** debería iniciar sesión en esa cuenta
|
||||
**Y** tener ambos métodos de autenticación disponibles
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
Paso 1: Ingreso de teléfono
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Ingresa con tu número de teléfono │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Número de teléfono │ │
|
||||
│ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │
|
||||
│ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │
|
||||
│ │ └────────┘ └──────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Enviar código │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ─────────────────── O continúa con ─────────────────── │
|
||||
│ │
|
||||
│ [Email] [Google] [Facebook] [Apple] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Paso 2: Verificación de código
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 📱 Ingresa el código que enviamos │
|
||||
│ │
|
||||
│ Enviamos un código a +1 (555) 123-4567 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
|
||||
│ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ │ │
|
||||
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ¿No recibiste el código? │
|
||||
│ Reenviar código (disponible en 58s) │
|
||||
│ │
|
||||
│ ← Cambiar número │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Paso 3: Completar perfil (solo registro nuevo)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🎉 ¡Bienvenido! Completa tu perfil │
|
||||
│ │
|
||||
│ ┌────────────────────────┐ ┌────────────────────────┐ │
|
||||
│ │ Nombre │ │ Apellido │ │
|
||||
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
|
||||
│ │ │ Juan │ │ │ │ Pérez │ │ │
|
||||
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
|
||||
│ └────────────────────────┘ └────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email (opcional) │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ juan@email.com │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ☑ Acepto los Términos de Servicio │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Crear mi cuenta │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN phone_verified_at TIMESTAMP;
|
||||
ALTER TABLE users ADD COLUMN phone_country_code VARCHAR(5);
|
||||
```
|
||||
- [ ] Tabla `phone_verification_codes`:
|
||||
```sql
|
||||
CREATE TABLE phone_verification_codes (
|
||||
id UUID PRIMARY KEY,
|
||||
phone_number VARCHAR(20) NOT NULL,
|
||||
code VARCHAR(6) NOT NULL,
|
||||
attempts INT DEFAULT 0,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
INDEX idx_phone_expires (phone_number, expires_at)
|
||||
);
|
||||
```
|
||||
- [ ] Tabla `phone_rate_limits`:
|
||||
```sql
|
||||
CREATE TABLE phone_rate_limits (
|
||||
id UUID PRIMARY KEY,
|
||||
phone_number VARCHAR(20) NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
attempts INT DEFAULT 1,
|
||||
window_start TIMESTAMP DEFAULT NOW(),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
INDEX idx_phone_window (phone_number, window_start)
|
||||
);
|
||||
```
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar cuenta de Twilio
|
||||
- [ ] Obtener Account SID y Auth Token
|
||||
- [ ] Configurar Twilio Phone Number
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/send-code`
|
||||
- Validar número con libphonenumber
|
||||
- Rate limiting (5 códigos / hora)
|
||||
- Generar código aleatorio de 6 dígitos
|
||||
- Guardar en DB con expiración
|
||||
- Enviar SMS via Twilio
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/verify-code`
|
||||
- Validar código
|
||||
- Verificar no expirado
|
||||
- Verificar intentos < 3
|
||||
- Crear o actualizar usuario
|
||||
- Generar JWT token
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/resend-code`
|
||||
- Invalidar código anterior
|
||||
- Generar nuevo código
|
||||
- Verificar cooldown de 60s
|
||||
- [ ] Service `TwilioSMSService`
|
||||
- `sendVerificationCode()`
|
||||
- `verifyCode()`
|
||||
- `formatPhoneNumber()`
|
||||
- [ ] Librería: `twilio` SDK
|
||||
- [ ] Librería: `libphonenumber-js` para validación
|
||||
- [ ] Tests unitarios (12 casos)
|
||||
- [ ] Tests de integración con mock de Twilio
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Componente `PhoneAuth.tsx`
|
||||
- [ ] Selector de país con banderas
|
||||
- [ ] Input de teléfono con formato automático
|
||||
- [ ] Componente `CodeInput.tsx` (6 dígitos)
|
||||
- [ ] Componente `CompleteProfile.tsx`
|
||||
- [ ] Validación con React Hook Form
|
||||
- [ ] Librería: `react-phone-number-input`
|
||||
- [ ] Contador regresivo para reenvío
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con teléfono completo
|
||||
- [ ] E2E: Login con teléfono existente
|
||||
- [ ] E2E: Código incorrecto (3 intentos)
|
||||
- [ ] E2E: Código expirado
|
||||
- [ ] E2E: Reenvío de código
|
||||
- [ ] E2E: Rate limiting
|
||||
- [ ] Test de integración con Twilio Test Credentials
|
||||
- [ ] Test de seguridad: Brute force protection
|
||||
- [ ] Performance: Envío de SMS < 2s
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Cuenta de Twilio activa
|
||||
- Twilio Phone Number comprado
|
||||
- Presupuesto para SMS (aprox $0.0075 USD por SMS)
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-001: Para vinculación de cuentas
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Cuenta de Twilio configurada
|
||||
- [ ] Twilio Phone Number asignado
|
||||
- [ ] Presupuesto aprobado para SMS
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Estrategia de rate limiting definida
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Twilio configurado en todos los ambientes
|
||||
- [ ] Rate limiting implementado
|
||||
- [ ] Logs y monitoring de SMS
|
||||
- [ ] Costos monitoreados
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Twilio SMS Flow
|
||||
|
||||
1. Usuario ingresa número de teléfono
|
||||
2. Frontend valida formato
|
||||
3. Frontend llama `POST /api/v1/auth/phone/send-code`
|
||||
4. Backend valida número con libphonenumber
|
||||
5. Backend verifica rate limits
|
||||
6. Backend genera código aleatorio (6 dígitos)
|
||||
7. Backend guarda código en DB (expira en 10 min)
|
||||
8. Backend envía SMS via Twilio API
|
||||
9. Usuario recibe SMS e ingresa código
|
||||
10. Frontend llama `POST /api/v1/auth/phone/verify-code`
|
||||
11. Backend valida código
|
||||
12. Backend crea/actualiza usuario
|
||||
13. Backend genera JWT token
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
TWILIO_ACCOUNT_SID=AC...
|
||||
TWILIO_AUTH_TOKEN=your_auth_token
|
||||
TWILIO_PHONE_NUMBER=+15551234567
|
||||
TWILIO_VERIFY_SERVICE_SID=VA... # Opcional: usar Twilio Verify
|
||||
```
|
||||
|
||||
### Twilio API Usage
|
||||
|
||||
```typescript
|
||||
import twilio from 'twilio';
|
||||
|
||||
const client = twilio(accountSid, authToken);
|
||||
|
||||
// Enviar SMS
|
||||
await client.messages.create({
|
||||
body: `Tu código de OrbiQuant es: ${code}\n\nVálido por 10 minutos.`,
|
||||
from: twilioPhoneNumber,
|
||||
to: userPhoneNumber
|
||||
});
|
||||
```
|
||||
|
||||
### Phone Number Validation
|
||||
|
||||
```typescript
|
||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
const phoneNumber = parsePhoneNumber(input, countryCode);
|
||||
if (!phoneNumber || !phoneNumber.isValid()) {
|
||||
throw new Error('Invalid phone number');
|
||||
}
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
|
||||
```typescript
|
||||
// Generar código de 6 dígitos
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
```
|
||||
|
||||
### Rate Limiting Strategy
|
||||
|
||||
- 5 códigos por número de teléfono por hora
|
||||
- 10 códigos por IP por hora
|
||||
- Cooldown de 60 segundos entre reenvíos
|
||||
- Máximo 3 intentos de verificación por código
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Códigos de 6 dígitos (1 millón de combinaciones)
|
||||
- Expiración de 10 minutos
|
||||
- Invalidar después de 3 intentos fallidos
|
||||
- Rate limiting estricto
|
||||
- Logs de todos los intentos
|
||||
- Validación de número en backend
|
||||
- No devolver información si el número existe o no
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
- SMS en USA: ~$0.0075 USD
|
||||
- SMS internacional: $0.0075 - $0.10 USD
|
||||
- Usar Twilio Verify Service para mejor pricing
|
||||
- Implementar captcha para prevenir abuso
|
||||
- Alertas si se excede presupuesto mensual
|
||||
|
||||
### Alternative: Twilio Verify Service
|
||||
|
||||
En lugar de manejar códigos manualmente, considerar usar Twilio Verify:
|
||||
- Manejo automático de códigos
|
||||
- Rate limiting incluido
|
||||
- Mejor pricing
|
||||
- Reenvíos automáticos
|
||||
- Múltiples canales (SMS, Voice, WhatsApp)
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-004: Autenticación por Teléfono](../requerimientos/RF-AUTH-004-phone.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md)
|
||||
---
|
||||
id: "US-AUTH-008"
|
||||
title: "Autenticacion con SMS (Twilio)"
|
||||
type: "User Story"
|
||||
status: "To Do"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 5
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-008: Autenticación con SMS (Twilio)
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 5
|
||||
**Prioridad:** P1 (Alta)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión usando mi número de teléfono y un código SMS
|
||||
**Para** tener un acceso rápido sin necesidad de recordar contraseñas
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Formulario de teléfono
|
||||
|
||||
**Dado** que estoy en la página de registro/login
|
||||
**Cuando** selecciono la opción de teléfono
|
||||
**Entonces** debería ver:
|
||||
- Selector de país con banderas (+1, +52, +34, etc.)
|
||||
- Campo para número de teléfono
|
||||
- Formato visual según el país seleccionado
|
||||
- Botón "Enviar código"
|
||||
|
||||
### AC-002: Validación de número
|
||||
|
||||
**Dado** que ingreso un número de teléfono
|
||||
**Cuando** el número no es válido para el país seleccionado
|
||||
**Entonces** debería ver un mensaje de error
|
||||
**Y** el botón "Enviar código" debería estar deshabilitado
|
||||
|
||||
### AC-003: Envío de código SMS
|
||||
|
||||
**Dado** que ingresé un número válido
|
||||
**Cuando** hago click en "Enviar código"
|
||||
**Entonces** debería:
|
||||
1. Ver un mensaje "Enviando código..."
|
||||
2. Recibir un SMS con un código de 6 dígitos
|
||||
3. Ver pantalla de verificación de código
|
||||
4. El código debería expirar en 10 minutos
|
||||
|
||||
### AC-004: Formato del SMS
|
||||
|
||||
**Dado** que solicité un código
|
||||
**Cuando** recibo el SMS
|
||||
**Entonces** debería tener el formato:
|
||||
```
|
||||
Tu código de OrbiQuant es: 123456
|
||||
|
||||
Válido por 10 minutos.
|
||||
No compartas este código.
|
||||
```
|
||||
|
||||
### AC-005: Ingreso de código
|
||||
|
||||
**Dado** que recibí el código por SMS
|
||||
**Cuando** ingreso el código en la app
|
||||
**Entonces** debería:
|
||||
- Autoformatear con espacios (123 456)
|
||||
- Auto-enviar al completar 6 dígitos
|
||||
- Validar el código en tiempo real
|
||||
|
||||
### AC-006: Código correcto - Primer registro
|
||||
|
||||
**Dado** que es mi primera vez usando este número
|
||||
**Cuando** ingreso el código correcto
|
||||
**Entonces** debería:
|
||||
1. Ver formulario para completar perfil (nombre, apellido, email opcional)
|
||||
2. Crear mi cuenta
|
||||
3. Recibir un JWT token
|
||||
4. Ser redirigido al dashboard
|
||||
|
||||
### AC-007: Código correcto - Login existente
|
||||
|
||||
**Dado** que ya tengo una cuenta con este número
|
||||
**Cuando** ingreso el código correcto
|
||||
**Entonces** debería:
|
||||
1. Iniciar sesión automáticamente
|
||||
2. Recibir un JWT token
|
||||
3. Ser redirigido al dashboard
|
||||
|
||||
### AC-008: Código incorrecto
|
||||
|
||||
**Dado** que ingreso un código incorrecto
|
||||
**Cuando** envío el código
|
||||
**Entonces** debería:
|
||||
- Ver mensaje "Código incorrecto"
|
||||
- Poder intentar nuevamente
|
||||
- Después de 3 intentos fallidos, invalidar el código
|
||||
- Poder solicitar un nuevo código
|
||||
|
||||
### AC-009: Código expirado
|
||||
|
||||
**Dado** que pasaron más de 10 minutos desde el envío
|
||||
**Cuando** intento usar el código
|
||||
**Entonces** debería ver mensaje "Código expirado"
|
||||
**Y** debería poder solicitar un nuevo código
|
||||
|
||||
### AC-010: Reenvío de código
|
||||
|
||||
**Dado** que no recibí el código o expiró
|
||||
**Cuando** hago click en "Reenviar código"
|
||||
**Entonces** debería:
|
||||
- Esperar 60 segundos antes de permitir reenvío
|
||||
- Ver contador regresivo "Reenviar en 59s..."
|
||||
- Recibir un nuevo código (el anterior se invalida)
|
||||
|
||||
### AC-011: Rate limiting
|
||||
|
||||
**Dado** que solicité 5 códigos en 1 hora
|
||||
**Cuando** intento solicitar otro
|
||||
**Entonces** debería ver mensaje:
|
||||
- "Demasiados intentos. Intenta en 1 hora"
|
||||
|
||||
### AC-012: Número ya registrado con email
|
||||
|
||||
**Dado** que mi número ya está vinculado a una cuenta de email
|
||||
**Cuando** completo la verificación SMS
|
||||
**Entonces** debería iniciar sesión en esa cuenta
|
||||
**Y** tener ambos métodos de autenticación disponibles
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
Paso 1: Ingreso de teléfono
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Ingresa con tu número de teléfono │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Número de teléfono │ │
|
||||
│ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │
|
||||
│ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │
|
||||
│ │ └────────┘ └──────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Enviar código │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ─────────────────── O continúa con ─────────────────── │
|
||||
│ │
|
||||
│ [Email] [Google] [Facebook] [Apple] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Paso 2: Verificación de código
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 📱 Ingresa el código que enviamos │
|
||||
│ │
|
||||
│ Enviamos un código a +1 (555) 123-4567 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
|
||||
│ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ │ │
|
||||
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ¿No recibiste el código? │
|
||||
│ Reenviar código (disponible en 58s) │
|
||||
│ │
|
||||
│ ← Cambiar número │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Paso 3: Completar perfil (solo registro nuevo)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🎉 ¡Bienvenido! Completa tu perfil │
|
||||
│ │
|
||||
│ ┌────────────────────────┐ ┌────────────────────────┐ │
|
||||
│ │ Nombre │ │ Apellido │ │
|
||||
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
|
||||
│ │ │ Juan │ │ │ │ Pérez │ │ │
|
||||
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │
|
||||
│ └────────────────────────┘ └────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email (opcional) │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ juan@email.com │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ☑ Acepto los Términos de Servicio │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Crear mi cuenta │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campos a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN phone_verified_at TIMESTAMP;
|
||||
ALTER TABLE users ADD COLUMN phone_country_code VARCHAR(5);
|
||||
```
|
||||
- [ ] Tabla `phone_verification_codes`:
|
||||
```sql
|
||||
CREATE TABLE phone_verification_codes (
|
||||
id UUID PRIMARY KEY,
|
||||
phone_number VARCHAR(20) NOT NULL,
|
||||
code VARCHAR(6) NOT NULL,
|
||||
attempts INT DEFAULT 0,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
INDEX idx_phone_expires (phone_number, expires_at)
|
||||
);
|
||||
```
|
||||
- [ ] Tabla `phone_rate_limits`:
|
||||
```sql
|
||||
CREATE TABLE phone_rate_limits (
|
||||
id UUID PRIMARY KEY,
|
||||
phone_number VARCHAR(20) NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
attempts INT DEFAULT 1,
|
||||
window_start TIMESTAMP DEFAULT NOW(),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
INDEX idx_phone_window (phone_number, window_start)
|
||||
);
|
||||
```
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar cuenta de Twilio
|
||||
- [ ] Obtener Account SID y Auth Token
|
||||
- [ ] Configurar Twilio Phone Number
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/send-code`
|
||||
- Validar número con libphonenumber
|
||||
- Rate limiting (5 códigos / hora)
|
||||
- Generar código aleatorio de 6 dígitos
|
||||
- Guardar en DB con expiración
|
||||
- Enviar SMS via Twilio
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/verify-code`
|
||||
- Validar código
|
||||
- Verificar no expirado
|
||||
- Verificar intentos < 3
|
||||
- Crear o actualizar usuario
|
||||
- Generar JWT token
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/resend-code`
|
||||
- Invalidar código anterior
|
||||
- Generar nuevo código
|
||||
- Verificar cooldown de 60s
|
||||
- [ ] Service `TwilioSMSService`
|
||||
- `sendVerificationCode()`
|
||||
- `verifyCode()`
|
||||
- `formatPhoneNumber()`
|
||||
- [ ] Librería: `twilio` SDK
|
||||
- [ ] Librería: `libphonenumber-js` para validación
|
||||
- [ ] Tests unitarios (12 casos)
|
||||
- [ ] Tests de integración con mock de Twilio
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Componente `PhoneAuth.tsx`
|
||||
- [ ] Selector de país con banderas
|
||||
- [ ] Input de teléfono con formato automático
|
||||
- [ ] Componente `CodeInput.tsx` (6 dígitos)
|
||||
- [ ] Componente `CompleteProfile.tsx`
|
||||
- [ ] Validación con React Hook Form
|
||||
- [ ] Librería: `react-phone-number-input`
|
||||
- [ ] Contador regresivo para reenvío
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con teléfono completo
|
||||
- [ ] E2E: Login con teléfono existente
|
||||
- [ ] E2E: Código incorrecto (3 intentos)
|
||||
- [ ] E2E: Código expirado
|
||||
- [ ] E2E: Reenvío de código
|
||||
- [ ] E2E: Rate limiting
|
||||
- [ ] Test de integración con Twilio Test Credentials
|
||||
- [ ] Test de seguridad: Brute force protection
|
||||
- [ ] Performance: Envío de SMS < 2s
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- Cuenta de Twilio activa
|
||||
- Twilio Phone Number comprado
|
||||
- Presupuesto para SMS (aprox $0.0075 USD por SMS)
|
||||
|
||||
- **Deseables:**
|
||||
- US-AUTH-001: Para vinculación de cuentas
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Cuenta de Twilio configurada
|
||||
- [ ] Twilio Phone Number asignado
|
||||
- [ ] Presupuesto aprobado para SMS
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
- [ ] Estrategia de rate limiting definida
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Twilio configurado en todos los ambientes
|
||||
- [ ] Rate limiting implementado
|
||||
- [ ] Logs y monitoring de SMS
|
||||
- [ ] Costos monitoreados
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### Twilio SMS Flow
|
||||
|
||||
1. Usuario ingresa número de teléfono
|
||||
2. Frontend valida formato
|
||||
3. Frontend llama `POST /api/v1/auth/phone/send-code`
|
||||
4. Backend valida número con libphonenumber
|
||||
5. Backend verifica rate limits
|
||||
6. Backend genera código aleatorio (6 dígitos)
|
||||
7. Backend guarda código en DB (expira en 10 min)
|
||||
8. Backend envía SMS via Twilio API
|
||||
9. Usuario recibe SMS e ingresa código
|
||||
10. Frontend llama `POST /api/v1/auth/phone/verify-code`
|
||||
11. Backend valida código
|
||||
12. Backend crea/actualiza usuario
|
||||
13. Backend genera JWT token
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
TWILIO_ACCOUNT_SID=AC...
|
||||
TWILIO_AUTH_TOKEN=your_auth_token
|
||||
TWILIO_PHONE_NUMBER=+15551234567
|
||||
TWILIO_VERIFY_SERVICE_SID=VA... # Opcional: usar Twilio Verify
|
||||
```
|
||||
|
||||
### Twilio API Usage
|
||||
|
||||
```typescript
|
||||
import twilio from 'twilio';
|
||||
|
||||
const client = twilio(accountSid, authToken);
|
||||
|
||||
// Enviar SMS
|
||||
await client.messages.create({
|
||||
body: `Tu código de OrbiQuant es: ${code}\n\nVálido por 10 minutos.`,
|
||||
from: twilioPhoneNumber,
|
||||
to: userPhoneNumber
|
||||
});
|
||||
```
|
||||
|
||||
### Phone Number Validation
|
||||
|
||||
```typescript
|
||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
const phoneNumber = parsePhoneNumber(input, countryCode);
|
||||
if (!phoneNumber || !phoneNumber.isValid()) {
|
||||
throw new Error('Invalid phone number');
|
||||
}
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
|
||||
```typescript
|
||||
// Generar código de 6 dígitos
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
```
|
||||
|
||||
### Rate Limiting Strategy
|
||||
|
||||
- 5 códigos por número de teléfono por hora
|
||||
- 10 códigos por IP por hora
|
||||
- Cooldown de 60 segundos entre reenvíos
|
||||
- Máximo 3 intentos de verificación por código
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Códigos de 6 dígitos (1 millón de combinaciones)
|
||||
- Expiración de 10 minutos
|
||||
- Invalidar después de 3 intentos fallidos
|
||||
- Rate limiting estricto
|
||||
- Logs de todos los intentos
|
||||
- Validación de número en backend
|
||||
- No devolver información si el número existe o no
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
- SMS en USA: ~$0.0075 USD
|
||||
- SMS internacional: $0.0075 - $0.10 USD
|
||||
- Usar Twilio Verify Service para mejor pricing
|
||||
- Implementar captcha para prevenir abuso
|
||||
- Alertas si se excede presupuesto mensual
|
||||
|
||||
### Alternative: Twilio Verify Service
|
||||
|
||||
En lugar de manejar códigos manualmente, considerar usar Twilio Verify:
|
||||
- Manejo automático de códigos
|
||||
- Rate limiting incluido
|
||||
- Mejor pricing
|
||||
- Reenvíos automáticos
|
||||
- Múltiples canales (SMS, Voice, WhatsApp)
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-004: Autenticación por Teléfono](../requerimientos/RF-AUTH-004-phone.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md)
|
||||
|
||||
@ -1,392 +1,404 @@
|
||||
# US-AUTH-009: Autenticación con WhatsApp
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P2 (Media)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión recibiendo el código por WhatsApp
|
||||
**Para** usar una aplicación que ya tengo instalada y me resulta más familiar que SMS
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Opción de WhatsApp
|
||||
|
||||
**Dado** que estoy en la pantalla de ingreso de teléfono
|
||||
**Cuando** ingreso mi número
|
||||
**Entonces** debería ver dos opciones:
|
||||
- "Enviar por SMS"
|
||||
- "Enviar por WhatsApp"
|
||||
**Y** debería poder seleccionar mi preferencia
|
||||
|
||||
### AC-002: Validación de WhatsApp
|
||||
|
||||
**Dado** que seleccioné la opción de WhatsApp
|
||||
**Cuando** hago click en "Enviar código"
|
||||
**Entonces** el sistema debería:
|
||||
1. Verificar que el número tiene WhatsApp activo
|
||||
2. Si no tiene WhatsApp, mostrar mensaje y ofrecer SMS
|
||||
3. Si tiene WhatsApp, enviar el código
|
||||
|
||||
### AC-003: Mensaje de WhatsApp
|
||||
|
||||
**Dado** que solicité código por WhatsApp
|
||||
**Cuando** recibo el mensaje
|
||||
**Entonces** debería tener el formato:
|
||||
```
|
||||
¡Hola! 👋
|
||||
|
||||
Tu código de verificación de OrbiQuant es:
|
||||
|
||||
*123456*
|
||||
|
||||
Válido por 10 minutos.
|
||||
No compartas este código con nadie.
|
||||
|
||||
OrbiQuant - Inversiones Inteligentes
|
||||
```
|
||||
|
||||
### AC-004: Código recibido
|
||||
|
||||
**Dado** que recibí el código por WhatsApp
|
||||
**Cuando** vuelvo a la app e ingreso el código
|
||||
**Entonces** debería funcionar igual que con SMS
|
||||
**Y** completar el registro o login
|
||||
|
||||
### AC-005: WhatsApp no disponible
|
||||
|
||||
**Dado** que mi número no tiene WhatsApp activo
|
||||
**Cuando** intento usar la opción de WhatsApp
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Este número no tiene WhatsApp activo"
|
||||
**Y** debería ver botón "Enviar por SMS"
|
||||
|
||||
### AC-006: Fallback a SMS
|
||||
|
||||
**Dado** que seleccioné WhatsApp pero el envío falló
|
||||
**Cuando** ocurre un error en WhatsApp
|
||||
**Entonces** debería:
|
||||
1. Ver mensaje "No pudimos enviar por WhatsApp"
|
||||
2. Ver opción "Enviar por SMS"
|
||||
3. Poder continuar con SMS sin reingresar el número
|
||||
|
||||
### AC-007: Preferencia guardada
|
||||
|
||||
**Dado** que usé WhatsApp exitosamente
|
||||
**Cuando** vuelvo a hacer login
|
||||
**Entonces** WhatsApp debería ser la opción preseleccionada
|
||||
|
||||
### AC-008: Rate limiting compartido
|
||||
|
||||
**Dado** que solicité códigos por SMS y WhatsApp
|
||||
**Cuando** sumo los intentos
|
||||
**Entonces** debería contar ambos hacia el límite de 5 por hora
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Ingresa con tu número de teléfono │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Número de teléfono │ │
|
||||
│ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │
|
||||
│ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │
|
||||
│ │ └────────┘ └──────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ¿Cómo quieres recibir el código? │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 📱 SMS │ │ 💬 WhatsApp │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ (Recomendado - más rápido) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Enviar código │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Mensaje de WhatsApp:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ WhatsApp 🔍 ⋮ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ◀ OrbiQuant ✓✓ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ ¡Hola! 👋 │ │
|
||||
│ │ │ │
|
||||
│ │ Tu código de verificación de OrbiQuant es: │ │
|
||||
│ │ │ │
|
||||
│ │ *123456* │ │
|
||||
│ │ │ │
|
||||
│ │ Válido por 10 minutos. │ │
|
||||
│ │ No compartas este código con nadie. │ │
|
||||
│ │ │ │
|
||||
│ │ OrbiQuant - Inversiones Inteligentes │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ 15:42 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Pantalla si WhatsApp no disponible:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ⚠️ WhatsApp no disponible │
|
||||
│ │
|
||||
│ Este número no tiene WhatsApp activo. │
|
||||
│ ¿Quieres recibir el código por SMS? │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Enviar por SMS │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ← Cambiar número │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campo a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN preferred_auth_channel VARCHAR(20) DEFAULT 'sms';
|
||||
-- Valores: 'sms', 'whatsapp'
|
||||
```
|
||||
- [ ] Agregar campo a `phone_verification_codes`:
|
||||
```sql
|
||||
ALTER TABLE phone_verification_codes
|
||||
ADD COLUMN channel VARCHAR(20) DEFAULT 'sms';
|
||||
-- Valores: 'sms', 'whatsapp'
|
||||
```
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar WhatsApp Business API o Twilio WhatsApp
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/send-code` (modificar)
|
||||
- Agregar parámetro `channel: 'sms' | 'whatsapp'`
|
||||
- Verificar si número tiene WhatsApp (usando Twilio Lookup)
|
||||
- Enviar por canal seleccionado
|
||||
- [ ] Service `WhatsAppService`
|
||||
- `hasWhatsApp(phoneNumber)`
|
||||
- `sendVerificationCode(phoneNumber, code)`
|
||||
- `formatMessage(code)`
|
||||
- [ ] Librería: `twilio` SDK (WhatsApp support)
|
||||
- [ ] Fallback automático a SMS si WhatsApp falla
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Modificar `PhoneAuth.tsx`
|
||||
- Agregar selector de canal (SMS/WhatsApp)
|
||||
- Mostrar logo de WhatsApp
|
||||
- Manejo de error si WhatsApp no disponible
|
||||
- [ ] Recordar preferencia del usuario
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con WhatsApp
|
||||
- [ ] E2E: WhatsApp no disponible (fallback a SMS)
|
||||
- [ ] E2E: Error en WhatsApp (fallback a SMS)
|
||||
- [ ] E2E: Preferencia guardada
|
||||
- [ ] Test de integración con Twilio WhatsApp Sandbox
|
||||
- [ ] Mock de Twilio Lookup API
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- US-AUTH-008: Infraestructura de SMS ya implementada
|
||||
- Twilio WhatsApp habilitado (requiere aprobación de Meta)
|
||||
- WhatsApp Business Profile aprobado
|
||||
|
||||
- **Alternativa:**
|
||||
- Usar Twilio WhatsApp Sandbox para desarrollo
|
||||
- Solicitar WhatsApp Business API access para producción
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Twilio WhatsApp configurado
|
||||
- [ ] WhatsApp Business Profile creado
|
||||
- [ ] Message templates aprobados por Meta (para producción)
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Twilio WhatsApp configurado en todos los ambientes
|
||||
- [ ] Fallback a SMS funcional
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Logs y monitoring
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### WhatsApp vs SMS
|
||||
|
||||
**Ventajas de WhatsApp:**
|
||||
- Más familiar para usuarios
|
||||
- Gratis para el usuario
|
||||
- Mayor tasa de apertura
|
||||
- Confirmación de entrega y lectura
|
||||
- Soporte para rich media
|
||||
|
||||
**Desventajas:**
|
||||
- Requiere número verificado de WhatsApp Business
|
||||
- Proceso de aprobación de Meta
|
||||
- Templates deben ser pre-aprobados (producción)
|
||||
- No todos tienen WhatsApp
|
||||
|
||||
### Twilio WhatsApp Integration
|
||||
|
||||
```typescript
|
||||
import twilio from 'twilio';
|
||||
|
||||
const client = twilio(accountSid, authToken);
|
||||
|
||||
// Verificar si número tiene WhatsApp
|
||||
const lookup = await client.lookups.v2
|
||||
.phoneNumbers(phoneNumber)
|
||||
.fetch({ fields: 'line_type_intelligence' });
|
||||
|
||||
const hasWhatsApp = lookup.lineTypeIntelligence?.type === 'whatsapp';
|
||||
|
||||
// Enviar mensaje por WhatsApp
|
||||
await client.messages.create({
|
||||
body: `¡Hola! 👋\n\nTu código de verificación de OrbiQuant es:\n\n*${code}*\n\nVálido por 10 minutos.\nNo compartas este código con nadie.\n\nOrbiQuant - Inversiones Inteligentes`,
|
||||
from: 'whatsapp:+14155238886', // Twilio WhatsApp number
|
||||
to: `whatsapp:${phoneNumber}`
|
||||
});
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886
|
||||
TWILIO_WHATSAPP_ENABLED=true
|
||||
```
|
||||
|
||||
### Development: WhatsApp Sandbox
|
||||
|
||||
Para desarrollo, Twilio ofrece un Sandbox que no requiere aprobación:
|
||||
|
||||
1. Usuario debe enviar un mensaje join code a Twilio Sandbox
|
||||
2. Luego puede recibir mensajes
|
||||
3. Útil para testing pero no para producción
|
||||
|
||||
### Production: WhatsApp Business API
|
||||
|
||||
Para producción:
|
||||
|
||||
1. Solicitar WhatsApp Business API access
|
||||
2. Crear WhatsApp Business Profile
|
||||
3. Verificar número de teléfono
|
||||
4. Crear y aprobar message templates
|
||||
5. Esperar aprobación de Meta (puede tardar días)
|
||||
|
||||
### Message Templates (Producción)
|
||||
|
||||
En producción, WhatsApp requiere templates pre-aprobados:
|
||||
|
||||
```
|
||||
Template Name: verification_code
|
||||
Category: AUTHENTICATION
|
||||
Language: es
|
||||
|
||||
Body:
|
||||
¡Hola! 👋
|
||||
|
||||
Tu código de verificación de OrbiQuant es:
|
||||
|
||||
*{{1}}*
|
||||
|
||||
Válido por 10 minutos.
|
||||
No compartas este código con nadie.
|
||||
|
||||
OrbiQuant - Inversiones Inteligentes
|
||||
```
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
```typescript
|
||||
async function sendVerificationCode(phone, code, channel) {
|
||||
try {
|
||||
if (channel === 'whatsapp') {
|
||||
// Verificar si tiene WhatsApp
|
||||
const hasWhatsApp = await whatsappService.hasWhatsApp(phone);
|
||||
|
||||
if (!hasWhatsApp) {
|
||||
// Automáticamente usar SMS
|
||||
return await smsService.sendCode(phone, code);
|
||||
}
|
||||
|
||||
// Intentar enviar por WhatsApp
|
||||
return await whatsappService.sendCode(phone, code);
|
||||
} else {
|
||||
// Usar SMS
|
||||
return await smsService.sendCode(phone, code);
|
||||
}
|
||||
} catch (error) {
|
||||
// Si WhatsApp falla, fallback a SMS
|
||||
logger.warn('WhatsApp failed, falling back to SMS', { phone, error });
|
||||
return await smsService.sendCode(phone, code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cost Comparison
|
||||
|
||||
- SMS: ~$0.0075 USD por mensaje
|
||||
- WhatsApp: ~$0.005 USD por mensaje (más barato)
|
||||
- WhatsApp también tiene mejor deliverability
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Mismo código puede usarse para SMS o WhatsApp
|
||||
- Rate limiting compartido entre canales
|
||||
- Validar que el canal solicitado sea válido
|
||||
- Logs separados por canal para auditoría
|
||||
- Fallback automático mantiene seguridad
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-004: Autenticación por Teléfono](../requerimientos/RF-AUTH-004-phone.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md)
|
||||
---
|
||||
id: "US-AUTH-009"
|
||||
title: "Autenticacion con WhatsApp"
|
||||
type: "User Story"
|
||||
status: "To Do"
|
||||
priority: "Media"
|
||||
epic: "OQI-001"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-009: Autenticación con WhatsApp
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Story Points:** 3
|
||||
**Prioridad:** P2 (Media)
|
||||
**Épica:** [OQI-001](../_MAP.md)
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario de OrbiQuant
|
||||
**Quiero** poder registrarme e iniciar sesión recibiendo el código por WhatsApp
|
||||
**Para** usar una aplicación que ya tengo instalada y me resulta más familiar que SMS
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### AC-001: Opción de WhatsApp
|
||||
|
||||
**Dado** que estoy en la pantalla de ingreso de teléfono
|
||||
**Cuando** ingreso mi número
|
||||
**Entonces** debería ver dos opciones:
|
||||
- "Enviar por SMS"
|
||||
- "Enviar por WhatsApp"
|
||||
**Y** debería poder seleccionar mi preferencia
|
||||
|
||||
### AC-002: Validación de WhatsApp
|
||||
|
||||
**Dado** que seleccioné la opción de WhatsApp
|
||||
**Cuando** hago click en "Enviar código"
|
||||
**Entonces** el sistema debería:
|
||||
1. Verificar que el número tiene WhatsApp activo
|
||||
2. Si no tiene WhatsApp, mostrar mensaje y ofrecer SMS
|
||||
3. Si tiene WhatsApp, enviar el código
|
||||
|
||||
### AC-003: Mensaje de WhatsApp
|
||||
|
||||
**Dado** que solicité código por WhatsApp
|
||||
**Cuando** recibo el mensaje
|
||||
**Entonces** debería tener el formato:
|
||||
```
|
||||
¡Hola! 👋
|
||||
|
||||
Tu código de verificación de OrbiQuant es:
|
||||
|
||||
*123456*
|
||||
|
||||
Válido por 10 minutos.
|
||||
No compartas este código con nadie.
|
||||
|
||||
OrbiQuant - Inversiones Inteligentes
|
||||
```
|
||||
|
||||
### AC-004: Código recibido
|
||||
|
||||
**Dado** que recibí el código por WhatsApp
|
||||
**Cuando** vuelvo a la app e ingreso el código
|
||||
**Entonces** debería funcionar igual que con SMS
|
||||
**Y** completar el registro o login
|
||||
|
||||
### AC-005: WhatsApp no disponible
|
||||
|
||||
**Dado** que mi número no tiene WhatsApp activo
|
||||
**Cuando** intento usar la opción de WhatsApp
|
||||
**Entonces** debería ver un mensaje:
|
||||
- "Este número no tiene WhatsApp activo"
|
||||
**Y** debería ver botón "Enviar por SMS"
|
||||
|
||||
### AC-006: Fallback a SMS
|
||||
|
||||
**Dado** que seleccioné WhatsApp pero el envío falló
|
||||
**Cuando** ocurre un error en WhatsApp
|
||||
**Entonces** debería:
|
||||
1. Ver mensaje "No pudimos enviar por WhatsApp"
|
||||
2. Ver opción "Enviar por SMS"
|
||||
3. Poder continuar con SMS sin reingresar el número
|
||||
|
||||
### AC-007: Preferencia guardada
|
||||
|
||||
**Dado** que usé WhatsApp exitosamente
|
||||
**Cuando** vuelvo a hacer login
|
||||
**Entonces** WhatsApp debería ser la opción preseleccionada
|
||||
|
||||
### AC-008: Rate limiting compartido
|
||||
|
||||
**Dado** que solicité códigos por SMS y WhatsApp
|
||||
**Cuando** sumo los intentos
|
||||
**Entonces** debería contar ambos hacia el límite de 5 por hora
|
||||
|
||||
---
|
||||
|
||||
## Mockup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 🌟 Ingresa con tu número de teléfono │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Número de teléfono │ │
|
||||
│ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │
|
||||
│ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │
|
||||
│ │ └────────┘ └──────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ¿Cómo quieres recibir el código? │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 📱 SMS │ │ 💬 WhatsApp │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────┘ │
|
||||
│ (Recomendado - más rápido) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Enviar código │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Mensaje de WhatsApp:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ WhatsApp 🔍 ⋮ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ◀ OrbiQuant ✓✓ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ ¡Hola! 👋 │ │
|
||||
│ │ │ │
|
||||
│ │ Tu código de verificación de OrbiQuant es: │ │
|
||||
│ │ │ │
|
||||
│ │ *123456* │ │
|
||||
│ │ │ │
|
||||
│ │ Válido por 10 minutos. │ │
|
||||
│ │ No compartas este código con nadie. │ │
|
||||
│ │ │ │
|
||||
│ │ OrbiQuant - Inversiones Inteligentes │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ 15:42 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Pantalla si WhatsApp no disponible:
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ⚠️ WhatsApp no disponible │
|
||||
│ │
|
||||
│ Este número no tiene WhatsApp activo. │
|
||||
│ ¿Quieres recibir el código por SMS? │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Enviar por SMS │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ← Cambiar número │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
### Database (DB)
|
||||
|
||||
- [ ] Agregar campo a tabla `users`:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN preferred_auth_channel VARCHAR(20) DEFAULT 'sms';
|
||||
-- Valores: 'sms', 'whatsapp'
|
||||
```
|
||||
- [ ] Agregar campo a `phone_verification_codes`:
|
||||
```sql
|
||||
ALTER TABLE phone_verification_codes
|
||||
ADD COLUMN channel VARCHAR(20) DEFAULT 'sms';
|
||||
-- Valores: 'sms', 'whatsapp'
|
||||
```
|
||||
|
||||
### Backend (BE)
|
||||
|
||||
- [ ] Configurar WhatsApp Business API o Twilio WhatsApp
|
||||
- [ ] Endpoint `POST /api/v1/auth/phone/send-code` (modificar)
|
||||
- Agregar parámetro `channel: 'sms' | 'whatsapp'`
|
||||
- Verificar si número tiene WhatsApp (usando Twilio Lookup)
|
||||
- Enviar por canal seleccionado
|
||||
- [ ] Service `WhatsAppService`
|
||||
- `hasWhatsApp(phoneNumber)`
|
||||
- `sendVerificationCode(phoneNumber, code)`
|
||||
- `formatMessage(code)`
|
||||
- [ ] Librería: `twilio` SDK (WhatsApp support)
|
||||
- [ ] Fallback automático a SMS si WhatsApp falla
|
||||
- [ ] Tests unitarios (8 casos)
|
||||
- [ ] Tests de integración con mock
|
||||
|
||||
### Frontend (FE)
|
||||
|
||||
- [ ] Modificar `PhoneAuth.tsx`
|
||||
- Agregar selector de canal (SMS/WhatsApp)
|
||||
- Mostrar logo de WhatsApp
|
||||
- Manejo de error si WhatsApp no disponible
|
||||
- [ ] Recordar preferencia del usuario
|
||||
- [ ] Tests con React Testing Library
|
||||
|
||||
### Testing (QA)
|
||||
|
||||
- [ ] E2E: Registro con WhatsApp
|
||||
- [ ] E2E: WhatsApp no disponible (fallback a SMS)
|
||||
- [ ] E2E: Error en WhatsApp (fallback a SMS)
|
||||
- [ ] E2E: Preferencia guardada
|
||||
- [ ] Test de integración con Twilio WhatsApp Sandbox
|
||||
- [ ] Mock de Twilio Lookup API
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- **Bloqueantes:**
|
||||
- US-AUTH-008: Infraestructura de SMS ya implementada
|
||||
- Twilio WhatsApp habilitado (requiere aprobación de Meta)
|
||||
- WhatsApp Business Profile aprobado
|
||||
|
||||
- **Alternativa:**
|
||||
- Usar Twilio WhatsApp Sandbox para desarrollo
|
||||
- Solicitar WhatsApp Business API access para producción
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [ ] Twilio WhatsApp configurado
|
||||
- [ ] WhatsApp Business Profile creado
|
||||
- [ ] Message templates aprobados por Meta (para producción)
|
||||
- [ ] Mockups aprobados
|
||||
- [ ] API contract definido
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios con 80%+ cobertura
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Tests E2E implementados
|
||||
- [ ] Twilio WhatsApp configurado en todos los ambientes
|
||||
- [ ] Fallback a SMS funcional
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Logs y monitoring
|
||||
- [ ] QA aprobado en staging
|
||||
- [ ] Deploy a producción exitoso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
### WhatsApp vs SMS
|
||||
|
||||
**Ventajas de WhatsApp:**
|
||||
- Más familiar para usuarios
|
||||
- Gratis para el usuario
|
||||
- Mayor tasa de apertura
|
||||
- Confirmación de entrega y lectura
|
||||
- Soporte para rich media
|
||||
|
||||
**Desventajas:**
|
||||
- Requiere número verificado de WhatsApp Business
|
||||
- Proceso de aprobación de Meta
|
||||
- Templates deben ser pre-aprobados (producción)
|
||||
- No todos tienen WhatsApp
|
||||
|
||||
### Twilio WhatsApp Integration
|
||||
|
||||
```typescript
|
||||
import twilio from 'twilio';
|
||||
|
||||
const client = twilio(accountSid, authToken);
|
||||
|
||||
// Verificar si número tiene WhatsApp
|
||||
const lookup = await client.lookups.v2
|
||||
.phoneNumbers(phoneNumber)
|
||||
.fetch({ fields: 'line_type_intelligence' });
|
||||
|
||||
const hasWhatsApp = lookup.lineTypeIntelligence?.type === 'whatsapp';
|
||||
|
||||
// Enviar mensaje por WhatsApp
|
||||
await client.messages.create({
|
||||
body: `¡Hola! 👋\n\nTu código de verificación de OrbiQuant es:\n\n*${code}*\n\nVálido por 10 minutos.\nNo compartas este código con nadie.\n\nOrbiQuant - Inversiones Inteligentes`,
|
||||
from: 'whatsapp:+14155238886', // Twilio WhatsApp number
|
||||
to: `whatsapp:${phoneNumber}`
|
||||
});
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886
|
||||
TWILIO_WHATSAPP_ENABLED=true
|
||||
```
|
||||
|
||||
### Development: WhatsApp Sandbox
|
||||
|
||||
Para desarrollo, Twilio ofrece un Sandbox que no requiere aprobación:
|
||||
|
||||
1. Usuario debe enviar un mensaje join code a Twilio Sandbox
|
||||
2. Luego puede recibir mensajes
|
||||
3. Útil para testing pero no para producción
|
||||
|
||||
### Production: WhatsApp Business API
|
||||
|
||||
Para producción:
|
||||
|
||||
1. Solicitar WhatsApp Business API access
|
||||
2. Crear WhatsApp Business Profile
|
||||
3. Verificar número de teléfono
|
||||
4. Crear y aprobar message templates
|
||||
5. Esperar aprobación de Meta (puede tardar días)
|
||||
|
||||
### Message Templates (Producción)
|
||||
|
||||
En producción, WhatsApp requiere templates pre-aprobados:
|
||||
|
||||
```
|
||||
Template Name: verification_code
|
||||
Category: AUTHENTICATION
|
||||
Language: es
|
||||
|
||||
Body:
|
||||
¡Hola! 👋
|
||||
|
||||
Tu código de verificación de OrbiQuant es:
|
||||
|
||||
*{{1}}*
|
||||
|
||||
Válido por 10 minutos.
|
||||
No compartas este código con nadie.
|
||||
|
||||
OrbiQuant - Inversiones Inteligentes
|
||||
```
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
```typescript
|
||||
async function sendVerificationCode(phone, code, channel) {
|
||||
try {
|
||||
if (channel === 'whatsapp') {
|
||||
// Verificar si tiene WhatsApp
|
||||
const hasWhatsApp = await whatsappService.hasWhatsApp(phone);
|
||||
|
||||
if (!hasWhatsApp) {
|
||||
// Automáticamente usar SMS
|
||||
return await smsService.sendCode(phone, code);
|
||||
}
|
||||
|
||||
// Intentar enviar por WhatsApp
|
||||
return await whatsappService.sendCode(phone, code);
|
||||
} else {
|
||||
// Usar SMS
|
||||
return await smsService.sendCode(phone, code);
|
||||
}
|
||||
} catch (error) {
|
||||
// Si WhatsApp falla, fallback a SMS
|
||||
logger.warn('WhatsApp failed, falling back to SMS', { phone, error });
|
||||
return await smsService.sendCode(phone, code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cost Comparison
|
||||
|
||||
- SMS: ~$0.0075 USD por mensaje
|
||||
- WhatsApp: ~$0.005 USD por mensaje (más barato)
|
||||
- WhatsApp también tiene mejor deliverability
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Mismo código puede usarse para SMS o WhatsApp
|
||||
- Rate limiting compartido entre canales
|
||||
- Validar que el canal solicitado sea válido
|
||||
- Logs separados por canal para auditoría
|
||||
- Fallback automático mantiene seguridad
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Relacionados
|
||||
|
||||
- [RF-AUTH-004: Autenticación por Teléfono](../requerimientos/RF-AUTH-004-phone.md)
|
||||
|
||||
## Especificaciones Relacionadas
|
||||
|
||||
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
||||
- [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md)
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
---
|
||||
id: "US-AUTH-010"
|
||||
title: "Configurar 2FA"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-001"
|
||||
story_points: 5
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-AUTH-010: Configurar 2FA
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,16 @@
|
||||
---
|
||||
id: "RF-AUTH-001"
|
||||
title: "OAuth Multi-proveedor"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "auth"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-AUTH-001: OAuth Multi-proveedor
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
---
|
||||
id: "RF-AUTH-002"
|
||||
title: "Autenticacion por Email"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "auth"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-AUTH-002: Autenticación por Email
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
---
|
||||
id: "RF-AUTH-003"
|
||||
title: "Autenticacion por Telefono"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "auth"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-AUTH-003: Autenticación por Teléfono
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
---
|
||||
id: "RF-AUTH-004"
|
||||
title: "Two-Factor Authentication (2FA)"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "auth"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-AUTH-004: Two-Factor Authentication (2FA)
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
---
|
||||
id: "RF-AUTH-005"
|
||||
title: "Gestion de Sesiones"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "auth"
|
||||
epic: "OQI-001"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-AUTH-005: Gestión de Sesiones
|
||||
|
||||
**Version:** 1.0.0
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
---
|
||||
id: "README"
|
||||
title: "Modulo Educativo"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# OQI-002: Modulo Educativo
|
||||
|
||||
**Estado:** ✅ Implementado
|
||||
|
||||
@ -1,235 +1,243 @@
|
||||
# _MAP: OQI-002 - Módulo Educativo
|
||||
|
||||
**Última actualización:** 2025-12-05
|
||||
**Estado:** Parcialmente Implementado
|
||||
**Versión:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Propósito
|
||||
|
||||
Esta épica implementa la plataforma educativa de OrbiQuant IA, permitiendo a los usuarios aprender trading a través de cursos estructurados con video, texto, quizzes y sistema de progreso con gamificación.
|
||||
|
||||
---
|
||||
|
||||
## Contenido del Directorio
|
||||
|
||||
```
|
||||
OQI-002-education/
|
||||
├── README.md # Documentación técnica existente
|
||||
├── _MAP.md # Este archivo - índice
|
||||
├── requerimientos/ # Documentos de requerimientos funcionales
|
||||
│ ├── RF-EDU-001-catalogo.md # Catálogo de cursos
|
||||
│ ├── RF-EDU-002-lecciones.md # Sistema de lecciones
|
||||
│ ├── RF-EDU-003-progreso.md # Tracking de progreso
|
||||
│ ├── RF-EDU-004-quizzes.md # Sistema de quizzes
|
||||
│ ├── RF-EDU-005-certificados.md # Certificaciones
|
||||
│ └── RF-EDU-006-gamificacion.md # XP y badges
|
||||
├── especificaciones/ # Especificaciones técnicas
|
||||
│ ├── ET-EDU-001-database.md # Modelo de datos
|
||||
│ ├── ET-EDU-002-api.md # Endpoints REST
|
||||
│ ├── ET-EDU-003-frontend.md # Componentes React
|
||||
│ ├── ET-EDU-004-video.md # Streaming de video
|
||||
│ ├── ET-EDU-005-quizzes.md # Motor de quizzes
|
||||
│ └── ET-EDU-006-gamification.md # Sistema de gamificación
|
||||
├── historias-usuario/ # User Stories
|
||||
│ ├── US-EDU-001-ver-catalogo.md
|
||||
│ ├── US-EDU-002-ver-curso.md
|
||||
│ ├── US-EDU-003-iniciar-leccion.md
|
||||
│ ├── US-EDU-004-ver-video.md
|
||||
│ ├── US-EDU-005-completar-leccion.md
|
||||
│ ├── US-EDU-006-realizar-quiz.md
|
||||
│ ├── US-EDU-007-ver-progreso.md
|
||||
│ ├── US-EDU-008-obtener-certificado.md
|
||||
│ ├── US-EDU-009-buscar-cursos.md
|
||||
│ ├── US-EDU-010-filtrar-categoria.md
|
||||
│ ├── US-EDU-011-marcar-favorito.md
|
||||
│ ├── US-EDU-012-dejar-review.md
|
||||
│ ├── US-EDU-013-ver-xp.md
|
||||
│ ├── US-EDU-014-desbloquear-badge.md
|
||||
│ └── US-EDU-015-continuar-donde-deje.md
|
||||
└── implementacion/ # Trazabilidad de implementación
|
||||
└── TRACEABILITY.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Funcionales
|
||||
|
||||
| ID | Nombre | Prioridad | SP | Estado |
|
||||
|----|--------|-----------|-----|--------|
|
||||
| RF-EDU-001 | Catálogo de Cursos | P0 | 8 | ✅ Implementado |
|
||||
| RF-EDU-002 | Sistema de Lecciones | P0 | 8 | Pendiente |
|
||||
| RF-EDU-003 | Tracking de Progreso | P0 | 8 | Pendiente |
|
||||
| RF-EDU-004 | Sistema de Quizzes | P1 | 8 | Pendiente |
|
||||
| RF-EDU-005 | Certificaciones | P2 | 5 | Pendiente |
|
||||
| RF-EDU-006 | Gamificación | P2 | 8 | Pendiente |
|
||||
|
||||
**Total:** 45 SP
|
||||
|
||||
---
|
||||
|
||||
## Especificaciones Técnicas
|
||||
|
||||
| ID | Nombre | Componente | Estado |
|
||||
|----|--------|------------|--------|
|
||||
| ET-EDU-001 | Database | Database | ✅ Schema existe |
|
||||
| ET-EDU-002 | API REST | Backend | ✅ Parcial |
|
||||
| ET-EDU-003 | Frontend | Frontend | ✅ Parcial |
|
||||
| ET-EDU-004 | Video Streaming | Backend | Pendiente |
|
||||
| ET-EDU-005 | Quiz Engine | Backend | Pendiente |
|
||||
| ET-EDU-006 | Gamification | Backend/Frontend | Pendiente |
|
||||
|
||||
---
|
||||
|
||||
## Historias de Usuario
|
||||
|
||||
| ID | Historia | Prioridad | SP | Estado |
|
||||
|----|----------|-----------|-----|--------|
|
||||
| US-EDU-001 | Ver catálogo de cursos | P0 | 3 | ✅ Implementado |
|
||||
| US-EDU-002 | Ver detalle de curso | P0 | 3 | ✅ Implementado |
|
||||
| US-EDU-003 | Iniciar una lección | P0 | 3 | Pendiente |
|
||||
| US-EDU-004 | Ver video de lección | P0 | 3 | Pendiente |
|
||||
| US-EDU-005 | Completar lección | P0 | 3 | Pendiente |
|
||||
| US-EDU-006 | Realizar quiz | P1 | 5 | Pendiente |
|
||||
| US-EDU-007 | Ver mi progreso | P0 | 3 | Pendiente |
|
||||
| US-EDU-008 | Obtener certificado | P2 | 3 | Pendiente |
|
||||
| US-EDU-009 | Buscar cursos | P1 | 2 | Pendiente |
|
||||
| US-EDU-010 | Filtrar por categoría | P1 | 2 | Pendiente |
|
||||
| US-EDU-011 | Marcar favorito | P2 | 2 | Pendiente |
|
||||
| US-EDU-012 | Dejar review | P2 | 3 | Pendiente |
|
||||
| US-EDU-013 | Ver XP acumulado | P2 | 2 | Pendiente |
|
||||
| US-EDU-014 | Desbloquear badge | P2 | 3 | Pendiente |
|
||||
| US-EDU-015 | Continuar donde dejé | P1 | 3 | Pendiente |
|
||||
|
||||
**Total:** 45 SP
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de:
|
||||
|
||||
- **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado
|
||||
- **OQI-005:** Pagos (compra de cursos premium) - Pendiente
|
||||
|
||||
### Bloquea:
|
||||
|
||||
- Ninguna
|
||||
|
||||
---
|
||||
|
||||
## Stack Técnico
|
||||
|
||||
| Capa | Tecnología | Uso |
|
||||
|------|------------|-----|
|
||||
| Frontend | React + Zustand | UI y estado |
|
||||
| Backend | Express.js | API REST |
|
||||
| Database | PostgreSQL | Persistencia |
|
||||
| Video | Cloudflare Stream / S3 | Hosting de videos |
|
||||
| CDN | Cloudflare | Assets estáticos |
|
||||
|
||||
---
|
||||
|
||||
## Entidades Principales
|
||||
|
||||
### Category
|
||||
- Categorías de cursos (Trading Básico, Análisis Técnico, etc.)
|
||||
|
||||
### Course
|
||||
- Curso con metadata, precio, nivel de dificultad
|
||||
|
||||
### Module
|
||||
- Agrupación de lecciones dentro de un curso
|
||||
|
||||
### Lesson
|
||||
- Contenido individual (video, artículo, quiz)
|
||||
|
||||
### Enrollment
|
||||
- Inscripción de usuario en curso
|
||||
|
||||
### Progress
|
||||
- Progreso del usuario por lección
|
||||
|
||||
---
|
||||
|
||||
## Niveles de Dificultad
|
||||
|
||||
| Nivel | Label | Color | Cursos típicos |
|
||||
|-------|-------|-------|----------------|
|
||||
| beginner | Principiante | Verde | Introducción al trading |
|
||||
| intermediate | Intermedio | Azul | Análisis técnico |
|
||||
| advanced | Avanzado | Naranja | Estrategias avanzadas |
|
||||
| expert | Experto | Rojo | Trading algorítmico |
|
||||
|
||||
---
|
||||
|
||||
## Gamificación (Fase 2)
|
||||
|
||||
### Sistema de XP
|
||||
- Completar lección: +10 XP
|
||||
- Aprobar quiz: +25 XP
|
||||
- Completar curso: +100 XP
|
||||
- Streak diario: +5 XP
|
||||
|
||||
### Badges
|
||||
- 🎓 "Primer Paso" - Completa tu primera lección
|
||||
- 📚 "Estudiante Dedicado" - Completa 10 lecciones
|
||||
- 🏆 "Graduado" - Completa tu primer curso
|
||||
- 🔥 "En Racha" - 7 días seguidos de estudio
|
||||
- 💯 "Perfeccionista" - 100% en un quiz
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### Funcionales
|
||||
|
||||
- [ ] Catálogo muestra cursos con filtros y búsqueda
|
||||
- [ ] Usuarios pueden inscribirse en cursos
|
||||
- [ ] Videos reproducen correctamente
|
||||
- [ ] Progreso se guarda automáticamente
|
||||
- [ ] Quizzes validan respuestas correctamente
|
||||
- [ ] Certificados se generan al completar curso
|
||||
|
||||
### No Funcionales
|
||||
|
||||
- [ ] Videos cargan en < 3 segundos
|
||||
- [ ] Catálogo carga en < 1 segundo
|
||||
- [ ] Responsive en mobile
|
||||
- [ ] Accesible (WCAG 2.1 AA)
|
||||
|
||||
### Técnicos
|
||||
|
||||
- [ ] Cobertura de tests > 70%
|
||||
- [ ] Documentación API completa
|
||||
- [ ] SEO optimizado para cursos públicos
|
||||
|
||||
---
|
||||
|
||||
## Hitos
|
||||
|
||||
| Hito | Entregables | Target |
|
||||
|------|-------------|--------|
|
||||
| M1 | Catálogo + detalle curso | Sprint 3 ✅ |
|
||||
| M2 | Lecciones + videos | Sprint 3 |
|
||||
| M3 | Progreso + quizzes | Sprint 4 |
|
||||
| M4 | Gamificación + certificados | Sprint 4 |
|
||||
|
||||
---
|
||||
|
||||
## Reutilización de GAMILIT
|
||||
|
||||
Esta épica reutiliza ~70% de la arquitectura del proyecto GAMILIT:
|
||||
- Estructura de cursos y lecciones
|
||||
- Sistema de progreso
|
||||
- Motor de quizzes
|
||||
- Sistema de gamificación (XP, badges)
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- [README Técnico](./README.md)
|
||||
- [Vision del Producto](../../00-vision-general/VISION-PRODUCTO.md)
|
||||
- [_MAP Fase MVP](../_MAP.md)
|
||||
---
|
||||
id: "MAP-OQI-002-education"
|
||||
title: "Mapa de OQI-002-education"
|
||||
type: "Index"
|
||||
project: "trading-platform"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# _MAP: OQI-002 - Módulo Educativo
|
||||
|
||||
**Última actualización:** 2025-12-05
|
||||
**Estado:** Parcialmente Implementado
|
||||
**Versión:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Propósito
|
||||
|
||||
Esta épica implementa la plataforma educativa de OrbiQuant IA, permitiendo a los usuarios aprender trading a través de cursos estructurados con video, texto, quizzes y sistema de progreso con gamificación.
|
||||
|
||||
---
|
||||
|
||||
## Contenido del Directorio
|
||||
|
||||
```
|
||||
OQI-002-education/
|
||||
├── README.md # Documentación técnica existente
|
||||
├── _MAP.md # Este archivo - índice
|
||||
├── requerimientos/ # Documentos de requerimientos funcionales
|
||||
│ ├── RF-EDU-001-catalogo.md # Catálogo de cursos
|
||||
│ ├── RF-EDU-002-lecciones.md # Sistema de lecciones
|
||||
│ ├── RF-EDU-003-progreso.md # Tracking de progreso
|
||||
│ ├── RF-EDU-004-quizzes.md # Sistema de quizzes
|
||||
│ ├── RF-EDU-005-certificados.md # Certificaciones
|
||||
│ └── RF-EDU-006-gamificacion.md # XP y badges
|
||||
├── especificaciones/ # Especificaciones técnicas
|
||||
│ ├── ET-EDU-001-database.md # Modelo de datos
|
||||
│ ├── ET-EDU-002-api.md # Endpoints REST
|
||||
│ ├── ET-EDU-003-frontend.md # Componentes React
|
||||
│ ├── ET-EDU-004-video.md # Streaming de video
|
||||
│ ├── ET-EDU-005-quizzes.md # Motor de quizzes
|
||||
│ └── ET-EDU-006-gamification.md # Sistema de gamificación
|
||||
├── historias-usuario/ # User Stories
|
||||
│ ├── US-EDU-001-ver-catalogo.md
|
||||
│ ├── US-EDU-002-ver-curso.md
|
||||
│ ├── US-EDU-003-iniciar-leccion.md
|
||||
│ ├── US-EDU-004-ver-video.md
|
||||
│ ├── US-EDU-005-completar-leccion.md
|
||||
│ ├── US-EDU-006-realizar-quiz.md
|
||||
│ ├── US-EDU-007-ver-progreso.md
|
||||
│ ├── US-EDU-008-obtener-certificado.md
|
||||
│ ├── US-EDU-009-buscar-cursos.md
|
||||
│ ├── US-EDU-010-filtrar-categoria.md
|
||||
│ ├── US-EDU-011-marcar-favorito.md
|
||||
│ ├── US-EDU-012-dejar-review.md
|
||||
│ ├── US-EDU-013-ver-xp.md
|
||||
│ ├── US-EDU-014-desbloquear-badge.md
|
||||
│ └── US-EDU-015-continuar-donde-deje.md
|
||||
└── implementacion/ # Trazabilidad de implementación
|
||||
└── TRACEABILITY.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Funcionales
|
||||
|
||||
| ID | Nombre | Prioridad | SP | Estado |
|
||||
|----|--------|-----------|-----|--------|
|
||||
| RF-EDU-001 | Catálogo de Cursos | P0 | 8 | ✅ Implementado |
|
||||
| RF-EDU-002 | Sistema de Lecciones | P0 | 8 | Pendiente |
|
||||
| RF-EDU-003 | Tracking de Progreso | P0 | 8 | Pendiente |
|
||||
| RF-EDU-004 | Sistema de Quizzes | P1 | 8 | Pendiente |
|
||||
| RF-EDU-005 | Certificaciones | P2 | 5 | Pendiente |
|
||||
| RF-EDU-006 | Gamificación | P2 | 8 | Pendiente |
|
||||
|
||||
**Total:** 45 SP
|
||||
|
||||
---
|
||||
|
||||
## Especificaciones Técnicas
|
||||
|
||||
| ID | Nombre | Componente | Estado |
|
||||
|----|--------|------------|--------|
|
||||
| ET-EDU-001 | Database | Database | ✅ Schema existe |
|
||||
| ET-EDU-002 | API REST | Backend | ✅ Parcial |
|
||||
| ET-EDU-003 | Frontend | Frontend | ✅ Parcial |
|
||||
| ET-EDU-004 | Video Streaming | Backend | Pendiente |
|
||||
| ET-EDU-005 | Quiz Engine | Backend | Pendiente |
|
||||
| ET-EDU-006 | Gamification | Backend/Frontend | Pendiente |
|
||||
|
||||
---
|
||||
|
||||
## Historias de Usuario
|
||||
|
||||
| ID | Historia | Prioridad | SP | Estado |
|
||||
|----|----------|-----------|-----|--------|
|
||||
| US-EDU-001 | Ver catálogo de cursos | P0 | 3 | ✅ Implementado |
|
||||
| US-EDU-002 | Ver detalle de curso | P0 | 3 | ✅ Implementado |
|
||||
| US-EDU-003 | Iniciar una lección | P0 | 3 | Pendiente |
|
||||
| US-EDU-004 | Ver video de lección | P0 | 3 | Pendiente |
|
||||
| US-EDU-005 | Completar lección | P0 | 3 | Pendiente |
|
||||
| US-EDU-006 | Realizar quiz | P1 | 5 | Pendiente |
|
||||
| US-EDU-007 | Ver mi progreso | P0 | 3 | Pendiente |
|
||||
| US-EDU-008 | Obtener certificado | P2 | 3 | Pendiente |
|
||||
| US-EDU-009 | Buscar cursos | P1 | 2 | Pendiente |
|
||||
| US-EDU-010 | Filtrar por categoría | P1 | 2 | Pendiente |
|
||||
| US-EDU-011 | Marcar favorito | P2 | 2 | Pendiente |
|
||||
| US-EDU-012 | Dejar review | P2 | 3 | Pendiente |
|
||||
| US-EDU-013 | Ver XP acumulado | P2 | 2 | Pendiente |
|
||||
| US-EDU-014 | Desbloquear badge | P2 | 3 | Pendiente |
|
||||
| US-EDU-015 | Continuar donde dejé | P1 | 3 | Pendiente |
|
||||
|
||||
**Total:** 45 SP
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de:
|
||||
|
||||
- **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado
|
||||
- **OQI-005:** Pagos (compra de cursos premium) - Pendiente
|
||||
|
||||
### Bloquea:
|
||||
|
||||
- Ninguna
|
||||
|
||||
---
|
||||
|
||||
## Stack Técnico
|
||||
|
||||
| Capa | Tecnología | Uso |
|
||||
|------|------------|-----|
|
||||
| Frontend | React + Zustand | UI y estado |
|
||||
| Backend | Express.js | API REST |
|
||||
| Database | PostgreSQL | Persistencia |
|
||||
| Video | Cloudflare Stream / S3 | Hosting de videos |
|
||||
| CDN | Cloudflare | Assets estáticos |
|
||||
|
||||
---
|
||||
|
||||
## Entidades Principales
|
||||
|
||||
### Category
|
||||
- Categorías de cursos (Trading Básico, Análisis Técnico, etc.)
|
||||
|
||||
### Course
|
||||
- Curso con metadata, precio, nivel de dificultad
|
||||
|
||||
### Module
|
||||
- Agrupación de lecciones dentro de un curso
|
||||
|
||||
### Lesson
|
||||
- Contenido individual (video, artículo, quiz)
|
||||
|
||||
### Enrollment
|
||||
- Inscripción de usuario en curso
|
||||
|
||||
### Progress
|
||||
- Progreso del usuario por lección
|
||||
|
||||
---
|
||||
|
||||
## Niveles de Dificultad
|
||||
|
||||
| Nivel | Label | Color | Cursos típicos |
|
||||
|-------|-------|-------|----------------|
|
||||
| beginner | Principiante | Verde | Introducción al trading |
|
||||
| intermediate | Intermedio | Azul | Análisis técnico |
|
||||
| advanced | Avanzado | Naranja | Estrategias avanzadas |
|
||||
| expert | Experto | Rojo | Trading algorítmico |
|
||||
|
||||
---
|
||||
|
||||
## Gamificación (Fase 2)
|
||||
|
||||
### Sistema de XP
|
||||
- Completar lección: +10 XP
|
||||
- Aprobar quiz: +25 XP
|
||||
- Completar curso: +100 XP
|
||||
- Streak diario: +5 XP
|
||||
|
||||
### Badges
|
||||
- 🎓 "Primer Paso" - Completa tu primera lección
|
||||
- 📚 "Estudiante Dedicado" - Completa 10 lecciones
|
||||
- 🏆 "Graduado" - Completa tu primer curso
|
||||
- 🔥 "En Racha" - 7 días seguidos de estudio
|
||||
- 💯 "Perfeccionista" - 100% en un quiz
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### Funcionales
|
||||
|
||||
- [ ] Catálogo muestra cursos con filtros y búsqueda
|
||||
- [ ] Usuarios pueden inscribirse en cursos
|
||||
- [ ] Videos reproducen correctamente
|
||||
- [ ] Progreso se guarda automáticamente
|
||||
- [ ] Quizzes validan respuestas correctamente
|
||||
- [ ] Certificados se generan al completar curso
|
||||
|
||||
### No Funcionales
|
||||
|
||||
- [ ] Videos cargan en < 3 segundos
|
||||
- [ ] Catálogo carga en < 1 segundo
|
||||
- [ ] Responsive en mobile
|
||||
- [ ] Accesible (WCAG 2.1 AA)
|
||||
|
||||
### Técnicos
|
||||
|
||||
- [ ] Cobertura de tests > 70%
|
||||
- [ ] Documentación API completa
|
||||
- [ ] SEO optimizado para cursos públicos
|
||||
|
||||
---
|
||||
|
||||
## Hitos
|
||||
|
||||
| Hito | Entregables | Target |
|
||||
|------|-------------|--------|
|
||||
| M1 | Catálogo + detalle curso | Sprint 3 ✅ |
|
||||
| M2 | Lecciones + videos | Sprint 3 |
|
||||
| M3 | Progreso + quizzes | Sprint 4 |
|
||||
| M4 | Gamificación + certificados | Sprint 4 |
|
||||
|
||||
---
|
||||
|
||||
## Reutilización de GAMILIT
|
||||
|
||||
Esta épica reutiliza ~70% de la arquitectura del proyecto GAMILIT:
|
||||
- Estructura de cursos y lecciones
|
||||
- Sistema de progreso
|
||||
- Motor de quizzes
|
||||
- Sistema de gamificación (XP, badges)
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- [README Técnico](./README.md)
|
||||
- [Vision del Producto](../../00-vision-general/VISION-PRODUCTO.md)
|
||||
- [_MAP Fase MVP](../_MAP.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,229 +1,238 @@
|
||||
# Especificaciones Técnicas - OQI-002 Módulo Educativo
|
||||
|
||||
Este directorio contiene las especificaciones técnicas detalladas para el módulo educativo de OrbiQuant IA.
|
||||
|
||||
## Índice de Especificaciones
|
||||
|
||||
### [ET-EDU-001: Modelo de Datos - Schema Education](./ET-EDU-001-database.md)
|
||||
**Componente:** Database
|
||||
**Tamaño:** ~30KB
|
||||
|
||||
Define el schema completo de PostgreSQL para el módulo educativo:
|
||||
- 11 tablas principales (categories, courses, modules, lessons, enrollments, progress, quizzes, quiz_questions, quiz_attempts, certificates, user_achievements)
|
||||
- 6 ENUMs personalizados
|
||||
- Triggers y funciones automáticas
|
||||
- Vistas optimizadas
|
||||
- Índices de performance
|
||||
- Row Level Security (RLS)
|
||||
|
||||
**Contenido clave:**
|
||||
- Schema education completo con relaciones
|
||||
- Triggers para auto-actualización de progreso
|
||||
- Vistas para queries complejas
|
||||
- Estrategias de indexación
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-002: API REST - Endpoints del Módulo](./ET-EDU-002-api.md)
|
||||
**Componente:** Backend
|
||||
**Tamaño:** ~42KB
|
||||
|
||||
Especificación completa de la API REST con Express.js + TypeScript:
|
||||
- 10 grupos de endpoints (Categories, Courses, Modules, Lessons, Enrollments, Progress, Quizzes, Certificates, Achievements, Gamification)
|
||||
- Request/Response con TypeScript interfaces
|
||||
- Autenticación JWT y autorización por roles
|
||||
- Rate limiting y paginación
|
||||
- Validación con Zod
|
||||
- Manejo de errores estandarizado
|
||||
|
||||
**Contenido clave:**
|
||||
- ~60 endpoints documentados
|
||||
- Middleware stack completo
|
||||
- Códigos de error estandarizados
|
||||
- Ejemplos de tests con Supertest
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-003: Componentes Frontend - React + TypeScript](./ET-EDU-003-frontend.md)
|
||||
**Componente:** Frontend
|
||||
**Tamaño:** ~46KB
|
||||
|
||||
Arquitectura frontend con React 18, TypeScript, Zustand y TailwindCSS:
|
||||
- 7 páginas principales (CoursesPage, CourseDetailPage, LessonPage, ProgressPage, QuizPage, CertificatesPage, AchievementsPage)
|
||||
- 20+ componentes reutilizables
|
||||
- Custom hooks para data fetching
|
||||
- Stores Zustand para state management
|
||||
- Integración con React Query
|
||||
|
||||
**Contenido clave:**
|
||||
- Código completo de componentes principales
|
||||
- Hooks personalizados (useCourses, useEnrollment, useVideoProgress, etc.)
|
||||
- Stores con Zustand (courseStore, progressStore, gamificationStore)
|
||||
- Configuración de TailwindCSS
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-004: Sistema de Streaming de Video](./ET-EDU-004-video.md)
|
||||
**Componente:** Backend/Infraestructura
|
||||
**Tamaño:** ~30KB
|
||||
|
||||
Integración de video streaming con Vimeo y AWS S3+CloudFront:
|
||||
- Configuración de Vimeo Pro/Business
|
||||
- Upload y gestión de videos
|
||||
- AWS S3 + CloudFront con signed URLs
|
||||
- Transcoding HLS con FFmpeg
|
||||
- Player configuration (Vimeo Player / Video.js)
|
||||
- Tracking de progreso de video
|
||||
- Subtítulos WebVTT
|
||||
|
||||
**Contenido clave:**
|
||||
- Servicios de upload a Vimeo
|
||||
- Generación de signed URLs en CloudFront
|
||||
- Pipeline de transcoding HLS multi-bitrate
|
||||
- Componentes de video player (VimeoPlayer, HLSPlayer)
|
||||
- Configuración de CloudFront Distribution
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-005: Motor de Evaluaciones y Quizzes](./ET-EDU-005-quizzes.md)
|
||||
**Componente:** Backend/Frontend
|
||||
**Tamaño:** ~33KB
|
||||
|
||||
Sistema completo de evaluaciones con múltiples tipos de preguntas:
|
||||
- 5 tipos de preguntas (Multiple Choice, True/False, Multiple Select, Fill in the Blank, Code Challenge)
|
||||
- Algoritmo de scoring con crédito parcial
|
||||
- Gestión de intentos con límites
|
||||
- Timer con auto-submit
|
||||
- Validación de respuestas (incluyendo fuzzy matching)
|
||||
- Analytics de quizzes
|
||||
|
||||
**Contenido clave:**
|
||||
- QuizScoringService con algoritmos completos
|
||||
- QuizAttemptService para flujo de quiz
|
||||
- Componentes de UI (QuizQuestion, QuizTimer, QuizResults)
|
||||
- Cálculo de dificultad de preguntas
|
||||
- Distribución de puntajes
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-006: Sistema de Gamificación](./ET-EDU-006-gamification.md)
|
||||
**Componente:** Backend/Frontend
|
||||
**Tamaño:** ~35KB
|
||||
|
||||
Sistema de gamificación para aumentar engagement:
|
||||
- Sistema de XP con múltiples fuentes
|
||||
- Fórmula de niveles: `Level = floor(sqrt(totalXP / 100))`
|
||||
- 15+ achievements predefinidos (common, uncommon, rare, epic, legendary)
|
||||
- Sistema de rachas diarias con rewards
|
||||
- Leaderboard global y por períodos
|
||||
- Notificaciones de logros
|
||||
|
||||
**Contenido clave:**
|
||||
- XPManagerService con cálculo de niveles
|
||||
- AchievementManagerService con verificación automática
|
||||
- StreakManagerService para rachas diarias
|
||||
- LeaderboardManagerService con caching
|
||||
- Configuración de achievements y recompensas
|
||||
- Componentes de UI (XPBar, LevelBadge, AchievementCard)
|
||||
|
||||
---
|
||||
|
||||
## Stack Tecnológico
|
||||
|
||||
### Backend
|
||||
- **Runtime:** Node.js 18+
|
||||
- **Framework:** Express.js
|
||||
- **Language:** TypeScript 5.3+
|
||||
- **Database:** PostgreSQL 15+
|
||||
- **ORM:** Prisma / TypeORM (opcional)
|
||||
- **Validation:** Zod
|
||||
- **Auth:** JWT (jsonwebtoken)
|
||||
- **Caching:** Redis 4+
|
||||
- **Video Processing:** FFmpeg
|
||||
|
||||
### Frontend
|
||||
- **Framework:** React 18
|
||||
- **Language:** TypeScript 5.3+
|
||||
- **State Management:** Zustand 4+
|
||||
- **Data Fetching:** TanStack React Query 5+
|
||||
- **Styling:** TailwindCSS 3+
|
||||
- **Routing:** React Router 6+
|
||||
- **Video Player:** Vimeo Player / Video.js
|
||||
- **Forms:** React Hook Form + Zod
|
||||
|
||||
### Infraestructura
|
||||
- **Video CDN:** Vimeo Pro/Business o AWS S3 + CloudFront
|
||||
- **Object Storage:** AWS S3
|
||||
- **Cache:** Redis
|
||||
- **Monitoring:** (TBD)
|
||||
|
||||
---
|
||||
|
||||
## Convenciones de Nomenclatura
|
||||
|
||||
### Archivos de Especificación
|
||||
```
|
||||
ET-EDU-XXX-{nombre}.md
|
||||
```
|
||||
- **ET:** Especificación Técnica
|
||||
- **EDU:** Módulo Education
|
||||
- **XXX:** Número secuencial (001-999)
|
||||
- **{nombre}:** Identificador descriptivo
|
||||
|
||||
### Versiones
|
||||
Todas las especificaciones están en versión **1.0.0** (2025-12-05)
|
||||
|
||||
---
|
||||
|
||||
## Cómo Usar Este Documento
|
||||
|
||||
1. **Para Desarrolladores Backend:**
|
||||
- Leer ET-EDU-001 (Database) y ET-EDU-002 (API)
|
||||
- Referencias: ET-EDU-004 (Video), ET-EDU-005 (Quizzes), ET-EDU-006 (Gamification)
|
||||
|
||||
2. **Para Desarrolladores Frontend:**
|
||||
- Leer ET-EDU-002 (API) y ET-EDU-003 (Frontend)
|
||||
- Referencias: ET-EDU-004 (Video Player), ET-EDU-005 (Quiz UI), ET-EDU-006 (Gamification UI)
|
||||
|
||||
3. **Para DevOps:**
|
||||
- Leer ET-EDU-001 (Database setup)
|
||||
- Leer ET-EDU-004 (Video Infrastructure)
|
||||
- Variables de entorno en cada especificación
|
||||
|
||||
4. **Para Product Managers:**
|
||||
- Todas las especificaciones contienen descripción y arquitectura
|
||||
- Ver sección de "Interfaces/Tipos" para data models
|
||||
|
||||
---
|
||||
|
||||
## Estado de Implementación
|
||||
|
||||
| Especificación | Estado | Prioridad | Notas |
|
||||
|---------------|--------|-----------|-------|
|
||||
| ET-EDU-001 | Pendiente | Alta | Base de datos requerida primero |
|
||||
| ET-EDU-002 | Pendiente | Alta | Depende de ET-EDU-001 |
|
||||
| ET-EDU-003 | Pendiente | Alta | Depende de ET-EDU-002 |
|
||||
| ET-EDU-004 | Pendiente | Media | Puede iniciar en paralelo |
|
||||
| ET-EDU-005 | Pendiente | Media | Depende de ET-EDU-001, ET-EDU-002 |
|
||||
| ET-EDU-006 | Pendiente | Baja | Feature post-MVP |
|
||||
|
||||
---
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. **Revisión Técnica:** Validar especificaciones con el equipo de desarrollo
|
||||
2. **Priorización:** Definir orden de implementación
|
||||
3. **Estimación:** Calcular esfuerzo de desarrollo por especificación
|
||||
4. **Asignación:** Distribuir tareas entre el equipo
|
||||
5. **Implementación:** Comenzar desarrollo siguiendo las especificaciones
|
||||
|
||||
---
|
||||
|
||||
## Contacto
|
||||
|
||||
Para preguntas o aclaraciones sobre estas especificaciones, contactar al **Requirements Analyst** del proyecto.
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2025-12-05
|
||||
**Versión del documento:** 1.0.0
|
||||
---
|
||||
id: "README"
|
||||
title: "Especificaciones Técnicas - OQI-002 Módulo Educativo"
|
||||
type: "Documentation"
|
||||
project: "trading-platform"
|
||||
version: "1.0.0"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# Especificaciones Técnicas - OQI-002 Módulo Educativo
|
||||
|
||||
Este directorio contiene las especificaciones técnicas detalladas para el módulo educativo de OrbiQuant IA.
|
||||
|
||||
## Índice de Especificaciones
|
||||
|
||||
### [ET-EDU-001: Modelo de Datos - Schema Education](./ET-EDU-001-database.md)
|
||||
**Componente:** Database
|
||||
**Tamaño:** ~30KB
|
||||
|
||||
Define el schema completo de PostgreSQL para el módulo educativo:
|
||||
- 11 tablas principales (categories, courses, modules, lessons, enrollments, progress, quizzes, quiz_questions, quiz_attempts, certificates, user_achievements)
|
||||
- 6 ENUMs personalizados
|
||||
- Triggers y funciones automáticas
|
||||
- Vistas optimizadas
|
||||
- Índices de performance
|
||||
- Row Level Security (RLS)
|
||||
|
||||
**Contenido clave:**
|
||||
- Schema education completo con relaciones
|
||||
- Triggers para auto-actualización de progreso
|
||||
- Vistas para queries complejas
|
||||
- Estrategias de indexación
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-002: API REST - Endpoints del Módulo](./ET-EDU-002-api.md)
|
||||
**Componente:** Backend
|
||||
**Tamaño:** ~42KB
|
||||
|
||||
Especificación completa de la API REST con Express.js + TypeScript:
|
||||
- 10 grupos de endpoints (Categories, Courses, Modules, Lessons, Enrollments, Progress, Quizzes, Certificates, Achievements, Gamification)
|
||||
- Request/Response con TypeScript interfaces
|
||||
- Autenticación JWT y autorización por roles
|
||||
- Rate limiting y paginación
|
||||
- Validación con Zod
|
||||
- Manejo de errores estandarizado
|
||||
|
||||
**Contenido clave:**
|
||||
- ~60 endpoints documentados
|
||||
- Middleware stack completo
|
||||
- Códigos de error estandarizados
|
||||
- Ejemplos de tests con Supertest
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-003: Componentes Frontend - React + TypeScript](./ET-EDU-003-frontend.md)
|
||||
**Componente:** Frontend
|
||||
**Tamaño:** ~46KB
|
||||
|
||||
Arquitectura frontend con React 18, TypeScript, Zustand y TailwindCSS:
|
||||
- 7 páginas principales (CoursesPage, CourseDetailPage, LessonPage, ProgressPage, QuizPage, CertificatesPage, AchievementsPage)
|
||||
- 20+ componentes reutilizables
|
||||
- Custom hooks para data fetching
|
||||
- Stores Zustand para state management
|
||||
- Integración con React Query
|
||||
|
||||
**Contenido clave:**
|
||||
- Código completo de componentes principales
|
||||
- Hooks personalizados (useCourses, useEnrollment, useVideoProgress, etc.)
|
||||
- Stores con Zustand (courseStore, progressStore, gamificationStore)
|
||||
- Configuración de TailwindCSS
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-004: Sistema de Streaming de Video](./ET-EDU-004-video.md)
|
||||
**Componente:** Backend/Infraestructura
|
||||
**Tamaño:** ~30KB
|
||||
|
||||
Integración de video streaming con Vimeo y AWS S3+CloudFront:
|
||||
- Configuración de Vimeo Pro/Business
|
||||
- Upload y gestión de videos
|
||||
- AWS S3 + CloudFront con signed URLs
|
||||
- Transcoding HLS con FFmpeg
|
||||
- Player configuration (Vimeo Player / Video.js)
|
||||
- Tracking de progreso de video
|
||||
- Subtítulos WebVTT
|
||||
|
||||
**Contenido clave:**
|
||||
- Servicios de upload a Vimeo
|
||||
- Generación de signed URLs en CloudFront
|
||||
- Pipeline de transcoding HLS multi-bitrate
|
||||
- Componentes de video player (VimeoPlayer, HLSPlayer)
|
||||
- Configuración de CloudFront Distribution
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-005: Motor de Evaluaciones y Quizzes](./ET-EDU-005-quizzes.md)
|
||||
**Componente:** Backend/Frontend
|
||||
**Tamaño:** ~33KB
|
||||
|
||||
Sistema completo de evaluaciones con múltiples tipos de preguntas:
|
||||
- 5 tipos de preguntas (Multiple Choice, True/False, Multiple Select, Fill in the Blank, Code Challenge)
|
||||
- Algoritmo de scoring con crédito parcial
|
||||
- Gestión de intentos con límites
|
||||
- Timer con auto-submit
|
||||
- Validación de respuestas (incluyendo fuzzy matching)
|
||||
- Analytics de quizzes
|
||||
|
||||
**Contenido clave:**
|
||||
- QuizScoringService con algoritmos completos
|
||||
- QuizAttemptService para flujo de quiz
|
||||
- Componentes de UI (QuizQuestion, QuizTimer, QuizResults)
|
||||
- Cálculo de dificultad de preguntas
|
||||
- Distribución de puntajes
|
||||
|
||||
---
|
||||
|
||||
### [ET-EDU-006: Sistema de Gamificación](./ET-EDU-006-gamification.md)
|
||||
**Componente:** Backend/Frontend
|
||||
**Tamaño:** ~35KB
|
||||
|
||||
Sistema de gamificación para aumentar engagement:
|
||||
- Sistema de XP con múltiples fuentes
|
||||
- Fórmula de niveles: `Level = floor(sqrt(totalXP / 100))`
|
||||
- 15+ achievements predefinidos (common, uncommon, rare, epic, legendary)
|
||||
- Sistema de rachas diarias con rewards
|
||||
- Leaderboard global y por períodos
|
||||
- Notificaciones de logros
|
||||
|
||||
**Contenido clave:**
|
||||
- XPManagerService con cálculo de niveles
|
||||
- AchievementManagerService con verificación automática
|
||||
- StreakManagerService para rachas diarias
|
||||
- LeaderboardManagerService con caching
|
||||
- Configuración de achievements y recompensas
|
||||
- Componentes de UI (XPBar, LevelBadge, AchievementCard)
|
||||
|
||||
---
|
||||
|
||||
## Stack Tecnológico
|
||||
|
||||
### Backend
|
||||
- **Runtime:** Node.js 18+
|
||||
- **Framework:** Express.js
|
||||
- **Language:** TypeScript 5.3+
|
||||
- **Database:** PostgreSQL 15+
|
||||
- **ORM:** Prisma / TypeORM (opcional)
|
||||
- **Validation:** Zod
|
||||
- **Auth:** JWT (jsonwebtoken)
|
||||
- **Caching:** Redis 4+
|
||||
- **Video Processing:** FFmpeg
|
||||
|
||||
### Frontend
|
||||
- **Framework:** React 18
|
||||
- **Language:** TypeScript 5.3+
|
||||
- **State Management:** Zustand 4+
|
||||
- **Data Fetching:** TanStack React Query 5+
|
||||
- **Styling:** TailwindCSS 3+
|
||||
- **Routing:** React Router 6+
|
||||
- **Video Player:** Vimeo Player / Video.js
|
||||
- **Forms:** React Hook Form + Zod
|
||||
|
||||
### Infraestructura
|
||||
- **Video CDN:** Vimeo Pro/Business o AWS S3 + CloudFront
|
||||
- **Object Storage:** AWS S3
|
||||
- **Cache:** Redis
|
||||
- **Monitoring:** (TBD)
|
||||
|
||||
---
|
||||
|
||||
## Convenciones de Nomenclatura
|
||||
|
||||
### Archivos de Especificación
|
||||
```
|
||||
ET-EDU-XXX-{nombre}.md
|
||||
```
|
||||
- **ET:** Especificación Técnica
|
||||
- **EDU:** Módulo Education
|
||||
- **XXX:** Número secuencial (001-999)
|
||||
- **{nombre}:** Identificador descriptivo
|
||||
|
||||
### Versiones
|
||||
Todas las especificaciones están en versión **1.0.0** (2025-12-05)
|
||||
|
||||
---
|
||||
|
||||
## Cómo Usar Este Documento
|
||||
|
||||
1. **Para Desarrolladores Backend:**
|
||||
- Leer ET-EDU-001 (Database) y ET-EDU-002 (API)
|
||||
- Referencias: ET-EDU-004 (Video), ET-EDU-005 (Quizzes), ET-EDU-006 (Gamification)
|
||||
|
||||
2. **Para Desarrolladores Frontend:**
|
||||
- Leer ET-EDU-002 (API) y ET-EDU-003 (Frontend)
|
||||
- Referencias: ET-EDU-004 (Video Player), ET-EDU-005 (Quiz UI), ET-EDU-006 (Gamification UI)
|
||||
|
||||
3. **Para DevOps:**
|
||||
- Leer ET-EDU-001 (Database setup)
|
||||
- Leer ET-EDU-004 (Video Infrastructure)
|
||||
- Variables de entorno en cada especificación
|
||||
|
||||
4. **Para Product Managers:**
|
||||
- Todas las especificaciones contienen descripción y arquitectura
|
||||
- Ver sección de "Interfaces/Tipos" para data models
|
||||
|
||||
---
|
||||
|
||||
## Estado de Implementación
|
||||
|
||||
| Especificación | Estado | Prioridad | Notas |
|
||||
|---------------|--------|-----------|-------|
|
||||
| ET-EDU-001 | Pendiente | Alta | Base de datos requerida primero |
|
||||
| ET-EDU-002 | Pendiente | Alta | Depende de ET-EDU-001 |
|
||||
| ET-EDU-003 | Pendiente | Alta | Depende de ET-EDU-002 |
|
||||
| ET-EDU-004 | Pendiente | Media | Puede iniciar en paralelo |
|
||||
| ET-EDU-005 | Pendiente | Media | Depende de ET-EDU-001, ET-EDU-002 |
|
||||
| ET-EDU-006 | Pendiente | Baja | Feature post-MVP |
|
||||
|
||||
---
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. **Revisión Técnica:** Validar especificaciones con el equipo de desarrollo
|
||||
2. **Priorización:** Definir orden de implementación
|
||||
3. **Estimación:** Calcular esfuerzo de desarrollo por especificación
|
||||
4. **Asignación:** Distribuir tareas entre el equipo
|
||||
5. **Implementación:** Comenzar desarrollo siguiendo las especificaciones
|
||||
|
||||
---
|
||||
|
||||
## Contacto
|
||||
|
||||
Para preguntas o aclaraciones sobre estas especificaciones, contactar al **Requirements Analyst** del proyecto.
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2025-12-05
|
||||
**Versión del documento:** 1.0.0
|
||||
|
||||
@ -1,314 +1,326 @@
|
||||
# US-EDU-001: Ver Catálogo de Cursos
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-001 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario interesado en aprender trading,
|
||||
**quiero** ver un catálogo completo de cursos disponibles con filtros y búsqueda,
|
||||
**para** descubrir contenido educativo relevante a mi nivel y áreas de interés.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a una página que muestre todos los cursos educativos disponibles en la plataforma. Debe poder filtrar por categoría (Fundamentos, Análisis Técnico, etc.), nivel de dificultad (Principiante, Intermedio, Avanzado), y buscar por palabras clave. Cada curso debe mostrar información clave como título, instructor, duración, número de estudiantes, rating, y el progreso del usuario si ya está inscrito.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CATÁLOGO DE CURSOS [🔍 Buscar cursos...] │
|
||||
│ │
|
||||
│ ┌────────────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ FILTROS │ │ Mostrando 24 de 47 cursos │ │
|
||||
│ │ │ │ Ordenar: [Más recientes ▼] │ │
|
||||
│ │ Categorías │ │ │ │
|
||||
│ │ □ Fundamentos │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│
|
||||
│ │ ✓ Análisis T. │ │ │[IMG] │ │[IMG] │ │[IMG] ││
|
||||
│ │ □ Gestión Riesgo│ │ │Fibonacci│ │Candlestk│ │Day Trad.││
|
||||
│ │ │ │ │básico │ │Avanzado │ │Pro ││
|
||||
│ │ Nivel │ │ │⭐ 4.8 │ │⭐ 4.9 │ │⭐ 4.7 ││
|
||||
│ │ ✓ Principiante │ │ │2h 30m │ │4h 15m │ │6h 45m ││
|
||||
│ │ ✓ Intermedio │ │ │1,234 👥 │ │892 👥 │ │567 👥 ││
|
||||
│ │ □ Avanzado │ │ │[Ver curso]│ │[60% ✓] │ │[Iniciar]││
|
||||
│ │ │ │ └─────────┘ └─────────┘ └─────────┘│
|
||||
│ │ Duración │ │ │ │
|
||||
│ │ □ < 2 horas │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│
|
||||
│ │ ✓ 2-5 horas │ │ │ ...más cursos... ││
|
||||
│ │ □ > 5 horas │ │ └─────────┘ └─────────┘ └─────────┘│
|
||||
│ │ │ │ │ │
|
||||
│ │ [Limpiar] │ │ [1] 2 3 4 ... 8 │ │
|
||||
│ └────────────────┘ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📚 Recomendado para ti │ │
|
||||
│ │ [Curso A] [Curso B] [Curso C] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver catálogo completo**
|
||||
```gherkin
|
||||
DADO que el usuario está autenticado
|
||||
CUANDO navega a /education/courses
|
||||
ENTONCES se muestra el catálogo de cursos
|
||||
Y se muestran 12 cursos por página
|
||||
Y cada curso muestra: imagen, título, instructor, duración, rating, estudiantes
|
||||
Y se muestran filtros en sidebar izquierdo
|
||||
Y se muestra barra de búsqueda
|
||||
Y se muestra contador "Mostrando X de Y cursos"
|
||||
```
|
||||
|
||||
**Escenario 2: Filtrar por categoría**
|
||||
```gherkin
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO selecciona filtro "Análisis Técnico"
|
||||
ENTONCES solo se muestran cursos de esa categoría
|
||||
Y el filtro se marca como activo (checkbox marcado)
|
||||
Y la URL se actualiza a ?category=technical-analysis
|
||||
Y el contador se actualiza "Mostrando X de Y cursos"
|
||||
```
|
||||
|
||||
**Escenario 3: Filtrar por nivel**
|
||||
```gherkin
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO selecciona "Principiante" e "Intermedio"
|
||||
ENTONCES solo se muestran cursos de esos niveles
|
||||
Y se pueden combinar con otros filtros activos
|
||||
Y se muestra badge de nivel en cada curso
|
||||
```
|
||||
|
||||
**Escenario 4: Buscar curso**
|
||||
```gherkin
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO escribe "fibonacci" en el buscador
|
||||
ENTONCES se filtran cursos en tiempo real
|
||||
Y se muestran solo cursos que contengan "fibonacci" en título o descripción
|
||||
Y se resalta el término buscado en resultados
|
||||
Y se muestra "X resultados para 'fibonacci'"
|
||||
```
|
||||
|
||||
**Escenario 5: Sin resultados**
|
||||
```gherkin
|
||||
DADO que el usuario aplicó filtros
|
||||
Y no hay cursos que cumplan los criterios
|
||||
ENTONCES se muestra mensaje "No se encontraron cursos"
|
||||
Y se sugiere "Intenta ajustar los filtros"
|
||||
Y se muestra botón "Limpiar filtros"
|
||||
```
|
||||
|
||||
**Escenario 6: Ver progreso en curso inscrito**
|
||||
```gherkin
|
||||
DADO que el usuario ya está inscrito en un curso
|
||||
CUANDO ve la tarjeta del curso en el catálogo
|
||||
ENTONCES se muestra barra de progreso (ej: "60% completado")
|
||||
Y el botón dice "Continuar" en lugar de "Ver curso"
|
||||
Y al hacer click, navega a la última lección vista
|
||||
```
|
||||
|
||||
**Escenario 7: Ver recomendaciones**
|
||||
```gherkin
|
||||
DADO que el usuario tiene cursos en progreso
|
||||
CUANDO accede al catálogo
|
||||
ENTONCES se muestra sección "Recomendado para ti"
|
||||
Y aparecen máximo 6 cursos relacionados
|
||||
Y se basa en: cursos en progreso, nivel del usuario, categorías de interés
|
||||
```
|
||||
|
||||
**Escenario 8: Ordenar resultados**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo el catálogo
|
||||
CUANDO selecciona ordenar por "Mejor valorados"
|
||||
ENTONCES los cursos se reordenan de mayor a menor rating
|
||||
Y la paginación se mantiene
|
||||
Y los filtros activos se mantienen
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Responsive design para móvil y tablet
|
||||
- [ ] Loading skeleton mientras cargan cursos
|
||||
- [ ] Infinite scroll opcional (además de paginación)
|
||||
- [ ] Animaciones suaves al filtrar
|
||||
- [ ] Badge "Nuevo" para cursos publicados hace < 30 días
|
||||
- [ ] Badge "Popular" para cursos con > 1000 estudiantes
|
||||
- [ ] Guardar filtros en localStorage para próxima visita
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-001: Verificar schema education.courses
|
||||
- [ ] DB-EDU-002: Verificar índices en category_id, level, published_at
|
||||
- [ ] DB-EDU-003: Vista courses_catalog con joins optimizados
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-001: Endpoint GET /education/courses (con paginación)
|
||||
- [ ] BE-EDU-002: Implementar filtros: category, level, duration, search
|
||||
- [ ] BE-EDU-003: Implementar ordenamiento: recent, popular, rating
|
||||
- [ ] BE-EDU-004: Endpoint GET /education/categories
|
||||
- [ ] BE-EDU-005: Implementar CourseService.getCatalog()
|
||||
- [ ] BE-EDU-006: Implementar lógica de recomendaciones
|
||||
- [ ] BE-EDU-007: Caché de catálogo en Redis (5 min)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-001: Crear página CoursesPage.tsx
|
||||
- [ ] FE-EDU-002: Crear componente CourseCard.tsx
|
||||
- [ ] FE-EDU-003: Crear componente CourseFilters.tsx
|
||||
- [ ] FE-EDU-004: Crear componente SearchBar.tsx
|
||||
- [ ] FE-EDU-005: Crear componente Pagination.tsx
|
||||
- [ ] FE-EDU-006: Implementar coursesStore (Zustand)
|
||||
- [ ] FE-EDU-007: Integrar con React Query para caché
|
||||
- [ ] FE-EDU-008: Skeleton loader para carga
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-001: Test unitario CourseService.getCatalog()
|
||||
- [ ] TEST-EDU-002: Test integración GET /courses con filtros
|
||||
- [ ] TEST-EDU-003: Test E2E búsqueda y filtrado
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-AUTH-001: Sistema de autenticación - Estado: ✅ Completado
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-002: Ver detalle de curso
|
||||
- [ ] US-EDU-003: Iniciar lección
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoints involucrados:**
|
||||
| Método | Endpoint | Descripción |
|
||||
|--------|----------|-------------|
|
||||
| GET | /education/courses | Catálogo con filtros y paginación |
|
||||
| GET | /education/categories | Listado de categorías |
|
||||
|
||||
**Query params para GET /courses:**
|
||||
```
|
||||
?page=1
|
||||
&limit=12
|
||||
&category=technical-analysis,fundamentals
|
||||
&level=beginner,intermediate
|
||||
&search=fibonacci
|
||||
&sortBy=recent
|
||||
&sortOrder=desc
|
||||
```
|
||||
|
||||
**Response GET /courses:**
|
||||
```typescript
|
||||
{
|
||||
courses: [
|
||||
{
|
||||
id: "uuid-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
shortDescription: "Aprende a usar Fibonacci...",
|
||||
thumbnail: "https://cdn.orbiquant.com/courses/fib.jpg",
|
||||
category: {
|
||||
id: "cat-1",
|
||||
name: "Análisis Técnico",
|
||||
slug: "technical-analysis",
|
||||
icon: "📊"
|
||||
},
|
||||
level: "beginner",
|
||||
duration: 150, // minutos
|
||||
moduleCount: 5,
|
||||
lessonCount: 23,
|
||||
studentCount: 1234,
|
||||
rating: 4.8,
|
||||
reviewCount: 89,
|
||||
instructor: {
|
||||
id: "inst-1",
|
||||
name: "Carlos Mendoza",
|
||||
avatar: "https://...",
|
||||
title: "Senior Trader"
|
||||
},
|
||||
isPremium: false,
|
||||
publishedAt: "2025-11-15T10:00:00Z",
|
||||
userProgress: {
|
||||
enrolledAt: "2025-12-01T14:30:00Z",
|
||||
progressPercent: 60,
|
||||
lastAccessedAt: "2025-12-04T18:20:00Z"
|
||||
}
|
||||
}
|
||||
// ... más cursos
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 12,
|
||||
total: 47,
|
||||
totalPages: 4
|
||||
},
|
||||
filters: {
|
||||
categories: [...],
|
||||
levels: ["beginner", "intermediate", "advanced", "expert"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.courses`: Catálogo de cursos
|
||||
- `education.categories`: Categorías
|
||||
- `education.course_enrollments`: Inscripciones y progreso
|
||||
- `education.instructors`: Información de instructores
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-001"
|
||||
title: "Ver Catalogo de Cursos"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-002"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-001: Ver Catálogo de Cursos
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-001 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario interesado en aprender trading,
|
||||
**quiero** ver un catálogo completo de cursos disponibles con filtros y búsqueda,
|
||||
**para** descubrir contenido educativo relevante a mi nivel y áreas de interés.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a una página que muestre todos los cursos educativos disponibles en la plataforma. Debe poder filtrar por categoría (Fundamentos, Análisis Técnico, etc.), nivel de dificultad (Principiante, Intermedio, Avanzado), y buscar por palabras clave. Cada curso debe mostrar información clave como título, instructor, duración, número de estudiantes, rating, y el progreso del usuario si ya está inscrito.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CATÁLOGO DE CURSOS [🔍 Buscar cursos...] │
|
||||
│ │
|
||||
│ ┌────────────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ FILTROS │ │ Mostrando 24 de 47 cursos │ │
|
||||
│ │ │ │ Ordenar: [Más recientes ▼] │ │
|
||||
│ │ Categorías │ │ │ │
|
||||
│ │ □ Fundamentos │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│
|
||||
│ │ ✓ Análisis T. │ │ │[IMG] │ │[IMG] │ │[IMG] ││
|
||||
│ │ □ Gestión Riesgo│ │ │Fibonacci│ │Candlestk│ │Day Trad.││
|
||||
│ │ │ │ │básico │ │Avanzado │ │Pro ││
|
||||
│ │ Nivel │ │ │⭐ 4.8 │ │⭐ 4.9 │ │⭐ 4.7 ││
|
||||
│ │ ✓ Principiante │ │ │2h 30m │ │4h 15m │ │6h 45m ││
|
||||
│ │ ✓ Intermedio │ │ │1,234 👥 │ │892 👥 │ │567 👥 ││
|
||||
│ │ □ Avanzado │ │ │[Ver curso]│ │[60% ✓] │ │[Iniciar]││
|
||||
│ │ │ │ └─────────┘ └─────────┘ └─────────┘│
|
||||
│ │ Duración │ │ │ │
|
||||
│ │ □ < 2 horas │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│
|
||||
│ │ ✓ 2-5 horas │ │ │ ...más cursos... ││
|
||||
│ │ □ > 5 horas │ │ └─────────┘ └─────────┘ └─────────┘│
|
||||
│ │ │ │ │ │
|
||||
│ │ [Limpiar] │ │ [1] 2 3 4 ... 8 │ │
|
||||
│ └────────────────┘ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📚 Recomendado para ti │ │
|
||||
│ │ [Curso A] [Curso B] [Curso C] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver catálogo completo**
|
||||
```gherkin
|
||||
DADO que el usuario está autenticado
|
||||
CUANDO navega a /education/courses
|
||||
ENTONCES se muestra el catálogo de cursos
|
||||
Y se muestran 12 cursos por página
|
||||
Y cada curso muestra: imagen, título, instructor, duración, rating, estudiantes
|
||||
Y se muestran filtros en sidebar izquierdo
|
||||
Y se muestra barra de búsqueda
|
||||
Y se muestra contador "Mostrando X de Y cursos"
|
||||
```
|
||||
|
||||
**Escenario 2: Filtrar por categoría**
|
||||
```gherkin
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO selecciona filtro "Análisis Técnico"
|
||||
ENTONCES solo se muestran cursos de esa categoría
|
||||
Y el filtro se marca como activo (checkbox marcado)
|
||||
Y la URL se actualiza a ?category=technical-analysis
|
||||
Y el contador se actualiza "Mostrando X de Y cursos"
|
||||
```
|
||||
|
||||
**Escenario 3: Filtrar por nivel**
|
||||
```gherkin
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO selecciona "Principiante" e "Intermedio"
|
||||
ENTONCES solo se muestran cursos de esos niveles
|
||||
Y se pueden combinar con otros filtros activos
|
||||
Y se muestra badge de nivel en cada curso
|
||||
```
|
||||
|
||||
**Escenario 4: Buscar curso**
|
||||
```gherkin
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO escribe "fibonacci" en el buscador
|
||||
ENTONCES se filtran cursos en tiempo real
|
||||
Y se muestran solo cursos que contengan "fibonacci" en título o descripción
|
||||
Y se resalta el término buscado en resultados
|
||||
Y se muestra "X resultados para 'fibonacci'"
|
||||
```
|
||||
|
||||
**Escenario 5: Sin resultados**
|
||||
```gherkin
|
||||
DADO que el usuario aplicó filtros
|
||||
Y no hay cursos que cumplan los criterios
|
||||
ENTONCES se muestra mensaje "No se encontraron cursos"
|
||||
Y se sugiere "Intenta ajustar los filtros"
|
||||
Y se muestra botón "Limpiar filtros"
|
||||
```
|
||||
|
||||
**Escenario 6: Ver progreso en curso inscrito**
|
||||
```gherkin
|
||||
DADO que el usuario ya está inscrito en un curso
|
||||
CUANDO ve la tarjeta del curso en el catálogo
|
||||
ENTONCES se muestra barra de progreso (ej: "60% completado")
|
||||
Y el botón dice "Continuar" en lugar de "Ver curso"
|
||||
Y al hacer click, navega a la última lección vista
|
||||
```
|
||||
|
||||
**Escenario 7: Ver recomendaciones**
|
||||
```gherkin
|
||||
DADO que el usuario tiene cursos en progreso
|
||||
CUANDO accede al catálogo
|
||||
ENTONCES se muestra sección "Recomendado para ti"
|
||||
Y aparecen máximo 6 cursos relacionados
|
||||
Y se basa en: cursos en progreso, nivel del usuario, categorías de interés
|
||||
```
|
||||
|
||||
**Escenario 8: Ordenar resultados**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo el catálogo
|
||||
CUANDO selecciona ordenar por "Mejor valorados"
|
||||
ENTONCES los cursos se reordenan de mayor a menor rating
|
||||
Y la paginación se mantiene
|
||||
Y los filtros activos se mantienen
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Responsive design para móvil y tablet
|
||||
- [ ] Loading skeleton mientras cargan cursos
|
||||
- [ ] Infinite scroll opcional (además de paginación)
|
||||
- [ ] Animaciones suaves al filtrar
|
||||
- [ ] Badge "Nuevo" para cursos publicados hace < 30 días
|
||||
- [ ] Badge "Popular" para cursos con > 1000 estudiantes
|
||||
- [ ] Guardar filtros en localStorage para próxima visita
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-001: Verificar schema education.courses
|
||||
- [ ] DB-EDU-002: Verificar índices en category_id, level, published_at
|
||||
- [ ] DB-EDU-003: Vista courses_catalog con joins optimizados
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-001: Endpoint GET /education/courses (con paginación)
|
||||
- [ ] BE-EDU-002: Implementar filtros: category, level, duration, search
|
||||
- [ ] BE-EDU-003: Implementar ordenamiento: recent, popular, rating
|
||||
- [ ] BE-EDU-004: Endpoint GET /education/categories
|
||||
- [ ] BE-EDU-005: Implementar CourseService.getCatalog()
|
||||
- [ ] BE-EDU-006: Implementar lógica de recomendaciones
|
||||
- [ ] BE-EDU-007: Caché de catálogo en Redis (5 min)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-001: Crear página CoursesPage.tsx
|
||||
- [ ] FE-EDU-002: Crear componente CourseCard.tsx
|
||||
- [ ] FE-EDU-003: Crear componente CourseFilters.tsx
|
||||
- [ ] FE-EDU-004: Crear componente SearchBar.tsx
|
||||
- [ ] FE-EDU-005: Crear componente Pagination.tsx
|
||||
- [ ] FE-EDU-006: Implementar coursesStore (Zustand)
|
||||
- [ ] FE-EDU-007: Integrar con React Query para caché
|
||||
- [ ] FE-EDU-008: Skeleton loader para carga
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-001: Test unitario CourseService.getCatalog()
|
||||
- [ ] TEST-EDU-002: Test integración GET /courses con filtros
|
||||
- [ ] TEST-EDU-003: Test E2E búsqueda y filtrado
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-AUTH-001: Sistema de autenticación - Estado: ✅ Completado
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-002: Ver detalle de curso
|
||||
- [ ] US-EDU-003: Iniciar lección
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoints involucrados:**
|
||||
| Método | Endpoint | Descripción |
|
||||
|--------|----------|-------------|
|
||||
| GET | /education/courses | Catálogo con filtros y paginación |
|
||||
| GET | /education/categories | Listado de categorías |
|
||||
|
||||
**Query params para GET /courses:**
|
||||
```
|
||||
?page=1
|
||||
&limit=12
|
||||
&category=technical-analysis,fundamentals
|
||||
&level=beginner,intermediate
|
||||
&search=fibonacci
|
||||
&sortBy=recent
|
||||
&sortOrder=desc
|
||||
```
|
||||
|
||||
**Response GET /courses:**
|
||||
```typescript
|
||||
{
|
||||
courses: [
|
||||
{
|
||||
id: "uuid-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
shortDescription: "Aprende a usar Fibonacci...",
|
||||
thumbnail: "https://cdn.orbiquant.com/courses/fib.jpg",
|
||||
category: {
|
||||
id: "cat-1",
|
||||
name: "Análisis Técnico",
|
||||
slug: "technical-analysis",
|
||||
icon: "📊"
|
||||
},
|
||||
level: "beginner",
|
||||
duration: 150, // minutos
|
||||
moduleCount: 5,
|
||||
lessonCount: 23,
|
||||
studentCount: 1234,
|
||||
rating: 4.8,
|
||||
reviewCount: 89,
|
||||
instructor: {
|
||||
id: "inst-1",
|
||||
name: "Carlos Mendoza",
|
||||
avatar: "https://...",
|
||||
title: "Senior Trader"
|
||||
},
|
||||
isPremium: false,
|
||||
publishedAt: "2025-11-15T10:00:00Z",
|
||||
userProgress: {
|
||||
enrolledAt: "2025-12-01T14:30:00Z",
|
||||
progressPercent: 60,
|
||||
lastAccessedAt: "2025-12-04T18:20:00Z"
|
||||
}
|
||||
}
|
||||
// ... más cursos
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 12,
|
||||
total: 47,
|
||||
totalPages: 4
|
||||
},
|
||||
filters: {
|
||||
categories: [...],
|
||||
levels: ["beginner", "intermediate", "advanced", "expert"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.courses`: Catálogo de cursos
|
||||
- `education.categories`: Categorías
|
||||
- `education.course_enrollments`: Inscripciones y progreso
|
||||
- `education.instructors`: Información de instructores
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,364 +1,376 @@
|
||||
# US-EDU-002: Ver Detalle de Curso
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-002 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario interesado en un curso,
|
||||
**quiero** ver información detallada del curso antes de inscribirme,
|
||||
**para** evaluar si el contenido se ajusta a mis objetivos de aprendizaje.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a una página de detalle que muestre información completa del curso: descripción extendida, temario desglosado por módulos y lecciones, instructor, requisitos previos, objetivos de aprendizaje, reseñas de otros estudiantes, y botón de inscripción. Si el usuario ya está inscrito, debe mostrar su progreso y botón para continuar.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver al catálogo 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────────┐ FIBONACCI RETRACEMENT BÁSICO │
|
||||
│ │ │ ⭐ 4.8 (89 reseñas) | 1,234 estudiantes│
|
||||
│ │ [VIDEO INTRO] │ Por: Carlos Mendoza | 2h 30m | 5 módul│
|
||||
│ │ ▶ Preview │ │
|
||||
│ │ │ [📥 Inscribirse gratis] │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PESTAÑAS: [Descripción] [Temario] [Instructor] [Reseñas] │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ QUÉ APRENDERÁS │
|
||||
│ ✓ Identificar niveles de Fibonacci en gráficos │
|
||||
│ ✓ Aplicar retrocesos en tendencias alcistas y bajistas │
|
||||
│ ✓ Combinar Fibonacci con otros indicadores │
|
||||
│ ✓ Realizar entradas y salidas precisas │
|
||||
│ │
|
||||
│ REQUISITOS │
|
||||
│ • Conocimientos básicos de trading │
|
||||
│ • Familiaridad con gráficos de velas │
|
||||
│ │
|
||||
│ DESCRIPCIÓN │
|
||||
│ Fibonacci es una herramienta fundamental del análisis técnico...│
|
||||
│ [Texto completo de descripción del curso] │
|
||||
│ │
|
||||
│ PARA QUIÉN ES ESTE CURSO │
|
||||
│ • Traders principiantes que quieren dominar Fibonacci │
|
||||
│ • Analistas técnicos que buscan mejorar su precisión │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PESTAÑA TEMARIO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📚 TEMARIO DEL CURSO 5 módulos | 23 lecciones │
|
||||
│ │
|
||||
│ ▼ Módulo 1: Introducción a Fibonacci (30 min) │
|
||||
│ 1. ¿Qué es Fibonacci? (5:30) [🎬 Video] [Gratis] │
|
||||
│ 2. Historia y fundamentos (8:45) [🎬 Video] [Gratis] │
|
||||
│ 3. La secuencia de Fibonacci (10:20) [📄 Artículo] │
|
||||
│ 4. Quiz: Fundamentos (15 preguntas) [📝 Quiz] │
|
||||
│ │
|
||||
│ ▼ Módulo 2: Niveles de Retroceso (45 min) │
|
||||
│ 1. Niveles principales: 38.2%, 50%, 61.8% (12:15) │
|
||||
│ 2. Cómo dibujar Fibonacci en gráfico (18:30) │
|
||||
│ 3. Práctica: Identificar retrocesos (20:00) │
|
||||
│ 4. Quiz: Niveles de retroceso │
|
||||
│ │
|
||||
│ ▶ Módulo 3: Fibonacci en Tendencias (35 min) [🔒 Bloqueado] │
|
||||
│ ▶ Módulo 4: Estrategias Avanzadas (25 min) [🔒 Bloqueado] │
|
||||
│ ▶ Módulo 5: Fibonacci + Otros Indicadores (15 min) [🔒] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver detalle de curso no inscrito**
|
||||
```gherkin
|
||||
DADO que el usuario está autenticado
|
||||
Y NO está inscrito en el curso
|
||||
CUANDO accede a /education/courses/fibonacci-retracement-basico
|
||||
ENTONCES se muestra la página de detalle del curso
|
||||
Y se muestra: título, rating, estudiantes, instructor, duración
|
||||
Y se muestra botón "Inscribirse" o "Inscribirse gratis"
|
||||
Y se muestra video preview del curso (si existe)
|
||||
Y se muestra descripción completa
|
||||
Y se muestra temario desglosado por módulos
|
||||
```
|
||||
|
||||
**Escenario 2: Ver detalle de curso inscrito**
|
||||
```gherkin
|
||||
DADO que el usuario está inscrito en el curso
|
||||
Y tiene 60% de progreso
|
||||
CUANDO accede al detalle del curso
|
||||
ENTONCES se muestra barra de progreso "60% completado"
|
||||
Y el botón principal dice "Continuar curso"
|
||||
Y se destaca la última lección accedida
|
||||
Y se muestra resumen de progreso: "14 de 23 lecciones completadas"
|
||||
```
|
||||
|
||||
**Escenario 3: Ver temario completo**
|
||||
```gherkin
|
||||
DADO que el usuario está en la pestaña "Temario"
|
||||
ENTONCES se muestran todos los módulos del curso
|
||||
Y cada módulo muestra: título, duración total, número de lecciones
|
||||
Y cada lección muestra: título, duración, tipo (video/artículo/quiz)
|
||||
Y se puede expandir/colapsar cada módulo
|
||||
Y lecciones gratuitas están marcadas como "Gratis"
|
||||
Y lecciones bloqueadas muestran candado 🔒
|
||||
```
|
||||
|
||||
**Escenario 4: Preview de lección gratuita**
|
||||
```gherkin
|
||||
DADO que el usuario NO está inscrito
|
||||
Y el curso tiene lecciones marcadas como "Gratis"
|
||||
CUANDO hace click en una lección gratuita
|
||||
ENTONCES puede ver el contenido sin inscribirse
|
||||
Y se muestra CTA "Inscríbete para acceder a todo el contenido"
|
||||
```
|
||||
|
||||
**Escenario 5: Ver información del instructor**
|
||||
```gherkin
|
||||
DADO que el usuario está en la pestaña "Instructor"
|
||||
ENTONCES se muestra: foto, nombre, título, biografía
|
||||
Y se muestra lista de otros cursos del instructor
|
||||
Y se muestran estadísticas: total estudiantes, cursos, rating promedio
|
||||
Y se muestra botón "Ver perfil completo" (opcional)
|
||||
```
|
||||
|
||||
**Escenario 6: Ver reseñas de estudiantes**
|
||||
```gherkin
|
||||
DADO que el usuario está en la pestaña "Reseñas"
|
||||
ENTONCES se muestra rating promedio (ej: 4.8/5)
|
||||
Y se muestra distribución de ratings (5★: 60%, 4★: 30%, etc.)
|
||||
Y se muestran las últimas 10 reseñas
|
||||
Y cada reseña muestra: nombre del usuario, fecha, rating, comentario
|
||||
Y se puede filtrar por: Todas, 5★, 4★, 3★, 2★, 1★
|
||||
```
|
||||
|
||||
**Escenario 7: Inscribirse en curso**
|
||||
```gherkin
|
||||
DADO que el usuario NO está inscrito
|
||||
Y el curso es gratuito
|
||||
CUANDO hace click en "Inscribirse gratis"
|
||||
ENTONCES se registra la inscripción en backend
|
||||
Y se muestra toast "¡Te has inscrito en el curso!"
|
||||
Y el botón cambia a "Comenzar curso"
|
||||
Y se navega a la primera lección al hacer click
|
||||
```
|
||||
|
||||
**Escenario 8: Curso premium sin suscripción**
|
||||
```gherkin
|
||||
DADO que el curso es premium
|
||||
Y el usuario NO tiene suscripción activa
|
||||
CUANDO intenta inscribirse
|
||||
ENTONCES se muestra modal "Este curso requiere suscripción Premium"
|
||||
Y se muestra botón "Ver planes" que lleva a /pricing
|
||||
Y NO se permite inscripción
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Video preview auto-play al cargar página (muted)
|
||||
- [ ] Compartir curso en redes sociales
|
||||
- [ ] Agregar curso a "Guardados" (wishlist)
|
||||
- [ ] Mostrar badge "Bestseller" si es top 10 más vendido
|
||||
- [ ] Mostrar "Última actualización: DD/MM/YYYY"
|
||||
- [ ] SEO optimizado con meta tags dinámicos
|
||||
- [ ] Structured data (schema.org) para Google
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-004: Verificar relación courses -> modules -> lessons
|
||||
- [ ] DB-EDU-005: Tabla course_reviews para reseñas
|
||||
- [ ] DB-EDU-006: Tabla course_enrollments para inscripciones
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-008: Endpoint GET /education/courses/:slug
|
||||
- [ ] BE-EDU-009: Endpoint POST /education/courses/:id/enroll
|
||||
- [ ] BE-EDU-010: Endpoint GET /education/courses/:id/reviews
|
||||
- [ ] BE-EDU-011: Implementar CourseService.getBySlug()
|
||||
- [ ] BE-EDU-012: Implementar EnrollmentService.enroll()
|
||||
- [ ] BE-EDU-013: Verificar acceso Premium para cursos pagos
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-009: Crear página CourseDetailPage.tsx
|
||||
- [ ] FE-EDU-010: Crear componente CourseHeader.tsx
|
||||
- [ ] FE-EDU-011: Crear componente CourseSyllabus.tsx (temario)
|
||||
- [ ] FE-EDU-012: Crear componente InstructorCard.tsx
|
||||
- [ ] FE-EDU-013: Crear componente CourseReviews.tsx
|
||||
- [ ] FE-EDU-014: Crear componente EnrollButton.tsx
|
||||
- [ ] FE-EDU-015: Implementar tabs de navegación
|
||||
- [ ] FE-EDU-016: Modal de confirmación de inscripción
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-004: Test inscripción en curso gratuito
|
||||
- [ ] TEST-EDU-005: Test bloqueo de curso premium
|
||||
- [ ] TEST-EDU-006: Test E2E ver detalle e inscribirse
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-001: Ver catálogo - Estado: Pendiente
|
||||
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-003: Iniciar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoints involucrados:**
|
||||
| Método | Endpoint | Descripción |
|
||||
|--------|----------|-------------|
|
||||
| GET | /education/courses/:slug | Detalle completo del curso |
|
||||
| POST | /education/courses/:id/enroll | Inscribirse en curso |
|
||||
| GET | /education/courses/:id/reviews | Reseñas del curso |
|
||||
|
||||
**Response GET /courses/:slug:**
|
||||
```typescript
|
||||
{
|
||||
course: {
|
||||
id: "uuid-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
description: "Descripción completa del curso...",
|
||||
learningObjectives: [
|
||||
"Identificar niveles de Fibonacci",
|
||||
"Aplicar retrocesos en tendencias"
|
||||
],
|
||||
requirements: [
|
||||
"Conocimientos básicos de trading",
|
||||
"Familiaridad con gráficos"
|
||||
],
|
||||
targetAudience: [
|
||||
"Traders principiantes",
|
||||
"Analistas técnicos"
|
||||
],
|
||||
category: {...},
|
||||
level: "beginner",
|
||||
duration: 150,
|
||||
moduleCount: 5,
|
||||
lessonCount: 23,
|
||||
studentCount: 1234,
|
||||
rating: 4.8,
|
||||
reviewCount: 89,
|
||||
instructor: {
|
||||
id: "inst-1",
|
||||
name: "Carlos Mendoza",
|
||||
avatar: "...",
|
||||
title: "Senior Trader",
|
||||
bio: "15 años de experiencia...",
|
||||
coursesCount: 8,
|
||||
studentsCount: 12500,
|
||||
rating: 4.9
|
||||
},
|
||||
isPremium: false,
|
||||
previewVideoUrl: "https://vimeo.com/...",
|
||||
updatedAt: "2025-11-20T10:00:00Z",
|
||||
publishedAt: "2025-11-15T10:00:00Z",
|
||||
|
||||
modules: [
|
||||
{
|
||||
id: "mod-1",
|
||||
title: "Introducción a Fibonacci",
|
||||
order: 1,
|
||||
duration: 30,
|
||||
lessonCount: 4,
|
||||
lessons: [
|
||||
{
|
||||
id: "les-1",
|
||||
title: "¿Qué es Fibonacci?",
|
||||
type: "video",
|
||||
duration: 5.5,
|
||||
isFree: true,
|
||||
isCompleted: false,
|
||||
order: 1
|
||||
}
|
||||
// ... más lecciones
|
||||
]
|
||||
}
|
||||
// ... más módulos
|
||||
],
|
||||
|
||||
userEnrollment: {
|
||||
enrolledAt: "2025-12-01T14:30:00Z",
|
||||
progressPercent: 60,
|
||||
lessonsCompleted: 14,
|
||||
lastAccessedLesson: {
|
||||
id: "les-14",
|
||||
title: "Fibonacci en tendencias bajistas"
|
||||
},
|
||||
lastAccessedAt: "2025-12-04T18:20:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.courses`
|
||||
- `education.modules`
|
||||
- `education.lessons`
|
||||
- `education.course_enrollments`
|
||||
- `education.course_reviews`
|
||||
- `education.instructors`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-002"
|
||||
title: "Ver Detalle de Curso"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-002"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-002: Ver Detalle de Curso
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-002 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario interesado en un curso,
|
||||
**quiero** ver información detallada del curso antes de inscribirme,
|
||||
**para** evaluar si el contenido se ajusta a mis objetivos de aprendizaje.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a una página de detalle que muestre información completa del curso: descripción extendida, temario desglosado por módulos y lecciones, instructor, requisitos previos, objetivos de aprendizaje, reseñas de otros estudiantes, y botón de inscripción. Si el usuario ya está inscrito, debe mostrar su progreso y botón para continuar.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver al catálogo 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────────┐ FIBONACCI RETRACEMENT BÁSICO │
|
||||
│ │ │ ⭐ 4.8 (89 reseñas) | 1,234 estudiantes│
|
||||
│ │ [VIDEO INTRO] │ Por: Carlos Mendoza | 2h 30m | 5 módul│
|
||||
│ │ ▶ Preview │ │
|
||||
│ │ │ [📥 Inscribirse gratis] │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PESTAÑAS: [Descripción] [Temario] [Instructor] [Reseñas] │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ QUÉ APRENDERÁS │
|
||||
│ ✓ Identificar niveles de Fibonacci en gráficos │
|
||||
│ ✓ Aplicar retrocesos en tendencias alcistas y bajistas │
|
||||
│ ✓ Combinar Fibonacci con otros indicadores │
|
||||
│ ✓ Realizar entradas y salidas precisas │
|
||||
│ │
|
||||
│ REQUISITOS │
|
||||
│ • Conocimientos básicos de trading │
|
||||
│ • Familiaridad con gráficos de velas │
|
||||
│ │
|
||||
│ DESCRIPCIÓN │
|
||||
│ Fibonacci es una herramienta fundamental del análisis técnico...│
|
||||
│ [Texto completo de descripción del curso] │
|
||||
│ │
|
||||
│ PARA QUIÉN ES ESTE CURSO │
|
||||
│ • Traders principiantes que quieren dominar Fibonacci │
|
||||
│ • Analistas técnicos que buscan mejorar su precisión │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PESTAÑA TEMARIO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📚 TEMARIO DEL CURSO 5 módulos | 23 lecciones │
|
||||
│ │
|
||||
│ ▼ Módulo 1: Introducción a Fibonacci (30 min) │
|
||||
│ 1. ¿Qué es Fibonacci? (5:30) [🎬 Video] [Gratis] │
|
||||
│ 2. Historia y fundamentos (8:45) [🎬 Video] [Gratis] │
|
||||
│ 3. La secuencia de Fibonacci (10:20) [📄 Artículo] │
|
||||
│ 4. Quiz: Fundamentos (15 preguntas) [📝 Quiz] │
|
||||
│ │
|
||||
│ ▼ Módulo 2: Niveles de Retroceso (45 min) │
|
||||
│ 1. Niveles principales: 38.2%, 50%, 61.8% (12:15) │
|
||||
│ 2. Cómo dibujar Fibonacci en gráfico (18:30) │
|
||||
│ 3. Práctica: Identificar retrocesos (20:00) │
|
||||
│ 4. Quiz: Niveles de retroceso │
|
||||
│ │
|
||||
│ ▶ Módulo 3: Fibonacci en Tendencias (35 min) [🔒 Bloqueado] │
|
||||
│ ▶ Módulo 4: Estrategias Avanzadas (25 min) [🔒 Bloqueado] │
|
||||
│ ▶ Módulo 5: Fibonacci + Otros Indicadores (15 min) [🔒] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver detalle de curso no inscrito**
|
||||
```gherkin
|
||||
DADO que el usuario está autenticado
|
||||
Y NO está inscrito en el curso
|
||||
CUANDO accede a /education/courses/fibonacci-retracement-basico
|
||||
ENTONCES se muestra la página de detalle del curso
|
||||
Y se muestra: título, rating, estudiantes, instructor, duración
|
||||
Y se muestra botón "Inscribirse" o "Inscribirse gratis"
|
||||
Y se muestra video preview del curso (si existe)
|
||||
Y se muestra descripción completa
|
||||
Y se muestra temario desglosado por módulos
|
||||
```
|
||||
|
||||
**Escenario 2: Ver detalle de curso inscrito**
|
||||
```gherkin
|
||||
DADO que el usuario está inscrito en el curso
|
||||
Y tiene 60% de progreso
|
||||
CUANDO accede al detalle del curso
|
||||
ENTONCES se muestra barra de progreso "60% completado"
|
||||
Y el botón principal dice "Continuar curso"
|
||||
Y se destaca la última lección accedida
|
||||
Y se muestra resumen de progreso: "14 de 23 lecciones completadas"
|
||||
```
|
||||
|
||||
**Escenario 3: Ver temario completo**
|
||||
```gherkin
|
||||
DADO que el usuario está en la pestaña "Temario"
|
||||
ENTONCES se muestran todos los módulos del curso
|
||||
Y cada módulo muestra: título, duración total, número de lecciones
|
||||
Y cada lección muestra: título, duración, tipo (video/artículo/quiz)
|
||||
Y se puede expandir/colapsar cada módulo
|
||||
Y lecciones gratuitas están marcadas como "Gratis"
|
||||
Y lecciones bloqueadas muestran candado 🔒
|
||||
```
|
||||
|
||||
**Escenario 4: Preview de lección gratuita**
|
||||
```gherkin
|
||||
DADO que el usuario NO está inscrito
|
||||
Y el curso tiene lecciones marcadas como "Gratis"
|
||||
CUANDO hace click en una lección gratuita
|
||||
ENTONCES puede ver el contenido sin inscribirse
|
||||
Y se muestra CTA "Inscríbete para acceder a todo el contenido"
|
||||
```
|
||||
|
||||
**Escenario 5: Ver información del instructor**
|
||||
```gherkin
|
||||
DADO que el usuario está en la pestaña "Instructor"
|
||||
ENTONCES se muestra: foto, nombre, título, biografía
|
||||
Y se muestra lista de otros cursos del instructor
|
||||
Y se muestran estadísticas: total estudiantes, cursos, rating promedio
|
||||
Y se muestra botón "Ver perfil completo" (opcional)
|
||||
```
|
||||
|
||||
**Escenario 6: Ver reseñas de estudiantes**
|
||||
```gherkin
|
||||
DADO que el usuario está en la pestaña "Reseñas"
|
||||
ENTONCES se muestra rating promedio (ej: 4.8/5)
|
||||
Y se muestra distribución de ratings (5★: 60%, 4★: 30%, etc.)
|
||||
Y se muestran las últimas 10 reseñas
|
||||
Y cada reseña muestra: nombre del usuario, fecha, rating, comentario
|
||||
Y se puede filtrar por: Todas, 5★, 4★, 3★, 2★, 1★
|
||||
```
|
||||
|
||||
**Escenario 7: Inscribirse en curso**
|
||||
```gherkin
|
||||
DADO que el usuario NO está inscrito
|
||||
Y el curso es gratuito
|
||||
CUANDO hace click en "Inscribirse gratis"
|
||||
ENTONCES se registra la inscripción en backend
|
||||
Y se muestra toast "¡Te has inscrito en el curso!"
|
||||
Y el botón cambia a "Comenzar curso"
|
||||
Y se navega a la primera lección al hacer click
|
||||
```
|
||||
|
||||
**Escenario 8: Curso premium sin suscripción**
|
||||
```gherkin
|
||||
DADO que el curso es premium
|
||||
Y el usuario NO tiene suscripción activa
|
||||
CUANDO intenta inscribirse
|
||||
ENTONCES se muestra modal "Este curso requiere suscripción Premium"
|
||||
Y se muestra botón "Ver planes" que lleva a /pricing
|
||||
Y NO se permite inscripción
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Video preview auto-play al cargar página (muted)
|
||||
- [ ] Compartir curso en redes sociales
|
||||
- [ ] Agregar curso a "Guardados" (wishlist)
|
||||
- [ ] Mostrar badge "Bestseller" si es top 10 más vendido
|
||||
- [ ] Mostrar "Última actualización: DD/MM/YYYY"
|
||||
- [ ] SEO optimizado con meta tags dinámicos
|
||||
- [ ] Structured data (schema.org) para Google
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-004: Verificar relación courses -> modules -> lessons
|
||||
- [ ] DB-EDU-005: Tabla course_reviews para reseñas
|
||||
- [ ] DB-EDU-006: Tabla course_enrollments para inscripciones
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-008: Endpoint GET /education/courses/:slug
|
||||
- [ ] BE-EDU-009: Endpoint POST /education/courses/:id/enroll
|
||||
- [ ] BE-EDU-010: Endpoint GET /education/courses/:id/reviews
|
||||
- [ ] BE-EDU-011: Implementar CourseService.getBySlug()
|
||||
- [ ] BE-EDU-012: Implementar EnrollmentService.enroll()
|
||||
- [ ] BE-EDU-013: Verificar acceso Premium para cursos pagos
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-009: Crear página CourseDetailPage.tsx
|
||||
- [ ] FE-EDU-010: Crear componente CourseHeader.tsx
|
||||
- [ ] FE-EDU-011: Crear componente CourseSyllabus.tsx (temario)
|
||||
- [ ] FE-EDU-012: Crear componente InstructorCard.tsx
|
||||
- [ ] FE-EDU-013: Crear componente CourseReviews.tsx
|
||||
- [ ] FE-EDU-014: Crear componente EnrollButton.tsx
|
||||
- [ ] FE-EDU-015: Implementar tabs de navegación
|
||||
- [ ] FE-EDU-016: Modal de confirmación de inscripción
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-004: Test inscripción en curso gratuito
|
||||
- [ ] TEST-EDU-005: Test bloqueo de curso premium
|
||||
- [ ] TEST-EDU-006: Test E2E ver detalle e inscribirse
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-001: Ver catálogo - Estado: Pendiente
|
||||
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-003: Iniciar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoints involucrados:**
|
||||
| Método | Endpoint | Descripción |
|
||||
|--------|----------|-------------|
|
||||
| GET | /education/courses/:slug | Detalle completo del curso |
|
||||
| POST | /education/courses/:id/enroll | Inscribirse en curso |
|
||||
| GET | /education/courses/:id/reviews | Reseñas del curso |
|
||||
|
||||
**Response GET /courses/:slug:**
|
||||
```typescript
|
||||
{
|
||||
course: {
|
||||
id: "uuid-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
description: "Descripción completa del curso...",
|
||||
learningObjectives: [
|
||||
"Identificar niveles de Fibonacci",
|
||||
"Aplicar retrocesos en tendencias"
|
||||
],
|
||||
requirements: [
|
||||
"Conocimientos básicos de trading",
|
||||
"Familiaridad con gráficos"
|
||||
],
|
||||
targetAudience: [
|
||||
"Traders principiantes",
|
||||
"Analistas técnicos"
|
||||
],
|
||||
category: {...},
|
||||
level: "beginner",
|
||||
duration: 150,
|
||||
moduleCount: 5,
|
||||
lessonCount: 23,
|
||||
studentCount: 1234,
|
||||
rating: 4.8,
|
||||
reviewCount: 89,
|
||||
instructor: {
|
||||
id: "inst-1",
|
||||
name: "Carlos Mendoza",
|
||||
avatar: "...",
|
||||
title: "Senior Trader",
|
||||
bio: "15 años de experiencia...",
|
||||
coursesCount: 8,
|
||||
studentsCount: 12500,
|
||||
rating: 4.9
|
||||
},
|
||||
isPremium: false,
|
||||
previewVideoUrl: "https://vimeo.com/...",
|
||||
updatedAt: "2025-11-20T10:00:00Z",
|
||||
publishedAt: "2025-11-15T10:00:00Z",
|
||||
|
||||
modules: [
|
||||
{
|
||||
id: "mod-1",
|
||||
title: "Introducción a Fibonacci",
|
||||
order: 1,
|
||||
duration: 30,
|
||||
lessonCount: 4,
|
||||
lessons: [
|
||||
{
|
||||
id: "les-1",
|
||||
title: "¿Qué es Fibonacci?",
|
||||
type: "video",
|
||||
duration: 5.5,
|
||||
isFree: true,
|
||||
isCompleted: false,
|
||||
order: 1
|
||||
}
|
||||
// ... más lecciones
|
||||
]
|
||||
}
|
||||
// ... más módulos
|
||||
],
|
||||
|
||||
userEnrollment: {
|
||||
enrolledAt: "2025-12-01T14:30:00Z",
|
||||
progressPercent: 60,
|
||||
lessonsCompleted: 14,
|
||||
lastAccessedLesson: {
|
||||
id: "les-14",
|
||||
title: "Fibonacci en tendencias bajistas"
|
||||
},
|
||||
lastAccessedAt: "2025-12-04T18:20:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.courses`
|
||||
- `education.modules`
|
||||
- `education.lessons`
|
||||
- `education.course_enrollments`
|
||||
- `education.course_reviews`
|
||||
- `education.instructors`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,360 +1,372 @@
|
||||
# US-EDU-003: Iniciar Lección
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-003 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario inscrito en un curso,
|
||||
**quiero** acceder y comenzar una lección específica,
|
||||
**para** consumir el contenido educativo y avanzar en mi aprendizaje.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder hacer click en una lección desde el temario del curso y acceder a la página de reproducción/visualización de la lección. El sistema debe validar que el usuario está inscrito, verificar si la lección está desbloqueada (según orden secuencial si aplica), cargar el contenido apropiado según el tipo de lección, y registrar que el usuario inició la lección.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver al curso [≡ Temario] 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Módulo 2: Niveles de Retroceso │
|
||||
│ Lección 1/4: Niveles principales (38.2%, 50%, 61.8%) │
|
||||
│ Progreso del curso: ████████░░░░░░░░ 45% │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ [REPRODUCTOR DE VIDEO] │ │
|
||||
│ │ │ │
|
||||
│ │ ▶ PLAY │ │
|
||||
│ │ │ │
|
||||
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12:15 │ │
|
||||
│ │ ⏮ ⏸ ⏭ 🔊──── ⚙ 1x CC ⛶ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ PESTAÑAS ────────────────────────────────────────────────┐ │
|
||||
│ │ [Descripción] [Recursos] [Mis Notas] [Q&A] │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 📝 DESCRIPCIÓN │
|
||||
│ En esta lección aprenderás los tres niveles principales de │
|
||||
│ Fibonacci: 38.2%, 50% y 61.8%. Veremos cómo identificarlos... │
|
||||
│ │
|
||||
│ 📥 RECURSOS (2) │
|
||||
│ • Plantilla de Fibonacci.xlsx (45 KB) [Descargar] │
|
||||
│ • Cheatsheet de niveles.pdf (120 KB) [Descargar] │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [← Anterior: Historia y fundamentos] │ │
|
||||
│ │ [Siguiente: Cómo dibujar Fibonacci →] │ │
|
||||
│ │ [✓ Marcar como completada] │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[SIDEBAR TEMARIO - Desplegable]
|
||||
┌────────────────────────┐
|
||||
│ TEMARIO │
|
||||
│ ✓ Módulo 1 (4/4) ✓ │
|
||||
│ ▼ Módulo 2 (1/4) │
|
||||
│ ✓ 1. Niveles princ. │ ← Lección actual
|
||||
│ ○ 2. Dibujar Fib. │
|
||||
│ 🔒 3. Práctica │
|
||||
│ 🔒 4. Quiz │
|
||||
│ 🔒 Módulo 3 │
|
||||
│ 🔒 Módulo 4 │
|
||||
│ 🔒 Módulo 5 │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Iniciar lección desbloqueada**
|
||||
```gherkin
|
||||
DADO que el usuario está inscrito en el curso
|
||||
Y la lección está desbloqueada
|
||||
CUANDO hace click en la lección desde el temario
|
||||
ENTONCES se navega a /education/courses/:slug/lessons/:lessonSlug
|
||||
Y se carga el contenido de la lección
|
||||
Y se registra en backend que el usuario inició la lección
|
||||
Y se actualiza "última lección accedida"
|
||||
Y se muestra sidebar con temario completo
|
||||
```
|
||||
|
||||
**Escenario 2: Intentar acceder a lección bloqueada**
|
||||
```gherkin
|
||||
DADO que el curso requiere orden secuencial
|
||||
Y el usuario NO ha completado la lección anterior
|
||||
CUANDO intenta acceder a una lección bloqueada
|
||||
ENTONCES se muestra modal "Debes completar lecciones anteriores"
|
||||
Y se sugiere la última lección disponible
|
||||
Y NO se carga el contenido
|
||||
```
|
||||
|
||||
**Escenario 3: Lección de video carga posición guardada**
|
||||
```gherkin
|
||||
DADO que el usuario vio 5:30 de un video de 12:15
|
||||
Y cerró la lección sin terminar
|
||||
CUANDO vuelve a abrir la lección
|
||||
ENTONCES el video se posiciona en 5:30
|
||||
Y se muestra toast "Continuando desde 5:30"
|
||||
Y se puede resetear posición si el usuario lo desea
|
||||
```
|
||||
|
||||
**Escenario 4: Usuario no inscrito intenta acceder**
|
||||
```gherkin
|
||||
DADO que el usuario NO está inscrito en el curso
|
||||
CUANDO intenta acceder directamente a una lección
|
||||
ENTONCES se redirige a la página del curso
|
||||
Y se muestra mensaje "Inscríbete para acceder a este curso"
|
||||
Y se muestra botón "Inscribirse"
|
||||
```
|
||||
|
||||
**Escenario 5: Ver recursos de la lección**
|
||||
```gherkin
|
||||
DADO que la lección tiene recursos descargables
|
||||
CUANDO el usuario accede a la pestaña "Recursos"
|
||||
ENTONCES se muestran todos los archivos disponibles
|
||||
Y se muestra: nombre, tipo, tamaño
|
||||
Y puede descargar cada archivo con un click
|
||||
Y se registra la descarga en analytics
|
||||
```
|
||||
|
||||
**Escenario 6: Tomar notas durante lección**
|
||||
```gherkin
|
||||
DADO que el usuario está en una lección
|
||||
CUANDO accede a la pestaña "Mis Notas"
|
||||
Y escribe texto en el editor
|
||||
ENTONCES las notas se guardan automáticamente cada 2s
|
||||
Y se muestra indicador "Guardado" cuando se persiste
|
||||
Y para videos se guarda el timestamp actual
|
||||
```
|
||||
|
||||
**Escenario 7: Navegar entre lecciones**
|
||||
```gherkin
|
||||
DADO que el usuario está en una lección
|
||||
Y existe una lección siguiente
|
||||
CUANDO hace click en "Siguiente"
|
||||
ENTONCES navega a la siguiente lección del módulo
|
||||
Y se carga el nuevo contenido
|
||||
Y se actualiza el sidebar
|
||||
```
|
||||
|
||||
**Escenario 8: Marcar lección como completada**
|
||||
```gherkin
|
||||
DADO que el usuario vio la lección completa
|
||||
CUANDO hace click en "Marcar como completada"
|
||||
ENTONCES se actualiza el progreso en backend
|
||||
Y aparece checkmark ✓ en el sidebar
|
||||
Y se actualiza la barra de progreso del curso
|
||||
Y si es secuencial, se desbloquea siguiente lección
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Auto-save de progreso de video cada 10s
|
||||
- [ ] Atajos de teclado: Espacio=play/pause, →=+10s, ←=-10s
|
||||
- [ ] Picture-in-Picture para videos
|
||||
- [ ] Modo cine (ocultar sidebar)
|
||||
- [ ] Velocidad de reproducción: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
|
||||
- [ ] Subtítulos si están disponibles
|
||||
- [ ] Analytics: tiempo en lección, pausas, rewinds
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-007: Tabla user_lesson_progress (started_at, completed_at, last_position)
|
||||
- [ ] DB-EDU-008: Tabla user_notes (lesson_id, content, timestamp)
|
||||
- [ ] DB-EDU-009: Índice en user_id + lesson_id para queries rápidas
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-014: Endpoint GET /education/courses/:id/lessons/:lessonId
|
||||
- [ ] BE-EDU-015: Validar inscripción y acceso a lección
|
||||
- [ ] BE-EDU-016: Endpoint POST /education/lessons/:id/progress (guardar posición)
|
||||
- [ ] BE-EDU-017: Endpoint POST /education/lessons/:id/complete
|
||||
- [ ] BE-EDU-018: Endpoint GET/POST /education/lessons/:id/notes
|
||||
- [ ] BE-EDU-019: Endpoint GET /education/lessons/:id/resources/:resourceId
|
||||
- [ ] BE-EDU-020: Implementar signed URLs para videos privados
|
||||
- [ ] BE-EDU-021: Verificar orden secuencial si aplica
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-017: Crear LessonPlayerPage.tsx
|
||||
- [ ] FE-EDU-018: Crear componente VideoPlayer.tsx
|
||||
- [ ] FE-EDU-019: Crear componente LessonSidebar.tsx
|
||||
- [ ] FE-EDU-020: Crear componente NotesEditor.tsx
|
||||
- [ ] FE-EDU-021: Crear componente ResourcesList.tsx
|
||||
- [ ] FE-EDU-022: Implementar auto-save de posición (cada 10s)
|
||||
- [ ] FE-EDU-023: Implementar lessonStore (Zustand)
|
||||
- [ ] FE-EDU-024: Navegación con teclas de flecha
|
||||
- [ ] FE-EDU-025: Toast notifications para acciones
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-007: Test validación de acceso a lección
|
||||
- [ ] TEST-EDU-008: Test bloqueo de lección secuencial
|
||||
- [ ] TEST-EDU-009: Test guardar y restaurar posición de video
|
||||
- [ ] TEST-EDU-010: Test E2E iniciar lección y marcar completada
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-002: Ver detalle de curso - Estado: Pendiente
|
||||
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-004: Ver video completo
|
||||
- [ ] US-EDU-005: Completar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoints involucrados:**
|
||||
| Método | Endpoint | Descripción |
|
||||
|--------|----------|-------------|
|
||||
| GET | /education/courses/:courseId/lessons/:lessonId | Obtener lección |
|
||||
| POST | /education/lessons/:id/progress | Guardar posición |
|
||||
| POST | /education/lessons/:id/complete | Marcar completada |
|
||||
| GET | /education/lessons/:id/notes | Obtener notas |
|
||||
| POST | /education/lessons/:id/notes | Crear/actualizar notas |
|
||||
|
||||
**Response GET /lessons/:id:**
|
||||
```typescript
|
||||
{
|
||||
lesson: {
|
||||
id: "les-5",
|
||||
courseId: "course-1",
|
||||
moduleId: "mod-2",
|
||||
title: "Niveles principales: 38.2%, 50%, 61.8%",
|
||||
slug: "niveles-principales",
|
||||
description: "En esta lección aprenderás...",
|
||||
type: "video",
|
||||
duration: 12.25, // minutos
|
||||
order: 1,
|
||||
isFree: false,
|
||||
|
||||
// Video específico
|
||||
videoUrl: "https://vimeo.com/signed-url-12345",
|
||||
videoProvider: "vimeo",
|
||||
subtitles: [
|
||||
{ language: "es", url: "..." },
|
||||
{ language: "en", url: "..." }
|
||||
],
|
||||
|
||||
resources: [
|
||||
{
|
||||
id: "res-1",
|
||||
name: "Plantilla de Fibonacci.xlsx",
|
||||
type: "application/vnd.ms-excel",
|
||||
size: 45120,
|
||||
url: "https://s3.../signed-url"
|
||||
}
|
||||
],
|
||||
|
||||
userProgress: {
|
||||
startedAt: "2025-12-04T10:30:00Z",
|
||||
completedAt: null,
|
||||
lastPosition: 5.5, // minutos
|
||||
timeSpent: 320, // segundos
|
||||
isCompleted: false
|
||||
},
|
||||
|
||||
navigation: {
|
||||
previous: {
|
||||
id: "les-4",
|
||||
title: "Historia y fundamentos",
|
||||
slug: "historia-fundamentos"
|
||||
},
|
||||
next: {
|
||||
id: "les-6",
|
||||
title: "Cómo dibujar Fibonacci",
|
||||
slug: "como-dibujar-fibonacci",
|
||||
isLocked: false
|
||||
}
|
||||
},
|
||||
|
||||
module: {
|
||||
id: "mod-2",
|
||||
title: "Niveles de Retroceso",
|
||||
lessonsCompleted: 1,
|
||||
totalLessons: 4
|
||||
},
|
||||
|
||||
course: {
|
||||
id: "course-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
progressPercent: 45,
|
||||
isSequential: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Validaciones backend:**
|
||||
1. Usuario autenticado
|
||||
2. Usuario inscrito en el curso
|
||||
3. Si curso es secuencial, validar lecciones anteriores completadas
|
||||
4. Si curso es premium, validar suscripción activa
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.lessons`
|
||||
- `education.user_lesson_progress`
|
||||
- `education.user_notes`
|
||||
- `education.lesson_resources`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-003"
|
||||
title: "Iniciar Leccion"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-002"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-003: Iniciar Lección
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-003 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario inscrito en un curso,
|
||||
**quiero** acceder y comenzar una lección específica,
|
||||
**para** consumir el contenido educativo y avanzar en mi aprendizaje.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder hacer click en una lección desde el temario del curso y acceder a la página de reproducción/visualización de la lección. El sistema debe validar que el usuario está inscrito, verificar si la lección está desbloqueada (según orden secuencial si aplica), cargar el contenido apropiado según el tipo de lección, y registrar que el usuario inició la lección.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver al curso [≡ Temario] 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Módulo 2: Niveles de Retroceso │
|
||||
│ Lección 1/4: Niveles principales (38.2%, 50%, 61.8%) │
|
||||
│ Progreso del curso: ████████░░░░░░░░ 45% │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ [REPRODUCTOR DE VIDEO] │ │
|
||||
│ │ │ │
|
||||
│ │ ▶ PLAY │ │
|
||||
│ │ │ │
|
||||
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12:15 │ │
|
||||
│ │ ⏮ ⏸ ⏭ 🔊──── ⚙ 1x CC ⛶ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ PESTAÑAS ────────────────────────────────────────────────┐ │
|
||||
│ │ [Descripción] [Recursos] [Mis Notas] [Q&A] │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 📝 DESCRIPCIÓN │
|
||||
│ En esta lección aprenderás los tres niveles principales de │
|
||||
│ Fibonacci: 38.2%, 50% y 61.8%. Veremos cómo identificarlos... │
|
||||
│ │
|
||||
│ 📥 RECURSOS (2) │
|
||||
│ • Plantilla de Fibonacci.xlsx (45 KB) [Descargar] │
|
||||
│ • Cheatsheet de niveles.pdf (120 KB) [Descargar] │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [← Anterior: Historia y fundamentos] │ │
|
||||
│ │ [Siguiente: Cómo dibujar Fibonacci →] │ │
|
||||
│ │ [✓ Marcar como completada] │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[SIDEBAR TEMARIO - Desplegable]
|
||||
┌────────────────────────┐
|
||||
│ TEMARIO │
|
||||
│ ✓ Módulo 1 (4/4) ✓ │
|
||||
│ ▼ Módulo 2 (1/4) │
|
||||
│ ✓ 1. Niveles princ. │ ← Lección actual
|
||||
│ ○ 2. Dibujar Fib. │
|
||||
│ 🔒 3. Práctica │
|
||||
│ 🔒 4. Quiz │
|
||||
│ 🔒 Módulo 3 │
|
||||
│ 🔒 Módulo 4 │
|
||||
│ 🔒 Módulo 5 │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Iniciar lección desbloqueada**
|
||||
```gherkin
|
||||
DADO que el usuario está inscrito en el curso
|
||||
Y la lección está desbloqueada
|
||||
CUANDO hace click en la lección desde el temario
|
||||
ENTONCES se navega a /education/courses/:slug/lessons/:lessonSlug
|
||||
Y se carga el contenido de la lección
|
||||
Y se registra en backend que el usuario inició la lección
|
||||
Y se actualiza "última lección accedida"
|
||||
Y se muestra sidebar con temario completo
|
||||
```
|
||||
|
||||
**Escenario 2: Intentar acceder a lección bloqueada**
|
||||
```gherkin
|
||||
DADO que el curso requiere orden secuencial
|
||||
Y el usuario NO ha completado la lección anterior
|
||||
CUANDO intenta acceder a una lección bloqueada
|
||||
ENTONCES se muestra modal "Debes completar lecciones anteriores"
|
||||
Y se sugiere la última lección disponible
|
||||
Y NO se carga el contenido
|
||||
```
|
||||
|
||||
**Escenario 3: Lección de video carga posición guardada**
|
||||
```gherkin
|
||||
DADO que el usuario vio 5:30 de un video de 12:15
|
||||
Y cerró la lección sin terminar
|
||||
CUANDO vuelve a abrir la lección
|
||||
ENTONCES el video se posiciona en 5:30
|
||||
Y se muestra toast "Continuando desde 5:30"
|
||||
Y se puede resetear posición si el usuario lo desea
|
||||
```
|
||||
|
||||
**Escenario 4: Usuario no inscrito intenta acceder**
|
||||
```gherkin
|
||||
DADO que el usuario NO está inscrito en el curso
|
||||
CUANDO intenta acceder directamente a una lección
|
||||
ENTONCES se redirige a la página del curso
|
||||
Y se muestra mensaje "Inscríbete para acceder a este curso"
|
||||
Y se muestra botón "Inscribirse"
|
||||
```
|
||||
|
||||
**Escenario 5: Ver recursos de la lección**
|
||||
```gherkin
|
||||
DADO que la lección tiene recursos descargables
|
||||
CUANDO el usuario accede a la pestaña "Recursos"
|
||||
ENTONCES se muestran todos los archivos disponibles
|
||||
Y se muestra: nombre, tipo, tamaño
|
||||
Y puede descargar cada archivo con un click
|
||||
Y se registra la descarga en analytics
|
||||
```
|
||||
|
||||
**Escenario 6: Tomar notas durante lección**
|
||||
```gherkin
|
||||
DADO que el usuario está en una lección
|
||||
CUANDO accede a la pestaña "Mis Notas"
|
||||
Y escribe texto en el editor
|
||||
ENTONCES las notas se guardan automáticamente cada 2s
|
||||
Y se muestra indicador "Guardado" cuando se persiste
|
||||
Y para videos se guarda el timestamp actual
|
||||
```
|
||||
|
||||
**Escenario 7: Navegar entre lecciones**
|
||||
```gherkin
|
||||
DADO que el usuario está en una lección
|
||||
Y existe una lección siguiente
|
||||
CUANDO hace click en "Siguiente"
|
||||
ENTONCES navega a la siguiente lección del módulo
|
||||
Y se carga el nuevo contenido
|
||||
Y se actualiza el sidebar
|
||||
```
|
||||
|
||||
**Escenario 8: Marcar lección como completada**
|
||||
```gherkin
|
||||
DADO que el usuario vio la lección completa
|
||||
CUANDO hace click en "Marcar como completada"
|
||||
ENTONCES se actualiza el progreso en backend
|
||||
Y aparece checkmark ✓ en el sidebar
|
||||
Y se actualiza la barra de progreso del curso
|
||||
Y si es secuencial, se desbloquea siguiente lección
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Auto-save de progreso de video cada 10s
|
||||
- [ ] Atajos de teclado: Espacio=play/pause, →=+10s, ←=-10s
|
||||
- [ ] Picture-in-Picture para videos
|
||||
- [ ] Modo cine (ocultar sidebar)
|
||||
- [ ] Velocidad de reproducción: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
|
||||
- [ ] Subtítulos si están disponibles
|
||||
- [ ] Analytics: tiempo en lección, pausas, rewinds
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-007: Tabla user_lesson_progress (started_at, completed_at, last_position)
|
||||
- [ ] DB-EDU-008: Tabla user_notes (lesson_id, content, timestamp)
|
||||
- [ ] DB-EDU-009: Índice en user_id + lesson_id para queries rápidas
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-014: Endpoint GET /education/courses/:id/lessons/:lessonId
|
||||
- [ ] BE-EDU-015: Validar inscripción y acceso a lección
|
||||
- [ ] BE-EDU-016: Endpoint POST /education/lessons/:id/progress (guardar posición)
|
||||
- [ ] BE-EDU-017: Endpoint POST /education/lessons/:id/complete
|
||||
- [ ] BE-EDU-018: Endpoint GET/POST /education/lessons/:id/notes
|
||||
- [ ] BE-EDU-019: Endpoint GET /education/lessons/:id/resources/:resourceId
|
||||
- [ ] BE-EDU-020: Implementar signed URLs para videos privados
|
||||
- [ ] BE-EDU-021: Verificar orden secuencial si aplica
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-017: Crear LessonPlayerPage.tsx
|
||||
- [ ] FE-EDU-018: Crear componente VideoPlayer.tsx
|
||||
- [ ] FE-EDU-019: Crear componente LessonSidebar.tsx
|
||||
- [ ] FE-EDU-020: Crear componente NotesEditor.tsx
|
||||
- [ ] FE-EDU-021: Crear componente ResourcesList.tsx
|
||||
- [ ] FE-EDU-022: Implementar auto-save de posición (cada 10s)
|
||||
- [ ] FE-EDU-023: Implementar lessonStore (Zustand)
|
||||
- [ ] FE-EDU-024: Navegación con teclas de flecha
|
||||
- [ ] FE-EDU-025: Toast notifications para acciones
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-007: Test validación de acceso a lección
|
||||
- [ ] TEST-EDU-008: Test bloqueo de lección secuencial
|
||||
- [ ] TEST-EDU-009: Test guardar y restaurar posición de video
|
||||
- [ ] TEST-EDU-010: Test E2E iniciar lección y marcar completada
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-002: Ver detalle de curso - Estado: Pendiente
|
||||
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-004: Ver video completo
|
||||
- [ ] US-EDU-005: Completar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoints involucrados:**
|
||||
| Método | Endpoint | Descripción |
|
||||
|--------|----------|-------------|
|
||||
| GET | /education/courses/:courseId/lessons/:lessonId | Obtener lección |
|
||||
| POST | /education/lessons/:id/progress | Guardar posición |
|
||||
| POST | /education/lessons/:id/complete | Marcar completada |
|
||||
| GET | /education/lessons/:id/notes | Obtener notas |
|
||||
| POST | /education/lessons/:id/notes | Crear/actualizar notas |
|
||||
|
||||
**Response GET /lessons/:id:**
|
||||
```typescript
|
||||
{
|
||||
lesson: {
|
||||
id: "les-5",
|
||||
courseId: "course-1",
|
||||
moduleId: "mod-2",
|
||||
title: "Niveles principales: 38.2%, 50%, 61.8%",
|
||||
slug: "niveles-principales",
|
||||
description: "En esta lección aprenderás...",
|
||||
type: "video",
|
||||
duration: 12.25, // minutos
|
||||
order: 1,
|
||||
isFree: false,
|
||||
|
||||
// Video específico
|
||||
videoUrl: "https://vimeo.com/signed-url-12345",
|
||||
videoProvider: "vimeo",
|
||||
subtitles: [
|
||||
{ language: "es", url: "..." },
|
||||
{ language: "en", url: "..." }
|
||||
],
|
||||
|
||||
resources: [
|
||||
{
|
||||
id: "res-1",
|
||||
name: "Plantilla de Fibonacci.xlsx",
|
||||
type: "application/vnd.ms-excel",
|
||||
size: 45120,
|
||||
url: "https://s3.../signed-url"
|
||||
}
|
||||
],
|
||||
|
||||
userProgress: {
|
||||
startedAt: "2025-12-04T10:30:00Z",
|
||||
completedAt: null,
|
||||
lastPosition: 5.5, // minutos
|
||||
timeSpent: 320, // segundos
|
||||
isCompleted: false
|
||||
},
|
||||
|
||||
navigation: {
|
||||
previous: {
|
||||
id: "les-4",
|
||||
title: "Historia y fundamentos",
|
||||
slug: "historia-fundamentos"
|
||||
},
|
||||
next: {
|
||||
id: "les-6",
|
||||
title: "Cómo dibujar Fibonacci",
|
||||
slug: "como-dibujar-fibonacci",
|
||||
isLocked: false
|
||||
}
|
||||
},
|
||||
|
||||
module: {
|
||||
id: "mod-2",
|
||||
title: "Niveles de Retroceso",
|
||||
lessonsCompleted: 1,
|
||||
totalLessons: 4
|
||||
},
|
||||
|
||||
course: {
|
||||
id: "course-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
progressPercent: 45,
|
||||
isSequential: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Validaciones backend:**
|
||||
1. Usuario autenticado
|
||||
2. Usuario inscrito en el curso
|
||||
3. Si curso es secuencial, validar lecciones anteriores completadas
|
||||
4. Si curso es premium, validar suscripción activa
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.lessons`
|
||||
- `education.user_lesson_progress`
|
||||
- `education.user_notes`
|
||||
- `education.lesson_resources`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,357 +1,369 @@
|
||||
# US-EDU-004: Ver Video de Lección
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-004 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario inscrito viendo una lección de video,
|
||||
**quiero** reproducir el video con controles completos y funcionalidades avanzadas,
|
||||
**para** consumir el contenido educativo de manera cómoda y eficiente.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder reproducir videos educativos con un reproductor profesional que incluya controles estándar (play/pause, volumen, pantalla completa), funcionalidades avanzadas (velocidad de reproducción, subtítulos, picture-in-picture), navegación temporal, y auto-guardado de progreso. El sistema debe recordar la posición de reproducción y permitir saltos rápidos.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Lección 2.1: Niveles principales de Fibonacci │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ [VIDEO REPRODUCIÉNDOSE] │ │
|
||||
│ │ │ │
|
||||
│ │ Carlos explicando │ │
|
||||
│ │ niveles de Fibonacci │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ━━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━ 05:30 / 12:15 │ │
|
||||
│ │ ⏮ ⏸ ⏭ 🔊──────● ⚙ 1.25x [CC] [PIP] ⛶ │ │
|
||||
│ │ │ │
|
||||
│ │ Capítulos: │ │
|
||||
│ │ • 0:00 - Introducción │ │
|
||||
│ │ • 2:15 - Nivel 38.2% │ │
|
||||
│ │ • 5:30 - Nivel 50% ← Actual │ │
|
||||
│ │ • 8:45 - Nivel 61.8% │ │
|
||||
│ │ • 11:00 - Conclusión │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ CONTROLES: │
|
||||
│ • Espacio: Play/Pause │
|
||||
│ • →: Adelantar 10s │
|
||||
│ • ←: Retroceder 10s │
|
||||
│ • F: Pantalla completa │
|
||||
│ • M: Silenciar │
|
||||
│ • 0-9: Saltar a ese % del video │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[MENÚ DE VELOCIDAD]
|
||||
┌──────────────┐
|
||||
│ Velocidad │
|
||||
│ ○ 0.5x │
|
||||
│ ○ 0.75x │
|
||||
│ ● 1x │ ← Seleccionado
|
||||
│ ○ 1.25x │
|
||||
│ ○ 1.5x │
|
||||
│ ○ 2x │
|
||||
└──────────────┘
|
||||
|
||||
[MENÚ DE CALIDAD]
|
||||
┌──────────────┐
|
||||
│ Calidad │
|
||||
│ ● Auto │ ← Adaptativa
|
||||
│ ○ 1080p │
|
||||
│ ○ 720p │
|
||||
│ ○ 480p │
|
||||
│ ○ 360p │
|
||||
└──────────────┘
|
||||
|
||||
[SUBTÍTULOS]
|
||||
┌──────────────┐
|
||||
│ Subtítulos │
|
||||
│ ● Desactivado│
|
||||
│ ○ Español │
|
||||
│ ○ English │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Reproducir video**
|
||||
```gherkin
|
||||
DADO que el usuario accedió a una lección de video
|
||||
CUANDO el reproductor carga
|
||||
ENTONCES se muestra el video con controles
|
||||
Y el video está pausado inicialmente
|
||||
Y se muestra duración total
|
||||
Y se carga en la última posición guardada (si existe)
|
||||
Y se muestra toast "Continuando desde X:XX"
|
||||
```
|
||||
|
||||
**Escenario 2: Controles básicos funcionan**
|
||||
```gherkin
|
||||
DADO que el video está cargado
|
||||
CUANDO el usuario hace click en Play
|
||||
ENTONCES el video se reproduce
|
||||
Y el botón cambia a Pause ⏸
|
||||
Y la barra de progreso avanza
|
||||
Y el tiempo actual se actualiza cada segundo
|
||||
```
|
||||
|
||||
**Escenario 3: Cambiar velocidad de reproducción**
|
||||
```gherkin
|
||||
DADO que el video se está reproduciendo a 1x
|
||||
CUANDO el usuario selecciona velocidad 1.5x
|
||||
ENTONCES el video se reproduce 50% más rápido
|
||||
Y el audio se ajusta automáticamente (sin distorsión)
|
||||
Y se muestra indicador "1.5x" en el reproductor
|
||||
Y la configuración se guarda para próximos videos
|
||||
```
|
||||
|
||||
**Escenario 4: Activar subtítulos**
|
||||
```gherkin
|
||||
DADO que el video tiene subtítulos en español
|
||||
CUANDO el usuario activa subtítulos
|
||||
ENTONCES aparecen subtítulos sincronizados con el audio
|
||||
Y se pueden personalizar tamaño y posición
|
||||
Y la preferencia se guarda para próximos videos
|
||||
```
|
||||
|
||||
**Escenario 5: Saltar a posición específica**
|
||||
```gherkin
|
||||
DADO que el video se está reproduciendo
|
||||
CUANDO el usuario hace click en la barra de progreso
|
||||
ENTONCES el video salta a esa posición
|
||||
Y se muestra preview al hacer hover sobre la barra
|
||||
Y la nueva posición se guarda automáticamente
|
||||
```
|
||||
|
||||
**Escenario 6: Auto-guardado de progreso**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
Y el video alcanza la posición 7:30
|
||||
CUANDO pasan 10 segundos
|
||||
ENTONCES se guarda la posición en backend
|
||||
Y si el usuario cierra la página y vuelve
|
||||
ENTONCES el video se carga en 7:30
|
||||
```
|
||||
|
||||
**Escenario 7: Atajos de teclado**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO presiona la tecla →
|
||||
ENTONCES el video avanza 10 segundos
|
||||
Y se muestra indicador "+10s"
|
||||
|
||||
CUANDO presiona la tecla ←
|
||||
ENTONCES el video retrocede 10 segundos
|
||||
Y se muestra indicador "-10s"
|
||||
|
||||
CUANDO presiona Espacio
|
||||
ENTONCES el video pausa/reanuda
|
||||
|
||||
CUANDO presiona F
|
||||
ENTONCES entra/sale de pantalla completa
|
||||
```
|
||||
|
||||
**Escenario 8: Picture-in-Picture**
|
||||
```gherkin
|
||||
DADO que el video se está reproduciendo
|
||||
CUANDO el usuario hace click en botón PIP
|
||||
ENTONCES el video se minimiza en una ventana flotante
|
||||
Y puede navegar a otras páginas mientras ve el video
|
||||
Y los controles básicos están disponibles en PIP
|
||||
Y al cerrar PIP, vuelve al reproductor normal
|
||||
```
|
||||
|
||||
**Escenario 9: Capítulos del video**
|
||||
```gherkin
|
||||
DADO que el video tiene capítulos definidos
|
||||
CUANDO el usuario hace click en un capítulo
|
||||
ENTONCES el video salta a ese timestamp
|
||||
Y se muestra marcador de capítulo en la barra de progreso
|
||||
Y al hacer hover en la barra, muestra nombre del capítulo
|
||||
```
|
||||
|
||||
**Escenario 10: Completar video automáticamente**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO el video alcanza el 90% de reproducción
|
||||
ENTONCES la lección se marca automáticamente como completada
|
||||
Y se muestra toast "Lección completada +10 XP"
|
||||
Y se actualiza el progreso del curso
|
||||
Y se desbloquea siguiente lección (si es secuencial)
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Calidad adaptativa según ancho de banda
|
||||
- [ ] Buffer progresivo para evitar cortes
|
||||
- [ ] Indicador de buffering cuando carga
|
||||
- [ ] Manejo de errores (video no disponible, error de red)
|
||||
- [ ] Analytics: pausas, rewinds, abandonos
|
||||
- [ ] Thumbnail preview al hover sobre barra de progreso
|
||||
- [ ] Continuar reproduciendo al cambiar de pestaña (background play)
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-010: Tabla video_chapters (video_id, time, title)
|
||||
- [ ] DB-EDU-011: Actualizar user_lesson_progress.last_position cada 10s
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-022: Endpoint POST /education/lessons/:id/progress
|
||||
- [ ] BE-EDU-023: Endpoint GET /education/videos/:id/chapters
|
||||
- [ ] BE-EDU-024: Generar signed URLs para Vimeo/S3
|
||||
- [ ] BE-EDU-025: Implementar validación de acceso a video
|
||||
- [ ] BE-EDU-026: Webhook de Vimeo para confirmar encoding completado
|
||||
- [ ] BE-EDU-027: Rate limiting en auto-save (máx cada 5s)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-026: Implementar VideoPlayer.tsx con React Player
|
||||
- [ ] FE-EDU-027: Custom controls overlay
|
||||
- [ ] FE-EDU-028: Speed control menu
|
||||
- [ ] FE-EDU-029: Subtitles toggle y customización
|
||||
- [ ] FE-EDU-030: Keyboard shortcuts (arrow keys, space, f, m)
|
||||
- [ ] FE-EDU-031: Picture-in-Picture implementation
|
||||
- [ ] FE-EDU-032: Progress bar con preview thumbnail
|
||||
- [ ] FE-EDU-033: Chapters navigation
|
||||
- [ ] FE-EDU-034: Auto-save de posición cada 10s
|
||||
- [ ] FE-EDU-035: Restore position on load
|
||||
- [ ] FE-EDU-036: Analytics tracking (play, pause, seek, complete)
|
||||
- [ ] FE-EDU-037: Loading spinner y error states
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-011: Test auto-save de progreso
|
||||
- [ ] TEST-EDU-012: Test restaurar posición guardada
|
||||
- [ ] TEST-EDU-013: Test marcar completado al 90%
|
||||
- [ ] TEST-EDU-014: Test E2E reproducir video completo
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
|
||||
- [ ] Video CDN: Vimeo Pro o AWS S3 + CloudFront
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-005: Completar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**CDN de Videos:**
|
||||
- Opción 1: Vimeo Pro (recomendado para MVP)
|
||||
- API robusta
|
||||
- Encoding automático
|
||||
- Streaming adaptativo HLS
|
||||
- Subtítulos integrados
|
||||
- Analytics incluido
|
||||
- Opción 2: AWS S3 + CloudFront + MediaConvert
|
||||
- Más control
|
||||
- Más setup inicial
|
||||
- Costos variables
|
||||
|
||||
**Librerías recomendadas:**
|
||||
- React Player: Wrapper para múltiples providers
|
||||
- Video.js: Player HTML5 completo y customizable
|
||||
- Plyr: Alternativa moderna y ligera
|
||||
|
||||
**Auto-save strategy:**
|
||||
```javascript
|
||||
// Guardar posición cada 10s mientras reproduce
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (isPlaying) {
|
||||
saveProgress(currentTime);
|
||||
}
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isPlaying, currentTime]);
|
||||
|
||||
// Guardar al pausar
|
||||
const handlePause = () => {
|
||||
saveProgress(currentTime);
|
||||
};
|
||||
|
||||
// Guardar antes de salir
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveProgress(currentTime);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Analytics events:**
|
||||
- `video_started`: Primera reproducción
|
||||
- `video_played`: Cada vez que presiona play
|
||||
- `video_paused`: Cada pausa
|
||||
- `video_seeked`: Salto manual
|
||||
- `video_completed`: Alcanza 90%
|
||||
- `video_speed_changed`: Cambia velocidad
|
||||
- `video_quality_changed`: Cambia calidad
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.lessons` (campo videoUrl)
|
||||
- `education.video_chapters`
|
||||
- `education.user_lesson_progress` (campo last_position)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-004"
|
||||
title: "Ver Video de Leccion"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-002"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-004: Ver Video de Lección
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-004 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario inscrito viendo una lección de video,
|
||||
**quiero** reproducir el video con controles completos y funcionalidades avanzadas,
|
||||
**para** consumir el contenido educativo de manera cómoda y eficiente.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder reproducir videos educativos con un reproductor profesional que incluya controles estándar (play/pause, volumen, pantalla completa), funcionalidades avanzadas (velocidad de reproducción, subtítulos, picture-in-picture), navegación temporal, y auto-guardado de progreso. El sistema debe recordar la posición de reproducción y permitir saltos rápidos.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Lección 2.1: Niveles principales de Fibonacci │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ [VIDEO REPRODUCIÉNDOSE] │ │
|
||||
│ │ │ │
|
||||
│ │ Carlos explicando │ │
|
||||
│ │ niveles de Fibonacci │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ ━━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━ 05:30 / 12:15 │ │
|
||||
│ │ ⏮ ⏸ ⏭ 🔊──────● ⚙ 1.25x [CC] [PIP] ⛶ │ │
|
||||
│ │ │ │
|
||||
│ │ Capítulos: │ │
|
||||
│ │ • 0:00 - Introducción │ │
|
||||
│ │ • 2:15 - Nivel 38.2% │ │
|
||||
│ │ • 5:30 - Nivel 50% ← Actual │ │
|
||||
│ │ • 8:45 - Nivel 61.8% │ │
|
||||
│ │ • 11:00 - Conclusión │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ CONTROLES: │
|
||||
│ • Espacio: Play/Pause │
|
||||
│ • →: Adelantar 10s │
|
||||
│ • ←: Retroceder 10s │
|
||||
│ • F: Pantalla completa │
|
||||
│ • M: Silenciar │
|
||||
│ • 0-9: Saltar a ese % del video │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[MENÚ DE VELOCIDAD]
|
||||
┌──────────────┐
|
||||
│ Velocidad │
|
||||
│ ○ 0.5x │
|
||||
│ ○ 0.75x │
|
||||
│ ● 1x │ ← Seleccionado
|
||||
│ ○ 1.25x │
|
||||
│ ○ 1.5x │
|
||||
│ ○ 2x │
|
||||
└──────────────┘
|
||||
|
||||
[MENÚ DE CALIDAD]
|
||||
┌──────────────┐
|
||||
│ Calidad │
|
||||
│ ● Auto │ ← Adaptativa
|
||||
│ ○ 1080p │
|
||||
│ ○ 720p │
|
||||
│ ○ 480p │
|
||||
│ ○ 360p │
|
||||
└──────────────┘
|
||||
|
||||
[SUBTÍTULOS]
|
||||
┌──────────────┐
|
||||
│ Subtítulos │
|
||||
│ ● Desactivado│
|
||||
│ ○ Español │
|
||||
│ ○ English │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Reproducir video**
|
||||
```gherkin
|
||||
DADO que el usuario accedió a una lección de video
|
||||
CUANDO el reproductor carga
|
||||
ENTONCES se muestra el video con controles
|
||||
Y el video está pausado inicialmente
|
||||
Y se muestra duración total
|
||||
Y se carga en la última posición guardada (si existe)
|
||||
Y se muestra toast "Continuando desde X:XX"
|
||||
```
|
||||
|
||||
**Escenario 2: Controles básicos funcionan**
|
||||
```gherkin
|
||||
DADO que el video está cargado
|
||||
CUANDO el usuario hace click en Play
|
||||
ENTONCES el video se reproduce
|
||||
Y el botón cambia a Pause ⏸
|
||||
Y la barra de progreso avanza
|
||||
Y el tiempo actual se actualiza cada segundo
|
||||
```
|
||||
|
||||
**Escenario 3: Cambiar velocidad de reproducción**
|
||||
```gherkin
|
||||
DADO que el video se está reproduciendo a 1x
|
||||
CUANDO el usuario selecciona velocidad 1.5x
|
||||
ENTONCES el video se reproduce 50% más rápido
|
||||
Y el audio se ajusta automáticamente (sin distorsión)
|
||||
Y se muestra indicador "1.5x" en el reproductor
|
||||
Y la configuración se guarda para próximos videos
|
||||
```
|
||||
|
||||
**Escenario 4: Activar subtítulos**
|
||||
```gherkin
|
||||
DADO que el video tiene subtítulos en español
|
||||
CUANDO el usuario activa subtítulos
|
||||
ENTONCES aparecen subtítulos sincronizados con el audio
|
||||
Y se pueden personalizar tamaño y posición
|
||||
Y la preferencia se guarda para próximos videos
|
||||
```
|
||||
|
||||
**Escenario 5: Saltar a posición específica**
|
||||
```gherkin
|
||||
DADO que el video se está reproduciendo
|
||||
CUANDO el usuario hace click en la barra de progreso
|
||||
ENTONCES el video salta a esa posición
|
||||
Y se muestra preview al hacer hover sobre la barra
|
||||
Y la nueva posición se guarda automáticamente
|
||||
```
|
||||
|
||||
**Escenario 6: Auto-guardado de progreso**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
Y el video alcanza la posición 7:30
|
||||
CUANDO pasan 10 segundos
|
||||
ENTONCES se guarda la posición en backend
|
||||
Y si el usuario cierra la página y vuelve
|
||||
ENTONCES el video se carga en 7:30
|
||||
```
|
||||
|
||||
**Escenario 7: Atajos de teclado**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO presiona la tecla →
|
||||
ENTONCES el video avanza 10 segundos
|
||||
Y se muestra indicador "+10s"
|
||||
|
||||
CUANDO presiona la tecla ←
|
||||
ENTONCES el video retrocede 10 segundos
|
||||
Y se muestra indicador "-10s"
|
||||
|
||||
CUANDO presiona Espacio
|
||||
ENTONCES el video pausa/reanuda
|
||||
|
||||
CUANDO presiona F
|
||||
ENTONCES entra/sale de pantalla completa
|
||||
```
|
||||
|
||||
**Escenario 8: Picture-in-Picture**
|
||||
```gherkin
|
||||
DADO que el video se está reproduciendo
|
||||
CUANDO el usuario hace click en botón PIP
|
||||
ENTONCES el video se minimiza en una ventana flotante
|
||||
Y puede navegar a otras páginas mientras ve el video
|
||||
Y los controles básicos están disponibles en PIP
|
||||
Y al cerrar PIP, vuelve al reproductor normal
|
||||
```
|
||||
|
||||
**Escenario 9: Capítulos del video**
|
||||
```gherkin
|
||||
DADO que el video tiene capítulos definidos
|
||||
CUANDO el usuario hace click en un capítulo
|
||||
ENTONCES el video salta a ese timestamp
|
||||
Y se muestra marcador de capítulo en la barra de progreso
|
||||
Y al hacer hover en la barra, muestra nombre del capítulo
|
||||
```
|
||||
|
||||
**Escenario 10: Completar video automáticamente**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO el video alcanza el 90% de reproducción
|
||||
ENTONCES la lección se marca automáticamente como completada
|
||||
Y se muestra toast "Lección completada +10 XP"
|
||||
Y se actualiza el progreso del curso
|
||||
Y se desbloquea siguiente lección (si es secuencial)
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Calidad adaptativa según ancho de banda
|
||||
- [ ] Buffer progresivo para evitar cortes
|
||||
- [ ] Indicador de buffering cuando carga
|
||||
- [ ] Manejo de errores (video no disponible, error de red)
|
||||
- [ ] Analytics: pausas, rewinds, abandonos
|
||||
- [ ] Thumbnail preview al hover sobre barra de progreso
|
||||
- [ ] Continuar reproduciendo al cambiar de pestaña (background play)
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-010: Tabla video_chapters (video_id, time, title)
|
||||
- [ ] DB-EDU-011: Actualizar user_lesson_progress.last_position cada 10s
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-022: Endpoint POST /education/lessons/:id/progress
|
||||
- [ ] BE-EDU-023: Endpoint GET /education/videos/:id/chapters
|
||||
- [ ] BE-EDU-024: Generar signed URLs para Vimeo/S3
|
||||
- [ ] BE-EDU-025: Implementar validación de acceso a video
|
||||
- [ ] BE-EDU-026: Webhook de Vimeo para confirmar encoding completado
|
||||
- [ ] BE-EDU-027: Rate limiting en auto-save (máx cada 5s)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-026: Implementar VideoPlayer.tsx con React Player
|
||||
- [ ] FE-EDU-027: Custom controls overlay
|
||||
- [ ] FE-EDU-028: Speed control menu
|
||||
- [ ] FE-EDU-029: Subtitles toggle y customización
|
||||
- [ ] FE-EDU-030: Keyboard shortcuts (arrow keys, space, f, m)
|
||||
- [ ] FE-EDU-031: Picture-in-Picture implementation
|
||||
- [ ] FE-EDU-032: Progress bar con preview thumbnail
|
||||
- [ ] FE-EDU-033: Chapters navigation
|
||||
- [ ] FE-EDU-034: Auto-save de posición cada 10s
|
||||
- [ ] FE-EDU-035: Restore position on load
|
||||
- [ ] FE-EDU-036: Analytics tracking (play, pause, seek, complete)
|
||||
- [ ] FE-EDU-037: Loading spinner y error states
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-011: Test auto-save de progreso
|
||||
- [ ] TEST-EDU-012: Test restaurar posición guardada
|
||||
- [ ] TEST-EDU-013: Test marcar completado al 90%
|
||||
- [ ] TEST-EDU-014: Test E2E reproducir video completo
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
|
||||
- [ ] Video CDN: Vimeo Pro o AWS S3 + CloudFront
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-005: Completar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**CDN de Videos:**
|
||||
- Opción 1: Vimeo Pro (recomendado para MVP)
|
||||
- API robusta
|
||||
- Encoding automático
|
||||
- Streaming adaptativo HLS
|
||||
- Subtítulos integrados
|
||||
- Analytics incluido
|
||||
- Opción 2: AWS S3 + CloudFront + MediaConvert
|
||||
- Más control
|
||||
- Más setup inicial
|
||||
- Costos variables
|
||||
|
||||
**Librerías recomendadas:**
|
||||
- React Player: Wrapper para múltiples providers
|
||||
- Video.js: Player HTML5 completo y customizable
|
||||
- Plyr: Alternativa moderna y ligera
|
||||
|
||||
**Auto-save strategy:**
|
||||
```javascript
|
||||
// Guardar posición cada 10s mientras reproduce
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (isPlaying) {
|
||||
saveProgress(currentTime);
|
||||
}
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isPlaying, currentTime]);
|
||||
|
||||
// Guardar al pausar
|
||||
const handlePause = () => {
|
||||
saveProgress(currentTime);
|
||||
};
|
||||
|
||||
// Guardar antes de salir
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveProgress(currentTime);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Analytics events:**
|
||||
- `video_started`: Primera reproducción
|
||||
- `video_played`: Cada vez que presiona play
|
||||
- `video_paused`: Cada pausa
|
||||
- `video_seeked`: Salto manual
|
||||
- `video_completed`: Alcanza 90%
|
||||
- `video_speed_changed`: Cambia velocidad
|
||||
- `video_quality_changed`: Cambia calidad
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.lessons` (campo videoUrl)
|
||||
- `education.video_chapters`
|
||||
- `education.user_lesson_progress` (campo last_position)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,376 +1,388 @@
|
||||
# US-EDU-005: Completar Lección
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-005 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario viendo una lección,
|
||||
**quiero** marcar la lección como completada y ganar recompensas,
|
||||
**para** registrar mi progreso, desbloquear siguiente contenido y acumular XP.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder completar lecciones de forma automática (al ver 90% del video o llegar al final de un artículo) o manual (checkbox "marcar como completada"). Al completar, el sistema debe registrar el progreso, otorgar XP, actualizar estadísticas del usuario, desbloquear la siguiente lección si el curso es secuencial, mostrar celebración visual, y sugerir el siguiente paso.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
[VIDEO AL 90%]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● 11:00 / 12:15 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[ANIMACIÓN DE CELEBRACIÓN]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ✨ ✨ ✨ │
|
||||
│ │
|
||||
│ ✓ ¡LECCIÓN COMPLETADA! │
|
||||
│ │
|
||||
│ Niveles principales de Fibonacci │
|
||||
│ │
|
||||
│ +10 XP ganados │
|
||||
│ │
|
||||
│ Progreso del curso: 50% │
|
||||
│ ████████████░░░░░░░░ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ [Siguiente lección →] │ │
|
||||
│ │ Cómo dibujar Fibonacci │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Volver al curso] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[ARTÍCULO - CHECKBOX AL FINAL]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ... contenido del artículo ... │
|
||||
│ │
|
||||
│ Conclusión │
|
||||
│ En esta lección aprendiste los fundamentos de... │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ☐ Marcar como completada │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [← Anterior] [Siguiente lección →] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[SIDEBAR ACTUALIZADO]
|
||||
┌────────────────────────┐
|
||||
│ TEMARIO │
|
||||
│ ✓ Módulo 1 (4/4) ✓ │
|
||||
│ ▼ Módulo 2 (2/4) │
|
||||
│ ✓ 1. Niveles princ. │ ← Completada
|
||||
│ ● 2. Dibujar Fib. │ ← Desbloqueada
|
||||
│ 🔒 3. Práctica │
|
||||
│ 🔒 4. Quiz │
|
||||
│ 🔒 Módulo 3 │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Completar video automáticamente**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO el video alcanza el 90% de reproducción (10:48 de 12:00)
|
||||
ENTONCES la lección se marca automáticamente como completada
|
||||
Y se registra completed_at en backend
|
||||
Y se muestra modal de celebración con confeti
|
||||
Y se muestra "+10 XP ganados"
|
||||
Y se actualiza barra de progreso del curso
|
||||
Y se actualiza sidebar con checkmark ✓
|
||||
Y si curso es secuencial, se desbloquea siguiente lección
|
||||
```
|
||||
|
||||
**Escenario 2: Completar artículo manualmente**
|
||||
```gherkin
|
||||
DADO que el usuario está leyendo un artículo
|
||||
Y scrolleó hasta el final
|
||||
CUANDO hace click en checkbox "Marcar como completada"
|
||||
ENTONCES se marca la lección como completada
|
||||
Y se registra en backend
|
||||
Y se muestra toast "+15 XP ganados"
|
||||
Y se habilita botón "Siguiente lección"
|
||||
Y checkbox cambia a checked ✓
|
||||
```
|
||||
|
||||
**Escenario 3: Ganar XP por completar**
|
||||
```gherkin
|
||||
DADO que el usuario completa una lección de video
|
||||
ENTONCES se otorgan 10 XP
|
||||
Y se actualiza totalXP del usuario
|
||||
Y se verifica si sube de nivel
|
||||
Y si sube de nivel, se muestra animación adicional
|
||||
Y se actualiza badge de nivel en UI
|
||||
```
|
||||
|
||||
**Escenario 4: Desbloquear siguiente lección**
|
||||
```gherkin
|
||||
DADO que el curso es secuencial
|
||||
Y el usuario completa lección 3 del módulo 2
|
||||
ENTONCES la lección 4 del módulo 2 se desbloquea
|
||||
Y el candado 🔒 se remueve del sidebar
|
||||
Y el usuario puede acceder a esa lección
|
||||
Y si intenta saltarse a lección 5, sigue bloqueada
|
||||
```
|
||||
|
||||
**Escenario 5: Completar módulo completo**
|
||||
```gherkin
|
||||
DADO que el usuario completa la última lección de un módulo
|
||||
CUANDO se marca la lección como completada
|
||||
ENTONCES también se completa el módulo
|
||||
Y se otorgan +50 XP adicionales por módulo
|
||||
Y se muestra "¡Módulo completado!"
|
||||
Y se desbloquea el siguiente módulo (si es secuencial)
|
||||
Y se actualiza contador "Módulo 2 (4/4) ✓"
|
||||
```
|
||||
|
||||
**Escenario 6: Completar curso completo**
|
||||
```gherkin
|
||||
DADO que el usuario completa la última lección del último módulo
|
||||
CUANDO se marca como completada
|
||||
ENTONCES se completa el curso
|
||||
Y se otorgan +200 XP adicionales por curso
|
||||
Y se muestra modal "¡CURSO COMPLETADO!"
|
||||
Y se genera certificado automáticamente
|
||||
Y se muestra botón "Descargar certificado"
|
||||
Y se envía email de felicitación
|
||||
```
|
||||
|
||||
**Escenario 7: Sugerir siguiente paso**
|
||||
```gherkin
|
||||
DADO que el usuario completó una lección
|
||||
CUANDO se muestra el modal de celebración
|
||||
ENTONCES se sugiere la siguiente lección
|
||||
Y se muestra título y duración de la siguiente
|
||||
Y botón "Siguiente lección" navega directamente
|
||||
Y también hay opción "Volver al curso"
|
||||
Y si no hay siguiente lección, sugiere otro curso
|
||||
```
|
||||
|
||||
**Escenario 8: Re-marcar como incompleta**
|
||||
```gherkin
|
||||
DADO que el usuario marcó una lección como completada
|
||||
Y quiere revisarla de nuevo
|
||||
CUANDO desmarca el checkbox
|
||||
ENTONCES la lección vuelve a estado incompleto
|
||||
Y el progreso del curso se actualiza (disminuye)
|
||||
Y NO se quita el XP ya ganado
|
||||
Y la lección puede volver a marcarse como completada
|
||||
```
|
||||
|
||||
**Escenario 9: Actualizar racha diaria**
|
||||
```gherkin
|
||||
DADO que es el primer día del usuario en la plataforma
|
||||
CUANDO completa su primera lección
|
||||
ENTONCES se inicia racha de 1 día
|
||||
Y se muestra toast "¡Racha iniciada! 🔥 1 día"
|
||||
|
||||
DADO que el usuario tiene racha de 5 días
|
||||
Y completa su primera lección del día
|
||||
ENTONCES la racha aumenta a 6 días
|
||||
Y se muestra "¡Racha de 6 días! 🔥"
|
||||
```
|
||||
|
||||
**Escenario 10: Primera lección del día bonus**
|
||||
```gherkin
|
||||
DADO que el usuario NO completó lecciones hoy
|
||||
CUANDO completa su primera lección del día
|
||||
ENTONCES recibe +5 XP adicionales de bonus
|
||||
Y se muestra "+10 XP + 5 XP (bonus diario)"
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Animación de confeti al completar
|
||||
- [ ] Sonido de celebración (opcional, con toggle)
|
||||
- [ ] Compartir logro en redes sociales
|
||||
- [ ] Badges especiales por milestones (10, 50, 100 lecciones)
|
||||
- [ ] Actualizar calendario de actividad
|
||||
- [ ] Notificación push si está habilitado
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-012: Campo completed_at en user_lesson_progress
|
||||
- [ ] DB-EDU-013: Tabla user_xp_transactions (log de XP ganado)
|
||||
- [ ] DB-EDU-014: Campo current_streak en users
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-028: Endpoint POST /education/lessons/:id/complete
|
||||
- [ ] BE-EDU-029: Implementar LessonService.markAsCompleted()
|
||||
- [ ] BE-EDU-030: Verificar si es primera del día (bonus XP)
|
||||
- [ ] BE-EDU-031: Otorgar XP según tipo de lección
|
||||
- [ ] BE-EDU-032: Verificar si completa módulo
|
||||
- [ ] BE-EDU-033: Verificar si completa curso
|
||||
- [ ] BE-EDU-034: Desbloquear siguiente lección/módulo
|
||||
- [ ] BE-EDU-035: Actualizar racha diaria
|
||||
- [ ] BE-EDU-036: Verificar si sube de nivel
|
||||
- [ ] BE-EDU-037: Event handler para generar certificado
|
||||
- [ ] BE-EDU-038: Enviar email de curso completado
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-038: Componente LessonCompleteModal.tsx
|
||||
- [ ] FE-EDU-039: Animación de confeti con react-confetti
|
||||
- [ ] FE-EDU-040: Actualizar sidebar en tiempo real
|
||||
- [ ] FE-EDU-041: Actualizar barra de progreso
|
||||
- [ ] FE-EDU-042: Toast notification de XP ganado
|
||||
- [ ] FE-EDU-043: Modal de curso completado
|
||||
- [ ] FE-EDU-044: Checkbox para artículos
|
||||
- [ ] FE-EDU-045: Auto-complete al 90% en videos
|
||||
- [ ] FE-EDU-046: Botón "Siguiente lección"
|
||||
- [ ] FE-EDU-047: Integrar con progressStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-015: Test completar lección otorga XP
|
||||
- [ ] TEST-EDU-016: Test desbloquear siguiente lección
|
||||
- [ ] TEST-EDU-017: Test completar módulo otorga bonus
|
||||
- [ ] TEST-EDU-018: Test completar curso genera certificado
|
||||
- [ ] TEST-EDU-019: Test actualizar racha diaria
|
||||
- [ ] TEST-EDU-020: Test E2E completar lección end-to-end
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
|
||||
- [ ] US-EDU-004: Ver video - Estado: Pendiente
|
||||
- [ ] RF-EDU-003: Sistema de progreso
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
- [ ] US-EDU-008: Obtener certificado
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoint POST /lessons/:id/complete:**
|
||||
```typescript
|
||||
// Request
|
||||
POST /education/lessons/lesson-uuid-123/complete
|
||||
{
|
||||
completedAt: "2025-12-05T15:30:00Z",
|
||||
timeSpent: 720, // segundos que pasó en la lección
|
||||
finalPosition: 12.15 // para videos
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
success: true,
|
||||
lesson: {
|
||||
id: "lesson-uuid-123",
|
||||
isCompleted: true,
|
||||
completedAt: "2025-12-05T15:30:00Z"
|
||||
},
|
||||
rewards: {
|
||||
xpEarned: 10,
|
||||
bonusXP: 5, // Si es primera del día
|
||||
totalXP: 15,
|
||||
newLevel: null, // Si subió de nivel, info del nuevo nivel
|
||||
badges: [] // Nuevos badges ganados
|
||||
},
|
||||
progress: {
|
||||
course: {
|
||||
progressPercent: 50,
|
||||
lessonsCompleted: 12,
|
||||
totalLessons: 23
|
||||
},
|
||||
module: {
|
||||
isCompleted: false,
|
||||
lessonsCompleted: 2,
|
||||
totalLessons: 4
|
||||
}
|
||||
},
|
||||
nextLesson: {
|
||||
id: "lesson-uuid-124",
|
||||
title: "Cómo dibujar Fibonacci",
|
||||
slug: "como-dibujar-fibonacci",
|
||||
isUnlocked: true
|
||||
},
|
||||
streak: {
|
||||
current: 6,
|
||||
isNewDay: true
|
||||
},
|
||||
courseCompleted: false
|
||||
}
|
||||
```
|
||||
|
||||
**Reglas de XP:**
|
||||
- Lección de video: 10 XP
|
||||
- Lección de artículo: 15 XP
|
||||
- Completar módulo: +50 XP
|
||||
- Completar curso: +200 XP
|
||||
- Primera lección del día: +5 XP
|
||||
- Aprobar quiz primera vez: +30 XP (ver US-EDU-006)
|
||||
|
||||
**Lógica de completitud:**
|
||||
```javascript
|
||||
// Video: 90% de duración
|
||||
isVideoComplete = currentTime >= (duration * 0.9);
|
||||
|
||||
// Artículo: Manual con checkbox
|
||||
// Quiz: Aprobar con score >= passingScore
|
||||
```
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.user_lesson_progress` (completed_at)
|
||||
- `gamification.user_xp_transactions`
|
||||
- `gamification.user_stats` (current_streak, total_xp, level)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-005"
|
||||
title: "Completar Leccion"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-002"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-005: Completar Lección
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-005 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 3 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario viendo una lección,
|
||||
**quiero** marcar la lección como completada y ganar recompensas,
|
||||
**para** registrar mi progreso, desbloquear siguiente contenido y acumular XP.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder completar lecciones de forma automática (al ver 90% del video o llegar al final de un artículo) o manual (checkbox "marcar como completada"). Al completar, el sistema debe registrar el progreso, otorgar XP, actualizar estadísticas del usuario, desbloquear la siguiente lección si el curso es secuencial, mostrar celebración visual, y sugerir el siguiente paso.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
[VIDEO AL 90%]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● 11:00 / 12:15 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[ANIMACIÓN DE CELEBRACIÓN]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ✨ ✨ ✨ │
|
||||
│ │
|
||||
│ ✓ ¡LECCIÓN COMPLETADA! │
|
||||
│ │
|
||||
│ Niveles principales de Fibonacci │
|
||||
│ │
|
||||
│ +10 XP ganados │
|
||||
│ │
|
||||
│ Progreso del curso: 50% │
|
||||
│ ████████████░░░░░░░░ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ [Siguiente lección →] │ │
|
||||
│ │ Cómo dibujar Fibonacci │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Volver al curso] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[ARTÍCULO - CHECKBOX AL FINAL]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ... contenido del artículo ... │
|
||||
│ │
|
||||
│ Conclusión │
|
||||
│ En esta lección aprendiste los fundamentos de... │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ☐ Marcar como completada │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [← Anterior] [Siguiente lección →] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[SIDEBAR ACTUALIZADO]
|
||||
┌────────────────────────┐
|
||||
│ TEMARIO │
|
||||
│ ✓ Módulo 1 (4/4) ✓ │
|
||||
│ ▼ Módulo 2 (2/4) │
|
||||
│ ✓ 1. Niveles princ. │ ← Completada
|
||||
│ ● 2. Dibujar Fib. │ ← Desbloqueada
|
||||
│ 🔒 3. Práctica │
|
||||
│ 🔒 4. Quiz │
|
||||
│ 🔒 Módulo 3 │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Completar video automáticamente**
|
||||
```gherkin
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO el video alcanza el 90% de reproducción (10:48 de 12:00)
|
||||
ENTONCES la lección se marca automáticamente como completada
|
||||
Y se registra completed_at en backend
|
||||
Y se muestra modal de celebración con confeti
|
||||
Y se muestra "+10 XP ganados"
|
||||
Y se actualiza barra de progreso del curso
|
||||
Y se actualiza sidebar con checkmark ✓
|
||||
Y si curso es secuencial, se desbloquea siguiente lección
|
||||
```
|
||||
|
||||
**Escenario 2: Completar artículo manualmente**
|
||||
```gherkin
|
||||
DADO que el usuario está leyendo un artículo
|
||||
Y scrolleó hasta el final
|
||||
CUANDO hace click en checkbox "Marcar como completada"
|
||||
ENTONCES se marca la lección como completada
|
||||
Y se registra en backend
|
||||
Y se muestra toast "+15 XP ganados"
|
||||
Y se habilita botón "Siguiente lección"
|
||||
Y checkbox cambia a checked ✓
|
||||
```
|
||||
|
||||
**Escenario 3: Ganar XP por completar**
|
||||
```gherkin
|
||||
DADO que el usuario completa una lección de video
|
||||
ENTONCES se otorgan 10 XP
|
||||
Y se actualiza totalXP del usuario
|
||||
Y se verifica si sube de nivel
|
||||
Y si sube de nivel, se muestra animación adicional
|
||||
Y se actualiza badge de nivel en UI
|
||||
```
|
||||
|
||||
**Escenario 4: Desbloquear siguiente lección**
|
||||
```gherkin
|
||||
DADO que el curso es secuencial
|
||||
Y el usuario completa lección 3 del módulo 2
|
||||
ENTONCES la lección 4 del módulo 2 se desbloquea
|
||||
Y el candado 🔒 se remueve del sidebar
|
||||
Y el usuario puede acceder a esa lección
|
||||
Y si intenta saltarse a lección 5, sigue bloqueada
|
||||
```
|
||||
|
||||
**Escenario 5: Completar módulo completo**
|
||||
```gherkin
|
||||
DADO que el usuario completa la última lección de un módulo
|
||||
CUANDO se marca la lección como completada
|
||||
ENTONCES también se completa el módulo
|
||||
Y se otorgan +50 XP adicionales por módulo
|
||||
Y se muestra "¡Módulo completado!"
|
||||
Y se desbloquea el siguiente módulo (si es secuencial)
|
||||
Y se actualiza contador "Módulo 2 (4/4) ✓"
|
||||
```
|
||||
|
||||
**Escenario 6: Completar curso completo**
|
||||
```gherkin
|
||||
DADO que el usuario completa la última lección del último módulo
|
||||
CUANDO se marca como completada
|
||||
ENTONCES se completa el curso
|
||||
Y se otorgan +200 XP adicionales por curso
|
||||
Y se muestra modal "¡CURSO COMPLETADO!"
|
||||
Y se genera certificado automáticamente
|
||||
Y se muestra botón "Descargar certificado"
|
||||
Y se envía email de felicitación
|
||||
```
|
||||
|
||||
**Escenario 7: Sugerir siguiente paso**
|
||||
```gherkin
|
||||
DADO que el usuario completó una lección
|
||||
CUANDO se muestra el modal de celebración
|
||||
ENTONCES se sugiere la siguiente lección
|
||||
Y se muestra título y duración de la siguiente
|
||||
Y botón "Siguiente lección" navega directamente
|
||||
Y también hay opción "Volver al curso"
|
||||
Y si no hay siguiente lección, sugiere otro curso
|
||||
```
|
||||
|
||||
**Escenario 8: Re-marcar como incompleta**
|
||||
```gherkin
|
||||
DADO que el usuario marcó una lección como completada
|
||||
Y quiere revisarla de nuevo
|
||||
CUANDO desmarca el checkbox
|
||||
ENTONCES la lección vuelve a estado incompleto
|
||||
Y el progreso del curso se actualiza (disminuye)
|
||||
Y NO se quita el XP ya ganado
|
||||
Y la lección puede volver a marcarse como completada
|
||||
```
|
||||
|
||||
**Escenario 9: Actualizar racha diaria**
|
||||
```gherkin
|
||||
DADO que es el primer día del usuario en la plataforma
|
||||
CUANDO completa su primera lección
|
||||
ENTONCES se inicia racha de 1 día
|
||||
Y se muestra toast "¡Racha iniciada! 🔥 1 día"
|
||||
|
||||
DADO que el usuario tiene racha de 5 días
|
||||
Y completa su primera lección del día
|
||||
ENTONCES la racha aumenta a 6 días
|
||||
Y se muestra "¡Racha de 6 días! 🔥"
|
||||
```
|
||||
|
||||
**Escenario 10: Primera lección del día bonus**
|
||||
```gherkin
|
||||
DADO que el usuario NO completó lecciones hoy
|
||||
CUANDO completa su primera lección del día
|
||||
ENTONCES recibe +5 XP adicionales de bonus
|
||||
Y se muestra "+10 XP + 5 XP (bonus diario)"
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Animación de confeti al completar
|
||||
- [ ] Sonido de celebración (opcional, con toggle)
|
||||
- [ ] Compartir logro en redes sociales
|
||||
- [ ] Badges especiales por milestones (10, 50, 100 lecciones)
|
||||
- [ ] Actualizar calendario de actividad
|
||||
- [ ] Notificación push si está habilitado
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-012: Campo completed_at en user_lesson_progress
|
||||
- [ ] DB-EDU-013: Tabla user_xp_transactions (log de XP ganado)
|
||||
- [ ] DB-EDU-014: Campo current_streak en users
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-028: Endpoint POST /education/lessons/:id/complete
|
||||
- [ ] BE-EDU-029: Implementar LessonService.markAsCompleted()
|
||||
- [ ] BE-EDU-030: Verificar si es primera del día (bonus XP)
|
||||
- [ ] BE-EDU-031: Otorgar XP según tipo de lección
|
||||
- [ ] BE-EDU-032: Verificar si completa módulo
|
||||
- [ ] BE-EDU-033: Verificar si completa curso
|
||||
- [ ] BE-EDU-034: Desbloquear siguiente lección/módulo
|
||||
- [ ] BE-EDU-035: Actualizar racha diaria
|
||||
- [ ] BE-EDU-036: Verificar si sube de nivel
|
||||
- [ ] BE-EDU-037: Event handler para generar certificado
|
||||
- [ ] BE-EDU-038: Enviar email de curso completado
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-038: Componente LessonCompleteModal.tsx
|
||||
- [ ] FE-EDU-039: Animación de confeti con react-confetti
|
||||
- [ ] FE-EDU-040: Actualizar sidebar en tiempo real
|
||||
- [ ] FE-EDU-041: Actualizar barra de progreso
|
||||
- [ ] FE-EDU-042: Toast notification de XP ganado
|
||||
- [ ] FE-EDU-043: Modal de curso completado
|
||||
- [ ] FE-EDU-044: Checkbox para artículos
|
||||
- [ ] FE-EDU-045: Auto-complete al 90% en videos
|
||||
- [ ] FE-EDU-046: Botón "Siguiente lección"
|
||||
- [ ] FE-EDU-047: Integrar con progressStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-015: Test completar lección otorga XP
|
||||
- [ ] TEST-EDU-016: Test desbloquear siguiente lección
|
||||
- [ ] TEST-EDU-017: Test completar módulo otorga bonus
|
||||
- [ ] TEST-EDU-018: Test completar curso genera certificado
|
||||
- [ ] TEST-EDU-019: Test actualizar racha diaria
|
||||
- [ ] TEST-EDU-020: Test E2E completar lección end-to-end
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
|
||||
- [ ] US-EDU-004: Ver video - Estado: Pendiente
|
||||
- [ ] RF-EDU-003: Sistema de progreso
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
- [ ] US-EDU-008: Obtener certificado
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoint POST /lessons/:id/complete:**
|
||||
```typescript
|
||||
// Request
|
||||
POST /education/lessons/lesson-uuid-123/complete
|
||||
{
|
||||
completedAt: "2025-12-05T15:30:00Z",
|
||||
timeSpent: 720, // segundos que pasó en la lección
|
||||
finalPosition: 12.15 // para videos
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
success: true,
|
||||
lesson: {
|
||||
id: "lesson-uuid-123",
|
||||
isCompleted: true,
|
||||
completedAt: "2025-12-05T15:30:00Z"
|
||||
},
|
||||
rewards: {
|
||||
xpEarned: 10,
|
||||
bonusXP: 5, // Si es primera del día
|
||||
totalXP: 15,
|
||||
newLevel: null, // Si subió de nivel, info del nuevo nivel
|
||||
badges: [] // Nuevos badges ganados
|
||||
},
|
||||
progress: {
|
||||
course: {
|
||||
progressPercent: 50,
|
||||
lessonsCompleted: 12,
|
||||
totalLessons: 23
|
||||
},
|
||||
module: {
|
||||
isCompleted: false,
|
||||
lessonsCompleted: 2,
|
||||
totalLessons: 4
|
||||
}
|
||||
},
|
||||
nextLesson: {
|
||||
id: "lesson-uuid-124",
|
||||
title: "Cómo dibujar Fibonacci",
|
||||
slug: "como-dibujar-fibonacci",
|
||||
isUnlocked: true
|
||||
},
|
||||
streak: {
|
||||
current: 6,
|
||||
isNewDay: true
|
||||
},
|
||||
courseCompleted: false
|
||||
}
|
||||
```
|
||||
|
||||
**Reglas de XP:**
|
||||
- Lección de video: 10 XP
|
||||
- Lección de artículo: 15 XP
|
||||
- Completar módulo: +50 XP
|
||||
- Completar curso: +200 XP
|
||||
- Primera lección del día: +5 XP
|
||||
- Aprobar quiz primera vez: +30 XP (ver US-EDU-006)
|
||||
|
||||
**Lógica de completitud:**
|
||||
```javascript
|
||||
// Video: 90% de duración
|
||||
isVideoComplete = currentTime >= (duration * 0.9);
|
||||
|
||||
// Artículo: Manual con checkbox
|
||||
// Quiz: Aprobar con score >= passingScore
|
||||
```
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.user_lesson_progress` (completed_at)
|
||||
- `gamification.user_xp_transactions`
|
||||
- `gamification.user_stats` (current_streak, total_xp, level)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,473 +1,485 @@
|
||||
# US-EDU-006: Realizar Quiz
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-006 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P1 |
|
||||
| **Story Points** | 5 |
|
||||
| **Sprint** | Sprint 4 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario inscrito en un curso,
|
||||
**quiero** realizar quizzes interactivos al final de cada módulo,
|
||||
**para** validar mi conocimiento, recibir feedback y aprobar las evaluaciones requeridas.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a quizzes que evalúan el conocimiento adquirido en las lecciones. Debe poder responder preguntas de múltiple opción, navegar entre preguntas, revisar sus respuestas antes de enviar, recibir calificación inmediata, ver explicaciones de respuestas correctas/incorrectas, y reintentar si no aprobó (según intentos permitidos).
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
[PANTALLA DE INTRODUCCIÓN AL QUIZ]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver al curso │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📝 QUIZ: FUNDAMENTOS DE FIBONACCI │
|
||||
│ │
|
||||
│ Módulo 1: Introducción a Fibonacci │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📊 Información del Quiz │ │
|
||||
│ │ │ │
|
||||
│ │ Preguntas: 10 │ │
|
||||
│ │ Tiempo límite: 15 minutos │ │
|
||||
│ │ Puntuación para aprobar: 70% │ │
|
||||
│ │ Intentos disponibles: 2 de 3 │ │
|
||||
│ │ Tipo: Evaluación │ │
|
||||
│ │ │ │
|
||||
│ │ ✓ Las preguntas están en orden aleatorio │ │
|
||||
│ │ ✓ Verás tus respuestas al finalizar │ │
|
||||
│ │ ✓ Puedes navegar libremente entre preguntas │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [🚀 Comenzar Quiz] │
|
||||
│ │
|
||||
│ Intentos anteriores: │
|
||||
│ • Intento 1: 65% - Reprobado (hace 2 días) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PANTALLA DE PREGUNTA]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Quiz: Fundamentos de Fibonacci ⏱ 12:45 restantes │
|
||||
│ Pregunta 3 de 10 ████████░░░░░░ 30% │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ¿Cuál es el nivel de retroceso de Fibonacci más utilizado? │
|
||||
│ │
|
||||
│ ○ 23.6% │
|
||||
│ ○ 38.2% │
|
||||
│ ● 61.8% ← Seleccionado │
|
||||
│ ○ 78.6% │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [← Anterior] [🚩 Marcar] [Siguiente →] │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ NAVEGADOR: │
|
||||
│ [●1] [●2] [●3] [○4] [○5] [○6] [○7] [○8] [🚩9] [○10] │
|
||||
│ ↑ ↑ ↑ Sin Sin Sin Sin Sin Marca Sin │
|
||||
│ Resp Resp Actual resp resp resp resp resp da resp │
|
||||
│ │
|
||||
│ [Finalizar Quiz] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[CONFIRMACIÓN DE ENVÍO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ⚠ ¿Estás seguro? │
|
||||
│ │
|
||||
│ Estás a punto de enviar tu quiz. │
|
||||
│ │
|
||||
│ Resumen: │
|
||||
│ • Preguntas respondidas: 9 de 10 │
|
||||
│ • Preguntas sin responder: 1 │
|
||||
│ • Preguntas marcadas para revisar: 1 │
|
||||
│ │
|
||||
│ ⚠ Una vez enviado, no podrás cambiar tus respuestas. │
|
||||
│ │
|
||||
│ [Cancelar] [Enviar Quiz] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[RESULTADO - APROBADO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ✨ ✨ ✨ │
|
||||
│ │
|
||||
│ ✅ ¡QUIZ APROBADO! │
|
||||
│ │
|
||||
│ Puntuación: 85% │
|
||||
│ Preguntas correctas: 8.5/10 │
|
||||
│ │
|
||||
│ +30 XP ganados │
|
||||
│ │
|
||||
│ 🎯 Desglose: │
|
||||
│ • Preguntas correctas: 8 │
|
||||
│ • Preguntas incorrectas: 2 │
|
||||
│ • Puntuación requerida: 70% │
|
||||
│ │
|
||||
│ Tiempo invertido: 10:32 de 15:00 │
|
||||
│ Intento: 2 de 3 │
|
||||
│ │
|
||||
│ [Ver respuestas y explicaciones] │
|
||||
│ [Continuar al siguiente contenido →] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver introducción del quiz**
|
||||
```gherkin
|
||||
DADO que el usuario está inscrito en el curso
|
||||
CUANDO accede a una lección tipo quiz
|
||||
ENTONCES se muestra pantalla de introducción
|
||||
Y se muestra: número de preguntas, tiempo límite, puntuación para aprobar
|
||||
Y se muestran intentos disponibles
|
||||
Y se muestra historial de intentos anteriores (si existen)
|
||||
Y se muestra botón "Comenzar Quiz"
|
||||
```
|
||||
|
||||
**Escenario 2: Comenzar quiz**
|
||||
```gherkin
|
||||
DADO que el usuario está en la introducción del quiz
|
||||
CUANDO hace click en "Comenzar Quiz"
|
||||
ENTONCES se registra el inicio del intento en backend
|
||||
Y se navega a la primera pregunta
|
||||
Y se inicia el timer countdown
|
||||
Y se carga el navegador de preguntas
|
||||
Y se registra timestamp de inicio
|
||||
```
|
||||
|
||||
**Escenario 3: Responder pregunta de opción múltiple**
|
||||
```gherkin
|
||||
DADO que el usuario está en una pregunta
|
||||
CUANDO selecciona una opción
|
||||
ENTONCES la opción se marca visualmente
|
||||
Y la pregunta se marca como "respondida" en el navegador
|
||||
Y puede cambiar su respuesta antes de enviar
|
||||
Y la respuesta se guarda temporalmente en el estado
|
||||
```
|
||||
|
||||
**Escenario 4: Navegar entre preguntas**
|
||||
```gherkin
|
||||
DADO que el usuario respondió la pregunta 3
|
||||
CUANDO hace click en "Siguiente"
|
||||
ENTONCES navega a la pregunta 4
|
||||
Y su respuesta anterior se mantiene guardada
|
||||
Y puede volver a pregunta 3 con "Anterior"
|
||||
Y puede saltar a cualquier pregunta desde el navegador
|
||||
```
|
||||
|
||||
**Escenario 5: Marcar pregunta para revisión**
|
||||
```gherkin
|
||||
DADO que el usuario está en una pregunta
|
||||
Y tiene dudas sobre su respuesta
|
||||
CUANDO hace click en "🚩 Marcar"
|
||||
ENTONCES la pregunta se marca con bandera en el navegador
|
||||
Y puede volver fácilmente a revisarla antes de enviar
|
||||
Y puede desmarcarla con otro click
|
||||
```
|
||||
|
||||
**Escenario 6: Quiz con tiempo límite expira**
|
||||
```gherkin
|
||||
DADO que el quiz tiene límite de 15 minutos
|
||||
Y el usuario está respondiendo
|
||||
CUANDO el timer llega a 0:00
|
||||
ENTONCES el quiz se envía automáticamente
|
||||
Y se muestra "Tiempo agotado"
|
||||
Y se califica con las respuestas hasta el momento
|
||||
Y preguntas sin responder cuentan como incorrectas
|
||||
```
|
||||
|
||||
**Escenario 7: Enviar quiz manualmente**
|
||||
```gherkin
|
||||
DADO que el usuario respondió todas las preguntas
|
||||
CUANDO hace click en "Finalizar Quiz"
|
||||
ENTONCES se muestra modal de confirmación
|
||||
Y muestra resumen: respondidas, sin responder, marcadas
|
||||
Y al confirmar, se envía al backend
|
||||
Y se calcula la puntuación
|
||||
Y se muestra pantalla de resultados
|
||||
```
|
||||
|
||||
**Escenario 8: Aprobar quiz**
|
||||
```gherkin
|
||||
DADO que el usuario envió el quiz
|
||||
Y obtuvo 85% de puntuación
|
||||
Y la puntuación mínima es 70%
|
||||
ENTONCES se muestra "¡QUIZ APROBADO!"
|
||||
Y se otorgan +30 XP
|
||||
Y se marca la lección quiz como completada
|
||||
Y se desbloquea siguiente contenido
|
||||
Y se actualiza progreso del curso
|
||||
```
|
||||
|
||||
**Escenario 9: Reprobar quiz con intentos disponibles**
|
||||
```gherkin
|
||||
DADO que el usuario obtuvo 65% (no aprobó)
|
||||
Y quedan 2 intentos disponibles
|
||||
ENTONCES se muestra "Quiz Reprobado"
|
||||
Y se muestra puntuación obtenida
|
||||
Y se muestra "Intentos restantes: 2"
|
||||
Y se muestra botón "Reintentar"
|
||||
Y NO se desbloquea siguiente contenido
|
||||
Y NO se otorga XP
|
||||
```
|
||||
|
||||
**Escenario 10: Reprobar quiz sin intentos**
|
||||
```gherkin
|
||||
DADO que el usuario agotó todos los intentos
|
||||
Y no aprobó el quiz
|
||||
ENTONCES se muestra mensaje "Sin intentos disponibles"
|
||||
Y se sugiere "Repasa las lecciones y contacta a soporte"
|
||||
Y el siguiente contenido permanece bloqueado
|
||||
Y se registra el bloqueo para seguimiento
|
||||
```
|
||||
|
||||
**Escenario 11: Ver explicaciones de respuestas**
|
||||
```gherkin
|
||||
DADO que el usuario completó el quiz
|
||||
CUANDO hace click en "Ver respuestas y explicaciones"
|
||||
ENTONCES se muestran todas las preguntas
|
||||
Y se destacan respuestas correctas en verde
|
||||
Y se destacan respuestas incorrectas en rojo
|
||||
Y se muestra la respuesta correcta
|
||||
Y se muestra explicación detallada de cada respuesta
|
||||
Y se sugieren lecciones relacionadas para repasar
|
||||
```
|
||||
|
||||
**Escenario 12: Reintentar quiz**
|
||||
```gherkin
|
||||
DADO que el usuario reprobó el quiz
|
||||
Y tiene intentos disponibles
|
||||
CUANDO hace click en "Reintentar"
|
||||
ENTONCES se inicia un nuevo intento
|
||||
Y las preguntas pueden estar en diferente orden
|
||||
Y las opciones pueden estar en diferente orden
|
||||
Y sus respuestas anteriores NO están pre-seleccionadas
|
||||
Y el contador de intentos se decrementa
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Auto-save de respuestas cada 30s (protección contra pérdida)
|
||||
- [ ] Advertencia antes de salir de la página sin enviar
|
||||
- [ ] Modo revisión: ver quiz aprobado sin poder cambiar respuestas
|
||||
- [ ] Estadísticas del quiz: % de aprobación, tiempo promedio
|
||||
- [ ] Preguntas con imágenes embebidas
|
||||
- [ ] Tipo de pregunta: multiple select (varias correctas)
|
||||
- [ ] Puntuación parcial en multiple select
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-015: Tabla education.quizzes
|
||||
- [ ] DB-EDU-016: Tabla education.questions (FK a quiz)
|
||||
- [ ] DB-EDU-017: Tabla education.question_options
|
||||
- [ ] DB-EDU-018: Tabla education.quiz_attempts
|
||||
- [ ] DB-EDU-019: Tabla education.quiz_answers (respuestas del usuario)
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-039: Endpoint GET /education/quizzes/:id
|
||||
- [ ] BE-EDU-040: Endpoint POST /education/quizzes/:id/start
|
||||
- [ ] BE-EDU-041: Endpoint POST /education/quizzes/:id/submit
|
||||
- [ ] BE-EDU-042: Endpoint GET /education/quizzes/:id/attempts
|
||||
- [ ] BE-EDU-043: Endpoint GET /education/quizzes/:id/results/:attemptId
|
||||
- [ ] BE-EDU-044: Implementar QuizService.gradeAttempt()
|
||||
- [ ] BE-EDU-045: Implementar shuffle de preguntas y opciones
|
||||
- [ ] BE-EDU-046: Validar intentos disponibles
|
||||
- [ ] BE-EDU-047: Implementar timer y auto-submit
|
||||
- [ ] BE-EDU-048: NO devolver respuestas correctas en GET (seguridad)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-048: Crear QuizIntroPage.tsx
|
||||
- [ ] FE-EDU-049: Crear QuizPlayerPage.tsx
|
||||
- [ ] FE-EDU-050: Crear componente QuestionRenderer.tsx
|
||||
- [ ] FE-EDU-051: Crear componente QuizNavigator.tsx (minimap)
|
||||
- [ ] FE-EDU-052: Crear componente QuizTimer.tsx
|
||||
- [ ] FE-EDU-053: Crear QuizResultsPage.tsx
|
||||
- [ ] FE-EDU-054: Crear componente AnswerExplanation.tsx
|
||||
- [ ] FE-EDU-055: Modal de confirmación de envío
|
||||
- [ ] FE-EDU-056: Auto-save de respuestas cada 30s
|
||||
- [ ] FE-EDU-057: Advertencia antes de salir (window.onbeforeunload)
|
||||
- [ ] FE-EDU-058: Implementar quizStore (Zustand)
|
||||
- [ ] FE-EDU-059: Animación de celebración al aprobar
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-021: Test calificación de quiz
|
||||
- [ ] TEST-EDU-022: Test aprobar quiz otorga XP
|
||||
- [ ] TEST-EDU-023: Test auto-submit cuando expira tiempo
|
||||
- [ ] TEST-EDU-024: Test límite de intentos
|
||||
- [ ] TEST-EDU-025: Test E2E realizar quiz completo
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
|
||||
- [ ] RF-EDU-004: Sistema de quizzes
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-005: Completar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoint GET /quizzes/:id (SIN respuestas correctas):**
|
||||
```typescript
|
||||
{
|
||||
quiz: {
|
||||
id: "quiz-1",
|
||||
title: "Fundamentos de Fibonacci",
|
||||
description: "Evalúa tu conocimiento...",
|
||||
timeLimit: 15, // minutos
|
||||
passingScore: 70, // 0-100
|
||||
maxAttempts: 3,
|
||||
questionCount: 10,
|
||||
totalPoints: 10,
|
||||
shuffleQuestions: true,
|
||||
shuffleOptions: true,
|
||||
mode: "assessment",
|
||||
|
||||
questions: [
|
||||
{
|
||||
id: "q-1",
|
||||
question: "¿Cuál es el nivel más utilizado?",
|
||||
type: "multiple_choice",
|
||||
points: 1,
|
||||
options: [
|
||||
{ id: "opt-1", text: "23.6%" },
|
||||
{ id: "opt-2", text: "38.2%" },
|
||||
{ id: "opt-3", text: "61.8%" },
|
||||
{ id: "opt-4", text: "78.6%" }
|
||||
]
|
||||
// NO incluir isCorrect en GET
|
||||
}
|
||||
],
|
||||
|
||||
userAttempts: [
|
||||
{
|
||||
attemptNumber: 1,
|
||||
score: 65,
|
||||
passed: false,
|
||||
submittedAt: "2025-12-03T10:30:00Z"
|
||||
}
|
||||
],
|
||||
attemptsRemaining: 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Endpoint POST /quizzes/:id/submit:**
|
||||
```typescript
|
||||
// Request
|
||||
{
|
||||
answers: {
|
||||
"q-1": "opt-3",
|
||||
"q-2": "opt-1",
|
||||
"q-3": ["opt-2", "opt-4"], // Multiple select
|
||||
// ...
|
||||
},
|
||||
timeSpent: 632 // segundos
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
attempt: {
|
||||
id: "attempt-uuid",
|
||||
quizId: "quiz-1",
|
||||
attemptNumber: 2,
|
||||
score: 85,
|
||||
passed: true,
|
||||
pointsEarned: 8.5,
|
||||
totalPoints: 10,
|
||||
submittedAt: "2025-12-05T16:45:00Z"
|
||||
},
|
||||
rewards: {
|
||||
xpEarned: 30,
|
||||
bonusXP: 20, // Si es 100%
|
||||
totalXP: 50
|
||||
},
|
||||
answers: [
|
||||
{
|
||||
questionId: "q-1",
|
||||
userAnswer: "opt-3",
|
||||
correctAnswer: "opt-3",
|
||||
isCorrect: true,
|
||||
pointsEarned: 1,
|
||||
explanation: "61.8% es el nivel dorado..."
|
||||
}
|
||||
// ...
|
||||
],
|
||||
attemptsRemaining: 1,
|
||||
canRetake: true
|
||||
}
|
||||
```
|
||||
|
||||
**Reglas de calificación:**
|
||||
- Multiple choice: 1 punto si es correcta, 0 si no
|
||||
- Multiple select: puntos parciales (0.5 si acierta 2 de 4)
|
||||
- True/False: 1 punto si es correcta
|
||||
- Sin responder: 0 puntos
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.quizzes`
|
||||
- `education.questions`
|
||||
- `education.question_options`
|
||||
- `education.quiz_attempts`
|
||||
- `education.quiz_answers`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-006"
|
||||
title: "Realizar Quiz"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-002"
|
||||
story_points: 5
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-006: Realizar Quiz
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-006 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P1 |
|
||||
| **Story Points** | 5 |
|
||||
| **Sprint** | Sprint 4 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario inscrito en un curso,
|
||||
**quiero** realizar quizzes interactivos al final de cada módulo,
|
||||
**para** validar mi conocimiento, recibir feedback y aprobar las evaluaciones requeridas.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a quizzes que evalúan el conocimiento adquirido en las lecciones. Debe poder responder preguntas de múltiple opción, navegar entre preguntas, revisar sus respuestas antes de enviar, recibir calificación inmediata, ver explicaciones de respuestas correctas/incorrectas, y reintentar si no aprobó (según intentos permitidos).
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
[PANTALLA DE INTRODUCCIÓN AL QUIZ]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver al curso │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📝 QUIZ: FUNDAMENTOS DE FIBONACCI │
|
||||
│ │
|
||||
│ Módulo 1: Introducción a Fibonacci │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📊 Información del Quiz │ │
|
||||
│ │ │ │
|
||||
│ │ Preguntas: 10 │ │
|
||||
│ │ Tiempo límite: 15 minutos │ │
|
||||
│ │ Puntuación para aprobar: 70% │ │
|
||||
│ │ Intentos disponibles: 2 de 3 │ │
|
||||
│ │ Tipo: Evaluación │ │
|
||||
│ │ │ │
|
||||
│ │ ✓ Las preguntas están en orden aleatorio │ │
|
||||
│ │ ✓ Verás tus respuestas al finalizar │ │
|
||||
│ │ ✓ Puedes navegar libremente entre preguntas │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [🚀 Comenzar Quiz] │
|
||||
│ │
|
||||
│ Intentos anteriores: │
|
||||
│ • Intento 1: 65% - Reprobado (hace 2 días) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PANTALLA DE PREGUNTA]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Quiz: Fundamentos de Fibonacci ⏱ 12:45 restantes │
|
||||
│ Pregunta 3 de 10 ████████░░░░░░ 30% │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ¿Cuál es el nivel de retroceso de Fibonacci más utilizado? │
|
||||
│ │
|
||||
│ ○ 23.6% │
|
||||
│ ○ 38.2% │
|
||||
│ ● 61.8% ← Seleccionado │
|
||||
│ ○ 78.6% │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [← Anterior] [🚩 Marcar] [Siguiente →] │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ NAVEGADOR: │
|
||||
│ [●1] [●2] [●3] [○4] [○5] [○6] [○7] [○8] [🚩9] [○10] │
|
||||
│ ↑ ↑ ↑ Sin Sin Sin Sin Sin Marca Sin │
|
||||
│ Resp Resp Actual resp resp resp resp resp da resp │
|
||||
│ │
|
||||
│ [Finalizar Quiz] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[CONFIRMACIÓN DE ENVÍO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ⚠ ¿Estás seguro? │
|
||||
│ │
|
||||
│ Estás a punto de enviar tu quiz. │
|
||||
│ │
|
||||
│ Resumen: │
|
||||
│ • Preguntas respondidas: 9 de 10 │
|
||||
│ • Preguntas sin responder: 1 │
|
||||
│ • Preguntas marcadas para revisar: 1 │
|
||||
│ │
|
||||
│ ⚠ Una vez enviado, no podrás cambiar tus respuestas. │
|
||||
│ │
|
||||
│ [Cancelar] [Enviar Quiz] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[RESULTADO - APROBADO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ✨ ✨ ✨ │
|
||||
│ │
|
||||
│ ✅ ¡QUIZ APROBADO! │
|
||||
│ │
|
||||
│ Puntuación: 85% │
|
||||
│ Preguntas correctas: 8.5/10 │
|
||||
│ │
|
||||
│ +30 XP ganados │
|
||||
│ │
|
||||
│ 🎯 Desglose: │
|
||||
│ • Preguntas correctas: 8 │
|
||||
│ • Preguntas incorrectas: 2 │
|
||||
│ • Puntuación requerida: 70% │
|
||||
│ │
|
||||
│ Tiempo invertido: 10:32 de 15:00 │
|
||||
│ Intento: 2 de 3 │
|
||||
│ │
|
||||
│ [Ver respuestas y explicaciones] │
|
||||
│ [Continuar al siguiente contenido →] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver introducción del quiz**
|
||||
```gherkin
|
||||
DADO que el usuario está inscrito en el curso
|
||||
CUANDO accede a una lección tipo quiz
|
||||
ENTONCES se muestra pantalla de introducción
|
||||
Y se muestra: número de preguntas, tiempo límite, puntuación para aprobar
|
||||
Y se muestran intentos disponibles
|
||||
Y se muestra historial de intentos anteriores (si existen)
|
||||
Y se muestra botón "Comenzar Quiz"
|
||||
```
|
||||
|
||||
**Escenario 2: Comenzar quiz**
|
||||
```gherkin
|
||||
DADO que el usuario está en la introducción del quiz
|
||||
CUANDO hace click en "Comenzar Quiz"
|
||||
ENTONCES se registra el inicio del intento en backend
|
||||
Y se navega a la primera pregunta
|
||||
Y se inicia el timer countdown
|
||||
Y se carga el navegador de preguntas
|
||||
Y se registra timestamp de inicio
|
||||
```
|
||||
|
||||
**Escenario 3: Responder pregunta de opción múltiple**
|
||||
```gherkin
|
||||
DADO que el usuario está en una pregunta
|
||||
CUANDO selecciona una opción
|
||||
ENTONCES la opción se marca visualmente
|
||||
Y la pregunta se marca como "respondida" en el navegador
|
||||
Y puede cambiar su respuesta antes de enviar
|
||||
Y la respuesta se guarda temporalmente en el estado
|
||||
```
|
||||
|
||||
**Escenario 4: Navegar entre preguntas**
|
||||
```gherkin
|
||||
DADO que el usuario respondió la pregunta 3
|
||||
CUANDO hace click en "Siguiente"
|
||||
ENTONCES navega a la pregunta 4
|
||||
Y su respuesta anterior se mantiene guardada
|
||||
Y puede volver a pregunta 3 con "Anterior"
|
||||
Y puede saltar a cualquier pregunta desde el navegador
|
||||
```
|
||||
|
||||
**Escenario 5: Marcar pregunta para revisión**
|
||||
```gherkin
|
||||
DADO que el usuario está en una pregunta
|
||||
Y tiene dudas sobre su respuesta
|
||||
CUANDO hace click en "🚩 Marcar"
|
||||
ENTONCES la pregunta se marca con bandera en el navegador
|
||||
Y puede volver fácilmente a revisarla antes de enviar
|
||||
Y puede desmarcarla con otro click
|
||||
```
|
||||
|
||||
**Escenario 6: Quiz con tiempo límite expira**
|
||||
```gherkin
|
||||
DADO que el quiz tiene límite de 15 minutos
|
||||
Y el usuario está respondiendo
|
||||
CUANDO el timer llega a 0:00
|
||||
ENTONCES el quiz se envía automáticamente
|
||||
Y se muestra "Tiempo agotado"
|
||||
Y se califica con las respuestas hasta el momento
|
||||
Y preguntas sin responder cuentan como incorrectas
|
||||
```
|
||||
|
||||
**Escenario 7: Enviar quiz manualmente**
|
||||
```gherkin
|
||||
DADO que el usuario respondió todas las preguntas
|
||||
CUANDO hace click en "Finalizar Quiz"
|
||||
ENTONCES se muestra modal de confirmación
|
||||
Y muestra resumen: respondidas, sin responder, marcadas
|
||||
Y al confirmar, se envía al backend
|
||||
Y se calcula la puntuación
|
||||
Y se muestra pantalla de resultados
|
||||
```
|
||||
|
||||
**Escenario 8: Aprobar quiz**
|
||||
```gherkin
|
||||
DADO que el usuario envió el quiz
|
||||
Y obtuvo 85% de puntuación
|
||||
Y la puntuación mínima es 70%
|
||||
ENTONCES se muestra "¡QUIZ APROBADO!"
|
||||
Y se otorgan +30 XP
|
||||
Y se marca la lección quiz como completada
|
||||
Y se desbloquea siguiente contenido
|
||||
Y se actualiza progreso del curso
|
||||
```
|
||||
|
||||
**Escenario 9: Reprobar quiz con intentos disponibles**
|
||||
```gherkin
|
||||
DADO que el usuario obtuvo 65% (no aprobó)
|
||||
Y quedan 2 intentos disponibles
|
||||
ENTONCES se muestra "Quiz Reprobado"
|
||||
Y se muestra puntuación obtenida
|
||||
Y se muestra "Intentos restantes: 2"
|
||||
Y se muestra botón "Reintentar"
|
||||
Y NO se desbloquea siguiente contenido
|
||||
Y NO se otorga XP
|
||||
```
|
||||
|
||||
**Escenario 10: Reprobar quiz sin intentos**
|
||||
```gherkin
|
||||
DADO que el usuario agotó todos los intentos
|
||||
Y no aprobó el quiz
|
||||
ENTONCES se muestra mensaje "Sin intentos disponibles"
|
||||
Y se sugiere "Repasa las lecciones y contacta a soporte"
|
||||
Y el siguiente contenido permanece bloqueado
|
||||
Y se registra el bloqueo para seguimiento
|
||||
```
|
||||
|
||||
**Escenario 11: Ver explicaciones de respuestas**
|
||||
```gherkin
|
||||
DADO que el usuario completó el quiz
|
||||
CUANDO hace click en "Ver respuestas y explicaciones"
|
||||
ENTONCES se muestran todas las preguntas
|
||||
Y se destacan respuestas correctas en verde
|
||||
Y se destacan respuestas incorrectas en rojo
|
||||
Y se muestra la respuesta correcta
|
||||
Y se muestra explicación detallada de cada respuesta
|
||||
Y se sugieren lecciones relacionadas para repasar
|
||||
```
|
||||
|
||||
**Escenario 12: Reintentar quiz**
|
||||
```gherkin
|
||||
DADO que el usuario reprobó el quiz
|
||||
Y tiene intentos disponibles
|
||||
CUANDO hace click en "Reintentar"
|
||||
ENTONCES se inicia un nuevo intento
|
||||
Y las preguntas pueden estar en diferente orden
|
||||
Y las opciones pueden estar en diferente orden
|
||||
Y sus respuestas anteriores NO están pre-seleccionadas
|
||||
Y el contador de intentos se decrementa
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Auto-save de respuestas cada 30s (protección contra pérdida)
|
||||
- [ ] Advertencia antes de salir de la página sin enviar
|
||||
- [ ] Modo revisión: ver quiz aprobado sin poder cambiar respuestas
|
||||
- [ ] Estadísticas del quiz: % de aprobación, tiempo promedio
|
||||
- [ ] Preguntas con imágenes embebidas
|
||||
- [ ] Tipo de pregunta: multiple select (varias correctas)
|
||||
- [ ] Puntuación parcial en multiple select
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-015: Tabla education.quizzes
|
||||
- [ ] DB-EDU-016: Tabla education.questions (FK a quiz)
|
||||
- [ ] DB-EDU-017: Tabla education.question_options
|
||||
- [ ] DB-EDU-018: Tabla education.quiz_attempts
|
||||
- [ ] DB-EDU-019: Tabla education.quiz_answers (respuestas del usuario)
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-039: Endpoint GET /education/quizzes/:id
|
||||
- [ ] BE-EDU-040: Endpoint POST /education/quizzes/:id/start
|
||||
- [ ] BE-EDU-041: Endpoint POST /education/quizzes/:id/submit
|
||||
- [ ] BE-EDU-042: Endpoint GET /education/quizzes/:id/attempts
|
||||
- [ ] BE-EDU-043: Endpoint GET /education/quizzes/:id/results/:attemptId
|
||||
- [ ] BE-EDU-044: Implementar QuizService.gradeAttempt()
|
||||
- [ ] BE-EDU-045: Implementar shuffle de preguntas y opciones
|
||||
- [ ] BE-EDU-046: Validar intentos disponibles
|
||||
- [ ] BE-EDU-047: Implementar timer y auto-submit
|
||||
- [ ] BE-EDU-048: NO devolver respuestas correctas en GET (seguridad)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-048: Crear QuizIntroPage.tsx
|
||||
- [ ] FE-EDU-049: Crear QuizPlayerPage.tsx
|
||||
- [ ] FE-EDU-050: Crear componente QuestionRenderer.tsx
|
||||
- [ ] FE-EDU-051: Crear componente QuizNavigator.tsx (minimap)
|
||||
- [ ] FE-EDU-052: Crear componente QuizTimer.tsx
|
||||
- [ ] FE-EDU-053: Crear QuizResultsPage.tsx
|
||||
- [ ] FE-EDU-054: Crear componente AnswerExplanation.tsx
|
||||
- [ ] FE-EDU-055: Modal de confirmación de envío
|
||||
- [ ] FE-EDU-056: Auto-save de respuestas cada 30s
|
||||
- [ ] FE-EDU-057: Advertencia antes de salir (window.onbeforeunload)
|
||||
- [ ] FE-EDU-058: Implementar quizStore (Zustand)
|
||||
- [ ] FE-EDU-059: Animación de celebración al aprobar
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-021: Test calificación de quiz
|
||||
- [ ] TEST-EDU-022: Test aprobar quiz otorga XP
|
||||
- [ ] TEST-EDU-023: Test auto-submit cuando expira tiempo
|
||||
- [ ] TEST-EDU-024: Test límite de intentos
|
||||
- [ ] TEST-EDU-025: Test E2E realizar quiz completo
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
|
||||
- [ ] RF-EDU-004: Sistema de quizzes
|
||||
|
||||
**Bloquea:**
|
||||
- [ ] US-EDU-005: Completar lección
|
||||
- [ ] US-EDU-007: Ver progreso
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoint GET /quizzes/:id (SIN respuestas correctas):**
|
||||
```typescript
|
||||
{
|
||||
quiz: {
|
||||
id: "quiz-1",
|
||||
title: "Fundamentos de Fibonacci",
|
||||
description: "Evalúa tu conocimiento...",
|
||||
timeLimit: 15, // minutos
|
||||
passingScore: 70, // 0-100
|
||||
maxAttempts: 3,
|
||||
questionCount: 10,
|
||||
totalPoints: 10,
|
||||
shuffleQuestions: true,
|
||||
shuffleOptions: true,
|
||||
mode: "assessment",
|
||||
|
||||
questions: [
|
||||
{
|
||||
id: "q-1",
|
||||
question: "¿Cuál es el nivel más utilizado?",
|
||||
type: "multiple_choice",
|
||||
points: 1,
|
||||
options: [
|
||||
{ id: "opt-1", text: "23.6%" },
|
||||
{ id: "opt-2", text: "38.2%" },
|
||||
{ id: "opt-3", text: "61.8%" },
|
||||
{ id: "opt-4", text: "78.6%" }
|
||||
]
|
||||
// NO incluir isCorrect en GET
|
||||
}
|
||||
],
|
||||
|
||||
userAttempts: [
|
||||
{
|
||||
attemptNumber: 1,
|
||||
score: 65,
|
||||
passed: false,
|
||||
submittedAt: "2025-12-03T10:30:00Z"
|
||||
}
|
||||
],
|
||||
attemptsRemaining: 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Endpoint POST /quizzes/:id/submit:**
|
||||
```typescript
|
||||
// Request
|
||||
{
|
||||
answers: {
|
||||
"q-1": "opt-3",
|
||||
"q-2": "opt-1",
|
||||
"q-3": ["opt-2", "opt-4"], // Multiple select
|
||||
// ...
|
||||
},
|
||||
timeSpent: 632 // segundos
|
||||
}
|
||||
|
||||
// Response
|
||||
{
|
||||
attempt: {
|
||||
id: "attempt-uuid",
|
||||
quizId: "quiz-1",
|
||||
attemptNumber: 2,
|
||||
score: 85,
|
||||
passed: true,
|
||||
pointsEarned: 8.5,
|
||||
totalPoints: 10,
|
||||
submittedAt: "2025-12-05T16:45:00Z"
|
||||
},
|
||||
rewards: {
|
||||
xpEarned: 30,
|
||||
bonusXP: 20, // Si es 100%
|
||||
totalXP: 50
|
||||
},
|
||||
answers: [
|
||||
{
|
||||
questionId: "q-1",
|
||||
userAnswer: "opt-3",
|
||||
correctAnswer: "opt-3",
|
||||
isCorrect: true,
|
||||
pointsEarned: 1,
|
||||
explanation: "61.8% es el nivel dorado..."
|
||||
}
|
||||
// ...
|
||||
],
|
||||
attemptsRemaining: 1,
|
||||
canRetake: true
|
||||
}
|
||||
```
|
||||
|
||||
**Reglas de calificación:**
|
||||
- Multiple choice: 1 punto si es correcta, 0 si no
|
||||
- Multiple select: puntos parciales (0.5 si acierta 2 de 4)
|
||||
- True/False: 1 punto si es correcta
|
||||
- Sin responder: 0 puntos
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.quizzes`
|
||||
- `education.questions`
|
||||
- `education.question_options`
|
||||
- `education.quiz_attempts`
|
||||
- `education.quiz_answers`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,422 +1,434 @@
|
||||
# US-EDU-007: Ver Progreso Educativo
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-007 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 4 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario activo en la plataforma educativa,
|
||||
**quiero** ver un dashboard con mi progreso de aprendizaje completo,
|
||||
**para** monitorear mi avance, mantenerme motivado y planificar mi siguiente paso.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a un dashboard educativo centralizado que muestre métricas clave de su aprendizaje: cursos en progreso con porcentajes, cursos completados, total de lecciones vistas, horas de estudio, racha actual, nivel y XP, actividad reciente, calendario de aprendizaje, y sugerencias de qué estudiar a continuación.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ MI APRENDIZAJE │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ RESUMEN GENERAL │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ 🔄 3 │ │ ✅ 12 │ │ 📚 156 │ │ ⏱ 42h │ │ │
|
||||
│ │ │ En prog│ │ Comple │ │ Lecc. │ │ Estudio│ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────────────┐ ┌─────────────────┐ │ │
|
||||
│ │ │ 🔥 RACHA: 12 DÍAS │ │ ⭐ NIVEL 15 │ │ │
|
||||
│ │ │ ¡Sigue así! │ │ 2,450 / 3,000 XP│ │ │
|
||||
│ │ │ 📅📅📅📅📅📅📅 │ │ ████████░░ 82% │ │ │
|
||||
│ │ └───────────────────┘ └─────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ CURSOS EN PROGRESO (3) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ [IMG] Fibonacci Retracement Básico │ │ │
|
||||
│ │ │ ████████████████░░░░ 75% completado │ │ │
|
||||
│ │ │ 18 de 23 lecciones | Módulo 4 de 5 │ │ │
|
||||
│ │ │ Última vez: Hace 2 horas │ │ │
|
||||
│ │ │ [Continuar →] Lección: Fibonacci en tendencias │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ [IMG] Day Trading para Principiantes │ │ │
|
||||
│ │ │ ██████░░░░░░░░░░░░░░ 30% completado │ │ │
|
||||
│ │ │ 9 de 30 lecciones | Módulo 2 de 6 │ │ │
|
||||
│ │ │ Última vez: Hace 1 día │ │ │
|
||||
│ │ │ [Continuar →] Lección: Análisis de volumen │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ [Ver todos los cursos en progreso] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ ACTIVIDAD RECIENTE │ │ CALENDARIO DE APRENDIZAJE │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Hoy, 15:30 │ │ L M X J V S D │ │
|
||||
│ │ ✓ Completaste │ │ ██ ██ ░░ ██ ██ ██ ░░ │ │
|
||||
│ │ "Niveles de Fib."│ │ ██ ██ ██ ██ ██ ░░ ░░ │ │
|
||||
│ │ +10 XP │ │ ██ ██ ██ ░░ ░░ ░░ ░░ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Hoy, 14:20 │ │ Días activos este mes: 18 │ │
|
||||
│ │ ✅ Aprobaste │ │ │ │
|
||||
│ │ Quiz Módulo 3 │ │ │ │
|
||||
│ │ +30 XP │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Ayer, 18:45 │ │ │ │
|
||||
│ │ 🎓 Obtuviste │ │ │ │
|
||||
│ │ badge "Week │ │ │ │
|
||||
│ │ Warrior" │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ [Ver más] │ │ │ │
|
||||
│ └─────────────────────┘ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📊 ESTADÍSTICAS │ │
|
||||
│ │ │ │
|
||||
│ │ Tiempo promedio por lección: 15 min │ │
|
||||
│ │ Cursos completados este mes: 3 │ │
|
||||
│ │ Tasa de completitud: 80% (12 de 15 cursos iniciados) │ │
|
||||
│ │ Categoría favorita: Análisis Técnico (6 cursos) │ │
|
||||
│ │ Mejor día de la semana: Miércoles (25 lecciones) │ │
|
||||
│ │ │ │
|
||||
│ │ [📈 Ver estadísticas detalladas] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver dashboard de progreso**
|
||||
```gherkin
|
||||
DADO que el usuario está autenticado
|
||||
CUANDO navega a /education/progress o /education/dashboard
|
||||
ENTONCES se muestra dashboard completo de aprendizaje
|
||||
Y se muestran métricas: cursos en progreso, completados, lecciones, horas
|
||||
Y se muestra racha actual con visualización de días
|
||||
Y se muestra nivel actual y progreso a siguiente nivel
|
||||
Y se muestran cursos en progreso con porcentajes
|
||||
Y se muestra actividad reciente
|
||||
```
|
||||
|
||||
**Escenario 2: Ver cursos en progreso**
|
||||
```gherkin
|
||||
DADO que el usuario tiene 3 cursos en progreso
|
||||
CUANDO ve la sección "Cursos en progreso"
|
||||
ENTONCES se muestran los 3 cursos ordenados por última actividad
|
||||
Y cada curso muestra: título, imagen, porcentaje, lecciones completadas
|
||||
Y se muestra "Última vez: hace X tiempo"
|
||||
Y se muestra botón "Continuar" con próxima lección
|
||||
Y al hacer click en "Continuar", navega a esa lección
|
||||
```
|
||||
|
||||
**Escenario 3: Ver racha activa**
|
||||
```gherkin
|
||||
DADO que el usuario tiene racha de 12 días
|
||||
CUANDO ve el widget de racha
|
||||
ENTONCES se muestra "🔥 RACHA: 12 DÍAS"
|
||||
Y se muestra visualización de últimos 7 días
|
||||
Y se muestra mensaje motivacional "¡Sigue así!"
|
||||
Y si no completó lección hoy, muestra "Completa 1 lección hoy para mantener racha"
|
||||
```
|
||||
|
||||
**Escenario 4: Ver nivel y XP**
|
||||
```gherkin
|
||||
DADO que el usuario es nivel 15 con 2450 XP
|
||||
Y necesita 3000 XP para nivel 16
|
||||
CUANDO ve el widget de nivel
|
||||
ENTONCES se muestra "⭐ NIVEL 15"
|
||||
Y se muestra "2,450 / 3,000 XP"
|
||||
Y se muestra barra de progreso al 82%
|
||||
Y se muestra cuántos XP faltan: "Faltan 550 XP para nivel 16"
|
||||
```
|
||||
|
||||
**Escenario 5: Ver calendario de actividad**
|
||||
```gherkin
|
||||
DADO que el usuario completó lecciones en 18 días este mes
|
||||
CUANDO ve el calendario
|
||||
ENTONCES se muestra grid estilo GitHub contributions
|
||||
Y días con actividad están resaltados (cuadros llenos)
|
||||
Y días sin actividad están vacíos
|
||||
Y al hacer hover sobre un día, muestra detalle: "3 lecciones, 45 min"
|
||||
Y se muestra contador "Días activos este mes: 18"
|
||||
```
|
||||
|
||||
**Escenario 6: Ver actividad reciente**
|
||||
```gherkin
|
||||
DADO que el usuario tiene actividad reciente
|
||||
CUANDO ve la sección "Actividad reciente"
|
||||
ENTONCES se muestran últimos 10 eventos
|
||||
Y cada evento muestra: timestamp, tipo, descripción, XP ganado
|
||||
Y eventos incluyen: lección completada, quiz aprobado, badge obtenido
|
||||
Y se ordenan de más reciente a más antiguo
|
||||
Y hay botón "Ver más" para ver historial completo
|
||||
```
|
||||
|
||||
**Escenario 7: Ver estadísticas**
|
||||
```gherkin
|
||||
DADO que el usuario tiene suficiente actividad
|
||||
CUANDO ve la sección "Estadísticas"
|
||||
ENTONCES se muestra tiempo promedio por lección
|
||||
Y se muestra cursos completados este mes
|
||||
Y se muestra tasa de completitud (cursos finalizados / iniciados)
|
||||
Y se muestra categoría favorita
|
||||
Y se muestra mejor día de la semana
|
||||
Y hay botón para ver estadísticas detalladas
|
||||
```
|
||||
|
||||
**Escenario 8: Continuar curso desde dashboard**
|
||||
```gherkin
|
||||
DADO que el usuario tiene curso en progreso
|
||||
Y la última lección accedida fue "Lección 18"
|
||||
CUANDO hace click en "Continuar" del curso
|
||||
ENTONCES navega directamente a la Lección 19 (siguiente)
|
||||
Y el reproductor se carga listo para comenzar
|
||||
```
|
||||
|
||||
**Escenario 9: Usuario sin actividad reciente**
|
||||
```gherkin
|
||||
DADO que el usuario no tiene actividad en últimos 7 días
|
||||
CUANDO accede al dashboard
|
||||
ENTONCES se muestra mensaje de bienvenida
|
||||
Y se sugieren cursos populares para comenzar
|
||||
Y se muestra motivación: "¡Comienza hoy tu racha de aprendizaje!"
|
||||
Y las métricas muestran valores cero elegantemente
|
||||
```
|
||||
|
||||
**Escenario 10: Racha en riesgo**
|
||||
```gherkin
|
||||
DADO que el usuario tiene racha de 15 días
|
||||
Y NO ha completado lecciones hoy
|
||||
Y es después de las 6pm hora local
|
||||
CUANDO accede al dashboard
|
||||
ENTONCES se muestra alerta "⚠ ¡Racha en riesgo!"
|
||||
Y se muestra "Completa 1 lección antes de medianoche"
|
||||
Y se sugiere lección corta: "Lección rápida (5 min): [título]"
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Gráfico de XP ganado por semana/mes
|
||||
- [ ] Comparación con usuarios similares (opcional)
|
||||
- [ ] Metas personales de aprendizaje
|
||||
- [ ] Exportar reporte de progreso en PDF
|
||||
- [ ] Compartir logros en redes sociales
|
||||
- [ ] Widget de "Próximas recompensas" (badge a desbloquear)
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-020: Vista materialized para stats de usuario
|
||||
- [ ] DB-EDU-021: Índices en user_activity_log por user_id + timestamp
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-049: Endpoint GET /education/progress/overview
|
||||
- [ ] BE-EDU-050: Endpoint GET /education/progress/courses
|
||||
- [ ] BE-EDU-051: Endpoint GET /education/progress/stats
|
||||
- [ ] BE-EDU-052: Endpoint GET /education/progress/activity
|
||||
- [ ] BE-EDU-053: Endpoint GET /education/progress/calendar
|
||||
- [ ] BE-EDU-054: Implementar ProgressService.getOverview()
|
||||
- [ ] BE-EDU-055: Job diario para calcular stats agregadas
|
||||
- [ ] BE-EDU-056: Caché de stats en Redis (15 min)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-060: Crear EducationDashboardPage.tsx
|
||||
- [ ] FE-EDU-061: Crear componente ProgressOverview.tsx
|
||||
- [ ] FE-EDU-062: Crear componente CourseProgressCard.tsx
|
||||
- [ ] FE-EDU-063: Crear componente StreakWidget.tsx
|
||||
- [ ] FE-EDU-064: Crear componente LevelWidget.tsx
|
||||
- [ ] FE-EDU-065: Crear componente ActivityCalendar.tsx (GitHub style)
|
||||
- [ ] FE-EDU-066: Crear componente RecentActivity.tsx
|
||||
- [ ] FE-EDU-067: Crear componente StatsPanel.tsx
|
||||
- [ ] FE-EDU-068: Implementar progressStore (Zustand)
|
||||
- [ ] FE-EDU-069: Skeleton loaders para cada sección
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-026: Test cálculo de tasa de completitud
|
||||
- [ ] TEST-EDU-027: Test cálculo de racha
|
||||
- [ ] TEST-EDU-028: Test stats agregadas
|
||||
- [ ] TEST-EDU-029: Test E2E visualizar dashboard completo
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
|
||||
- [ ] RF-EDU-003: Sistema de progreso
|
||||
- [ ] RF-EDU-006: Gamificación (para nivel y XP)
|
||||
|
||||
**Bloquea:**
|
||||
- Ninguna (es página de visualización)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoint GET /education/progress/overview:**
|
||||
```typescript
|
||||
{
|
||||
overview: {
|
||||
coursesInProgress: 3,
|
||||
coursesCompleted: 12,
|
||||
coursesSaved: 5,
|
||||
lessonsCompleted: 156,
|
||||
totalLearningTime: 2520, // minutos (42h)
|
||||
currentStreak: 12,
|
||||
longestStreak: 18,
|
||||
totalXP: 2450,
|
||||
currentLevel: 15,
|
||||
xpToNextLevel: 550
|
||||
},
|
||||
|
||||
coursesInProgress: [
|
||||
{
|
||||
courseId: "course-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
thumbnail: "...",
|
||||
progressPercent: 75,
|
||||
lessonsCompleted: 18,
|
||||
totalLessons: 23,
|
||||
modulesCompleted: 3,
|
||||
totalModules: 5,
|
||||
lastAccessedAt: "2025-12-05T13:30:00Z",
|
||||
nextLesson: {
|
||||
id: "les-19",
|
||||
title: "Fibonacci en tendencias bajistas",
|
||||
slug: "fibonacci-tendencias-bajistas",
|
||||
duration: 12
|
||||
}
|
||||
}
|
||||
// ... más cursos
|
||||
],
|
||||
|
||||
recentActivity: [
|
||||
{
|
||||
type: "lesson_completed",
|
||||
title: "Completaste 'Niveles de Fibonacci'",
|
||||
description: "Lección 2.1 del curso Fibonacci Básico",
|
||||
timestamp: "2025-12-05T15:30:00Z",
|
||||
xpEarned: 10,
|
||||
icon: "✓"
|
||||
},
|
||||
{
|
||||
type: "quiz_passed",
|
||||
title: "Aprobaste Quiz Módulo 3",
|
||||
description: "Puntuación: 85%",
|
||||
timestamp: "2025-12-05T14:20:00Z",
|
||||
xpEarned: 30,
|
||||
icon: "✅"
|
||||
}
|
||||
// ... más actividad
|
||||
],
|
||||
|
||||
calendar: [
|
||||
{ date: "2025-12-01", lessonsCompleted: 3, minutesLearned: 45 },
|
||||
{ date: "2025-12-02", lessonsCompleted: 2, minutesLearned: 30 },
|
||||
{ date: "2025-12-03", lessonsCompleted: 0, minutesLearned: 0 },
|
||||
// ...
|
||||
],
|
||||
|
||||
stats: {
|
||||
avgTimePerLesson: 15,
|
||||
coursesThisMonth: 3,
|
||||
completionRate: 80, // 12 completados de 15 iniciados
|
||||
activeDays: 18,
|
||||
favoriteCategory: "Análisis Técnico",
|
||||
bestDayOfWeek: "Wednesday",
|
||||
preferredTimeOfDay: "Evening"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Cálculos importantes:**
|
||||
```javascript
|
||||
// Racha actual
|
||||
currentStreak = countConsecutiveDaysWithActivity(today, lookback=365);
|
||||
|
||||
// Tasa de completitud
|
||||
completionRate = (coursesCompleted / coursesStarted) * 100;
|
||||
|
||||
// Tiempo promedio por lección
|
||||
avgTimePerLesson = totalLearningTime / lessonsCompleted;
|
||||
|
||||
// Categoría favorita
|
||||
favoriteCategory = categoryWithMostCompletedCourses();
|
||||
```
|
||||
|
||||
**Optimizaciones:**
|
||||
- Usar materialized views para stats agregadas
|
||||
- Calcular stats en background job nocturno
|
||||
- Cachear overview en Redis (15 min)
|
||||
- Lazy load de secciones con IntersectionObserver
|
||||
- Implementar skeleton loading para mejor UX
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.user_course_progress`
|
||||
- `education.user_lesson_progress`
|
||||
- `education.user_activity_log`
|
||||
- `gamification.user_stats`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-007"
|
||||
title: "Ver Progreso Educativo"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
epic: "OQI-002"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-007: Ver Progreso Educativo
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-007 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P0 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 4 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario activo en la plataforma educativa,
|
||||
**quiero** ver un dashboard con mi progreso de aprendizaje completo,
|
||||
**para** monitorear mi avance, mantenerme motivado y planificar mi siguiente paso.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe poder acceder a un dashboard educativo centralizado que muestre métricas clave de su aprendizaje: cursos en progreso con porcentajes, cursos completados, total de lecciones vistas, horas de estudio, racha actual, nivel y XP, actividad reciente, calendario de aprendizaje, y sugerencias de qué estudiar a continuación.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ MI APRENDIZAJE │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ RESUMEN GENERAL │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ 🔄 3 │ │ ✅ 12 │ │ 📚 156 │ │ ⏱ 42h │ │ │
|
||||
│ │ │ En prog│ │ Comple │ │ Lecc. │ │ Estudio│ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────────────┐ ┌─────────────────┐ │ │
|
||||
│ │ │ 🔥 RACHA: 12 DÍAS │ │ ⭐ NIVEL 15 │ │ │
|
||||
│ │ │ ¡Sigue así! │ │ 2,450 / 3,000 XP│ │ │
|
||||
│ │ │ 📅📅📅📅📅📅📅 │ │ ████████░░ 82% │ │ │
|
||||
│ │ └───────────────────┘ └─────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ CURSOS EN PROGRESO (3) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ [IMG] Fibonacci Retracement Básico │ │ │
|
||||
│ │ │ ████████████████░░░░ 75% completado │ │ │
|
||||
│ │ │ 18 de 23 lecciones | Módulo 4 de 5 │ │ │
|
||||
│ │ │ Última vez: Hace 2 horas │ │ │
|
||||
│ │ │ [Continuar →] Lección: Fibonacci en tendencias │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ [IMG] Day Trading para Principiantes │ │ │
|
||||
│ │ │ ██████░░░░░░░░░░░░░░ 30% completado │ │ │
|
||||
│ │ │ 9 de 30 lecciones | Módulo 2 de 6 │ │ │
|
||||
│ │ │ Última vez: Hace 1 día │ │ │
|
||||
│ │ │ [Continuar →] Lección: Análisis de volumen │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ [Ver todos los cursos en progreso] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ ACTIVIDAD RECIENTE │ │ CALENDARIO DE APRENDIZAJE │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Hoy, 15:30 │ │ L M X J V S D │ │
|
||||
│ │ ✓ Completaste │ │ ██ ██ ░░ ██ ██ ██ ░░ │ │
|
||||
│ │ "Niveles de Fib."│ │ ██ ██ ██ ██ ██ ░░ ░░ │ │
|
||||
│ │ +10 XP │ │ ██ ██ ██ ░░ ░░ ░░ ░░ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Hoy, 14:20 │ │ Días activos este mes: 18 │ │
|
||||
│ │ ✅ Aprobaste │ │ │ │
|
||||
│ │ Quiz Módulo 3 │ │ │ │
|
||||
│ │ +30 XP │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Ayer, 18:45 │ │ │ │
|
||||
│ │ 🎓 Obtuviste │ │ │ │
|
||||
│ │ badge "Week │ │ │ │
|
||||
│ │ Warrior" │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ [Ver más] │ │ │ │
|
||||
│ └─────────────────────┘ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 📊 ESTADÍSTICAS │ │
|
||||
│ │ │ │
|
||||
│ │ Tiempo promedio por lección: 15 min │ │
|
||||
│ │ Cursos completados este mes: 3 │ │
|
||||
│ │ Tasa de completitud: 80% (12 de 15 cursos iniciados) │ │
|
||||
│ │ Categoría favorita: Análisis Técnico (6 cursos) │ │
|
||||
│ │ Mejor día de la semana: Miércoles (25 lecciones) │ │
|
||||
│ │ │ │
|
||||
│ │ [📈 Ver estadísticas detalladas] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Ver dashboard de progreso**
|
||||
```gherkin
|
||||
DADO que el usuario está autenticado
|
||||
CUANDO navega a /education/progress o /education/dashboard
|
||||
ENTONCES se muestra dashboard completo de aprendizaje
|
||||
Y se muestran métricas: cursos en progreso, completados, lecciones, horas
|
||||
Y se muestra racha actual con visualización de días
|
||||
Y se muestra nivel actual y progreso a siguiente nivel
|
||||
Y se muestran cursos en progreso con porcentajes
|
||||
Y se muestra actividad reciente
|
||||
```
|
||||
|
||||
**Escenario 2: Ver cursos en progreso**
|
||||
```gherkin
|
||||
DADO que el usuario tiene 3 cursos en progreso
|
||||
CUANDO ve la sección "Cursos en progreso"
|
||||
ENTONCES se muestran los 3 cursos ordenados por última actividad
|
||||
Y cada curso muestra: título, imagen, porcentaje, lecciones completadas
|
||||
Y se muestra "Última vez: hace X tiempo"
|
||||
Y se muestra botón "Continuar" con próxima lección
|
||||
Y al hacer click en "Continuar", navega a esa lección
|
||||
```
|
||||
|
||||
**Escenario 3: Ver racha activa**
|
||||
```gherkin
|
||||
DADO que el usuario tiene racha de 12 días
|
||||
CUANDO ve el widget de racha
|
||||
ENTONCES se muestra "🔥 RACHA: 12 DÍAS"
|
||||
Y se muestra visualización de últimos 7 días
|
||||
Y se muestra mensaje motivacional "¡Sigue así!"
|
||||
Y si no completó lección hoy, muestra "Completa 1 lección hoy para mantener racha"
|
||||
```
|
||||
|
||||
**Escenario 4: Ver nivel y XP**
|
||||
```gherkin
|
||||
DADO que el usuario es nivel 15 con 2450 XP
|
||||
Y necesita 3000 XP para nivel 16
|
||||
CUANDO ve el widget de nivel
|
||||
ENTONCES se muestra "⭐ NIVEL 15"
|
||||
Y se muestra "2,450 / 3,000 XP"
|
||||
Y se muestra barra de progreso al 82%
|
||||
Y se muestra cuántos XP faltan: "Faltan 550 XP para nivel 16"
|
||||
```
|
||||
|
||||
**Escenario 5: Ver calendario de actividad**
|
||||
```gherkin
|
||||
DADO que el usuario completó lecciones en 18 días este mes
|
||||
CUANDO ve el calendario
|
||||
ENTONCES se muestra grid estilo GitHub contributions
|
||||
Y días con actividad están resaltados (cuadros llenos)
|
||||
Y días sin actividad están vacíos
|
||||
Y al hacer hover sobre un día, muestra detalle: "3 lecciones, 45 min"
|
||||
Y se muestra contador "Días activos este mes: 18"
|
||||
```
|
||||
|
||||
**Escenario 6: Ver actividad reciente**
|
||||
```gherkin
|
||||
DADO que el usuario tiene actividad reciente
|
||||
CUANDO ve la sección "Actividad reciente"
|
||||
ENTONCES se muestran últimos 10 eventos
|
||||
Y cada evento muestra: timestamp, tipo, descripción, XP ganado
|
||||
Y eventos incluyen: lección completada, quiz aprobado, badge obtenido
|
||||
Y se ordenan de más reciente a más antiguo
|
||||
Y hay botón "Ver más" para ver historial completo
|
||||
```
|
||||
|
||||
**Escenario 7: Ver estadísticas**
|
||||
```gherkin
|
||||
DADO que el usuario tiene suficiente actividad
|
||||
CUANDO ve la sección "Estadísticas"
|
||||
ENTONCES se muestra tiempo promedio por lección
|
||||
Y se muestra cursos completados este mes
|
||||
Y se muestra tasa de completitud (cursos finalizados / iniciados)
|
||||
Y se muestra categoría favorita
|
||||
Y se muestra mejor día de la semana
|
||||
Y hay botón para ver estadísticas detalladas
|
||||
```
|
||||
|
||||
**Escenario 8: Continuar curso desde dashboard**
|
||||
```gherkin
|
||||
DADO que el usuario tiene curso en progreso
|
||||
Y la última lección accedida fue "Lección 18"
|
||||
CUANDO hace click en "Continuar" del curso
|
||||
ENTONCES navega directamente a la Lección 19 (siguiente)
|
||||
Y el reproductor se carga listo para comenzar
|
||||
```
|
||||
|
||||
**Escenario 9: Usuario sin actividad reciente**
|
||||
```gherkin
|
||||
DADO que el usuario no tiene actividad en últimos 7 días
|
||||
CUANDO accede al dashboard
|
||||
ENTONCES se muestra mensaje de bienvenida
|
||||
Y se sugieren cursos populares para comenzar
|
||||
Y se muestra motivación: "¡Comienza hoy tu racha de aprendizaje!"
|
||||
Y las métricas muestran valores cero elegantemente
|
||||
```
|
||||
|
||||
**Escenario 10: Racha en riesgo**
|
||||
```gherkin
|
||||
DADO que el usuario tiene racha de 15 días
|
||||
Y NO ha completado lecciones hoy
|
||||
Y es después de las 6pm hora local
|
||||
CUANDO accede al dashboard
|
||||
ENTONCES se muestra alerta "⚠ ¡Racha en riesgo!"
|
||||
Y se muestra "Completa 1 lección antes de medianoche"
|
||||
Y se sugiere lección corta: "Lección rápida (5 min): [título]"
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Gráfico de XP ganado por semana/mes
|
||||
- [ ] Comparación con usuarios similares (opcional)
|
||||
- [ ] Metas personales de aprendizaje
|
||||
- [ ] Exportar reporte de progreso en PDF
|
||||
- [ ] Compartir logros en redes sociales
|
||||
- [ ] Widget de "Próximas recompensas" (badge a desbloquear)
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-020: Vista materialized para stats de usuario
|
||||
- [ ] DB-EDU-021: Índices en user_activity_log por user_id + timestamp
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-049: Endpoint GET /education/progress/overview
|
||||
- [ ] BE-EDU-050: Endpoint GET /education/progress/courses
|
||||
- [ ] BE-EDU-051: Endpoint GET /education/progress/stats
|
||||
- [ ] BE-EDU-052: Endpoint GET /education/progress/activity
|
||||
- [ ] BE-EDU-053: Endpoint GET /education/progress/calendar
|
||||
- [ ] BE-EDU-054: Implementar ProgressService.getOverview()
|
||||
- [ ] BE-EDU-055: Job diario para calcular stats agregadas
|
||||
- [ ] BE-EDU-056: Caché de stats en Redis (15 min)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-060: Crear EducationDashboardPage.tsx
|
||||
- [ ] FE-EDU-061: Crear componente ProgressOverview.tsx
|
||||
- [ ] FE-EDU-062: Crear componente CourseProgressCard.tsx
|
||||
- [ ] FE-EDU-063: Crear componente StreakWidget.tsx
|
||||
- [ ] FE-EDU-064: Crear componente LevelWidget.tsx
|
||||
- [ ] FE-EDU-065: Crear componente ActivityCalendar.tsx (GitHub style)
|
||||
- [ ] FE-EDU-066: Crear componente RecentActivity.tsx
|
||||
- [ ] FE-EDU-067: Crear componente StatsPanel.tsx
|
||||
- [ ] FE-EDU-068: Implementar progressStore (Zustand)
|
||||
- [ ] FE-EDU-069: Skeleton loaders para cada sección
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-026: Test cálculo de tasa de completitud
|
||||
- [ ] TEST-EDU-027: Test cálculo de racha
|
||||
- [ ] TEST-EDU-028: Test stats agregadas
|
||||
- [ ] TEST-EDU-029: Test E2E visualizar dashboard completo
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
|
||||
- [ ] RF-EDU-003: Sistema de progreso
|
||||
- [ ] RF-EDU-006: Gamificación (para nivel y XP)
|
||||
|
||||
**Bloquea:**
|
||||
- Ninguna (es página de visualización)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Endpoint GET /education/progress/overview:**
|
||||
```typescript
|
||||
{
|
||||
overview: {
|
||||
coursesInProgress: 3,
|
||||
coursesCompleted: 12,
|
||||
coursesSaved: 5,
|
||||
lessonsCompleted: 156,
|
||||
totalLearningTime: 2520, // minutos (42h)
|
||||
currentStreak: 12,
|
||||
longestStreak: 18,
|
||||
totalXP: 2450,
|
||||
currentLevel: 15,
|
||||
xpToNextLevel: 550
|
||||
},
|
||||
|
||||
coursesInProgress: [
|
||||
{
|
||||
courseId: "course-1",
|
||||
title: "Fibonacci Retracement Básico",
|
||||
slug: "fibonacci-retracement-basico",
|
||||
thumbnail: "...",
|
||||
progressPercent: 75,
|
||||
lessonsCompleted: 18,
|
||||
totalLessons: 23,
|
||||
modulesCompleted: 3,
|
||||
totalModules: 5,
|
||||
lastAccessedAt: "2025-12-05T13:30:00Z",
|
||||
nextLesson: {
|
||||
id: "les-19",
|
||||
title: "Fibonacci en tendencias bajistas",
|
||||
slug: "fibonacci-tendencias-bajistas",
|
||||
duration: 12
|
||||
}
|
||||
}
|
||||
// ... más cursos
|
||||
],
|
||||
|
||||
recentActivity: [
|
||||
{
|
||||
type: "lesson_completed",
|
||||
title: "Completaste 'Niveles de Fibonacci'",
|
||||
description: "Lección 2.1 del curso Fibonacci Básico",
|
||||
timestamp: "2025-12-05T15:30:00Z",
|
||||
xpEarned: 10,
|
||||
icon: "✓"
|
||||
},
|
||||
{
|
||||
type: "quiz_passed",
|
||||
title: "Aprobaste Quiz Módulo 3",
|
||||
description: "Puntuación: 85%",
|
||||
timestamp: "2025-12-05T14:20:00Z",
|
||||
xpEarned: 30,
|
||||
icon: "✅"
|
||||
}
|
||||
// ... más actividad
|
||||
],
|
||||
|
||||
calendar: [
|
||||
{ date: "2025-12-01", lessonsCompleted: 3, minutesLearned: 45 },
|
||||
{ date: "2025-12-02", lessonsCompleted: 2, minutesLearned: 30 },
|
||||
{ date: "2025-12-03", lessonsCompleted: 0, minutesLearned: 0 },
|
||||
// ...
|
||||
],
|
||||
|
||||
stats: {
|
||||
avgTimePerLesson: 15,
|
||||
coursesThisMonth: 3,
|
||||
completionRate: 80, // 12 completados de 15 iniciados
|
||||
activeDays: 18,
|
||||
favoriteCategory: "Análisis Técnico",
|
||||
bestDayOfWeek: "Wednesday",
|
||||
preferredTimeOfDay: "Evening"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Cálculos importantes:**
|
||||
```javascript
|
||||
// Racha actual
|
||||
currentStreak = countConsecutiveDaysWithActivity(today, lookback=365);
|
||||
|
||||
// Tasa de completitud
|
||||
completionRate = (coursesCompleted / coursesStarted) * 100;
|
||||
|
||||
// Tiempo promedio por lección
|
||||
avgTimePerLesson = totalLearningTime / lessonsCompleted;
|
||||
|
||||
// Categoría favorita
|
||||
favoriteCategory = categoryWithMostCompletedCourses();
|
||||
```
|
||||
|
||||
**Optimizaciones:**
|
||||
- Usar materialized views para stats agregadas
|
||||
- Calcular stats en background job nocturno
|
||||
- Cachear overview en Redis (15 min)
|
||||
- Lazy load de secciones con IntersectionObserver
|
||||
- Implementar skeleton loading para mejor UX
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.user_course_progress`
|
||||
- `education.user_lesson_progress`
|
||||
- `education.user_activity_log`
|
||||
- `gamification.user_stats`
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,437 +1,449 @@
|
||||
# US-EDU-008: Obtener Certificado
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-008 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P2 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 5 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario que completó un curso,
|
||||
**quiero** obtener un certificado digital verificable,
|
||||
**para** validar mi logro, agregarlo a mi perfil profesional y compartirlo en redes sociales.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe recibir automáticamente un certificado digital en formato PDF al completar el 100% de un curso. El certificado debe tener diseño profesional con logo de OrbiQuant, nombre del usuario, título del curso, fecha de finalización, ID único de verificación, y QR code. El usuario debe poder descargar el PDF, compartir en LinkedIn, y el certificado debe ser verificable públicamente.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
[MODAL DE CURSO COMPLETADO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ✨ 🎓 ✨ │
|
||||
│ │
|
||||
│ ¡FELICIDADES! CURSO COMPLETADO │
|
||||
│ │
|
||||
│ Fibonacci Retracement Básico │
|
||||
│ │
|
||||
│ +200 XP ganados │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ [PREVIEW DEL CERTIFICADO] │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────┐ │ │
|
||||
│ │ │ [LOGO ORBIQUANT] │ │ │
|
||||
│ │ │ CERTIFICADO DE FINALIZACIÓN │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ Se certifica que │ │ │
|
||||
│ │ │ JUAN PÉREZ │ │ │
|
||||
│ │ │ Ha completado exitosamente │ │ │
|
||||
│ │ │ "Fibonacci Retracement Básico" │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ 05/12/2025 │ │ │
|
||||
│ │ │ OQI-EDU-A3F8D291 │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ [Firma Instructor] [Firma OQI] │ │ │
|
||||
│ │ │ [QR CODE] │ │ │
|
||||
│ │ └──────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [📥 Descargar PDF] [💼 Compartir en LinkedIn] [✕ Cerrar] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PÁGINA DE CERTIFICADOS]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MIS CERTIFICADOS [🔍] │
|
||||
│ │
|
||||
│ Has obtenido 12 certificados │
|
||||
│ │
|
||||
│ Filtros: [Todos ▼] [Más recientes ▼] │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Fibonacci │ │ Day Trading │ │ Candlestick │ │
|
||||
│ │ Básico │ │ Pro │ │ Patterns │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ 05/12/2025 │ │ 28/11/2025 │ │ 15/11/2025 │ │
|
||||
│ │ OQI-...-291 │ │ OQI-...-8F2 │ │ OQI-...-4A1 │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ [Ver] [📥] │ │ [Ver] [📥] │ │ [Ver] [📥] │ │
|
||||
│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
│ [1] 2 3 4 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PÁGINA DE VERIFICACIÓN PÚBLICA]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [LOGO] OrbiQuant IA │
|
||||
│ │
|
||||
│ VERIFICACIÓN DE CERTIFICADO │
|
||||
│ │
|
||||
│ Certificado: OQI-EDU-A3F8D291 │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ✓ CERTIFICADO VÁLIDO │ │
|
||||
│ │ │ │
|
||||
│ │ Otorgado a: Juan Pérez │ │
|
||||
│ │ Curso: Fibonacci Retracement Básico │ │
|
||||
│ │ Categoría: Análisis Técnico │ │
|
||||
│ │ Fecha de finalización: 05/12/2025 │ │
|
||||
│ │ Duración del curso: 2.5 horas │ │
|
||||
│ │ Módulos: 5 | Lecciones: 23 │ │
|
||||
│ │ │ │
|
||||
│ │ Instructor: Carlos Mendoza │ │
|
||||
│ │ Institución: OrbiQuant IA │ │
|
||||
│ │ │ │
|
||||
│ │ Estado: ✅ Activo │ │
|
||||
│ │ Emitido: 05/12/2025 15:45:00 UTC │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Este certificado puede ser verificado en cualquier momento en: │
|
||||
│ orbiquant.com/verify/OQI-EDU-A3F8D291 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Completar curso genera certificado**
|
||||
```gherkin
|
||||
DADO que el usuario completó todas las lecciones de un curso
|
||||
Y aprobó todos los quizzes obligatorios
|
||||
CUANDO se marca la última lección como completada
|
||||
ENTONCES se genera automáticamente un certificado
|
||||
Y se registra en backend con ID único (OQI-EDU-XXXXXXXX)
|
||||
Y se genera PDF con diseño profesional
|
||||
Y se almacena PDF en S3 o CDN
|
||||
Y se muestra modal de felicitación
|
||||
Y se envía email con certificado adjunto
|
||||
```
|
||||
|
||||
**Escenario 2: Ver certificado en modal**
|
||||
```gherkin
|
||||
DADO que se generó el certificado
|
||||
CUANDO se muestra el modal de curso completado
|
||||
ENTONCES se muestra preview del certificado
|
||||
Y se muestra botón "Descargar PDF"
|
||||
Y se muestra botón "Compartir en LinkedIn"
|
||||
Y se muestra "Ver todos mis certificados"
|
||||
```
|
||||
|
||||
**Escenario 3: Descargar certificado en PDF**
|
||||
```gherkin
|
||||
DADO que el usuario tiene un certificado
|
||||
CUANDO hace click en "Descargar PDF"
|
||||
ENTONCES se descarga archivo PDF
|
||||
Y el PDF contiene:
|
||||
- Logo de OrbiQuant IA
|
||||
- Título "Certificado de Finalización"
|
||||
- Nombre completo del usuario
|
||||
- Título del curso
|
||||
- Fecha de finalización
|
||||
- ID único del certificado
|
||||
- Firmas digitales (instructor + plataforma)
|
||||
- QR code para verificación
|
||||
- Footer con URL de verificación
|
||||
```
|
||||
|
||||
**Escenario 4: Compartir en LinkedIn**
|
||||
```gherkin
|
||||
DADO que el usuario quiere compartir su certificado
|
||||
CUANDO hace click en "Compartir en LinkedIn"
|
||||
ENTONCES se abre nueva pestaña de LinkedIn
|
||||
Y el formulario de certificación está pre-llenado con:
|
||||
- Nombre: "Fibonacci Retracement Básico"
|
||||
- Organización: "OrbiQuant IA"
|
||||
- Fecha de emisión: "Diciembre 2025"
|
||||
- ID de certificado: "OQI-EDU-A3F8D291"
|
||||
- URL de verificación: "orbiquant.com/verify/..."
|
||||
```
|
||||
|
||||
**Escenario 5: Ver galería de certificados**
|
||||
```gherkin
|
||||
DADO que el usuario tiene 12 certificados
|
||||
CUANDO accede a /education/certificates
|
||||
ENTONCES se muestra galería de todos los certificados
|
||||
Y cada certificado muestra: thumbnail, título del curso, fecha
|
||||
Y se muestra contador "Has obtenido 12 certificados"
|
||||
Y se pueden filtrar por: categoría, fecha
|
||||
Y se pueden ordenar por: más reciente, alfabético
|
||||
```
|
||||
|
||||
**Escenario 6: Verificar certificado públicamente**
|
||||
```gherkin
|
||||
DADO que alguien tiene el ID de un certificado
|
||||
CUANDO accede a orbiquant.com/verify/OQI-EDU-A3F8D291
|
||||
ENTONCES se muestra página de verificación pública
|
||||
Y NO requiere login
|
||||
Y se muestra:
|
||||
- Estado: ✅ Válido
|
||||
- Nombre del usuario
|
||||
- Título del curso
|
||||
- Fecha de finalización
|
||||
- Detalles del curso
|
||||
Y se confirma autenticidad del certificado
|
||||
```
|
||||
|
||||
**Escenario 7: Verificar certificado inválido**
|
||||
```gherkin
|
||||
DADO que alguien accede con ID inválido
|
||||
CUANDO accede a /verify/INVALID-ID-123
|
||||
ENTONCES se muestra "Certificado no encontrado"
|
||||
Y se muestra sugerencia "Verifica el ID ingresado"
|
||||
Y se muestra link "¿Cómo verificar un certificado?"
|
||||
```
|
||||
|
||||
**Escenario 8: Email de certificado**
|
||||
```gherkin
|
||||
DADO que se generó un certificado
|
||||
CUANDO se envía el email
|
||||
ENTONCES el email contiene:
|
||||
- Asunto: "¡Felicidades! Certificado de [Curso]"
|
||||
- Mensaje de felicitación personalizado
|
||||
- Estadísticas: duración, lecciones completadas
|
||||
- PDF adjunto del certificado
|
||||
- Botones: Ver certificado, Compartir en LinkedIn
|
||||
- Sugerencias de próximos cursos relacionados
|
||||
```
|
||||
|
||||
**Escenario 9: Curso sin certificado disponible**
|
||||
```gherkin
|
||||
DADO que un curso está marcado como "no certifiable"
|
||||
CUANDO el usuario completa el curso
|
||||
ENTONCES NO se genera certificado
|
||||
Y se muestra "Curso completado" sin opción de certificado
|
||||
Y se explica "Este curso no otorga certificado"
|
||||
```
|
||||
|
||||
**Escenario 10: Certificado con requisitos adicionales**
|
||||
```gherkin
|
||||
DADO que un curso requiere quiz final aprobado
|
||||
Y el usuario completó todas las lecciones
|
||||
PERO no aprobó el quiz final
|
||||
CUANDO intenta obtener certificado
|
||||
ENTONCES se muestra "Debes aprobar el quiz final"
|
||||
Y se muestra score actual del quiz
|
||||
Y se muestra "Intentos restantes: X"
|
||||
Y el certificado NO se genera hasta aprobar
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Watermark en PDF para evitar falsificación
|
||||
- [ ] Blockchain verification (opcional, fase 2)
|
||||
- [ ] Traducción del certificado a inglés
|
||||
- [ ] Certificado físico por correo (premium)
|
||||
- [ ] Badge de LinkedIn auto-agregado via API
|
||||
- [ ] Opción de hacer certificado público/privado
|
||||
- [ ] Perfil público con todos los certificados del usuario
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-022: Tabla education.certificates
|
||||
- [ ] DB-EDU-023: Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status
|
||||
- [ ] DB-EDU-024: Tabla certificate_verifications (log de verificaciones)
|
||||
- [ ] DB-EDU-025: Índice único en certificate_number
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-057: Endpoint POST /education/certificates/generate
|
||||
- [ ] BE-EDU-058: Endpoint GET /education/certificates (del usuario)
|
||||
- [ ] BE-EDU-059: Endpoint GET /education/certificates/:id
|
||||
- [ ] BE-EDU-060: Endpoint GET /api/public/certificates/verify/:number
|
||||
- [ ] BE-EDU-061: Implementar CertificateService.generate()
|
||||
- [ ] BE-EDU-062: Generar PDF con Puppeteer o PDFKit
|
||||
- [ ] BE-EDU-063: Generar QR code con qrcode library
|
||||
- [ ] BE-EDU-064: Upload de PDF a S3 con signed URL
|
||||
- [ ] BE-EDU-065: Event handler en course completion
|
||||
- [ ] BE-EDU-066: Email service para enviar certificado
|
||||
- [ ] BE-EDU-067: Rate limiting en verificación (100/hora por IP)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-070: Modal CourseCompletedModal.tsx con preview
|
||||
- [ ] FE-EDU-071: Crear CertificatesPage.tsx
|
||||
- [ ] FE-EDU-072: Crear componente CertificateCard.tsx
|
||||
- [ ] FE-EDU-073: Crear VerifyCertificatePage.tsx (pública)
|
||||
- [ ] FE-EDU-074: Botón "Compartir en LinkedIn" con pre-fill
|
||||
- [ ] FE-EDU-075: Preview de PDF en modal
|
||||
- [ ] FE-EDU-076: Galería con filtros y búsqueda
|
||||
- [ ] FE-EDU-077: Implementar certificatesStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-030: Test generación de certificado
|
||||
- [ ] TEST-EDU-031: Test validación de certificado válido
|
||||
- [ ] TEST-EDU-032: Test verificación de certificado inválido
|
||||
- [ ] TEST-EDU-033: Test E2E completar curso y obtener certificado
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
|
||||
- [ ] RF-EDU-005: Sistema de certificados
|
||||
- [ ] PDF generation library (Puppeteer/PDFKit)
|
||||
- [ ] S3 bucket para almacenar PDFs
|
||||
- [ ] Email service
|
||||
|
||||
**Bloquea:**
|
||||
- Ninguna (es funcionalidad final)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Generación del certificado:**
|
||||
```javascript
|
||||
// Triggered on course completion
|
||||
async function onCourseCompleted(userId, courseId) {
|
||||
// 1. Validar requisitos
|
||||
const isEligible = await validateCertificateEligibility(userId, courseId);
|
||||
if (!isEligible) return;
|
||||
|
||||
// 2. Generar ID único
|
||||
const certificateNumber = generateCertificateId(); // OQI-EDU-A3F8D291
|
||||
|
||||
// 3. Generar PDF
|
||||
const pdfBuffer = await generateCertificatePDF({
|
||||
userName,
|
||||
courseName,
|
||||
completedDate,
|
||||
certificateNumber
|
||||
});
|
||||
|
||||
// 4. Upload a S3
|
||||
const pdfUrl = await uploadToS3(pdfBuffer, certificateNumber);
|
||||
|
||||
// 5. Guardar en DB
|
||||
await saveCertificate({
|
||||
userId,
|
||||
courseId,
|
||||
certificateNumber,
|
||||
pdfUrl
|
||||
});
|
||||
|
||||
// 6. Enviar email
|
||||
await sendCertificateEmail(userId, pdfUrl);
|
||||
|
||||
// 7. Otorgar XP bonus
|
||||
await awardXP(userId, 100, 'certificate_earned');
|
||||
}
|
||||
```
|
||||
|
||||
**Endpoint GET /api/public/certificates/verify/:number:**
|
||||
```typescript
|
||||
// Response para certificado válido
|
||||
{
|
||||
valid: true,
|
||||
certificate: {
|
||||
certificateNumber: "OQI-EDU-A3F8D291",
|
||||
recipientName: "Juan Pérez",
|
||||
courseTitle: "Fibonacci Retracement Básico",
|
||||
courseCategory: "Análisis Técnico",
|
||||
completedAt: "2025-12-05T15:45:00Z",
|
||||
issuedAt: "2025-12-05T15:45:00Z",
|
||||
courseDuration: 150, // minutos
|
||||
moduleCount: 5,
|
||||
lessonCount: 23,
|
||||
instructor: "Carlos Mendoza",
|
||||
status: "active"
|
||||
}
|
||||
}
|
||||
|
||||
// Response para certificado inválido
|
||||
{
|
||||
valid: false,
|
||||
error: "Certificate not found"
|
||||
}
|
||||
```
|
||||
|
||||
**Template del PDF:**
|
||||
- Usar HTML + CSS para diseño
|
||||
- Puppeteer para generar PDF desde HTML
|
||||
- Incluir logo en base64 para evitar carga externa
|
||||
- QR code generado con library qrcode.js
|
||||
- Firmas como imágenes PNG embebidas
|
||||
|
||||
**LinkedIn pre-fill URL:**
|
||||
```javascript
|
||||
const linkedInUrl = `https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=${encodeURIComponent(courseTitle)}&organizationName=OrbiQuant%20IA&issueYear=${year}&issueMonth=${month}&certUrl=${encodeURIComponent(verifyUrl)}&certId=${certificateNumber}`;
|
||||
```
|
||||
|
||||
**Seguridad:**
|
||||
- Rate limiting en endpoint de verificación
|
||||
- Signed URLs de S3 con expiración de 1 hora para descargas
|
||||
- No exponer lista de todos los certificados (solo del usuario logueado)
|
||||
- Validar que usuario solo puede descargar sus propios certificados
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.certificates`
|
||||
- `education.certificate_verifications` (log)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "US-EDU-008"
|
||||
title: "Obtener Certificado"
|
||||
type: "User Story"
|
||||
status: "Done"
|
||||
priority: "Media"
|
||||
epic: "OQI-002"
|
||||
story_points: 3
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# US-EDU-008: Obtener Certificado
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-EDU-008 |
|
||||
| **Épica** | OQI-002 - Módulo Educativo |
|
||||
| **Módulo** | education |
|
||||
| **Prioridad** | P2 |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | Sprint 5 |
|
||||
| **Estado** | Pendiente |
|
||||
| **Asignado a** | Por asignar |
|
||||
|
||||
---
|
||||
|
||||
## Historia de Usuario
|
||||
|
||||
**Como** usuario que completó un curso,
|
||||
**quiero** obtener un certificado digital verificable,
|
||||
**para** validar mi logro, agregarlo a mi perfil profesional y compartirlo en redes sociales.
|
||||
|
||||
## Descripción Detallada
|
||||
|
||||
El usuario debe recibir automáticamente un certificado digital en formato PDF al completar el 100% de un curso. El certificado debe tener diseño profesional con logo de OrbiQuant, nombre del usuario, título del curso, fecha de finalización, ID único de verificación, y QR code. El usuario debe poder descargar el PDF, compartir en LinkedIn, y el certificado debe ser verificable públicamente.
|
||||
|
||||
## Mockups/Wireframes
|
||||
|
||||
```
|
||||
[MODAL DE CURSO COMPLETADO]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ✨ 🎓 ✨ │
|
||||
│ │
|
||||
│ ¡FELICIDADES! CURSO COMPLETADO │
|
||||
│ │
|
||||
│ Fibonacci Retracement Básico │
|
||||
│ │
|
||||
│ +200 XP ganados │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ [PREVIEW DEL CERTIFICADO] │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────┐ │ │
|
||||
│ │ │ [LOGO ORBIQUANT] │ │ │
|
||||
│ │ │ CERTIFICADO DE FINALIZACIÓN │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ Se certifica que │ │ │
|
||||
│ │ │ JUAN PÉREZ │ │ │
|
||||
│ │ │ Ha completado exitosamente │ │ │
|
||||
│ │ │ "Fibonacci Retracement Básico" │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ 05/12/2025 │ │ │
|
||||
│ │ │ OQI-EDU-A3F8D291 │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ [Firma Instructor] [Firma OQI] │ │ │
|
||||
│ │ │ [QR CODE] │ │ │
|
||||
│ │ └──────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [📥 Descargar PDF] [💼 Compartir en LinkedIn] [✕ Cerrar] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PÁGINA DE CERTIFICADOS]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ MIS CERTIFICADOS [🔍] │
|
||||
│ │
|
||||
│ Has obtenido 12 certificados │
|
||||
│ │
|
||||
│ Filtros: [Todos ▼] [Más recientes ▼] │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Fibonacci │ │ Day Trading │ │ Candlestick │ │
|
||||
│ │ Básico │ │ Pro │ │ Patterns │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ 05/12/2025 │ │ 28/11/2025 │ │ 15/11/2025 │ │
|
||||
│ │ OQI-...-291 │ │ OQI-...-8F2 │ │ OQI-...-4A1 │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ [Ver] [📥] │ │ [Ver] [📥] │ │ [Ver] [📥] │ │
|
||||
│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
│ [1] 2 3 4 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[PÁGINA DE VERIFICACIÓN PÚBLICA]
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [LOGO] OrbiQuant IA │
|
||||
│ │
|
||||
│ VERIFICACIÓN DE CERTIFICADO │
|
||||
│ │
|
||||
│ Certificado: OQI-EDU-A3F8D291 │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ✓ CERTIFICADO VÁLIDO │ │
|
||||
│ │ │ │
|
||||
│ │ Otorgado a: Juan Pérez │ │
|
||||
│ │ Curso: Fibonacci Retracement Básico │ │
|
||||
│ │ Categoría: Análisis Técnico │ │
|
||||
│ │ Fecha de finalización: 05/12/2025 │ │
|
||||
│ │ Duración del curso: 2.5 horas │ │
|
||||
│ │ Módulos: 5 | Lecciones: 23 │ │
|
||||
│ │ │ │
|
||||
│ │ Instructor: Carlos Mendoza │ │
|
||||
│ │ Institución: OrbiQuant IA │ │
|
||||
│ │ │ │
|
||||
│ │ Estado: ✅ Activo │ │
|
||||
│ │ Emitido: 05/12/2025 15:45:00 UTC │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Este certificado puede ser verificado en cualquier momento en: │
|
||||
│ orbiquant.com/verify/OQI-EDU-A3F8D291 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
**Escenario 1: Completar curso genera certificado**
|
||||
```gherkin
|
||||
DADO que el usuario completó todas las lecciones de un curso
|
||||
Y aprobó todos los quizzes obligatorios
|
||||
CUANDO se marca la última lección como completada
|
||||
ENTONCES se genera automáticamente un certificado
|
||||
Y se registra en backend con ID único (OQI-EDU-XXXXXXXX)
|
||||
Y se genera PDF con diseño profesional
|
||||
Y se almacena PDF en S3 o CDN
|
||||
Y se muestra modal de felicitación
|
||||
Y se envía email con certificado adjunto
|
||||
```
|
||||
|
||||
**Escenario 2: Ver certificado en modal**
|
||||
```gherkin
|
||||
DADO que se generó el certificado
|
||||
CUANDO se muestra el modal de curso completado
|
||||
ENTONCES se muestra preview del certificado
|
||||
Y se muestra botón "Descargar PDF"
|
||||
Y se muestra botón "Compartir en LinkedIn"
|
||||
Y se muestra "Ver todos mis certificados"
|
||||
```
|
||||
|
||||
**Escenario 3: Descargar certificado en PDF**
|
||||
```gherkin
|
||||
DADO que el usuario tiene un certificado
|
||||
CUANDO hace click en "Descargar PDF"
|
||||
ENTONCES se descarga archivo PDF
|
||||
Y el PDF contiene:
|
||||
- Logo de OrbiQuant IA
|
||||
- Título "Certificado de Finalización"
|
||||
- Nombre completo del usuario
|
||||
- Título del curso
|
||||
- Fecha de finalización
|
||||
- ID único del certificado
|
||||
- Firmas digitales (instructor + plataforma)
|
||||
- QR code para verificación
|
||||
- Footer con URL de verificación
|
||||
```
|
||||
|
||||
**Escenario 4: Compartir en LinkedIn**
|
||||
```gherkin
|
||||
DADO que el usuario quiere compartir su certificado
|
||||
CUANDO hace click en "Compartir en LinkedIn"
|
||||
ENTONCES se abre nueva pestaña de LinkedIn
|
||||
Y el formulario de certificación está pre-llenado con:
|
||||
- Nombre: "Fibonacci Retracement Básico"
|
||||
- Organización: "OrbiQuant IA"
|
||||
- Fecha de emisión: "Diciembre 2025"
|
||||
- ID de certificado: "OQI-EDU-A3F8D291"
|
||||
- URL de verificación: "orbiquant.com/verify/..."
|
||||
```
|
||||
|
||||
**Escenario 5: Ver galería de certificados**
|
||||
```gherkin
|
||||
DADO que el usuario tiene 12 certificados
|
||||
CUANDO accede a /education/certificates
|
||||
ENTONCES se muestra galería de todos los certificados
|
||||
Y cada certificado muestra: thumbnail, título del curso, fecha
|
||||
Y se muestra contador "Has obtenido 12 certificados"
|
||||
Y se pueden filtrar por: categoría, fecha
|
||||
Y se pueden ordenar por: más reciente, alfabético
|
||||
```
|
||||
|
||||
**Escenario 6: Verificar certificado públicamente**
|
||||
```gherkin
|
||||
DADO que alguien tiene el ID de un certificado
|
||||
CUANDO accede a orbiquant.com/verify/OQI-EDU-A3F8D291
|
||||
ENTONCES se muestra página de verificación pública
|
||||
Y NO requiere login
|
||||
Y se muestra:
|
||||
- Estado: ✅ Válido
|
||||
- Nombre del usuario
|
||||
- Título del curso
|
||||
- Fecha de finalización
|
||||
- Detalles del curso
|
||||
Y se confirma autenticidad del certificado
|
||||
```
|
||||
|
||||
**Escenario 7: Verificar certificado inválido**
|
||||
```gherkin
|
||||
DADO que alguien accede con ID inválido
|
||||
CUANDO accede a /verify/INVALID-ID-123
|
||||
ENTONCES se muestra "Certificado no encontrado"
|
||||
Y se muestra sugerencia "Verifica el ID ingresado"
|
||||
Y se muestra link "¿Cómo verificar un certificado?"
|
||||
```
|
||||
|
||||
**Escenario 8: Email de certificado**
|
||||
```gherkin
|
||||
DADO que se generó un certificado
|
||||
CUANDO se envía el email
|
||||
ENTONCES el email contiene:
|
||||
- Asunto: "¡Felicidades! Certificado de [Curso]"
|
||||
- Mensaje de felicitación personalizado
|
||||
- Estadísticas: duración, lecciones completadas
|
||||
- PDF adjunto del certificado
|
||||
- Botones: Ver certificado, Compartir en LinkedIn
|
||||
- Sugerencias de próximos cursos relacionados
|
||||
```
|
||||
|
||||
**Escenario 9: Curso sin certificado disponible**
|
||||
```gherkin
|
||||
DADO que un curso está marcado como "no certifiable"
|
||||
CUANDO el usuario completa el curso
|
||||
ENTONCES NO se genera certificado
|
||||
Y se muestra "Curso completado" sin opción de certificado
|
||||
Y se explica "Este curso no otorga certificado"
|
||||
```
|
||||
|
||||
**Escenario 10: Certificado con requisitos adicionales**
|
||||
```gherkin
|
||||
DADO que un curso requiere quiz final aprobado
|
||||
Y el usuario completó todas las lecciones
|
||||
PERO no aprobó el quiz final
|
||||
CUANDO intenta obtener certificado
|
||||
ENTONCES se muestra "Debes aprobar el quiz final"
|
||||
Y se muestra score actual del quiz
|
||||
Y se muestra "Intentos restantes: X"
|
||||
Y el certificado NO se genera hasta aprobar
|
||||
```
|
||||
|
||||
## Criterios Adicionales
|
||||
|
||||
- [ ] Watermark en PDF para evitar falsificación
|
||||
- [ ] Blockchain verification (opcional, fase 2)
|
||||
- [ ] Traducción del certificado a inglés
|
||||
- [ ] Certificado físico por correo (premium)
|
||||
- [ ] Badge de LinkedIn auto-agregado via API
|
||||
- [ ] Opción de hacer certificado público/privado
|
||||
- [ ] Perfil público con todos los certificados del usuario
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] DB-EDU-022: Tabla education.certificates
|
||||
- [ ] DB-EDU-023: Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status
|
||||
- [ ] DB-EDU-024: Tabla certificate_verifications (log de verificaciones)
|
||||
- [ ] DB-EDU-025: Índice único en certificate_number
|
||||
|
||||
**Backend:**
|
||||
- [ ] BE-EDU-057: Endpoint POST /education/certificates/generate
|
||||
- [ ] BE-EDU-058: Endpoint GET /education/certificates (del usuario)
|
||||
- [ ] BE-EDU-059: Endpoint GET /education/certificates/:id
|
||||
- [ ] BE-EDU-060: Endpoint GET /api/public/certificates/verify/:number
|
||||
- [ ] BE-EDU-061: Implementar CertificateService.generate()
|
||||
- [ ] BE-EDU-062: Generar PDF con Puppeteer o PDFKit
|
||||
- [ ] BE-EDU-063: Generar QR code con qrcode library
|
||||
- [ ] BE-EDU-064: Upload de PDF a S3 con signed URL
|
||||
- [ ] BE-EDU-065: Event handler en course completion
|
||||
- [ ] BE-EDU-066: Email service para enviar certificado
|
||||
- [ ] BE-EDU-067: Rate limiting en verificación (100/hora por IP)
|
||||
|
||||
**Frontend:**
|
||||
- [ ] FE-EDU-070: Modal CourseCompletedModal.tsx con preview
|
||||
- [ ] FE-EDU-071: Crear CertificatesPage.tsx
|
||||
- [ ] FE-EDU-072: Crear componente CertificateCard.tsx
|
||||
- [ ] FE-EDU-073: Crear VerifyCertificatePage.tsx (pública)
|
||||
- [ ] FE-EDU-074: Botón "Compartir en LinkedIn" con pre-fill
|
||||
- [ ] FE-EDU-075: Preview de PDF en modal
|
||||
- [ ] FE-EDU-076: Galería con filtros y búsqueda
|
||||
- [ ] FE-EDU-077: Implementar certificatesStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] TEST-EDU-030: Test generación de certificado
|
||||
- [ ] TEST-EDU-031: Test validación de certificado válido
|
||||
- [ ] TEST-EDU-032: Test verificación de certificado inválido
|
||||
- [ ] TEST-EDU-033: Test E2E completar curso y obtener certificado
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Depende de:**
|
||||
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
|
||||
- [ ] RF-EDU-005: Sistema de certificados
|
||||
- [ ] PDF generation library (Puppeteer/PDFKit)
|
||||
- [ ] S3 bucket para almacenar PDFs
|
||||
- [ ] Email service
|
||||
|
||||
**Bloquea:**
|
||||
- Ninguna (es funcionalidad final)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
**Generación del certificado:**
|
||||
```javascript
|
||||
// Triggered on course completion
|
||||
async function onCourseCompleted(userId, courseId) {
|
||||
// 1. Validar requisitos
|
||||
const isEligible = await validateCertificateEligibility(userId, courseId);
|
||||
if (!isEligible) return;
|
||||
|
||||
// 2. Generar ID único
|
||||
const certificateNumber = generateCertificateId(); // OQI-EDU-A3F8D291
|
||||
|
||||
// 3. Generar PDF
|
||||
const pdfBuffer = await generateCertificatePDF({
|
||||
userName,
|
||||
courseName,
|
||||
completedDate,
|
||||
certificateNumber
|
||||
});
|
||||
|
||||
// 4. Upload a S3
|
||||
const pdfUrl = await uploadToS3(pdfBuffer, certificateNumber);
|
||||
|
||||
// 5. Guardar en DB
|
||||
await saveCertificate({
|
||||
userId,
|
||||
courseId,
|
||||
certificateNumber,
|
||||
pdfUrl
|
||||
});
|
||||
|
||||
// 6. Enviar email
|
||||
await sendCertificateEmail(userId, pdfUrl);
|
||||
|
||||
// 7. Otorgar XP bonus
|
||||
await awardXP(userId, 100, 'certificate_earned');
|
||||
}
|
||||
```
|
||||
|
||||
**Endpoint GET /api/public/certificates/verify/:number:**
|
||||
```typescript
|
||||
// Response para certificado válido
|
||||
{
|
||||
valid: true,
|
||||
certificate: {
|
||||
certificateNumber: "OQI-EDU-A3F8D291",
|
||||
recipientName: "Juan Pérez",
|
||||
courseTitle: "Fibonacci Retracement Básico",
|
||||
courseCategory: "Análisis Técnico",
|
||||
completedAt: "2025-12-05T15:45:00Z",
|
||||
issuedAt: "2025-12-05T15:45:00Z",
|
||||
courseDuration: 150, // minutos
|
||||
moduleCount: 5,
|
||||
lessonCount: 23,
|
||||
instructor: "Carlos Mendoza",
|
||||
status: "active"
|
||||
}
|
||||
}
|
||||
|
||||
// Response para certificado inválido
|
||||
{
|
||||
valid: false,
|
||||
error: "Certificate not found"
|
||||
}
|
||||
```
|
||||
|
||||
**Template del PDF:**
|
||||
- Usar HTML + CSS para diseño
|
||||
- Puppeteer para generar PDF desde HTML
|
||||
- Incluir logo en base64 para evitar carga externa
|
||||
- QR code generado con library qrcode.js
|
||||
- Firmas como imágenes PNG embebidas
|
||||
|
||||
**LinkedIn pre-fill URL:**
|
||||
```javascript
|
||||
const linkedInUrl = `https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=${encodeURIComponent(courseTitle)}&organizationName=OrbiQuant%20IA&issueYear=${year}&issueMonth=${month}&certUrl=${encodeURIComponent(verifyUrl)}&certId=${certificateNumber}`;
|
||||
```
|
||||
|
||||
**Seguridad:**
|
||||
- Rate limiting en endpoint de verificación
|
||||
- Signed URLs de S3 con expiración de 1 hora para descargas
|
||||
- No exponer lista de todos los certificados (solo del usuario logueado)
|
||||
- Validar que usuario solo puede descargar sus propios certificados
|
||||
|
||||
**Entidades/Tablas:**
|
||||
- `education.certificates`
|
||||
- `education.certificate_verifications` (log)
|
||||
|
||||
---
|
||||
|
||||
## Definition of Ready (DoR)
|
||||
|
||||
- [x] Historia claramente escrita
|
||||
- [x] Criterios de aceptación definidos
|
||||
- [x] Story points estimados
|
||||
- [x] Dependencias identificadas
|
||||
- [x] Sin bloqueadores
|
||||
- [x] Diseño/mockup disponible
|
||||
- [x] API spec disponible
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
- [ ] Código implementado según criterios
|
||||
- [ ] Tests unitarios escritos y pasando
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Code review aprobado
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] QA aprobado
|
||||
- [ ] Desplegado en ambiente de pruebas
|
||||
|
||||
---
|
||||
|
||||
## Historial de Cambios
|
||||
|
||||
| Fecha | Cambio | Autor |
|
||||
|-------|--------|-------|
|
||||
| 2025-12-05 | Creación | Requirements-Analyst |
|
||||
|
||||
---
|
||||
|
||||
**Creada por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
# TRACEABILITY.yml - OQI-002 Módulo Educativo
|
||||
# Mapeo de requerimientos a implementación
|
||||
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
epic: OQI-002
|
||||
name: "Módulo Educativo - Cursos de Trading"
|
||||
updated: "2025-12-05"
|
||||
status: pending
|
||||
updated: "2026-01-04"
|
||||
status: in_progress
|
||||
|
||||
# Resumen de trazabilidad
|
||||
summary:
|
||||
@ -71,7 +71,14 @@ requirements:
|
||||
|
||||
RF-EDU-002:
|
||||
name: "Sistema de Lecciones"
|
||||
status: pending
|
||||
status: implemented
|
||||
implementation_notes:
|
||||
- date: "2026-01-04"
|
||||
changes:
|
||||
- "Creado Lesson.tsx para visualización de lecciones con video player"
|
||||
- "Soporte para contenido: video, article, text, quiz, exercise"
|
||||
- "Navegación entre lecciones con sidebar de progreso"
|
||||
- "Integrado en App.tsx con rutas /education/courses/:courseSlug/lesson/:lessonId"
|
||||
specs:
|
||||
- ET-EDU-001
|
||||
- ET-EDU-004
|
||||
@ -164,7 +171,15 @@ requirements:
|
||||
|
||||
RF-EDU-004:
|
||||
name: "Sistema de Quizzes"
|
||||
status: pending
|
||||
status: implemented
|
||||
implementation_notes:
|
||||
- date: "2026-01-04"
|
||||
changes:
|
||||
- "Creado Quiz.tsx con estados: intro, in_progress, submitted"
|
||||
- "Timer de cuenta regresiva para quizzes con límite de tiempo"
|
||||
- "Soporte para tipos: multiple_choice, true_false, multi_select"
|
||||
- "Sistema de puntos y XP integrado"
|
||||
- "Ruta: /education/courses/:courseSlug/lesson/:lessonId/quiz"
|
||||
specs:
|
||||
- ET-EDU-005
|
||||
user_stories:
|
||||
@ -462,3 +477,67 @@ notes:
|
||||
- "Certificados con QR de verificación único"
|
||||
- "Quizzes con intentos ilimitados pero score máximo registrado"
|
||||
- "Sistema de puntos: 10 por lección, 50 por quiz aprobado, 100 por certificado"
|
||||
|
||||
# Implementación 2026-01-04
|
||||
recent_changes:
|
||||
- date: "2026-01-04"
|
||||
developer: "Claude Code"
|
||||
session: 1
|
||||
changes:
|
||||
- type: frontend
|
||||
files:
|
||||
- apps/frontend/src/modules/education/pages/Lesson.tsx
|
||||
- apps/frontend/src/modules/education/pages/Quiz.tsx
|
||||
- apps/frontend/src/App.tsx (rutas añadidas)
|
||||
- apps/frontend/src/types/education.types.ts (props añadidas)
|
||||
description: "Páginas de lección y quiz implementadas"
|
||||
- type: database
|
||||
files:
|
||||
- apps/database/seeds/prod/education/01-education-courses.sql
|
||||
description: "Seeds de cursos ICT/IPDA con 1 curso, 7 módulos, 28 lecciones, 5 quizzes"
|
||||
issues_found:
|
||||
- "DDL loader no carga 00-extensions.sql ni 01-enums.sql correctamente"
|
||||
- "Script usa DB_NAME=orbiquant pero Docker usa orbiquant_trading/orbiquant_platform"
|
||||
|
||||
- date: "2026-01-04"
|
||||
developer: "Claude Code"
|
||||
session: 2
|
||||
changes:
|
||||
- type: database_scripts
|
||||
files:
|
||||
- apps/database/scripts/create-database.sh
|
||||
description: "Correcciones críticas al DDL loader"
|
||||
fixes:
|
||||
- "Agregada carga de 00-extensions.sql antes de enums"
|
||||
- "Búsqueda de enums en 00-enums.sql y 01-enums.sql"
|
||||
- "Defaults unificados: DB_NAME=orbiquantia_platform, DB_PORT=5433"
|
||||
- "Credenciales: orbiquantia/orbiquantia_dev_2025"
|
||||
- type: database_ddl
|
||||
files:
|
||||
- apps/database/ddl/schemas/auth/tables/01-users.sql
|
||||
- apps/database/ddl/schemas/auth/tables/99-deferred-constraints.sql (nuevo)
|
||||
description: "Resolución de dependencia circular en auth.users"
|
||||
fixes:
|
||||
- "Removido constraint password_or_oauth (PostgreSQL no soporta subqueries en CHECK)"
|
||||
- "Documentado en 99-deferred-constraints.sql con alternativa de trigger"
|
||||
- type: database_seeds
|
||||
files:
|
||||
- apps/database/seeds/prod/education/01-education-courses.sql
|
||||
description: "Correcciones de columnas en seeds de educación"
|
||||
fixes:
|
||||
- "Lecciones: removido duration_minutes (28 filas corregidas)"
|
||||
- "Quizzes: passing_score → passing_score_percentage"
|
||||
- "Questions: single_choice → multiple_choice"
|
||||
- "Questions: correct_answers removido (respuesta en options.isCorrect)"
|
||||
- "Questions: formato options actualizado a [{id,text,isCorrect}]"
|
||||
validation:
|
||||
status: passed
|
||||
database_recreated: true
|
||||
counts:
|
||||
categories: 5
|
||||
courses: 1
|
||||
modules: 7
|
||||
lessons: 28
|
||||
quizzes: 5
|
||||
questions: 14
|
||||
command: "DB_PORT=5433 ./drop-and-recreate-database.sh"
|
||||
|
||||
@ -1,285 +1,298 @@
|
||||
# RF-EDU-001: Catálogo de Cursos
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P0
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un catálogo completo de cursos educativos sobre trading e inversiones, con capacidades avanzadas de filtrado, búsqueda y categorización que permitan a los usuarios descubrir contenido relevante según su nivel de experiencia y áreas de interés.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-001.1: Listado de Cursos
|
||||
|
||||
El sistema debe:
|
||||
- Mostrar todos los cursos activos en formato de tarjetas (cards)
|
||||
- Incluir para cada curso: título, descripción breve, imagen, nivel, duración, módulos, progreso
|
||||
- Mostrar badge de "Nuevo" para cursos publicados en últimos 30 días
|
||||
- Mostrar badge de "En curso" para cursos iniciados por el usuario
|
||||
- Mostrar badge de "Completado" con porcentaje para cursos en progreso
|
||||
- Implementar paginación (12 cursos por página)
|
||||
|
||||
### RF-EDU-001.2: Categorías
|
||||
|
||||
El sistema debe soportar las siguientes categorías:
|
||||
|
||||
| Categoría | Slug | Descripción |
|
||||
|-----------|------|-------------|
|
||||
| Fundamentos | fundamentals | Conceptos básicos de trading |
|
||||
| Análisis Técnico | technical-analysis | Indicadores y patrones |
|
||||
| Análisis Fundamental | fundamental-analysis | Valoración de activos |
|
||||
| Gestión de Riesgo | risk-management | Money management |
|
||||
| Psicología del Trading | trading-psychology | Control emocional |
|
||||
| Estrategias Avanzadas | advanced-strategies | Sistemas complejos |
|
||||
| Criptomonedas | crypto | Trading de cripto |
|
||||
| IA y Trading | ai-trading | Machine Learning aplicado |
|
||||
|
||||
### RF-EDU-001.3: Niveles de Dificultad
|
||||
|
||||
El sistema debe clasificar cursos en:
|
||||
- **Principiante:** Sin conocimientos previos requeridos
|
||||
- **Intermedio:** Requiere conocimientos básicos
|
||||
- **Avanzado:** Para traders experimentados
|
||||
- **Experto:** Contenido especializado
|
||||
|
||||
### RF-EDU-001.4: Filtros
|
||||
|
||||
El sistema debe permitir filtrar por:
|
||||
- Categoría (múltiple selección)
|
||||
- Nivel de dificultad (múltiple selección)
|
||||
- Duración (rangos: <2h, 2-5h, 5-10h, >10h)
|
||||
- Estado: Nuevos, En curso, Completados, No iniciados
|
||||
- Instructor
|
||||
- Gratuitos vs Premium
|
||||
|
||||
### RF-EDU-001.5: Búsqueda
|
||||
|
||||
El sistema debe:
|
||||
- Implementar barra de búsqueda en tiempo real
|
||||
- Buscar en: título, descripción, tags, nombre de instructor
|
||||
- Mostrar resultados mientras el usuario escribe (debounce 300ms)
|
||||
- Resaltar términos coincidentes en resultados
|
||||
- Mostrar sugerencias de búsqueda basadas en términos populares
|
||||
- Guardar historial de búsquedas del usuario
|
||||
|
||||
### RF-EDU-001.6: Ordenamiento
|
||||
|
||||
El sistema debe permitir ordenar por:
|
||||
- Más recientes
|
||||
- Más populares (por número de estudiantes)
|
||||
- Mejor valorados (rating)
|
||||
- Duración (ascendente/descendente)
|
||||
- Alfabético (A-Z, Z-A)
|
||||
- Progreso del usuario (para cursos iniciados)
|
||||
|
||||
### RF-EDU-001.7: Recomendaciones
|
||||
|
||||
El sistema debe:
|
||||
- Mostrar sección "Recomendado para ti" basado en:
|
||||
- Cursos en progreso del usuario
|
||||
- Nivel de experiencia del perfil
|
||||
- Cursos completados previamente
|
||||
- Categorías de interés
|
||||
- Mostrar sección "Continuar aprendiendo" con cursos incompletos
|
||||
- Mostrar "Cursos relacionados" al ver detalle de curso
|
||||
|
||||
---
|
||||
|
||||
## Datos de Entrada
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| page | number | Número de página (default: 1) |
|
||||
| limit | number | Elementos por página (default: 12, max: 50) |
|
||||
| category | string[] | IDs de categorías a filtrar |
|
||||
| level | string[] | Niveles de dificultad |
|
||||
| search | string | Término de búsqueda |
|
||||
| sortBy | string | Campo de ordenamiento |
|
||||
| sortOrder | asc/desc | Dirección del ordenamiento |
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
shortDescription: string;
|
||||
thumbnail: string;
|
||||
category: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
icon: string;
|
||||
};
|
||||
level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
|
||||
duration: number; // minutos
|
||||
moduleCount: number;
|
||||
lessonCount: number;
|
||||
studentCount: number;
|
||||
rating: number; // 0-5
|
||||
reviewCount: number;
|
||||
instructor: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
title: string;
|
||||
};
|
||||
tags: string[];
|
||||
isPremium: boolean;
|
||||
publishedAt: string;
|
||||
userProgress?: {
|
||||
enrolledAt: string;
|
||||
progressPercent: number;
|
||||
lastAccessedAt: string;
|
||||
isCompleted: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface CatalogResponse {
|
||||
courses: Course[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
filters: {
|
||||
categories: Category[];
|
||||
levels: string[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Cursos activos:** Solo mostrar cursos con status 'published'
|
||||
2. **Acceso Premium:** Cursos premium requieren suscripción activa
|
||||
3. **Visibilidad:** Cursos draft solo visibles para instructores y admins
|
||||
4. **Límite de paginación:** Máximo 50 cursos por página
|
||||
5. **Caché:** Catálogo se cachea por 5 minutos
|
||||
6. **Búsqueda mínima:** Al menos 2 caracteres para búsqueda
|
||||
7. **Recomendaciones:** Máximo 6 cursos en sección recomendados
|
||||
8. **Orden por defecto:** Más recientes primero para usuarios nuevos
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario visualiza catálogo de cursos
|
||||
DADO que el usuario está autenticado
|
||||
Y está en la página de educación
|
||||
CUANDO accede a /education/courses
|
||||
ENTONCES se muestra un listado de cursos
|
||||
Y se muestran 12 cursos por página
|
||||
Y cada curso muestra: título, imagen, nivel, duración, rating
|
||||
Y se muestran filtros en sidebar izquierdo
|
||||
Y se muestra barra de búsqueda en header
|
||||
|
||||
Escenario: Usuario filtra por categoría
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO selecciona la categoría "Análisis Técnico"
|
||||
ENTONCES se muestran solo cursos de esa categoría
|
||||
Y el filtro se marca como activo
|
||||
Y la URL se actualiza con ?category=technical-analysis
|
||||
Y se mantienen otros filtros activos
|
||||
|
||||
Escenario: Usuario busca curso
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO escribe "fibonacci" en la búsqueda
|
||||
ENTONCES se muestran resultados en tiempo real
|
||||
Y se resalta el término "fibonacci" en resultados
|
||||
Y se muestra contador "X resultados para 'fibonacci'"
|
||||
|
||||
Escenario: Usuario sin resultados
|
||||
DADO que el usuario busca "xyz123"
|
||||
Y no hay cursos que coincidan
|
||||
ENTONCES se muestra mensaje "No se encontraron cursos"
|
||||
Y se sugieren búsquedas alternativas
|
||||
Y se muestran cursos populares como alternativa
|
||||
|
||||
Escenario: Ver cursos recomendados
|
||||
DADO que el usuario tiene cursos en progreso
|
||||
CUANDO accede al catálogo
|
||||
ENTONCES se muestra sección "Recomendado para ti"
|
||||
Y aparecen máximo 6 cursos relacionados
|
||||
Y se muestra sección "Continuar aprendiendo"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- Education API para datos de cursos
|
||||
- PostgreSQL schema education
|
||||
- Redis para caché de catálogo
|
||||
- Elasticsearch para búsqueda (opcional, mejora performance)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Implementar virtual scrolling para listas largas
|
||||
- Usar React Query para caché de frontend
|
||||
- Implementar skeleton loading durante carga
|
||||
- Optimizar imágenes con lazy loading
|
||||
- Considerar SSR para mejor SEO
|
||||
- Implementar analytics de búsquedas para mejorar recomendaciones
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema database: `/backend/src/database/schemas/education.sql`
|
||||
- API endpoints: `/backend/src/modules/courses/courses.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/Courses.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Verificar índices en education.courses (title, category_id, level, published_at)
|
||||
- [ ] Crear vista courses_catalog con joins pre-calculados
|
||||
- [ ] Implementar full-text search en PostgreSQL
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/courses con paginación y filtros
|
||||
- [ ] Endpoint GET /education/categories
|
||||
- [ ] Implementar CourseService.getCatalog()
|
||||
- [ ] Implementar sistema de recomendaciones básico
|
||||
- [ ] Agregar rate limiting a búsqueda
|
||||
- [ ] Implementar caché Redis para catálogo
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear página CoursesPage.tsx
|
||||
- [ ] Crear componente CourseCard.tsx
|
||||
- [ ] Crear componente CourseFilters.tsx
|
||||
- [ ] Crear componente SearchBar.tsx
|
||||
- [ ] Implementar coursesStore (Zustand)
|
||||
- [ ] Implementar infinite scroll opcional
|
||||
- [ ] Agregar analytics de búsqueda
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test unitario CourseService
|
||||
- [ ] Test integración GET /courses con filtros
|
||||
- [ ] Test E2E navegación y búsqueda
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "RF-EDU-001"
|
||||
title: "Catalogo de Cursos"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "education"
|
||||
epic: "OQI-002"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-EDU-001: Catálogo de Cursos
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P0
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un catálogo completo de cursos educativos sobre trading e inversiones, con capacidades avanzadas de filtrado, búsqueda y categorización que permitan a los usuarios descubrir contenido relevante según su nivel de experiencia y áreas de interés.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-001.1: Listado de Cursos
|
||||
|
||||
El sistema debe:
|
||||
- Mostrar todos los cursos activos en formato de tarjetas (cards)
|
||||
- Incluir para cada curso: título, descripción breve, imagen, nivel, duración, módulos, progreso
|
||||
- Mostrar badge de "Nuevo" para cursos publicados en últimos 30 días
|
||||
- Mostrar badge de "En curso" para cursos iniciados por el usuario
|
||||
- Mostrar badge de "Completado" con porcentaje para cursos en progreso
|
||||
- Implementar paginación (12 cursos por página)
|
||||
|
||||
### RF-EDU-001.2: Categorías
|
||||
|
||||
El sistema debe soportar las siguientes categorías:
|
||||
|
||||
| Categoría | Slug | Descripción |
|
||||
|-----------|------|-------------|
|
||||
| Fundamentos | fundamentals | Conceptos básicos de trading |
|
||||
| Análisis Técnico | technical-analysis | Indicadores y patrones |
|
||||
| Análisis Fundamental | fundamental-analysis | Valoración de activos |
|
||||
| Gestión de Riesgo | risk-management | Money management |
|
||||
| Psicología del Trading | trading-psychology | Control emocional |
|
||||
| Estrategias Avanzadas | advanced-strategies | Sistemas complejos |
|
||||
| Criptomonedas | crypto | Trading de cripto |
|
||||
| IA y Trading | ai-trading | Machine Learning aplicado |
|
||||
|
||||
### RF-EDU-001.3: Niveles de Dificultad
|
||||
|
||||
El sistema debe clasificar cursos en:
|
||||
- **Principiante:** Sin conocimientos previos requeridos
|
||||
- **Intermedio:** Requiere conocimientos básicos
|
||||
- **Avanzado:** Para traders experimentados
|
||||
- **Experto:** Contenido especializado
|
||||
|
||||
### RF-EDU-001.4: Filtros
|
||||
|
||||
El sistema debe permitir filtrar por:
|
||||
- Categoría (múltiple selección)
|
||||
- Nivel de dificultad (múltiple selección)
|
||||
- Duración (rangos: <2h, 2-5h, 5-10h, >10h)
|
||||
- Estado: Nuevos, En curso, Completados, No iniciados
|
||||
- Instructor
|
||||
- Gratuitos vs Premium
|
||||
|
||||
### RF-EDU-001.5: Búsqueda
|
||||
|
||||
El sistema debe:
|
||||
- Implementar barra de búsqueda en tiempo real
|
||||
- Buscar en: título, descripción, tags, nombre de instructor
|
||||
- Mostrar resultados mientras el usuario escribe (debounce 300ms)
|
||||
- Resaltar términos coincidentes en resultados
|
||||
- Mostrar sugerencias de búsqueda basadas en términos populares
|
||||
- Guardar historial de búsquedas del usuario
|
||||
|
||||
### RF-EDU-001.6: Ordenamiento
|
||||
|
||||
El sistema debe permitir ordenar por:
|
||||
- Más recientes
|
||||
- Más populares (por número de estudiantes)
|
||||
- Mejor valorados (rating)
|
||||
- Duración (ascendente/descendente)
|
||||
- Alfabético (A-Z, Z-A)
|
||||
- Progreso del usuario (para cursos iniciados)
|
||||
|
||||
### RF-EDU-001.7: Recomendaciones
|
||||
|
||||
El sistema debe:
|
||||
- Mostrar sección "Recomendado para ti" basado en:
|
||||
- Cursos en progreso del usuario
|
||||
- Nivel de experiencia del perfil
|
||||
- Cursos completados previamente
|
||||
- Categorías de interés
|
||||
- Mostrar sección "Continuar aprendiendo" con cursos incompletos
|
||||
- Mostrar "Cursos relacionados" al ver detalle de curso
|
||||
|
||||
---
|
||||
|
||||
## Datos de Entrada
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| page | number | Número de página (default: 1) |
|
||||
| limit | number | Elementos por página (default: 12, max: 50) |
|
||||
| category | string[] | IDs de categorías a filtrar |
|
||||
| level | string[] | Niveles de dificultad |
|
||||
| search | string | Término de búsqueda |
|
||||
| sortBy | string | Campo de ordenamiento |
|
||||
| sortOrder | asc/desc | Dirección del ordenamiento |
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
shortDescription: string;
|
||||
thumbnail: string;
|
||||
category: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
icon: string;
|
||||
};
|
||||
level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
|
||||
duration: number; // minutos
|
||||
moduleCount: number;
|
||||
lessonCount: number;
|
||||
studentCount: number;
|
||||
rating: number; // 0-5
|
||||
reviewCount: number;
|
||||
instructor: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
title: string;
|
||||
};
|
||||
tags: string[];
|
||||
isPremium: boolean;
|
||||
publishedAt: string;
|
||||
userProgress?: {
|
||||
enrolledAt: string;
|
||||
progressPercent: number;
|
||||
lastAccessedAt: string;
|
||||
isCompleted: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface CatalogResponse {
|
||||
courses: Course[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
filters: {
|
||||
categories: Category[];
|
||||
levels: string[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Cursos activos:** Solo mostrar cursos con status 'published'
|
||||
2. **Acceso Premium:** Cursos premium requieren suscripción activa
|
||||
3. **Visibilidad:** Cursos draft solo visibles para instructores y admins
|
||||
4. **Límite de paginación:** Máximo 50 cursos por página
|
||||
5. **Caché:** Catálogo se cachea por 5 minutos
|
||||
6. **Búsqueda mínima:** Al menos 2 caracteres para búsqueda
|
||||
7. **Recomendaciones:** Máximo 6 cursos en sección recomendados
|
||||
8. **Orden por defecto:** Más recientes primero para usuarios nuevos
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario visualiza catálogo de cursos
|
||||
DADO que el usuario está autenticado
|
||||
Y está en la página de educación
|
||||
CUANDO accede a /education/courses
|
||||
ENTONCES se muestra un listado de cursos
|
||||
Y se muestran 12 cursos por página
|
||||
Y cada curso muestra: título, imagen, nivel, duración, rating
|
||||
Y se muestran filtros en sidebar izquierdo
|
||||
Y se muestra barra de búsqueda en header
|
||||
|
||||
Escenario: Usuario filtra por categoría
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO selecciona la categoría "Análisis Técnico"
|
||||
ENTONCES se muestran solo cursos de esa categoría
|
||||
Y el filtro se marca como activo
|
||||
Y la URL se actualiza con ?category=technical-analysis
|
||||
Y se mantienen otros filtros activos
|
||||
|
||||
Escenario: Usuario busca curso
|
||||
DADO que el usuario está en el catálogo
|
||||
CUANDO escribe "fibonacci" en la búsqueda
|
||||
ENTONCES se muestran resultados en tiempo real
|
||||
Y se resalta el término "fibonacci" en resultados
|
||||
Y se muestra contador "X resultados para 'fibonacci'"
|
||||
|
||||
Escenario: Usuario sin resultados
|
||||
DADO que el usuario busca "xyz123"
|
||||
Y no hay cursos que coincidan
|
||||
ENTONCES se muestra mensaje "No se encontraron cursos"
|
||||
Y se sugieren búsquedas alternativas
|
||||
Y se muestran cursos populares como alternativa
|
||||
|
||||
Escenario: Ver cursos recomendados
|
||||
DADO que el usuario tiene cursos en progreso
|
||||
CUANDO accede al catálogo
|
||||
ENTONCES se muestra sección "Recomendado para ti"
|
||||
Y aparecen máximo 6 cursos relacionados
|
||||
Y se muestra sección "Continuar aprendiendo"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- Education API para datos de cursos
|
||||
- PostgreSQL schema education
|
||||
- Redis para caché de catálogo
|
||||
- Elasticsearch para búsqueda (opcional, mejora performance)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Implementar virtual scrolling para listas largas
|
||||
- Usar React Query para caché de frontend
|
||||
- Implementar skeleton loading durante carga
|
||||
- Optimizar imágenes con lazy loading
|
||||
- Considerar SSR para mejor SEO
|
||||
- Implementar analytics de búsquedas para mejorar recomendaciones
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema database: `/backend/src/database/schemas/education.sql`
|
||||
- API endpoints: `/backend/src/modules/courses/courses.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/Courses.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Verificar índices en education.courses (title, category_id, level, published_at)
|
||||
- [ ] Crear vista courses_catalog con joins pre-calculados
|
||||
- [ ] Implementar full-text search en PostgreSQL
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/courses con paginación y filtros
|
||||
- [ ] Endpoint GET /education/categories
|
||||
- [ ] Implementar CourseService.getCatalog()
|
||||
- [ ] Implementar sistema de recomendaciones básico
|
||||
- [ ] Agregar rate limiting a búsqueda
|
||||
- [ ] Implementar caché Redis para catálogo
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear página CoursesPage.tsx
|
||||
- [ ] Crear componente CourseCard.tsx
|
||||
- [ ] Crear componente CourseFilters.tsx
|
||||
- [ ] Crear componente SearchBar.tsx
|
||||
- [ ] Implementar coursesStore (Zustand)
|
||||
- [ ] Implementar infinite scroll opcional
|
||||
- [ ] Agregar analytics de búsqueda
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test unitario CourseService
|
||||
- [ ] Test integración GET /courses con filtros
|
||||
- [ ] Test E2E navegación y búsqueda
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,321 +1,334 @@
|
||||
# RF-EDU-002: Sistema de Lecciones
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P0
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un reproductor multimedia completo que permita a los usuarios consumir contenido educativo en múltiples formatos (video, texto, código interactivo, quizzes) con controles de navegación, seguimiento de progreso y experiencia de aprendizaje optimizada.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-002.1: Tipos de Lecciones
|
||||
|
||||
El sistema debe soportar:
|
||||
|
||||
| Tipo | Descripción | Características |
|
||||
|------|-------------|-----------------|
|
||||
| **Video** | Contenido en video | Reproductor, subtítulos, velocidad |
|
||||
| **Artículo** | Contenido de texto | Markdown, imágenes, código |
|
||||
| **Quiz** | Evaluación interactiva | Preguntas, feedback inmediato |
|
||||
| **Código** | Ejercicio práctico | Editor, ejecución, validación |
|
||||
| **Recursos** | Descargables | PDFs, hojas de cálculo, código |
|
||||
|
||||
### RF-EDU-002.2: Reproductor de Video
|
||||
|
||||
El sistema debe:
|
||||
- Reproducir videos desde CDN (Vimeo/YouTube/S3)
|
||||
- Controles: play/pause, volumen, pantalla completa
|
||||
- Velocidades: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
|
||||
- Subtítulos en español e inglés (opcional)
|
||||
- Recordar posición de reproducción
|
||||
- Saltar 10s adelante/atrás con teclas de flecha
|
||||
- Atajos de teclado (espacio=play/pause, f=fullscreen, m=mute)
|
||||
- Barra de progreso con preview al hover
|
||||
- Marcadores de secciones importantes
|
||||
- Calidad adaptativa según ancho de banda
|
||||
|
||||
### RF-EDU-002.3: Lecciones de Artículo
|
||||
|
||||
El sistema debe:
|
||||
- Renderizar Markdown con syntax highlighting
|
||||
- Soportar: headers, listas, tablas, imágenes, videos embebidos
|
||||
- Mostrar tabla de contenidos (TOC) para artículos largos
|
||||
- Estimación de tiempo de lectura
|
||||
- Resaltar código con Prism.js o similar
|
||||
- Copiar código con un click
|
||||
- Modo oscuro/claro para lectura
|
||||
- Marcar artículo como completado con checkbox al final
|
||||
|
||||
### RF-EDU-002.4: Navegación entre Lecciones
|
||||
|
||||
El sistema debe:
|
||||
- Mostrar sidebar con estructura del curso (módulos > lecciones)
|
||||
- Indicar lección actual destacada
|
||||
- Mostrar checkmarks en lecciones completadas
|
||||
- Mostrar progreso en módulos (X/Y lecciones)
|
||||
- Botones "Anterior" y "Siguiente" lección
|
||||
- Bloquear lecciones futuras si curso es secuencial
|
||||
- Permitir saltar libremente si curso es no-secuencial
|
||||
- Collapse/expand de módulos en sidebar
|
||||
|
||||
### RF-EDU-002.5: Recursos Descargables
|
||||
|
||||
El sistema debe:
|
||||
- Listar recursos disponibles para la lección
|
||||
- Mostrar: nombre, tipo de archivo, tamaño
|
||||
- Permitir descargar con un click
|
||||
- Trackear descargas para analytics
|
||||
- Validar acceso antes de descargar
|
||||
- Soportar: PDF, XLSX, CSV, ZIP, código fuente
|
||||
|
||||
### RF-EDU-002.6: Notas del Usuario
|
||||
|
||||
El sistema debe:
|
||||
- Permitir tomar notas durante lección
|
||||
- Editor de texto enriquecido (bold, italic, listas)
|
||||
- Guardar automáticamente (debounce 2s)
|
||||
- Timestamp de la nota (para videos)
|
||||
- Listar todas las notas del curso
|
||||
- Buscar en notas
|
||||
- Exportar notas a PDF/Markdown
|
||||
|
||||
### RF-EDU-002.7: Marcadores y Favoritos
|
||||
|
||||
El sistema debe:
|
||||
- Permitir marcar timestamp en videos
|
||||
- Agregar comentario al marcador
|
||||
- Listar marcadores en sidebar
|
||||
- Saltar a marcador con click
|
||||
- Exportar marcadores
|
||||
|
||||
---
|
||||
|
||||
## Datos de Entrada
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| courseId | string | UUID del curso |
|
||||
| lessonId | string | UUID de la lección |
|
||||
| timestamp | number | Posición en video (segundos) |
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Lesson {
|
||||
id: string;
|
||||
moduleId: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
type: 'video' | 'article' | 'quiz' | 'code' | 'resource';
|
||||
order: number;
|
||||
duration: number; // minutos
|
||||
isFree: boolean;
|
||||
isCompleted: boolean;
|
||||
|
||||
// Video específico
|
||||
videoUrl?: string;
|
||||
videoProvider?: 'vimeo' | 'youtube' | 's3';
|
||||
videoId?: string;
|
||||
subtitles?: {
|
||||
language: string;
|
||||
url: string;
|
||||
}[];
|
||||
|
||||
// Artículo específico
|
||||
content?: string; // Markdown
|
||||
readingTime?: number; // minutos
|
||||
|
||||
// Quiz específico
|
||||
quizId?: string;
|
||||
questionsCount?: number;
|
||||
passingScore?: number;
|
||||
|
||||
// Recursos
|
||||
resources?: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
url: string;
|
||||
size: number;
|
||||
}[];
|
||||
|
||||
// Progreso del usuario
|
||||
userProgress?: {
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
lastPosition: number; // Para videos
|
||||
timeSpent: number; // segundos
|
||||
notes?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LessonNavigation {
|
||||
currentLesson: Lesson;
|
||||
previousLesson?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
nextLesson?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
module: {
|
||||
id: string;
|
||||
title: string;
|
||||
lessons: {
|
||||
id: string;
|
||||
title: string;
|
||||
isCompleted: boolean;
|
||||
isLocked: boolean;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Orden secuencial:** Si curso requiere orden, solo lección actual y anteriores están desbloqueadas
|
||||
2. **Completar lección:** Video: ver 90%, Artículo: scroll al final, Quiz: aprobar
|
||||
3. **Acceso Premium:** Lecciones no-free requieren suscripción activa
|
||||
4. **Auto-save progreso:** Guardar posición cada 10 segundos
|
||||
5. **Marcado manual:** Usuario puede marcar completado manualmente
|
||||
6. **Recursos solo para enrollados:** No se pueden descargar recursos sin estar inscrito
|
||||
7. **Notas privadas:** Solo visibles para el usuario que las creó
|
||||
8. **Tiempo mínimo:** Video debe reproducirse al menos 30s para contar progreso
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario visualiza lección de video
|
||||
DADO que el usuario está inscrito en curso
|
||||
Y está en /education/courses/:slug/lessons/:lessonSlug
|
||||
CUANDO la lección es tipo video
|
||||
ENTONCES se muestra reproductor de video
|
||||
Y se muestran controles de reproducción
|
||||
Y se muestra sidebar con estructura del curso
|
||||
Y se carga la posición guardada anteriormente
|
||||
|
||||
Escenario: Usuario completa lección de video
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO el video alcanza el 90% de reproducción
|
||||
ENTONCES la lección se marca como completada
|
||||
Y se muestra checkmark en sidebar
|
||||
Y se actualiza barra de progreso del curso
|
||||
Y se habilita siguiente lección si estaba bloqueada
|
||||
|
||||
Escenario: Usuario lee artículo
|
||||
DADO que la lección es tipo artículo
|
||||
CUANDO el usuario accede a la lección
|
||||
ENTONCES se muestra contenido renderizado desde Markdown
|
||||
Y se muestra tabla de contenidos si artículo >500 palabras
|
||||
Y se muestra tiempo estimado de lectura
|
||||
Y se muestra checkbox "Marcar como completado"
|
||||
|
||||
Escenario: Usuario toma notas
|
||||
DADO que el usuario está en una lección
|
||||
CUANDO hace click en pestaña "Mis notas"
|
||||
ENTONCES se muestra editor de texto
|
||||
Y puede escribir notas
|
||||
Y las notas se guardan automáticamente
|
||||
Y para videos se guarda timestamp actual
|
||||
|
||||
Escenario: Navegación entre lecciones
|
||||
DADO que el usuario completó una lección
|
||||
CUANDO hace click en "Siguiente lección"
|
||||
ENTONCES navega a la siguiente lección
|
||||
Y se carga el contenido correspondiente
|
||||
Y se actualiza sidebar destacando nueva lección
|
||||
|
||||
Escenario: Descargar recursos
|
||||
DADO que la lección tiene recursos descargables
|
||||
CUANDO el usuario hace click en "Descargar"
|
||||
ENTONCES se descarga el archivo
|
||||
Y se registra la descarga en analytics
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- Video CDN (Vimeo/YouTube/AWS S3 + CloudFront)
|
||||
- PostgreSQL para metadata de lecciones
|
||||
- Redis para caché de progreso
|
||||
- S3 para archivos descargables
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Usar React Player o Video.js para reproductor
|
||||
- Implementar PIP (Picture-in-Picture) para videos
|
||||
- Considerar HLS para streaming adaptativo
|
||||
- Implementar lazy loading de módulos en sidebar
|
||||
- Guardar progreso en IndexedDB local como backup
|
||||
- Usar Web Workers para procesamiento de Markdown pesado
|
||||
- Implementar analytics de engagement (pausas, rewinds, abandono)
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/lessons.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/LessonPlayer.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Verificar schema education.lessons
|
||||
- [ ] Tabla user_lesson_progress con campos: started_at, completed_at, last_position
|
||||
- [ ] Tabla user_notes con FK a lesson
|
||||
- [ ] Tabla user_bookmarks para marcadores
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/courses/:id/lessons/:lessonId
|
||||
- [ ] Endpoint POST /education/lessons/:id/progress (guardar posición)
|
||||
- [ ] Endpoint POST /education/lessons/:id/complete
|
||||
- [ ] Endpoint GET/POST/PUT/DELETE /education/lessons/:id/notes
|
||||
- [ ] Endpoint GET /education/lessons/:id/resources/:resourceId/download
|
||||
- [ ] Implementar signed URLs para videos privados
|
||||
- [ ] Rate limiting en download de recursos
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear LessonPlayerPage.tsx
|
||||
- [ ] Crear componente VideoPlayer.tsx
|
||||
- [ ] Crear componente ArticleViewer.tsx
|
||||
- [ ] Crear componente LessonSidebar.tsx
|
||||
- [ ] Crear componente NotesEditor.tsx
|
||||
- [ ] Crear componente ResourcesList.tsx
|
||||
- [ ] Implementar lessonStore para progreso
|
||||
- [ ] Auto-save de posición cada 10s
|
||||
- [ ] Atajos de teclado para navegación
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test unitario LessonService
|
||||
- [ ] Test integración actualización de progreso
|
||||
- [ ] Test E2E completar lección y desbloquear siguiente
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "RF-EDU-002"
|
||||
title: "Sistema de Lecciones"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "education"
|
||||
epic: "OQI-002"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-EDU-002: Sistema de Lecciones
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P0
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un reproductor multimedia completo que permita a los usuarios consumir contenido educativo en múltiples formatos (video, texto, código interactivo, quizzes) con controles de navegación, seguimiento de progreso y experiencia de aprendizaje optimizada.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-002.1: Tipos de Lecciones
|
||||
|
||||
El sistema debe soportar:
|
||||
|
||||
| Tipo | Descripción | Características |
|
||||
|------|-------------|-----------------|
|
||||
| **Video** | Contenido en video | Reproductor, subtítulos, velocidad |
|
||||
| **Artículo** | Contenido de texto | Markdown, imágenes, código |
|
||||
| **Quiz** | Evaluación interactiva | Preguntas, feedback inmediato |
|
||||
| **Código** | Ejercicio práctico | Editor, ejecución, validación |
|
||||
| **Recursos** | Descargables | PDFs, hojas de cálculo, código |
|
||||
|
||||
### RF-EDU-002.2: Reproductor de Video
|
||||
|
||||
El sistema debe:
|
||||
- Reproducir videos desde CDN (Vimeo/YouTube/S3)
|
||||
- Controles: play/pause, volumen, pantalla completa
|
||||
- Velocidades: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
|
||||
- Subtítulos en español e inglés (opcional)
|
||||
- Recordar posición de reproducción
|
||||
- Saltar 10s adelante/atrás con teclas de flecha
|
||||
- Atajos de teclado (espacio=play/pause, f=fullscreen, m=mute)
|
||||
- Barra de progreso con preview al hover
|
||||
- Marcadores de secciones importantes
|
||||
- Calidad adaptativa según ancho de banda
|
||||
|
||||
### RF-EDU-002.3: Lecciones de Artículo
|
||||
|
||||
El sistema debe:
|
||||
- Renderizar Markdown con syntax highlighting
|
||||
- Soportar: headers, listas, tablas, imágenes, videos embebidos
|
||||
- Mostrar tabla de contenidos (TOC) para artículos largos
|
||||
- Estimación de tiempo de lectura
|
||||
- Resaltar código con Prism.js o similar
|
||||
- Copiar código con un click
|
||||
- Modo oscuro/claro para lectura
|
||||
- Marcar artículo como completado con checkbox al final
|
||||
|
||||
### RF-EDU-002.4: Navegación entre Lecciones
|
||||
|
||||
El sistema debe:
|
||||
- Mostrar sidebar con estructura del curso (módulos > lecciones)
|
||||
- Indicar lección actual destacada
|
||||
- Mostrar checkmarks en lecciones completadas
|
||||
- Mostrar progreso en módulos (X/Y lecciones)
|
||||
- Botones "Anterior" y "Siguiente" lección
|
||||
- Bloquear lecciones futuras si curso es secuencial
|
||||
- Permitir saltar libremente si curso es no-secuencial
|
||||
- Collapse/expand de módulos en sidebar
|
||||
|
||||
### RF-EDU-002.5: Recursos Descargables
|
||||
|
||||
El sistema debe:
|
||||
- Listar recursos disponibles para la lección
|
||||
- Mostrar: nombre, tipo de archivo, tamaño
|
||||
- Permitir descargar con un click
|
||||
- Trackear descargas para analytics
|
||||
- Validar acceso antes de descargar
|
||||
- Soportar: PDF, XLSX, CSV, ZIP, código fuente
|
||||
|
||||
### RF-EDU-002.6: Notas del Usuario
|
||||
|
||||
El sistema debe:
|
||||
- Permitir tomar notas durante lección
|
||||
- Editor de texto enriquecido (bold, italic, listas)
|
||||
- Guardar automáticamente (debounce 2s)
|
||||
- Timestamp de la nota (para videos)
|
||||
- Listar todas las notas del curso
|
||||
- Buscar en notas
|
||||
- Exportar notas a PDF/Markdown
|
||||
|
||||
### RF-EDU-002.7: Marcadores y Favoritos
|
||||
|
||||
El sistema debe:
|
||||
- Permitir marcar timestamp en videos
|
||||
- Agregar comentario al marcador
|
||||
- Listar marcadores en sidebar
|
||||
- Saltar a marcador con click
|
||||
- Exportar marcadores
|
||||
|
||||
---
|
||||
|
||||
## Datos de Entrada
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| courseId | string | UUID del curso |
|
||||
| lessonId | string | UUID de la lección |
|
||||
| timestamp | number | Posición en video (segundos) |
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Lesson {
|
||||
id: string;
|
||||
moduleId: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
type: 'video' | 'article' | 'quiz' | 'code' | 'resource';
|
||||
order: number;
|
||||
duration: number; // minutos
|
||||
isFree: boolean;
|
||||
isCompleted: boolean;
|
||||
|
||||
// Video específico
|
||||
videoUrl?: string;
|
||||
videoProvider?: 'vimeo' | 'youtube' | 's3';
|
||||
videoId?: string;
|
||||
subtitles?: {
|
||||
language: string;
|
||||
url: string;
|
||||
}[];
|
||||
|
||||
// Artículo específico
|
||||
content?: string; // Markdown
|
||||
readingTime?: number; // minutos
|
||||
|
||||
// Quiz específico
|
||||
quizId?: string;
|
||||
questionsCount?: number;
|
||||
passingScore?: number;
|
||||
|
||||
// Recursos
|
||||
resources?: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
url: string;
|
||||
size: number;
|
||||
}[];
|
||||
|
||||
// Progreso del usuario
|
||||
userProgress?: {
|
||||
startedAt: string;
|
||||
completedAt?: string;
|
||||
lastPosition: number; // Para videos
|
||||
timeSpent: number; // segundos
|
||||
notes?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LessonNavigation {
|
||||
currentLesson: Lesson;
|
||||
previousLesson?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
nextLesson?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
module: {
|
||||
id: string;
|
||||
title: string;
|
||||
lessons: {
|
||||
id: string;
|
||||
title: string;
|
||||
isCompleted: boolean;
|
||||
isLocked: boolean;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Orden secuencial:** Si curso requiere orden, solo lección actual y anteriores están desbloqueadas
|
||||
2. **Completar lección:** Video: ver 90%, Artículo: scroll al final, Quiz: aprobar
|
||||
3. **Acceso Premium:** Lecciones no-free requieren suscripción activa
|
||||
4. **Auto-save progreso:** Guardar posición cada 10 segundos
|
||||
5. **Marcado manual:** Usuario puede marcar completado manualmente
|
||||
6. **Recursos solo para enrollados:** No se pueden descargar recursos sin estar inscrito
|
||||
7. **Notas privadas:** Solo visibles para el usuario que las creó
|
||||
8. **Tiempo mínimo:** Video debe reproducirse al menos 30s para contar progreso
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario visualiza lección de video
|
||||
DADO que el usuario está inscrito en curso
|
||||
Y está en /education/courses/:slug/lessons/:lessonSlug
|
||||
CUANDO la lección es tipo video
|
||||
ENTONCES se muestra reproductor de video
|
||||
Y se muestran controles de reproducción
|
||||
Y se muestra sidebar con estructura del curso
|
||||
Y se carga la posición guardada anteriormente
|
||||
|
||||
Escenario: Usuario completa lección de video
|
||||
DADO que el usuario está viendo un video
|
||||
CUANDO el video alcanza el 90% de reproducción
|
||||
ENTONCES la lección se marca como completada
|
||||
Y se muestra checkmark en sidebar
|
||||
Y se actualiza barra de progreso del curso
|
||||
Y se habilita siguiente lección si estaba bloqueada
|
||||
|
||||
Escenario: Usuario lee artículo
|
||||
DADO que la lección es tipo artículo
|
||||
CUANDO el usuario accede a la lección
|
||||
ENTONCES se muestra contenido renderizado desde Markdown
|
||||
Y se muestra tabla de contenidos si artículo >500 palabras
|
||||
Y se muestra tiempo estimado de lectura
|
||||
Y se muestra checkbox "Marcar como completado"
|
||||
|
||||
Escenario: Usuario toma notas
|
||||
DADO que el usuario está en una lección
|
||||
CUANDO hace click en pestaña "Mis notas"
|
||||
ENTONCES se muestra editor de texto
|
||||
Y puede escribir notas
|
||||
Y las notas se guardan automáticamente
|
||||
Y para videos se guarda timestamp actual
|
||||
|
||||
Escenario: Navegación entre lecciones
|
||||
DADO que el usuario completó una lección
|
||||
CUANDO hace click en "Siguiente lección"
|
||||
ENTONCES navega a la siguiente lección
|
||||
Y se carga el contenido correspondiente
|
||||
Y se actualiza sidebar destacando nueva lección
|
||||
|
||||
Escenario: Descargar recursos
|
||||
DADO que la lección tiene recursos descargables
|
||||
CUANDO el usuario hace click en "Descargar"
|
||||
ENTONCES se descarga el archivo
|
||||
Y se registra la descarga en analytics
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- Video CDN (Vimeo/YouTube/AWS S3 + CloudFront)
|
||||
- PostgreSQL para metadata de lecciones
|
||||
- Redis para caché de progreso
|
||||
- S3 para archivos descargables
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Usar React Player o Video.js para reproductor
|
||||
- Implementar PIP (Picture-in-Picture) para videos
|
||||
- Considerar HLS para streaming adaptativo
|
||||
- Implementar lazy loading de módulos en sidebar
|
||||
- Guardar progreso en IndexedDB local como backup
|
||||
- Usar Web Workers para procesamiento de Markdown pesado
|
||||
- Implementar analytics de engagement (pausas, rewinds, abandono)
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/lessons.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/LessonPlayer.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Verificar schema education.lessons
|
||||
- [ ] Tabla user_lesson_progress con campos: started_at, completed_at, last_position
|
||||
- [ ] Tabla user_notes con FK a lesson
|
||||
- [ ] Tabla user_bookmarks para marcadores
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/courses/:id/lessons/:lessonId
|
||||
- [ ] Endpoint POST /education/lessons/:id/progress (guardar posición)
|
||||
- [ ] Endpoint POST /education/lessons/:id/complete
|
||||
- [ ] Endpoint GET/POST/PUT/DELETE /education/lessons/:id/notes
|
||||
- [ ] Endpoint GET /education/lessons/:id/resources/:resourceId/download
|
||||
- [ ] Implementar signed URLs para videos privados
|
||||
- [ ] Rate limiting en download de recursos
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear LessonPlayerPage.tsx
|
||||
- [ ] Crear componente VideoPlayer.tsx
|
||||
- [ ] Crear componente ArticleViewer.tsx
|
||||
- [ ] Crear componente LessonSidebar.tsx
|
||||
- [ ] Crear componente NotesEditor.tsx
|
||||
- [ ] Crear componente ResourcesList.tsx
|
||||
- [ ] Implementar lessonStore para progreso
|
||||
- [ ] Auto-save de posición cada 10s
|
||||
- [ ] Atajos de teclado para navegación
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test unitario LessonService
|
||||
- [ ] Test integración actualización de progreso
|
||||
- [ ] Test E2E completar lección y desbloquear siguiente
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,355 +1,368 @@
|
||||
# RF-EDU-003: Tracking de Progreso
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P0
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un sistema completo de seguimiento y visualización del progreso educativo del usuario, incluyendo métricas de avance, estadísticas de aprendizaje, historial de actividades y reportes de rendimiento para mantener la motivación y permitir evaluación del desempeño.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-003.1: Dashboard de Progreso
|
||||
|
||||
El sistema debe mostrar:
|
||||
- Resumen general de aprendizaje del usuario
|
||||
- Total de cursos: En progreso, Completados, Guardados
|
||||
- Total de lecciones completadas
|
||||
- Total de horas de aprendizaje
|
||||
- Racha actual (días consecutivos de actividad)
|
||||
- Racha más larga histórica
|
||||
- XP total acumulado
|
||||
- Nivel actual del usuario
|
||||
- Gráfico de actividad semanal/mensual
|
||||
|
||||
### RF-EDU-003.2: Progreso por Curso
|
||||
|
||||
El sistema debe mostrar para cada curso:
|
||||
- Porcentaje de completitud (0-100%)
|
||||
- Lecciones completadas / Total de lecciones
|
||||
- Módulos completados / Total de módulos
|
||||
- Tiempo invertido en el curso
|
||||
- Última vez que accedió al curso
|
||||
- Fecha de inscripción
|
||||
- Fecha de finalización (si completó)
|
||||
- Próxima lección sugerida
|
||||
- Barra de progreso visual
|
||||
|
||||
### RF-EDU-003.3: Historial de Actividad
|
||||
|
||||
El sistema debe registrar:
|
||||
- Timeline de actividades del usuario
|
||||
- Eventos: inscripción, lección completada, quiz aprobado, certificado obtenido
|
||||
- Fecha y hora de cada evento
|
||||
- Filtros por tipo de evento y rango de fechas
|
||||
- Exportar historial a CSV
|
||||
|
||||
Tipos de eventos:
|
||||
```typescript
|
||||
enum ActivityType {
|
||||
COURSE_ENROLLED = 'course_enrolled',
|
||||
LESSON_STARTED = 'lesson_started',
|
||||
LESSON_COMPLETED = 'lesson_completed',
|
||||
MODULE_COMPLETED = 'module_completed',
|
||||
COURSE_COMPLETED = 'course_completed',
|
||||
QUIZ_PASSED = 'quiz_passed',
|
||||
QUIZ_FAILED = 'quiz_failed',
|
||||
CERTIFICATE_EARNED = 'certificate_earned',
|
||||
NOTE_CREATED = 'note_created',
|
||||
RESOURCE_DOWNLOADED = 'resource_downloaded',
|
||||
}
|
||||
```
|
||||
|
||||
### RF-EDU-003.4: Estadísticas de Aprendizaje
|
||||
|
||||
El sistema debe calcular y mostrar:
|
||||
- **Tiempo promedio por lección:** Total minutos / lecciones completadas
|
||||
- **Cursos por mes:** Cursos completados en último mes
|
||||
- **Tasa de completitud:** % de cursos iniciados que fueron completados
|
||||
- **Días activos:** Días con al menos 1 lección completada
|
||||
- **Mejor día de la semana:** Día con más actividad
|
||||
- **Hora preferida:** Franja horaria con más actividad
|
||||
- **Categoría favorita:** Categoría con más cursos completados
|
||||
- **Velocidad de aprendizaje:** Comparación con promedio de usuarios
|
||||
|
||||
### RF-EDU-003.5: Racha de Aprendizaje (Streak)
|
||||
|
||||
El sistema debe:
|
||||
- Calcular racha actual: días consecutivos con actividad
|
||||
- Definir actividad como: completar al menos 1 lección
|
||||
- Resetear racha si pasa 1 día sin actividad
|
||||
- Guardar racha más larga histórica
|
||||
- Mostrar calendario de actividad (estilo GitHub contributions)
|
||||
- Enviar notificación si racha está en riesgo (no actividad hoy)
|
||||
- Otorgar badges especiales por rachas: 7, 30, 100, 365 días
|
||||
|
||||
### RF-EDU-003.6: Sistema de Niveles
|
||||
|
||||
El sistema debe:
|
||||
- Asignar nivel al usuario basado en XP acumulado
|
||||
- XP se gana por:
|
||||
- Completar lección: 10 XP
|
||||
- Completar módulo: 50 XP
|
||||
- Completar curso: 200 XP
|
||||
- Aprobar quiz primera vez: 30 XP
|
||||
- Obtener certificado: 100 XP
|
||||
- Racha de 7 días: 100 XP
|
||||
- Niveles del 1 al 50
|
||||
- XP requerido por nivel aumenta progresivamente
|
||||
|
||||
Fórmula XP por nivel:
|
||||
```
|
||||
XP_needed(level) = 100 * level * (level + 1) / 2
|
||||
```
|
||||
|
||||
| Nivel | XP Requerido | XP Acumulado |
|
||||
|-------|--------------|--------------|
|
||||
| 1 | 0 | 0 |
|
||||
| 2 | 100 | 100 |
|
||||
| 3 | 200 | 300 |
|
||||
| 5 | 400 | 1000 |
|
||||
| 10 | 900 | 5500 |
|
||||
| 20 | 1900 | 21000 |
|
||||
| 50 | 4900 | 127500 |
|
||||
|
||||
### RF-EDU-003.7: Reportes de Progreso
|
||||
|
||||
El sistema debe generar:
|
||||
- Reporte semanal por email (opcional)
|
||||
- Reporte mensual con estadísticas
|
||||
- Exportar progreso a PDF
|
||||
- Comparación mes a mes
|
||||
- Metas vs realidad
|
||||
|
||||
### RF-EDU-003.8: Metas de Aprendizaje
|
||||
|
||||
El sistema debe permitir:
|
||||
- Establecer meta de lecciones por semana
|
||||
- Establecer meta de cursos por mes
|
||||
- Establecer meta de minutos de estudio por día
|
||||
- Visualizar progreso hacia metas
|
||||
- Notificaciones si está rezagado
|
||||
- Celebración al cumplir meta
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface UserProgress {
|
||||
userId: string;
|
||||
overview: {
|
||||
coursesInProgress: number;
|
||||
coursesCompleted: number;
|
||||
coursesSaved: number;
|
||||
lessonsCompleted: number;
|
||||
totalLearningTime: number; // minutos
|
||||
currentStreak: number;
|
||||
longestStreak: number;
|
||||
totalXP: number;
|
||||
currentLevel: number;
|
||||
xpToNextLevel: number;
|
||||
};
|
||||
|
||||
courses: {
|
||||
courseId: string;
|
||||
courseTitle: string;
|
||||
thumbnail: string;
|
||||
progress: {
|
||||
percent: number;
|
||||
lessonsCompleted: number;
|
||||
totalLessons: number;
|
||||
modulesCompleted: number;
|
||||
totalModules: number;
|
||||
timeSpent: number;
|
||||
enrolledAt: string;
|
||||
completedAt?: string;
|
||||
lastAccessedAt: string;
|
||||
nextLesson?: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
|
||||
stats: {
|
||||
avgTimePerLesson: number;
|
||||
coursesThisMonth: number;
|
||||
completionRate: number; // 0-100
|
||||
activeDays: number;
|
||||
favoriteCategory: string;
|
||||
bestDayOfWeek: string;
|
||||
preferredTimeOfDay: string;
|
||||
};
|
||||
|
||||
recentActivity: {
|
||||
type: ActivityType;
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: string;
|
||||
metadata?: any;
|
||||
}[];
|
||||
|
||||
calendar: {
|
||||
date: string; // YYYY-MM-DD
|
||||
lessonsCompleted: number;
|
||||
minutesLearned: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface LevelInfo {
|
||||
currentLevel: number;
|
||||
currentXP: number;
|
||||
xpForCurrentLevel: number;
|
||||
xpForNextLevel: number;
|
||||
progressToNextLevel: number; // 0-100
|
||||
title: string; // "Novice Trader", "Advanced Analyst", etc.
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Actividad mínima:** 1 lección completada para contar como día activo
|
||||
2. **Racha:** Se resetea si pasan >24h sin actividad
|
||||
3. **XP no se pierde:** Una vez ganado, el XP es permanente
|
||||
4. **Nivel no baja:** Los niveles solo suben, nunca bajan
|
||||
5. **Tiempo de aprendizaje:** Solo cuenta tiempo activo (reproducción de video, scroll en artículo)
|
||||
6. **Caché de stats:** Estadísticas se calculan cada hora, no en tiempo real
|
||||
7. **Zona horaria:** Racha se calcula según timezone del usuario
|
||||
8. **Reporte semanal:** Se envía lunes a las 8am hora local
|
||||
9. **Completitud de curso:** 100% cuando todas las lecciones están completas
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario visualiza dashboard de progreso
|
||||
DADO que el usuario está autenticado
|
||||
CUANDO accede a /education/progress
|
||||
ENTONCES se muestra resumen general de aprendizaje
|
||||
Y se muestran estadísticas: cursos, lecciones, horas
|
||||
Y se muestra racha actual y más larga
|
||||
Y se muestra nivel y XP
|
||||
Y se muestra gráfico de actividad reciente
|
||||
|
||||
Escenario: Usuario visualiza progreso de curso
|
||||
DADO que el usuario está inscrito en un curso
|
||||
CUANDO ve la tarjeta del curso en el dashboard
|
||||
ENTONCES se muestra barra de progreso con porcentaje
|
||||
Y se muestra "X/Y lecciones completadas"
|
||||
Y se muestra tiempo invertido
|
||||
Y se muestra botón "Continuar" que lleva a próxima lección
|
||||
|
||||
Escenario: Usuario mantiene racha activa
|
||||
DADO que el usuario tiene racha de 5 días
|
||||
CUANDO completa 1 lección hoy
|
||||
ENTONCES la racha aumenta a 6 días
|
||||
Y se muestra animación de celebración
|
||||
Y se actualiza calendario de actividad
|
||||
|
||||
Escenario: Usuario rompe racha
|
||||
DADO que el usuario tiene racha de 10 días
|
||||
Y no completó lecciones ayer
|
||||
CUANDO accede hoy
|
||||
ENTONCES la racha se resetea a 0 o 1 (según actividad de hoy)
|
||||
Y se muestra mensaje "Racha reiniciada"
|
||||
Y se guarda racha anterior como "mejor racha"
|
||||
|
||||
Escenario: Usuario sube de nivel
|
||||
DADO que el usuario tiene 950 XP (nivel 9)
|
||||
CUANDO completa un curso y gana 200 XP
|
||||
ENTONCES sube a nivel 10
|
||||
Y se muestra animación "¡Subiste de nivel!"
|
||||
Y se desbloquea nuevo badge
|
||||
Y se envía notificación
|
||||
|
||||
Escenario: Ver historial de actividad
|
||||
DADO que el usuario accede a historial
|
||||
CUANDO filtra por "últimos 7 días"
|
||||
ENTONCES se muestran todas las actividades de la semana
|
||||
Y se agrupan por día
|
||||
Y se muestra timeline visual
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PostgreSQL para almacenar progreso
|
||||
- Redis para caché de estadísticas
|
||||
- Cron jobs para calcular stats diarias
|
||||
- Email service para reportes semanales
|
||||
- Analytics para tracking de eventos
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Calcular estadísticas agregadas en background jobs (no en request)
|
||||
- Usar materialized views para queries pesadas
|
||||
- Implementar cache warming para stats de usuarios activos
|
||||
- Considerar Event Sourcing para historial de actividades
|
||||
- Optimizar queries con índices en user_id + timestamp
|
||||
- Implementar rate limiting en export de reportes
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/progress.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/EducationDashboard.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla user_course_progress: percent, lessons_completed, time_spent
|
||||
- [ ] Tabla user_activity_log: tipo, timestamp, metadata
|
||||
- [ ] Tabla user_stats: nivel, xp, racha, cache de stats
|
||||
- [ ] Tabla user_goals: meta, progreso, fecha
|
||||
- [ ] Índices en user_id + timestamp para queries rápidas
|
||||
- [ ] Materialized view para stats agregadas
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/progress (dashboard completo)
|
||||
- [ ] Endpoint GET /education/progress/stats
|
||||
- [ ] Endpoint GET /education/progress/activity (historial)
|
||||
- [ ] Endpoint POST /education/goals (crear meta)
|
||||
- [ ] Implementar ProgressService.calculateLevel()
|
||||
- [ ] Implementar ProgressService.updateStreak() (cron daily)
|
||||
- [ ] Job para generar reportes semanales
|
||||
- [ ] Event handlers para actualizar XP en actividades
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear EducationDashboardPage.tsx
|
||||
- [ ] Crear componente ProgressOverview.tsx
|
||||
- [ ] Crear componente CourseProgressCard.tsx
|
||||
- [ ] Crear componente ActivityCalendar.tsx (estilo GitHub)
|
||||
- [ ] Crear componente LevelProgress.tsx
|
||||
- [ ] Crear componente ActivityTimeline.tsx
|
||||
- [ ] Crear componente StatsCharts.tsx
|
||||
- [ ] Animaciones para level up y racha
|
||||
- [ ] Implementar progressStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test cálculo de nivel según XP
|
||||
- [ ] Test cálculo de racha con diferentes escenarios
|
||||
- [ ] Test E2E completar lección y ver progreso actualizado
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "RF-EDU-003"
|
||||
title: "Tracking de Progreso"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "education"
|
||||
epic: "OQI-002"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-EDU-003: Tracking de Progreso
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P0
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un sistema completo de seguimiento y visualización del progreso educativo del usuario, incluyendo métricas de avance, estadísticas de aprendizaje, historial de actividades y reportes de rendimiento para mantener la motivación y permitir evaluación del desempeño.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-003.1: Dashboard de Progreso
|
||||
|
||||
El sistema debe mostrar:
|
||||
- Resumen general de aprendizaje del usuario
|
||||
- Total de cursos: En progreso, Completados, Guardados
|
||||
- Total de lecciones completadas
|
||||
- Total de horas de aprendizaje
|
||||
- Racha actual (días consecutivos de actividad)
|
||||
- Racha más larga histórica
|
||||
- XP total acumulado
|
||||
- Nivel actual del usuario
|
||||
- Gráfico de actividad semanal/mensual
|
||||
|
||||
### RF-EDU-003.2: Progreso por Curso
|
||||
|
||||
El sistema debe mostrar para cada curso:
|
||||
- Porcentaje de completitud (0-100%)
|
||||
- Lecciones completadas / Total de lecciones
|
||||
- Módulos completados / Total de módulos
|
||||
- Tiempo invertido en el curso
|
||||
- Última vez que accedió al curso
|
||||
- Fecha de inscripción
|
||||
- Fecha de finalización (si completó)
|
||||
- Próxima lección sugerida
|
||||
- Barra de progreso visual
|
||||
|
||||
### RF-EDU-003.3: Historial de Actividad
|
||||
|
||||
El sistema debe registrar:
|
||||
- Timeline de actividades del usuario
|
||||
- Eventos: inscripción, lección completada, quiz aprobado, certificado obtenido
|
||||
- Fecha y hora de cada evento
|
||||
- Filtros por tipo de evento y rango de fechas
|
||||
- Exportar historial a CSV
|
||||
|
||||
Tipos de eventos:
|
||||
```typescript
|
||||
enum ActivityType {
|
||||
COURSE_ENROLLED = 'course_enrolled',
|
||||
LESSON_STARTED = 'lesson_started',
|
||||
LESSON_COMPLETED = 'lesson_completed',
|
||||
MODULE_COMPLETED = 'module_completed',
|
||||
COURSE_COMPLETED = 'course_completed',
|
||||
QUIZ_PASSED = 'quiz_passed',
|
||||
QUIZ_FAILED = 'quiz_failed',
|
||||
CERTIFICATE_EARNED = 'certificate_earned',
|
||||
NOTE_CREATED = 'note_created',
|
||||
RESOURCE_DOWNLOADED = 'resource_downloaded',
|
||||
}
|
||||
```
|
||||
|
||||
### RF-EDU-003.4: Estadísticas de Aprendizaje
|
||||
|
||||
El sistema debe calcular y mostrar:
|
||||
- **Tiempo promedio por lección:** Total minutos / lecciones completadas
|
||||
- **Cursos por mes:** Cursos completados en último mes
|
||||
- **Tasa de completitud:** % de cursos iniciados que fueron completados
|
||||
- **Días activos:** Días con al menos 1 lección completada
|
||||
- **Mejor día de la semana:** Día con más actividad
|
||||
- **Hora preferida:** Franja horaria con más actividad
|
||||
- **Categoría favorita:** Categoría con más cursos completados
|
||||
- **Velocidad de aprendizaje:** Comparación con promedio de usuarios
|
||||
|
||||
### RF-EDU-003.5: Racha de Aprendizaje (Streak)
|
||||
|
||||
El sistema debe:
|
||||
- Calcular racha actual: días consecutivos con actividad
|
||||
- Definir actividad como: completar al menos 1 lección
|
||||
- Resetear racha si pasa 1 día sin actividad
|
||||
- Guardar racha más larga histórica
|
||||
- Mostrar calendario de actividad (estilo GitHub contributions)
|
||||
- Enviar notificación si racha está en riesgo (no actividad hoy)
|
||||
- Otorgar badges especiales por rachas: 7, 30, 100, 365 días
|
||||
|
||||
### RF-EDU-003.6: Sistema de Niveles
|
||||
|
||||
El sistema debe:
|
||||
- Asignar nivel al usuario basado en XP acumulado
|
||||
- XP se gana por:
|
||||
- Completar lección: 10 XP
|
||||
- Completar módulo: 50 XP
|
||||
- Completar curso: 200 XP
|
||||
- Aprobar quiz primera vez: 30 XP
|
||||
- Obtener certificado: 100 XP
|
||||
- Racha de 7 días: 100 XP
|
||||
- Niveles del 1 al 50
|
||||
- XP requerido por nivel aumenta progresivamente
|
||||
|
||||
Fórmula XP por nivel:
|
||||
```
|
||||
XP_needed(level) = 100 * level * (level + 1) / 2
|
||||
```
|
||||
|
||||
| Nivel | XP Requerido | XP Acumulado |
|
||||
|-------|--------------|--------------|
|
||||
| 1 | 0 | 0 |
|
||||
| 2 | 100 | 100 |
|
||||
| 3 | 200 | 300 |
|
||||
| 5 | 400 | 1000 |
|
||||
| 10 | 900 | 5500 |
|
||||
| 20 | 1900 | 21000 |
|
||||
| 50 | 4900 | 127500 |
|
||||
|
||||
### RF-EDU-003.7: Reportes de Progreso
|
||||
|
||||
El sistema debe generar:
|
||||
- Reporte semanal por email (opcional)
|
||||
- Reporte mensual con estadísticas
|
||||
- Exportar progreso a PDF
|
||||
- Comparación mes a mes
|
||||
- Metas vs realidad
|
||||
|
||||
### RF-EDU-003.8: Metas de Aprendizaje
|
||||
|
||||
El sistema debe permitir:
|
||||
- Establecer meta de lecciones por semana
|
||||
- Establecer meta de cursos por mes
|
||||
- Establecer meta de minutos de estudio por día
|
||||
- Visualizar progreso hacia metas
|
||||
- Notificaciones si está rezagado
|
||||
- Celebración al cumplir meta
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface UserProgress {
|
||||
userId: string;
|
||||
overview: {
|
||||
coursesInProgress: number;
|
||||
coursesCompleted: number;
|
||||
coursesSaved: number;
|
||||
lessonsCompleted: number;
|
||||
totalLearningTime: number; // minutos
|
||||
currentStreak: number;
|
||||
longestStreak: number;
|
||||
totalXP: number;
|
||||
currentLevel: number;
|
||||
xpToNextLevel: number;
|
||||
};
|
||||
|
||||
courses: {
|
||||
courseId: string;
|
||||
courseTitle: string;
|
||||
thumbnail: string;
|
||||
progress: {
|
||||
percent: number;
|
||||
lessonsCompleted: number;
|
||||
totalLessons: number;
|
||||
modulesCompleted: number;
|
||||
totalModules: number;
|
||||
timeSpent: number;
|
||||
enrolledAt: string;
|
||||
completedAt?: string;
|
||||
lastAccessedAt: string;
|
||||
nextLesson?: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
|
||||
stats: {
|
||||
avgTimePerLesson: number;
|
||||
coursesThisMonth: number;
|
||||
completionRate: number; // 0-100
|
||||
activeDays: number;
|
||||
favoriteCategory: string;
|
||||
bestDayOfWeek: string;
|
||||
preferredTimeOfDay: string;
|
||||
};
|
||||
|
||||
recentActivity: {
|
||||
type: ActivityType;
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: string;
|
||||
metadata?: any;
|
||||
}[];
|
||||
|
||||
calendar: {
|
||||
date: string; // YYYY-MM-DD
|
||||
lessonsCompleted: number;
|
||||
minutesLearned: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface LevelInfo {
|
||||
currentLevel: number;
|
||||
currentXP: number;
|
||||
xpForCurrentLevel: number;
|
||||
xpForNextLevel: number;
|
||||
progressToNextLevel: number; // 0-100
|
||||
title: string; // "Novice Trader", "Advanced Analyst", etc.
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Actividad mínima:** 1 lección completada para contar como día activo
|
||||
2. **Racha:** Se resetea si pasan >24h sin actividad
|
||||
3. **XP no se pierde:** Una vez ganado, el XP es permanente
|
||||
4. **Nivel no baja:** Los niveles solo suben, nunca bajan
|
||||
5. **Tiempo de aprendizaje:** Solo cuenta tiempo activo (reproducción de video, scroll en artículo)
|
||||
6. **Caché de stats:** Estadísticas se calculan cada hora, no en tiempo real
|
||||
7. **Zona horaria:** Racha se calcula según timezone del usuario
|
||||
8. **Reporte semanal:** Se envía lunes a las 8am hora local
|
||||
9. **Completitud de curso:** 100% cuando todas las lecciones están completas
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario visualiza dashboard de progreso
|
||||
DADO que el usuario está autenticado
|
||||
CUANDO accede a /education/progress
|
||||
ENTONCES se muestra resumen general de aprendizaje
|
||||
Y se muestran estadísticas: cursos, lecciones, horas
|
||||
Y se muestra racha actual y más larga
|
||||
Y se muestra nivel y XP
|
||||
Y se muestra gráfico de actividad reciente
|
||||
|
||||
Escenario: Usuario visualiza progreso de curso
|
||||
DADO que el usuario está inscrito en un curso
|
||||
CUANDO ve la tarjeta del curso en el dashboard
|
||||
ENTONCES se muestra barra de progreso con porcentaje
|
||||
Y se muestra "X/Y lecciones completadas"
|
||||
Y se muestra tiempo invertido
|
||||
Y se muestra botón "Continuar" que lleva a próxima lección
|
||||
|
||||
Escenario: Usuario mantiene racha activa
|
||||
DADO que el usuario tiene racha de 5 días
|
||||
CUANDO completa 1 lección hoy
|
||||
ENTONCES la racha aumenta a 6 días
|
||||
Y se muestra animación de celebración
|
||||
Y se actualiza calendario de actividad
|
||||
|
||||
Escenario: Usuario rompe racha
|
||||
DADO que el usuario tiene racha de 10 días
|
||||
Y no completó lecciones ayer
|
||||
CUANDO accede hoy
|
||||
ENTONCES la racha se resetea a 0 o 1 (según actividad de hoy)
|
||||
Y se muestra mensaje "Racha reiniciada"
|
||||
Y se guarda racha anterior como "mejor racha"
|
||||
|
||||
Escenario: Usuario sube de nivel
|
||||
DADO que el usuario tiene 950 XP (nivel 9)
|
||||
CUANDO completa un curso y gana 200 XP
|
||||
ENTONCES sube a nivel 10
|
||||
Y se muestra animación "¡Subiste de nivel!"
|
||||
Y se desbloquea nuevo badge
|
||||
Y se envía notificación
|
||||
|
||||
Escenario: Ver historial de actividad
|
||||
DADO que el usuario accede a historial
|
||||
CUANDO filtra por "últimos 7 días"
|
||||
ENTONCES se muestran todas las actividades de la semana
|
||||
Y se agrupan por día
|
||||
Y se muestra timeline visual
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PostgreSQL para almacenar progreso
|
||||
- Redis para caché de estadísticas
|
||||
- Cron jobs para calcular stats diarias
|
||||
- Email service para reportes semanales
|
||||
- Analytics para tracking de eventos
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Calcular estadísticas agregadas en background jobs (no en request)
|
||||
- Usar materialized views para queries pesadas
|
||||
- Implementar cache warming para stats de usuarios activos
|
||||
- Considerar Event Sourcing para historial de actividades
|
||||
- Optimizar queries con índices en user_id + timestamp
|
||||
- Implementar rate limiting en export de reportes
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/progress.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/EducationDashboard.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla user_course_progress: percent, lessons_completed, time_spent
|
||||
- [ ] Tabla user_activity_log: tipo, timestamp, metadata
|
||||
- [ ] Tabla user_stats: nivel, xp, racha, cache de stats
|
||||
- [ ] Tabla user_goals: meta, progreso, fecha
|
||||
- [ ] Índices en user_id + timestamp para queries rápidas
|
||||
- [ ] Materialized view para stats agregadas
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/progress (dashboard completo)
|
||||
- [ ] Endpoint GET /education/progress/stats
|
||||
- [ ] Endpoint GET /education/progress/activity (historial)
|
||||
- [ ] Endpoint POST /education/goals (crear meta)
|
||||
- [ ] Implementar ProgressService.calculateLevel()
|
||||
- [ ] Implementar ProgressService.updateStreak() (cron daily)
|
||||
- [ ] Job para generar reportes semanales
|
||||
- [ ] Event handlers para actualizar XP en actividades
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear EducationDashboardPage.tsx
|
||||
- [ ] Crear componente ProgressOverview.tsx
|
||||
- [ ] Crear componente CourseProgressCard.tsx
|
||||
- [ ] Crear componente ActivityCalendar.tsx (estilo GitHub)
|
||||
- [ ] Crear componente LevelProgress.tsx
|
||||
- [ ] Crear componente ActivityTimeline.tsx
|
||||
- [ ] Crear componente StatsCharts.tsx
|
||||
- [ ] Animaciones para level up y racha
|
||||
- [ ] Implementar progressStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test cálculo de nivel según XP
|
||||
- [ ] Test cálculo de racha con diferentes escenarios
|
||||
- [ ] Test E2E completar lección y ver progreso actualizado
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,404 +1,417 @@
|
||||
# RF-EDU-004: Sistema de Quizzes
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P1
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un sistema completo de evaluaciones interactivas (quizzes) que permita validar el conocimiento adquirido por los usuarios, con soporte para múltiples tipos de preguntas, feedback inmediato, intentos limitados, calificaciones y análisis de resultados.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-004.1: Tipos de Preguntas
|
||||
|
||||
El sistema debe soportar:
|
||||
|
||||
| Tipo | Descripción | Características |
|
||||
|------|-------------|-----------------|
|
||||
| **Multiple Choice** | Una respuesta correcta | 2-6 opciones |
|
||||
| **Multiple Select** | Varias respuestas correctas | Checkbox, puntuación parcial |
|
||||
| **True/False** | Verdadero o falso | 2 opciones |
|
||||
| **Fill in the Blank** | Completar espacios | Input de texto, validación |
|
||||
| **Matching** | Emparejar elementos | Drag & drop opcional |
|
||||
| **Ordering** | Ordenar elementos | Secuencia correcta |
|
||||
|
||||
### RF-EDU-004.2: Estructura de Quiz
|
||||
|
||||
Cada quiz debe tener:
|
||||
- Título y descripción
|
||||
- Tiempo límite (opcional)
|
||||
- Número de preguntas
|
||||
- Puntuación mínima para aprobar (% o puntos)
|
||||
- Número de intentos permitidos (ilimitado, 1, 2, 3...)
|
||||
- Modo: Práctica (sin límite) o Evaluación (formal)
|
||||
- Mostrar respuestas correctas: Inmediato, Al finalizar, Nunca
|
||||
- Barajear preguntas (randomizar orden)
|
||||
- Barajear opciones de respuesta
|
||||
|
||||
### RF-EDU-004.3: Interfaz de Quiz
|
||||
|
||||
El sistema debe mostrar:
|
||||
- Contador de preguntas (Pregunta 1 de 10)
|
||||
- Barra de progreso del quiz
|
||||
- Timer countdown si hay límite de tiempo
|
||||
- Pregunta actual con opciones
|
||||
- Botones: "Anterior", "Siguiente", "Marcar para revisión"
|
||||
- Navegador de preguntas (minimap con estado: respondida, marcada, pendiente)
|
||||
- Botón "Finalizar quiz" (requiere confirmación)
|
||||
- Auto-submit cuando expira el tiempo
|
||||
|
||||
### RF-EDU-004.4: Navegación y Estados
|
||||
|
||||
Estados de pregunta:
|
||||
- **No respondida:** Sin respuesta seleccionada
|
||||
- **Respondida:** Respuesta seleccionada
|
||||
- **Marcada:** Flagged para revisión posterior
|
||||
- **Correcta:** Solo visible después de submit (si configurado)
|
||||
- **Incorrecta:** Solo visible después de submit (si configurado)
|
||||
|
||||
El usuario debe poder:
|
||||
- Navegar libremente entre preguntas antes de submit
|
||||
- Cambiar respuestas antes de finalizar
|
||||
- Marcar preguntas para revisar después
|
||||
- Ver resumen antes de enviar
|
||||
|
||||
### RF-EDU-004.5: Calificación y Resultados
|
||||
|
||||
Al finalizar el quiz, mostrar:
|
||||
- Puntuación obtenida (X/Y puntos o %)
|
||||
- Estado: Aprobado / Reprobado
|
||||
- Tiempo invertido
|
||||
- Feedback general basado en score
|
||||
- Desglose por pregunta (si configurado):
|
||||
- Pregunta
|
||||
- Tu respuesta
|
||||
- Respuesta correcta
|
||||
- Explicación
|
||||
- Intentos restantes
|
||||
- Botón "Reintentar" si aplica
|
||||
- Botón "Continuar al siguiente contenido"
|
||||
|
||||
### RF-EDU-004.6: Historial de Intentos
|
||||
|
||||
El sistema debe:
|
||||
- Guardar todos los intentos del usuario
|
||||
- Mostrar tabla con: fecha, puntuación, tiempo, estado
|
||||
- Permitir ver detalle de intento anterior
|
||||
- Mostrar mejor intento destacado
|
||||
- Calcular promedio de intentos
|
||||
- Guardar última puntuación como oficial
|
||||
|
||||
### RF-EDU-004.7: Feedback y Explicaciones
|
||||
|
||||
El sistema debe permitir:
|
||||
- Explicación de respuesta correcta (markdown)
|
||||
- Explicación de por qué otras opciones son incorrectas
|
||||
- Links a recursos relacionados
|
||||
- Video explicativo opcional
|
||||
- Sugerencias de lecciones para repasar
|
||||
|
||||
### RF-EDU-004.8: Analítica de Quiz
|
||||
|
||||
Para cada pregunta, rastrear:
|
||||
- Número de veces respondida
|
||||
- Número de respuestas correctas
|
||||
- Número de respuestas incorrectas
|
||||
- Tasa de éxito global (%)
|
||||
- Tiempo promedio de respuesta
|
||||
- Opción más elegida (para detectar confusión)
|
||||
|
||||
Para cada quiz, rastrear:
|
||||
- Número de intentos totales
|
||||
- Tasa de aprobación (%)
|
||||
- Puntuación promedio
|
||||
- Tiempo promedio de completitud
|
||||
- Pregunta más difícil (menor % acierto)
|
||||
- Pregunta más fácil (mayor % acierto)
|
||||
|
||||
---
|
||||
|
||||
## Datos de Entrada
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| quizId | string | UUID del quiz |
|
||||
| answers | object | Mapa de questionId -> respuesta |
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Quiz {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
lessonId?: string;
|
||||
courseId: string;
|
||||
timeLimit?: number; // minutos
|
||||
passingScore: number; // 0-100
|
||||
maxAttempts: number; // 0 = ilimitado
|
||||
questionCount: number;
|
||||
totalPoints: number;
|
||||
shuffleQuestions: boolean;
|
||||
shuffleOptions: boolean;
|
||||
showAnswers: 'immediate' | 'after_submit' | 'never';
|
||||
mode: 'practice' | 'assessment';
|
||||
}
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
quizId: string;
|
||||
type: 'multiple_choice' | 'multiple_select' | 'true_false' | 'fill_blank' | 'matching' | 'ordering';
|
||||
question: string; // Markdown
|
||||
points: number;
|
||||
order: number;
|
||||
|
||||
// Para multiple choice/select
|
||||
options?: {
|
||||
id: string;
|
||||
text: string;
|
||||
isCorrect: boolean;
|
||||
explanation?: string;
|
||||
}[];
|
||||
|
||||
// Para fill in the blank
|
||||
correctAnswers?: string[];
|
||||
caseSensitive?: boolean;
|
||||
|
||||
// Para matching
|
||||
pairs?: {
|
||||
left: string;
|
||||
right: string;
|
||||
}[];
|
||||
|
||||
// Para ordering
|
||||
correctOrder?: string[];
|
||||
|
||||
explanation?: string; // Explicación general
|
||||
hint?: string;
|
||||
relatedResources?: {
|
||||
type: 'lesson' | 'article' | 'video';
|
||||
id: string;
|
||||
title: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface QuizAttempt {
|
||||
id: string;
|
||||
quizId: string;
|
||||
userId: string;
|
||||
attemptNumber: number;
|
||||
startedAt: string;
|
||||
submittedAt?: string;
|
||||
timeSpent: number; // segundos
|
||||
|
||||
answers: {
|
||||
questionId: string;
|
||||
userAnswer: any;
|
||||
isCorrect: boolean;
|
||||
pointsEarned: number;
|
||||
}[];
|
||||
|
||||
score: number; // 0-100
|
||||
pointsEarned: number;
|
||||
totalPoints: number;
|
||||
passed: boolean;
|
||||
|
||||
analytics: {
|
||||
questionsCorrect: number;
|
||||
questionsIncorrect: number;
|
||||
questionsSkipped: number;
|
||||
avgTimePerQuestion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface QuizResults {
|
||||
attempt: QuizAttempt;
|
||||
quiz: Quiz;
|
||||
questions: (Question & {
|
||||
userAnswer: any;
|
||||
isCorrect: boolean;
|
||||
pointsEarned: number;
|
||||
})[];
|
||||
feedback: {
|
||||
title: string;
|
||||
message: string;
|
||||
suggestions?: string[];
|
||||
};
|
||||
attemptsRemaining: number;
|
||||
canRetake: boolean;
|
||||
nextContent?: {
|
||||
type: 'lesson' | 'quiz' | 'module';
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Puntuación mínima:** Default 70% para aprobar
|
||||
2. **Intentos:** Si se agotan intentos, usuario debe esperar 24h o contactar soporte
|
||||
3. **Tiempo límite:** Si expira, se auto-submit con respuestas actuales
|
||||
4. **Preguntas obligatorias:** No se puede submit sin responder todas (modo evaluación)
|
||||
5. **Modo práctica:** Sin límite de intentos, sin guardar en historial oficial
|
||||
6. **Partial credit:** Multiple select otorga puntos parciales (50% si elige 2/4 correctas)
|
||||
7. **Shuffle:** Si está activado, orden diferente en cada intento
|
||||
8. **Feedback inmediato:** Solo en modo práctica
|
||||
9. **Certificación:** Quiz final de curso debe aprobarse para certificado
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario inicia quiz
|
||||
DADO que el usuario está en una lección con quiz
|
||||
CUANDO hace click en "Iniciar quiz"
|
||||
ENTONCES se muestra pantalla de introducción del quiz
|
||||
Y se muestra título, descripción, número de preguntas
|
||||
Y se muestra tiempo límite si aplica
|
||||
Y se muestra intentos disponibles
|
||||
Y se muestra puntuación requerida para aprobar
|
||||
Y se muestra botón "Comenzar"
|
||||
|
||||
Escenario: Usuario responde preguntas
|
||||
DADO que el usuario comenzó el quiz
|
||||
CUANDO selecciona una respuesta
|
||||
ENTONCES la opción se marca como seleccionada
|
||||
Y la pregunta se marca como "respondida"
|
||||
Y puede navegar a siguiente pregunta
|
||||
Y puede volver a preguntas anteriores
|
||||
Y puede cambiar respuesta antes de submit
|
||||
|
||||
Escenario: Usuario finaliza quiz exitosamente
|
||||
DADO que el usuario respondió todas las preguntas
|
||||
CUANDO hace click en "Finalizar quiz"
|
||||
Y confirma en el modal
|
||||
ENTONCES se calcula la puntuación
|
||||
Y se muestra pantalla de resultados
|
||||
Y se muestra "Aprobado" si score >= passing score
|
||||
Y se desbloquea siguiente contenido
|
||||
Y se otorga XP por aprobar
|
||||
|
||||
Escenario: Usuario reprueba quiz
|
||||
DADO que el usuario envió el quiz
|
||||
Y la puntuación es < passing score
|
||||
ENTONCES se muestra pantalla de resultados
|
||||
Y se muestra "Reprobado"
|
||||
Y se muestra feedback con áreas a mejorar
|
||||
Y se muestra "Intentos restantes: X"
|
||||
Y se muestra botón "Reintentar"
|
||||
Y siguiente contenido permanece bloqueado
|
||||
|
||||
Escenario: Quiz con tiempo límite expira
|
||||
DADO que el quiz tiene tiempo límite de 30 minutos
|
||||
Y el usuario está en la pregunta 5 de 10
|
||||
CUANDO el tiempo llega a 0
|
||||
ENTONCES el quiz se envía automáticamente
|
||||
Y se califica con respuestas hasta el momento
|
||||
Y preguntas sin responder cuentan como incorrectas
|
||||
|
||||
Escenario: Ver explicación de respuestas
|
||||
DADO que el quiz permite ver respuestas
|
||||
Y el usuario envió el quiz
|
||||
CUANDO ve los resultados
|
||||
ENTONCES se muestran todas las preguntas
|
||||
Y se destacan respuestas correctas en verde
|
||||
Y se destacan respuestas incorrectas en rojo
|
||||
Y se muestra explicación de cada respuesta
|
||||
Y se muestran recursos relacionados
|
||||
|
||||
Escenario: Reintentar quiz
|
||||
DADO que el usuario reprobó un quiz
|
||||
Y tiene intentos disponibles
|
||||
CUANDO hace click en "Reintentar"
|
||||
ENTONCES se inicia nuevo intento
|
||||
Y preguntas pueden estar en diferente orden
|
||||
Y respuestas anteriores no están pre-seleccionadas
|
||||
Y contador de intentos se decrementa
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PostgreSQL para quizzes y resultados
|
||||
- Redis para cachear quizzes activos
|
||||
- WebSocket para timer en tiempo real (opcional)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Implementar auto-save cada 30s para evitar pérdida de progreso
|
||||
- Usar WebSockets para sincronizar timer entre tabs
|
||||
- Encriptar respuestas correctas en frontend
|
||||
- Validar respuestas en backend (nunca confiar en frontend)
|
||||
- Implementar rate limiting para prevenir brute force
|
||||
- Usar optimistic updates para mejor UX
|
||||
- Considerar adaptive quizzes (ajustar dificultad según respuestas)
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/quizzes.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/QuizPlayer.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla education.quizzes
|
||||
- [ ] Tabla education.questions con FK a quiz
|
||||
- [ ] Tabla education.question_options
|
||||
- [ ] Tabla education.quiz_attempts
|
||||
- [ ] Tabla education.quiz_answers
|
||||
- [ ] Índices para queries por usuario y quiz
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/quizzes/:id (sin respuestas correctas)
|
||||
- [ ] Endpoint POST /education/quizzes/:id/start
|
||||
- [ ] Endpoint POST /education/quizzes/:id/submit
|
||||
- [ ] Endpoint GET /education/quizzes/:id/attempts (historial)
|
||||
- [ ] Endpoint GET /education/quizzes/:id/results/:attemptId
|
||||
- [ ] Implementar QuizService.gradeAttempt()
|
||||
- [ ] Implementar shuffle de preguntas y opciones
|
||||
- [ ] Rate limiting en submit
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear QuizIntroPage.tsx
|
||||
- [ ] Crear QuizPlayerPage.tsx
|
||||
- [ ] Crear componente QuestionRenderer.tsx (soporta todos los tipos)
|
||||
- [ ] Crear componente QuizNavigator.tsx
|
||||
- [ ] Crear componente QuizTimer.tsx
|
||||
- [ ] Crear QuizResultsPage.tsx
|
||||
- [ ] Crear componente QuestionExplanation.tsx
|
||||
- [ ] Auto-save de respuestas cada 30s
|
||||
- [ ] Implementar quizStore
|
||||
- [ ] Confirmación antes de salir (window.onbeforeunload)
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test calificación de quiz con diferentes tipos de preguntas
|
||||
- [ ] Test partial credit en multiple select
|
||||
- [ ] Test expiración de tiempo
|
||||
- [ ] Test E2E completar quiz y aprobar
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "RF-EDU-004"
|
||||
title: "Sistema de Quizzes"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Alta"
|
||||
module: "education"
|
||||
epic: "OQI-002"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-EDU-004: Sistema de Quizzes
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P1
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar un sistema completo de evaluaciones interactivas (quizzes) que permita validar el conocimiento adquirido por los usuarios, con soporte para múltiples tipos de preguntas, feedback inmediato, intentos limitados, calificaciones y análisis de resultados.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-004.1: Tipos de Preguntas
|
||||
|
||||
El sistema debe soportar:
|
||||
|
||||
| Tipo | Descripción | Características |
|
||||
|------|-------------|-----------------|
|
||||
| **Multiple Choice** | Una respuesta correcta | 2-6 opciones |
|
||||
| **Multiple Select** | Varias respuestas correctas | Checkbox, puntuación parcial |
|
||||
| **True/False** | Verdadero o falso | 2 opciones |
|
||||
| **Fill in the Blank** | Completar espacios | Input de texto, validación |
|
||||
| **Matching** | Emparejar elementos | Drag & drop opcional |
|
||||
| **Ordering** | Ordenar elementos | Secuencia correcta |
|
||||
|
||||
### RF-EDU-004.2: Estructura de Quiz
|
||||
|
||||
Cada quiz debe tener:
|
||||
- Título y descripción
|
||||
- Tiempo límite (opcional)
|
||||
- Número de preguntas
|
||||
- Puntuación mínima para aprobar (% o puntos)
|
||||
- Número de intentos permitidos (ilimitado, 1, 2, 3...)
|
||||
- Modo: Práctica (sin límite) o Evaluación (formal)
|
||||
- Mostrar respuestas correctas: Inmediato, Al finalizar, Nunca
|
||||
- Barajear preguntas (randomizar orden)
|
||||
- Barajear opciones de respuesta
|
||||
|
||||
### RF-EDU-004.3: Interfaz de Quiz
|
||||
|
||||
El sistema debe mostrar:
|
||||
- Contador de preguntas (Pregunta 1 de 10)
|
||||
- Barra de progreso del quiz
|
||||
- Timer countdown si hay límite de tiempo
|
||||
- Pregunta actual con opciones
|
||||
- Botones: "Anterior", "Siguiente", "Marcar para revisión"
|
||||
- Navegador de preguntas (minimap con estado: respondida, marcada, pendiente)
|
||||
- Botón "Finalizar quiz" (requiere confirmación)
|
||||
- Auto-submit cuando expira el tiempo
|
||||
|
||||
### RF-EDU-004.4: Navegación y Estados
|
||||
|
||||
Estados de pregunta:
|
||||
- **No respondida:** Sin respuesta seleccionada
|
||||
- **Respondida:** Respuesta seleccionada
|
||||
- **Marcada:** Flagged para revisión posterior
|
||||
- **Correcta:** Solo visible después de submit (si configurado)
|
||||
- **Incorrecta:** Solo visible después de submit (si configurado)
|
||||
|
||||
El usuario debe poder:
|
||||
- Navegar libremente entre preguntas antes de submit
|
||||
- Cambiar respuestas antes de finalizar
|
||||
- Marcar preguntas para revisar después
|
||||
- Ver resumen antes de enviar
|
||||
|
||||
### RF-EDU-004.5: Calificación y Resultados
|
||||
|
||||
Al finalizar el quiz, mostrar:
|
||||
- Puntuación obtenida (X/Y puntos o %)
|
||||
- Estado: Aprobado / Reprobado
|
||||
- Tiempo invertido
|
||||
- Feedback general basado en score
|
||||
- Desglose por pregunta (si configurado):
|
||||
- Pregunta
|
||||
- Tu respuesta
|
||||
- Respuesta correcta
|
||||
- Explicación
|
||||
- Intentos restantes
|
||||
- Botón "Reintentar" si aplica
|
||||
- Botón "Continuar al siguiente contenido"
|
||||
|
||||
### RF-EDU-004.6: Historial de Intentos
|
||||
|
||||
El sistema debe:
|
||||
- Guardar todos los intentos del usuario
|
||||
- Mostrar tabla con: fecha, puntuación, tiempo, estado
|
||||
- Permitir ver detalle de intento anterior
|
||||
- Mostrar mejor intento destacado
|
||||
- Calcular promedio de intentos
|
||||
- Guardar última puntuación como oficial
|
||||
|
||||
### RF-EDU-004.7: Feedback y Explicaciones
|
||||
|
||||
El sistema debe permitir:
|
||||
- Explicación de respuesta correcta (markdown)
|
||||
- Explicación de por qué otras opciones son incorrectas
|
||||
- Links a recursos relacionados
|
||||
- Video explicativo opcional
|
||||
- Sugerencias de lecciones para repasar
|
||||
|
||||
### RF-EDU-004.8: Analítica de Quiz
|
||||
|
||||
Para cada pregunta, rastrear:
|
||||
- Número de veces respondida
|
||||
- Número de respuestas correctas
|
||||
- Número de respuestas incorrectas
|
||||
- Tasa de éxito global (%)
|
||||
- Tiempo promedio de respuesta
|
||||
- Opción más elegida (para detectar confusión)
|
||||
|
||||
Para cada quiz, rastrear:
|
||||
- Número de intentos totales
|
||||
- Tasa de aprobación (%)
|
||||
- Puntuación promedio
|
||||
- Tiempo promedio de completitud
|
||||
- Pregunta más difícil (menor % acierto)
|
||||
- Pregunta más fácil (mayor % acierto)
|
||||
|
||||
---
|
||||
|
||||
## Datos de Entrada
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| quizId | string | UUID del quiz |
|
||||
| answers | object | Mapa de questionId -> respuesta |
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Quiz {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
lessonId?: string;
|
||||
courseId: string;
|
||||
timeLimit?: number; // minutos
|
||||
passingScore: number; // 0-100
|
||||
maxAttempts: number; // 0 = ilimitado
|
||||
questionCount: number;
|
||||
totalPoints: number;
|
||||
shuffleQuestions: boolean;
|
||||
shuffleOptions: boolean;
|
||||
showAnswers: 'immediate' | 'after_submit' | 'never';
|
||||
mode: 'practice' | 'assessment';
|
||||
}
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
quizId: string;
|
||||
type: 'multiple_choice' | 'multiple_select' | 'true_false' | 'fill_blank' | 'matching' | 'ordering';
|
||||
question: string; // Markdown
|
||||
points: number;
|
||||
order: number;
|
||||
|
||||
// Para multiple choice/select
|
||||
options?: {
|
||||
id: string;
|
||||
text: string;
|
||||
isCorrect: boolean;
|
||||
explanation?: string;
|
||||
}[];
|
||||
|
||||
// Para fill in the blank
|
||||
correctAnswers?: string[];
|
||||
caseSensitive?: boolean;
|
||||
|
||||
// Para matching
|
||||
pairs?: {
|
||||
left: string;
|
||||
right: string;
|
||||
}[];
|
||||
|
||||
// Para ordering
|
||||
correctOrder?: string[];
|
||||
|
||||
explanation?: string; // Explicación general
|
||||
hint?: string;
|
||||
relatedResources?: {
|
||||
type: 'lesson' | 'article' | 'video';
|
||||
id: string;
|
||||
title: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface QuizAttempt {
|
||||
id: string;
|
||||
quizId: string;
|
||||
userId: string;
|
||||
attemptNumber: number;
|
||||
startedAt: string;
|
||||
submittedAt?: string;
|
||||
timeSpent: number; // segundos
|
||||
|
||||
answers: {
|
||||
questionId: string;
|
||||
userAnswer: any;
|
||||
isCorrect: boolean;
|
||||
pointsEarned: number;
|
||||
}[];
|
||||
|
||||
score: number; // 0-100
|
||||
pointsEarned: number;
|
||||
totalPoints: number;
|
||||
passed: boolean;
|
||||
|
||||
analytics: {
|
||||
questionsCorrect: number;
|
||||
questionsIncorrect: number;
|
||||
questionsSkipped: number;
|
||||
avgTimePerQuestion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface QuizResults {
|
||||
attempt: QuizAttempt;
|
||||
quiz: Quiz;
|
||||
questions: (Question & {
|
||||
userAnswer: any;
|
||||
isCorrect: boolean;
|
||||
pointsEarned: number;
|
||||
})[];
|
||||
feedback: {
|
||||
title: string;
|
||||
message: string;
|
||||
suggestions?: string[];
|
||||
};
|
||||
attemptsRemaining: number;
|
||||
canRetake: boolean;
|
||||
nextContent?: {
|
||||
type: 'lesson' | 'quiz' | 'module';
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Puntuación mínima:** Default 70% para aprobar
|
||||
2. **Intentos:** Si se agotan intentos, usuario debe esperar 24h o contactar soporte
|
||||
3. **Tiempo límite:** Si expira, se auto-submit con respuestas actuales
|
||||
4. **Preguntas obligatorias:** No se puede submit sin responder todas (modo evaluación)
|
||||
5. **Modo práctica:** Sin límite de intentos, sin guardar en historial oficial
|
||||
6. **Partial credit:** Multiple select otorga puntos parciales (50% si elige 2/4 correctas)
|
||||
7. **Shuffle:** Si está activado, orden diferente en cada intento
|
||||
8. **Feedback inmediato:** Solo en modo práctica
|
||||
9. **Certificación:** Quiz final de curso debe aprobarse para certificado
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario inicia quiz
|
||||
DADO que el usuario está en una lección con quiz
|
||||
CUANDO hace click en "Iniciar quiz"
|
||||
ENTONCES se muestra pantalla de introducción del quiz
|
||||
Y se muestra título, descripción, número de preguntas
|
||||
Y se muestra tiempo límite si aplica
|
||||
Y se muestra intentos disponibles
|
||||
Y se muestra puntuación requerida para aprobar
|
||||
Y se muestra botón "Comenzar"
|
||||
|
||||
Escenario: Usuario responde preguntas
|
||||
DADO que el usuario comenzó el quiz
|
||||
CUANDO selecciona una respuesta
|
||||
ENTONCES la opción se marca como seleccionada
|
||||
Y la pregunta se marca como "respondida"
|
||||
Y puede navegar a siguiente pregunta
|
||||
Y puede volver a preguntas anteriores
|
||||
Y puede cambiar respuesta antes de submit
|
||||
|
||||
Escenario: Usuario finaliza quiz exitosamente
|
||||
DADO que el usuario respondió todas las preguntas
|
||||
CUANDO hace click en "Finalizar quiz"
|
||||
Y confirma en el modal
|
||||
ENTONCES se calcula la puntuación
|
||||
Y se muestra pantalla de resultados
|
||||
Y se muestra "Aprobado" si score >= passing score
|
||||
Y se desbloquea siguiente contenido
|
||||
Y se otorga XP por aprobar
|
||||
|
||||
Escenario: Usuario reprueba quiz
|
||||
DADO que el usuario envió el quiz
|
||||
Y la puntuación es < passing score
|
||||
ENTONCES se muestra pantalla de resultados
|
||||
Y se muestra "Reprobado"
|
||||
Y se muestra feedback con áreas a mejorar
|
||||
Y se muestra "Intentos restantes: X"
|
||||
Y se muestra botón "Reintentar"
|
||||
Y siguiente contenido permanece bloqueado
|
||||
|
||||
Escenario: Quiz con tiempo límite expira
|
||||
DADO que el quiz tiene tiempo límite de 30 minutos
|
||||
Y el usuario está en la pregunta 5 de 10
|
||||
CUANDO el tiempo llega a 0
|
||||
ENTONCES el quiz se envía automáticamente
|
||||
Y se califica con respuestas hasta el momento
|
||||
Y preguntas sin responder cuentan como incorrectas
|
||||
|
||||
Escenario: Ver explicación de respuestas
|
||||
DADO que el quiz permite ver respuestas
|
||||
Y el usuario envió el quiz
|
||||
CUANDO ve los resultados
|
||||
ENTONCES se muestran todas las preguntas
|
||||
Y se destacan respuestas correctas en verde
|
||||
Y se destacan respuestas incorrectas en rojo
|
||||
Y se muestra explicación de cada respuesta
|
||||
Y se muestran recursos relacionados
|
||||
|
||||
Escenario: Reintentar quiz
|
||||
DADO que el usuario reprobó un quiz
|
||||
Y tiene intentos disponibles
|
||||
CUANDO hace click en "Reintentar"
|
||||
ENTONCES se inicia nuevo intento
|
||||
Y preguntas pueden estar en diferente orden
|
||||
Y respuestas anteriores no están pre-seleccionadas
|
||||
Y contador de intentos se decrementa
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PostgreSQL para quizzes y resultados
|
||||
- Redis para cachear quizzes activos
|
||||
- WebSocket para timer en tiempo real (opcional)
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Implementar auto-save cada 30s para evitar pérdida de progreso
|
||||
- Usar WebSockets para sincronizar timer entre tabs
|
||||
- Encriptar respuestas correctas en frontend
|
||||
- Validar respuestas en backend (nunca confiar en frontend)
|
||||
- Implementar rate limiting para prevenir brute force
|
||||
- Usar optimistic updates para mejor UX
|
||||
- Considerar adaptive quizzes (ajustar dificultad según respuestas)
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/quizzes.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/QuizPlayer.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla education.quizzes
|
||||
- [ ] Tabla education.questions con FK a quiz
|
||||
- [ ] Tabla education.question_options
|
||||
- [ ] Tabla education.quiz_attempts
|
||||
- [ ] Tabla education.quiz_answers
|
||||
- [ ] Índices para queries por usuario y quiz
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /education/quizzes/:id (sin respuestas correctas)
|
||||
- [ ] Endpoint POST /education/quizzes/:id/start
|
||||
- [ ] Endpoint POST /education/quizzes/:id/submit
|
||||
- [ ] Endpoint GET /education/quizzes/:id/attempts (historial)
|
||||
- [ ] Endpoint GET /education/quizzes/:id/results/:attemptId
|
||||
- [ ] Implementar QuizService.gradeAttempt()
|
||||
- [ ] Implementar shuffle de preguntas y opciones
|
||||
- [ ] Rate limiting en submit
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear QuizIntroPage.tsx
|
||||
- [ ] Crear QuizPlayerPage.tsx
|
||||
- [ ] Crear componente QuestionRenderer.tsx (soporta todos los tipos)
|
||||
- [ ] Crear componente QuizNavigator.tsx
|
||||
- [ ] Crear componente QuizTimer.tsx
|
||||
- [ ] Crear QuizResultsPage.tsx
|
||||
- [ ] Crear componente QuestionExplanation.tsx
|
||||
- [ ] Auto-save de respuestas cada 30s
|
||||
- [ ] Implementar quizStore
|
||||
- [ ] Confirmación antes de salir (window.onbeforeunload)
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test calificación de quiz con diferentes tipos de preguntas
|
||||
- [ ] Test partial credit en multiple select
|
||||
- [ ] Test expiración de tiempo
|
||||
- [ ] Test E2E completar quiz y aprobar
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,323 +1,336 @@
|
||||
# RF-EDU-005: Sistema de Certificados
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P2
|
||||
**Story Points:** 5
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar certificados digitales verificables que se otorgan automáticamente al completar cursos, validando el conocimiento adquirido y permitiendo a los usuarios compartir sus logros profesionales en redes sociales y plataformas de empleo.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-005.1: Generación de Certificados
|
||||
|
||||
El sistema debe:
|
||||
- Generar certificado automáticamente al completar 100% de un curso
|
||||
- Validar que todos los quizzes obligatorios estén aprobados
|
||||
- Validar que todas las lecciones estén marcadas como completadas
|
||||
- Asignar ID único de certificado (formato: OQI-EDU-XXXXXXXX)
|
||||
- Generar PDF con diseño profesional
|
||||
- Almacenar PDF en S3 o similar
|
||||
- Registrar en blockchain para verificación (opcional, fase 2)
|
||||
|
||||
### RF-EDU-005.2: Contenido del Certificado
|
||||
|
||||
Cada certificado debe incluir:
|
||||
- Logo de OrbiQuant IA
|
||||
- Título: "Certificado de Finalización"
|
||||
- Nombre completo del usuario
|
||||
- Título del curso completado
|
||||
- Fecha de finalización
|
||||
- ID único del certificado
|
||||
- Firma digital del instructor (imagen)
|
||||
- Firma digital de la plataforma
|
||||
- QR code para verificación online
|
||||
- Footer: "Verifica este certificado en orbiquant.com/verify/{certificateId}"
|
||||
|
||||
Template:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ [LOGO ORBIQUANT] │
|
||||
│ │
|
||||
│ CERTIFICADO DE FINALIZACIÓN │
|
||||
│ │
|
||||
│ Se certifica que │
|
||||
│ │
|
||||
│ [NOMBRE USUARIO] │
|
||||
│ │
|
||||
│ Ha completado exitosamente el curso │
|
||||
│ │
|
||||
│ "[TÍTULO DEL CURSO]" │
|
||||
│ │
|
||||
│ Fecha: [DD/MM/YYYY] │
|
||||
│ Certificado: OQI-EDU-XXXXXXXX │
|
||||
│ │
|
||||
│ ___________________ ___________________ │
|
||||
│ [Firma Instructor] [Firma Plataforma] │
|
||||
│ │
|
||||
│ [QR CODE] │
|
||||
│ Verifica en orbiquant.com/verify/XXXX │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### RF-EDU-005.3: Verificación de Certificados
|
||||
|
||||
El sistema debe:
|
||||
- Proveer página pública /verify/:certificateId
|
||||
- Mostrar información del certificado sin login
|
||||
- Validar que el ID existe en base de datos
|
||||
- Mostrar: nombre, curso, fecha, estado (válido/revocado)
|
||||
- Proteger contra scraping (rate limiting, captcha)
|
||||
- API pública GET /api/certificates/verify/:id
|
||||
- Responder en JSON para integraciones
|
||||
|
||||
### RF-EDU-005.4: Galería de Certificados del Usuario
|
||||
|
||||
El sistema debe:
|
||||
- Página /education/certificates con todos los certificados del usuario
|
||||
- Mostrar: thumbnail, título del curso, fecha
|
||||
- Filtrar por: fecha, curso, categoría
|
||||
- Buscar por nombre de curso
|
||||
- Ordenar por: más reciente, alfabético, categoría
|
||||
- Vista de cuadrícula o lista
|
||||
- Contador: "Has obtenido X certificados"
|
||||
|
||||
### RF-EDU-005.5: Descarga y Compartir
|
||||
|
||||
El sistema debe permitir:
|
||||
- Descargar PDF del certificado
|
||||
- Botón "Compartir en LinkedIn" (pre-rellenado)
|
||||
- Botón "Compartir en Twitter/X"
|
||||
- Botón "Copiar link de verificación"
|
||||
- Generar imagen social (Open Graph) para compartir
|
||||
- Agregar a perfil público del usuario (opcional)
|
||||
|
||||
Integración LinkedIn:
|
||||
```javascript
|
||||
// Pre-llenar certificación en LinkedIn
|
||||
const linkedInUrl = `https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=${encodeURIComponent(courseTitle)}&organizationName=OrbiQuant IA&issueYear=${year}&issueMonth=${month}&certUrl=${verifyUrl}&certId=${certificateId}`;
|
||||
```
|
||||
|
||||
### RF-EDU-005.6: Perfil Público de Certificados
|
||||
|
||||
El sistema debe:
|
||||
- Permitir al usuario crear perfil público opcional
|
||||
- URL: orbiquant.com/u/:username/certificates
|
||||
- Mostrar solo certificados que el usuario hizo públicos
|
||||
- Galería visual de certificados
|
||||
- Bio del usuario
|
||||
- Enlaces a redes sociales
|
||||
- No requiere login para ver
|
||||
|
||||
### RF-EDU-005.7: Revocación de Certificados
|
||||
|
||||
El sistema debe permitir (solo admins):
|
||||
- Revocar certificado por fraude
|
||||
- Agregar motivo de revocación
|
||||
- Notificar al usuario por email
|
||||
- Marcar certificado como "REVOKED" en verificación
|
||||
- Mantener historial de revocaciones
|
||||
|
||||
### RF-EDU-005.8: Plantillas de Certificados
|
||||
|
||||
El sistema debe soportar:
|
||||
- Múltiples plantillas (por categoría o nivel)
|
||||
- Plantilla estándar para todos los cursos
|
||||
- Plantilla especial para cursos premium
|
||||
- Plantilla con colores de marca
|
||||
- Editor de plantillas para admins (fase 2)
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Certificate {
|
||||
id: string;
|
||||
certificateNumber: string; // OQI-EDU-XXXXXXXX
|
||||
userId: string;
|
||||
userName: string;
|
||||
courseId: string;
|
||||
courseTitle: string;
|
||||
courseCategory: string;
|
||||
completedAt: string;
|
||||
issuedAt: string;
|
||||
pdfUrl: string;
|
||||
verifyUrl: string;
|
||||
qrCodeUrl: string;
|
||||
status: 'active' | 'revoked';
|
||||
revocationReason?: string;
|
||||
instructorSignature: string;
|
||||
metadata: {
|
||||
duration: number; // horas del curso
|
||||
moduleCount: number;
|
||||
lessonCount: number;
|
||||
finalScore?: number; // Si hay quiz final
|
||||
};
|
||||
}
|
||||
|
||||
interface VerificationResult {
|
||||
valid: boolean;
|
||||
certificate?: {
|
||||
certificateNumber: string;
|
||||
recipientName: string;
|
||||
courseTitle: string;
|
||||
completedAt: string;
|
||||
status: 'active' | 'revoked';
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Requisitos para certificado:**
|
||||
- 100% de lecciones completadas
|
||||
- Todos los quizzes aprobados (si aplica)
|
||||
- Curso debe estar marcado como "completable" (algunos cursos no otorgan certificado)
|
||||
2. **ID único:** Formato OQI-EDU-{8 dígitos hex aleatorios}
|
||||
3. **Nombre en certificado:** Se usa nombre completo del perfil del usuario
|
||||
4. **Fecha:** Fecha de finalización del curso (última lección completada)
|
||||
5. **PDF inmutable:** Una vez generado, el PDF no se regenera aunque el usuario cambie su nombre
|
||||
6. **Caducidad:** Los certificados no caducan
|
||||
7. **Límite de verificaciones:** 100 verificaciones por IP por hora
|
||||
8. **Perfil público:** Opt-in, deshabilitado por default
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario completa curso y obtiene certificado
|
||||
DADO que el usuario completó todas las lecciones de un curso
|
||||
Y aprobó todos los quizzes obligatorios
|
||||
CUANDO se marca la última lección como completada
|
||||
ENTONCES se genera automáticamente un certificado
|
||||
Y se muestra modal de felicitación
|
||||
Y se envía email con el certificado adjunto
|
||||
Y se muestra botón "Ver certificado"
|
||||
|
||||
Escenario: Usuario descarga certificado
|
||||
DADO que el usuario tiene un certificado
|
||||
CUANDO accede a /education/certificates
|
||||
Y hace click en "Descargar PDF"
|
||||
ENTONCES se descarga archivo PDF con el certificado
|
||||
Y el PDF contiene: nombre, curso, fecha, ID, firmas, QR
|
||||
|
||||
Escenario: Usuario comparte en LinkedIn
|
||||
DADO que el usuario está viendo su certificado
|
||||
CUANDO hace click en "Compartir en LinkedIn"
|
||||
ENTONCES se abre LinkedIn en nueva pestaña
|
||||
Y el formulario está pre-llenado con:
|
||||
- Nombre del curso
|
||||
- Organización: OrbiQuant IA
|
||||
- Fecha de emisión
|
||||
- URL de verificación
|
||||
- ID del certificado
|
||||
|
||||
Escenario: Tercero verifica certificado
|
||||
DADO que alguien tiene el ID de un certificado
|
||||
CUANDO accede a orbiquant.com/verify/OQI-EDU-12345678
|
||||
ENTONCES se muestra página de verificación
|
||||
Y se muestra: nombre del usuario, curso, fecha
|
||||
Y se muestra badge "✓ Certificado Válido"
|
||||
Y NO requiere login para ver
|
||||
|
||||
Escenario: Verificar certificado inválido
|
||||
DADO que alguien accede a /verify/INVALID-ID
|
||||
CUANDO el ID no existe en la base de datos
|
||||
ENTONCES se muestra "Certificado no encontrado"
|
||||
Y se sugiere verificar el ID ingresado
|
||||
|
||||
Escenario: Ver certificado revocado
|
||||
DADO que un certificado fue revocado por admin
|
||||
CUANDO alguien intenta verificarlo
|
||||
ENTONCES se muestra "Certificado Revocado"
|
||||
Y se muestra motivo de revocación
|
||||
Y se marca en rojo como inválido
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PDF generation library (PDFKit, Puppeteer, o similar)
|
||||
- S3 para almacenar PDFs
|
||||
- QR code generator
|
||||
- Email service para enviar certificados
|
||||
- LinkedIn API para integración
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Usar Puppeteer para generar PDFs desde HTML template
|
||||
- Almacenar PDFs con nombre: certificates/{userId}/{certificateId}.pdf
|
||||
- Generar QR codes con librería qrcode.js
|
||||
- Implementar caché de verificaciones (Redis) para reducir load
|
||||
- Considerar watermark en PDFs para prevenir falsificación
|
||||
- Usar signed URLs de S3 para descargas seguras
|
||||
- Implementar rate limiting agresivo en endpoint de verificación
|
||||
- Para blockchain: Guardar hash del certificado en Ethereum/Polygon
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/certificates.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/Certificates.tsx`
|
||||
- Templates: `/backend/src/templates/certificate-template.html`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla education.certificates
|
||||
- [ ] Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status
|
||||
- [ ] Tabla certificate_verifications (log de verificaciones)
|
||||
- [ ] Índice único en certificate_number
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint POST /education/certificates/generate (triggered on course completion)
|
||||
- [ ] Endpoint GET /education/certificates (listar del usuario)
|
||||
- [ ] Endpoint GET /education/certificates/:id
|
||||
- [ ] Endpoint GET /api/public/certificates/verify/:number (público)
|
||||
- [ ] Endpoint POST /admin/certificates/:id/revoke (admin only)
|
||||
- [ ] Implementar CertificateService.generatePDF()
|
||||
- [ ] Implementar generación de QR code
|
||||
- [ ] Event handler en course completion
|
||||
- [ ] Rate limiting en verificación
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear CertificatesPage.tsx
|
||||
- [ ] Crear componente CertificateCard.tsx
|
||||
- [ ] Crear CertificateDetailPage.tsx
|
||||
- [ ] Crear VerifyCertificatePage.tsx (pública)
|
||||
- [ ] Crear modal de celebración al obtener certificado
|
||||
- [ ] Botones de compartir social media
|
||||
- [ ] Preview de PDF en modal
|
||||
- [ ] Implementar certificatesStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test generación de PDF
|
||||
- [ ] Test verificación de certificado válido/inválido
|
||||
- [ ] Test E2E completar curso y obtener certificado
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "RF-EDU-005"
|
||||
title: "Sistema de Certificados"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Media"
|
||||
module: "education"
|
||||
epic: "OQI-002"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-EDU-005: Sistema de Certificados
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P2
|
||||
**Story Points:** 5
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe proporcionar certificados digitales verificables que se otorgan automáticamente al completar cursos, validando el conocimiento adquirido y permitiendo a los usuarios compartir sus logros profesionales en redes sociales y plataformas de empleo.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-005.1: Generación de Certificados
|
||||
|
||||
El sistema debe:
|
||||
- Generar certificado automáticamente al completar 100% de un curso
|
||||
- Validar que todos los quizzes obligatorios estén aprobados
|
||||
- Validar que todas las lecciones estén marcadas como completadas
|
||||
- Asignar ID único de certificado (formato: OQI-EDU-XXXXXXXX)
|
||||
- Generar PDF con diseño profesional
|
||||
- Almacenar PDF en S3 o similar
|
||||
- Registrar en blockchain para verificación (opcional, fase 2)
|
||||
|
||||
### RF-EDU-005.2: Contenido del Certificado
|
||||
|
||||
Cada certificado debe incluir:
|
||||
- Logo de OrbiQuant IA
|
||||
- Título: "Certificado de Finalización"
|
||||
- Nombre completo del usuario
|
||||
- Título del curso completado
|
||||
- Fecha de finalización
|
||||
- ID único del certificado
|
||||
- Firma digital del instructor (imagen)
|
||||
- Firma digital de la plataforma
|
||||
- QR code para verificación online
|
||||
- Footer: "Verifica este certificado en orbiquant.com/verify/{certificateId}"
|
||||
|
||||
Template:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ [LOGO ORBIQUANT] │
|
||||
│ │
|
||||
│ CERTIFICADO DE FINALIZACIÓN │
|
||||
│ │
|
||||
│ Se certifica que │
|
||||
│ │
|
||||
│ [NOMBRE USUARIO] │
|
||||
│ │
|
||||
│ Ha completado exitosamente el curso │
|
||||
│ │
|
||||
│ "[TÍTULO DEL CURSO]" │
|
||||
│ │
|
||||
│ Fecha: [DD/MM/YYYY] │
|
||||
│ Certificado: OQI-EDU-XXXXXXXX │
|
||||
│ │
|
||||
│ ___________________ ___________________ │
|
||||
│ [Firma Instructor] [Firma Plataforma] │
|
||||
│ │
|
||||
│ [QR CODE] │
|
||||
│ Verifica en orbiquant.com/verify/XXXX │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### RF-EDU-005.3: Verificación de Certificados
|
||||
|
||||
El sistema debe:
|
||||
- Proveer página pública /verify/:certificateId
|
||||
- Mostrar información del certificado sin login
|
||||
- Validar que el ID existe en base de datos
|
||||
- Mostrar: nombre, curso, fecha, estado (válido/revocado)
|
||||
- Proteger contra scraping (rate limiting, captcha)
|
||||
- API pública GET /api/certificates/verify/:id
|
||||
- Responder en JSON para integraciones
|
||||
|
||||
### RF-EDU-005.4: Galería de Certificados del Usuario
|
||||
|
||||
El sistema debe:
|
||||
- Página /education/certificates con todos los certificados del usuario
|
||||
- Mostrar: thumbnail, título del curso, fecha
|
||||
- Filtrar por: fecha, curso, categoría
|
||||
- Buscar por nombre de curso
|
||||
- Ordenar por: más reciente, alfabético, categoría
|
||||
- Vista de cuadrícula o lista
|
||||
- Contador: "Has obtenido X certificados"
|
||||
|
||||
### RF-EDU-005.5: Descarga y Compartir
|
||||
|
||||
El sistema debe permitir:
|
||||
- Descargar PDF del certificado
|
||||
- Botón "Compartir en LinkedIn" (pre-rellenado)
|
||||
- Botón "Compartir en Twitter/X"
|
||||
- Botón "Copiar link de verificación"
|
||||
- Generar imagen social (Open Graph) para compartir
|
||||
- Agregar a perfil público del usuario (opcional)
|
||||
|
||||
Integración LinkedIn:
|
||||
```javascript
|
||||
// Pre-llenar certificación en LinkedIn
|
||||
const linkedInUrl = `https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&name=${encodeURIComponent(courseTitle)}&organizationName=OrbiQuant IA&issueYear=${year}&issueMonth=${month}&certUrl=${verifyUrl}&certId=${certificateId}`;
|
||||
```
|
||||
|
||||
### RF-EDU-005.6: Perfil Público de Certificados
|
||||
|
||||
El sistema debe:
|
||||
- Permitir al usuario crear perfil público opcional
|
||||
- URL: orbiquant.com/u/:username/certificates
|
||||
- Mostrar solo certificados que el usuario hizo públicos
|
||||
- Galería visual de certificados
|
||||
- Bio del usuario
|
||||
- Enlaces a redes sociales
|
||||
- No requiere login para ver
|
||||
|
||||
### RF-EDU-005.7: Revocación de Certificados
|
||||
|
||||
El sistema debe permitir (solo admins):
|
||||
- Revocar certificado por fraude
|
||||
- Agregar motivo de revocación
|
||||
- Notificar al usuario por email
|
||||
- Marcar certificado como "REVOKED" en verificación
|
||||
- Mantener historial de revocaciones
|
||||
|
||||
### RF-EDU-005.8: Plantillas de Certificados
|
||||
|
||||
El sistema debe soportar:
|
||||
- Múltiples plantillas (por categoría o nivel)
|
||||
- Plantilla estándar para todos los cursos
|
||||
- Plantilla especial para cursos premium
|
||||
- Plantilla con colores de marca
|
||||
- Editor de plantillas para admins (fase 2)
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface Certificate {
|
||||
id: string;
|
||||
certificateNumber: string; // OQI-EDU-XXXXXXXX
|
||||
userId: string;
|
||||
userName: string;
|
||||
courseId: string;
|
||||
courseTitle: string;
|
||||
courseCategory: string;
|
||||
completedAt: string;
|
||||
issuedAt: string;
|
||||
pdfUrl: string;
|
||||
verifyUrl: string;
|
||||
qrCodeUrl: string;
|
||||
status: 'active' | 'revoked';
|
||||
revocationReason?: string;
|
||||
instructorSignature: string;
|
||||
metadata: {
|
||||
duration: number; // horas del curso
|
||||
moduleCount: number;
|
||||
lessonCount: number;
|
||||
finalScore?: number; // Si hay quiz final
|
||||
};
|
||||
}
|
||||
|
||||
interface VerificationResult {
|
||||
valid: boolean;
|
||||
certificate?: {
|
||||
certificateNumber: string;
|
||||
recipientName: string;
|
||||
courseTitle: string;
|
||||
completedAt: string;
|
||||
status: 'active' | 'revoked';
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **Requisitos para certificado:**
|
||||
- 100% de lecciones completadas
|
||||
- Todos los quizzes aprobados (si aplica)
|
||||
- Curso debe estar marcado como "completable" (algunos cursos no otorgan certificado)
|
||||
2. **ID único:** Formato OQI-EDU-{8 dígitos hex aleatorios}
|
||||
3. **Nombre en certificado:** Se usa nombre completo del perfil del usuario
|
||||
4. **Fecha:** Fecha de finalización del curso (última lección completada)
|
||||
5. **PDF inmutable:** Una vez generado, el PDF no se regenera aunque el usuario cambie su nombre
|
||||
6. **Caducidad:** Los certificados no caducan
|
||||
7. **Límite de verificaciones:** 100 verificaciones por IP por hora
|
||||
8. **Perfil público:** Opt-in, deshabilitado por default
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario completa curso y obtiene certificado
|
||||
DADO que el usuario completó todas las lecciones de un curso
|
||||
Y aprobó todos los quizzes obligatorios
|
||||
CUANDO se marca la última lección como completada
|
||||
ENTONCES se genera automáticamente un certificado
|
||||
Y se muestra modal de felicitación
|
||||
Y se envía email con el certificado adjunto
|
||||
Y se muestra botón "Ver certificado"
|
||||
|
||||
Escenario: Usuario descarga certificado
|
||||
DADO que el usuario tiene un certificado
|
||||
CUANDO accede a /education/certificates
|
||||
Y hace click en "Descargar PDF"
|
||||
ENTONCES se descarga archivo PDF con el certificado
|
||||
Y el PDF contiene: nombre, curso, fecha, ID, firmas, QR
|
||||
|
||||
Escenario: Usuario comparte en LinkedIn
|
||||
DADO que el usuario está viendo su certificado
|
||||
CUANDO hace click en "Compartir en LinkedIn"
|
||||
ENTONCES se abre LinkedIn en nueva pestaña
|
||||
Y el formulario está pre-llenado con:
|
||||
- Nombre del curso
|
||||
- Organización: OrbiQuant IA
|
||||
- Fecha de emisión
|
||||
- URL de verificación
|
||||
- ID del certificado
|
||||
|
||||
Escenario: Tercero verifica certificado
|
||||
DADO que alguien tiene el ID de un certificado
|
||||
CUANDO accede a orbiquant.com/verify/OQI-EDU-12345678
|
||||
ENTONCES se muestra página de verificación
|
||||
Y se muestra: nombre del usuario, curso, fecha
|
||||
Y se muestra badge "✓ Certificado Válido"
|
||||
Y NO requiere login para ver
|
||||
|
||||
Escenario: Verificar certificado inválido
|
||||
DADO que alguien accede a /verify/INVALID-ID
|
||||
CUANDO el ID no existe en la base de datos
|
||||
ENTONCES se muestra "Certificado no encontrado"
|
||||
Y se sugiere verificar el ID ingresado
|
||||
|
||||
Escenario: Ver certificado revocado
|
||||
DADO que un certificado fue revocado por admin
|
||||
CUANDO alguien intenta verificarlo
|
||||
ENTONCES se muestra "Certificado Revocado"
|
||||
Y se muestra motivo de revocación
|
||||
Y se marca en rojo como inválido
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PDF generation library (PDFKit, Puppeteer, o similar)
|
||||
- S3 para almacenar PDFs
|
||||
- QR code generator
|
||||
- Email service para enviar certificados
|
||||
- LinkedIn API para integración
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Usar Puppeteer para generar PDFs desde HTML template
|
||||
- Almacenar PDFs con nombre: certificates/{userId}/{certificateId}.pdf
|
||||
- Generar QR codes con librería qrcode.js
|
||||
- Implementar caché de verificaciones (Redis) para reducir load
|
||||
- Considerar watermark en PDFs para prevenir falsificación
|
||||
- Usar signed URLs de S3 para descargas seguras
|
||||
- Implementar rate limiting agresivo en endpoint de verificación
|
||||
- Para blockchain: Guardar hash del certificado en Ethereum/Polygon
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/education.sql`
|
||||
- API: `/backend/src/modules/courses/certificates.routes.ts`
|
||||
- Frontend: `/frontend/src/pages/Certificates.tsx`
|
||||
- Templates: `/backend/src/templates/certificate-template.html`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla education.certificates
|
||||
- [ ] Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status
|
||||
- [ ] Tabla certificate_verifications (log de verificaciones)
|
||||
- [ ] Índice único en certificate_number
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint POST /education/certificates/generate (triggered on course completion)
|
||||
- [ ] Endpoint GET /education/certificates (listar del usuario)
|
||||
- [ ] Endpoint GET /education/certificates/:id
|
||||
- [ ] Endpoint GET /api/public/certificates/verify/:number (público)
|
||||
- [ ] Endpoint POST /admin/certificates/:id/revoke (admin only)
|
||||
- [ ] Implementar CertificateService.generatePDF()
|
||||
- [ ] Implementar generación de QR code
|
||||
- [ ] Event handler en course completion
|
||||
- [ ] Rate limiting en verificación
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear CertificatesPage.tsx
|
||||
- [ ] Crear componente CertificateCard.tsx
|
||||
- [ ] Crear CertificateDetailPage.tsx
|
||||
- [ ] Crear VerifyCertificatePage.tsx (pública)
|
||||
- [ ] Crear modal de celebración al obtener certificado
|
||||
- [ ] Botones de compartir social media
|
||||
- [ ] Preview de PDF en modal
|
||||
- [ ] Implementar certificatesStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test generación de PDF
|
||||
- [ ] Test verificación de certificado válido/inválido
|
||||
- [ ] Test E2E completar curso y obtener certificado
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
@ -1,431 +1,444 @@
|
||||
# RF-EDU-006: Sistema de Gamificación
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P2
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe implementar mecánicas de gamificación que aumenten el engagement y motivación de los usuarios mediante puntos de experiencia (XP), niveles, badges, logros, leaderboards y recompensas, creando una experiencia de aprendizaje más inmersiva y competitiva.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-006.1: Sistema de XP (Puntos de Experiencia)
|
||||
|
||||
El sistema debe otorgar XP por las siguientes acciones:
|
||||
|
||||
| Acción | XP | Frecuencia |
|
||||
|--------|----|-----------|
|
||||
| Completar lección de video | 10 | Por lección |
|
||||
| Completar lección de artículo | 15 | Por lección |
|
||||
| Completar módulo | 50 | Por módulo |
|
||||
| Completar curso | 200 | Por curso |
|
||||
| Aprobar quiz (primera vez) | 30 | Por quiz |
|
||||
| Aprobar quiz con 100% | 50 | Por quiz |
|
||||
| Racha de 7 días consecutivos | 100 | Por milestone |
|
||||
| Racha de 30 días consecutivos | 500 | Por milestone |
|
||||
| Tomar notas en lección | 5 | Máx 1 por lección |
|
||||
| Descargar recursos | 2 | Máx 1 por lección |
|
||||
| Compartir certificado | 25 | Por certificado |
|
||||
| Referir a un amigo que se registre | 100 | Por referido |
|
||||
| Completar perfil 100% | 50 | Una vez |
|
||||
| Primera lección del día | 5 | Diario (bonus) |
|
||||
|
||||
**Bonificaciones:**
|
||||
- **Fin de semana:** +50% XP sábados y domingos
|
||||
- **Perfectionist:** +20% XP si completa curso con todos los quizzes al 100%
|
||||
- **Speed learner:** +30% XP si completa curso en <50% del tiempo estimado
|
||||
|
||||
### RF-EDU-006.2: Sistema de Niveles
|
||||
|
||||
Niveles del 1 al 100 con títulos temáticos:
|
||||
|
||||
| Nivel | XP Acumulado | Título | Descripción |
|
||||
|-------|--------------|--------|-------------|
|
||||
| 1-10 | 0 - 5,500 | Novice Trader | Iniciando el viaje |
|
||||
| 11-20 | 5,500 - 23,100 | Apprentice Analyst | Aprendiendo fundamentos |
|
||||
| 21-30 | 23,100 - 50,700 | Skilled Trader | Dominando estrategias |
|
||||
| 31-40 | 50,700 - 88,300 | Expert Strategist | Conocimiento avanzado |
|
||||
| 41-50 | 88,300 - 136,500 | Master Investor | Elite del trading |
|
||||
| 51-60 | 136,500 - 196,100 | Quant Analyst | Análisis cuantitativo |
|
||||
| 61-70 | 196,100 - 267,700 | Portfolio Manager | Gestión profesional |
|
||||
| 71-80 | 267,700 - 351,300 | Market Wizard | Dominio del mercado |
|
||||
| 81-90 | 351,300 - 447,900 | Trading Legend | Leyenda del trading |
|
||||
| 91-100 | 447,900 - 556,500 | Quant Master | Maestría absoluta |
|
||||
|
||||
Recompensas por nivel:
|
||||
- **Nivel 5:** Desbloquea tema oscuro premium
|
||||
- **Nivel 10:** Badge especial + Avatar frame
|
||||
- **Nivel 15:** Acceso a cursos exclusivos
|
||||
- **Nivel 20:** Descuento 10% en suscripción
|
||||
- **Nivel 25:** Prioridad en soporte
|
||||
- **Nivel 30:** Acceso a comunidad premium
|
||||
- **Nivel 50:** Certificado de "Elite Trader"
|
||||
- **Nivel 75:** Reunión 1-on-1 con instructor
|
||||
- **Nivel 100:** Trofeo físico + Lifetime discount 20%
|
||||
|
||||
### RF-EDU-006.3: Sistema de Badges (Insignias)
|
||||
|
||||
Categorías de badges:
|
||||
|
||||
**Logros de Curso:**
|
||||
- First Steps (completar primer curso)
|
||||
- Knowledge Seeker (completar 5 cursos)
|
||||
- Learning Machine (completar 10 cursos)
|
||||
- Master Scholar (completar 25 cursos)
|
||||
- Completionist (completar todos los cursos de una categoría)
|
||||
|
||||
**Logros de Velocidad:**
|
||||
- Fast Learner (completar curso en 1 día)
|
||||
- Speed Demon (completar 3 cursos en 1 semana)
|
||||
- Marathon Runner (completar curso de >10h)
|
||||
|
||||
**Logros de Calidad:**
|
||||
- Perfectionist (aprobar todos los quizzes al 100%)
|
||||
- Overachiever (superar 95% en todos los quizzes de un curso)
|
||||
- Note Taker (tomar notas en 50 lecciones)
|
||||
|
||||
**Logros de Racha:**
|
||||
- Week Warrior (racha de 7 días)
|
||||
- Month Master (racha de 30 días)
|
||||
- Unstoppable (racha de 100 días)
|
||||
- Year Legend (racha de 365 días)
|
||||
|
||||
**Logros Sociales:**
|
||||
- Influencer (compartir 5 certificados)
|
||||
- Recruiter (referir 10 usuarios)
|
||||
- Helper (responder 25 preguntas en foro)
|
||||
|
||||
**Logros Especiales:**
|
||||
- Early Bird (completar lección antes de las 6am)
|
||||
- Night Owl (completar lección después de las 11pm)
|
||||
- Weekend Warrior (completar 5 lecciones en fin de semana)
|
||||
- Category Master (completar todos los cursos de una categoría)
|
||||
|
||||
Cada badge tiene:
|
||||
- Nombre
|
||||
- Descripción
|
||||
- Icono (SVG/PNG)
|
||||
- Rareza: Común, Raro, Épico, Legendario
|
||||
- Fecha de obtención
|
||||
- Progreso hacia obtenerlo (si aplica)
|
||||
|
||||
### RF-EDU-006.4: Leaderboard (Tabla de Clasificación)
|
||||
|
||||
El sistema debe proveer leaderboards:
|
||||
|
||||
**Global:**
|
||||
- Top 100 usuarios por XP total
|
||||
- Actualización: Tiempo real
|
||||
|
||||
**Por Período:**
|
||||
- Esta semana (lunes a domingo)
|
||||
- Este mes
|
||||
- Este año
|
||||
- Histórico
|
||||
|
||||
**Por Categoría:**
|
||||
- Leaderboard por cada categoría de curso
|
||||
- Top learners de Análisis Técnico, etc.
|
||||
|
||||
**Por Métrica:**
|
||||
- Más cursos completados
|
||||
- Más racha consecutiva
|
||||
- Más badges obtenidos
|
||||
- Más rápido en completar curso X
|
||||
|
||||
Información mostrada:
|
||||
- Posición (#1, #2, ...)
|
||||
- Avatar del usuario
|
||||
- Nombre/username
|
||||
- XP total o métrica relevante
|
||||
- Badge de top 3 (oro, plata, bronce)
|
||||
- Indicador de subida/bajada de posición
|
||||
|
||||
Privacidad:
|
||||
- Usuario puede optar por salir del leaderboard
|
||||
- Por default, solo muestra username, no nombre completo
|
||||
- Top 10 siempre visible, resto opcional
|
||||
|
||||
### RF-EDU-006.5: Sistema de Logros (Achievements)
|
||||
|
||||
Logros son metas específicas que otorgan recompensas:
|
||||
|
||||
```typescript
|
||||
interface Achievement {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
category: 'course' | 'speed' | 'quality' | 'streak' | 'social';
|
||||
rarity: 'common' | 'rare' | 'epic' | 'legendary';
|
||||
xpReward: number;
|
||||
badgeReward?: string; // ID del badge que se otorga
|
||||
|
||||
requirements: {
|
||||
type: 'courses_completed' | 'quiz_score' | 'streak_days' | 'custom';
|
||||
target: number;
|
||||
metadata?: any;
|
||||
};
|
||||
|
||||
progress?: {
|
||||
current: number;
|
||||
target: number;
|
||||
percentage: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Ejemplos:
|
||||
- **"Trading Fundamentals Master":** Completar todos los cursos de categoría Fundamentos (200 XP + badge)
|
||||
- **"Quiz Perfectionist":** Obtener 100% en 20 quizzes (150 XP + badge épico)
|
||||
- **"Dedicated Learner":** Mantener racha de 30 días (500 XP + badge raro)
|
||||
|
||||
### RF-EDU-006.6: Recompensas y Premios
|
||||
|
||||
El sistema debe permitir canjear XP o logros por:
|
||||
- Descuentos en suscripción premium (1000 XP = 5% descuento)
|
||||
- Acceso early a nuevos cursos (500 XP)
|
||||
- Merch de OrbiQuant (camisetas, stickers) (5000 XP)
|
||||
- Consulta 1-on-1 con instructor (10,000 XP)
|
||||
- Features premium temporales (2,000 XP = 1 mes)
|
||||
|
||||
Tienda de recompensas:
|
||||
- Catálogo de items canjeables
|
||||
- Historial de canjes
|
||||
- Balance de XP disponible
|
||||
|
||||
### RF-EDU-006.7: Notificaciones y Celebraciones
|
||||
|
||||
El sistema debe mostrar animaciones/notificaciones para:
|
||||
- Subir de nivel (modal con confeti)
|
||||
- Obtener nuevo badge (toast notification)
|
||||
- Completar logro (modal con progreso)
|
||||
- Entrar al top 100 del leaderboard (email)
|
||||
- Alcanzar milestone de racha (confeti)
|
||||
- Obtener XP ("+10 XP" flotante en pantalla)
|
||||
|
||||
### RF-EDU-006.8: Perfil Gamificado
|
||||
|
||||
Página de perfil del usuario debe mostrar:
|
||||
- Avatar con marco según nivel
|
||||
- Nivel actual y barra de progreso
|
||||
- XP actual / XP para próximo nivel
|
||||
- Total de badges obtenidos
|
||||
- Galería de badges (destacar raros/épicos)
|
||||
- Logros recientes
|
||||
- Estadísticas: cursos, lecciones, quizzes, racha
|
||||
- Posición en leaderboard global
|
||||
- Gráfico de XP ganado por mes
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface UserGamification {
|
||||
userId: string;
|
||||
totalXP: number;
|
||||
currentLevel: number;
|
||||
levelTitle: string;
|
||||
xpForCurrentLevel: number;
|
||||
xpForNextLevel: number;
|
||||
progressToNextLevel: number; // 0-100
|
||||
|
||||
badges: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
rarity: string;
|
||||
earnedAt: string;
|
||||
}[];
|
||||
|
||||
achievements: Achievement[];
|
||||
|
||||
leaderboard: {
|
||||
globalRank: number;
|
||||
weeklyRank: number;
|
||||
categoryRanks: {
|
||||
category: string;
|
||||
rank: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
stats: {
|
||||
coursesCompleted: number;
|
||||
quizzesPassed: number;
|
||||
currentStreak: number;
|
||||
longestStreak: number;
|
||||
totalBadges: number;
|
||||
rareBadges: number;
|
||||
epicBadges: number;
|
||||
legendaryBadges: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
userId: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
totalXP: number;
|
||||
level: number;
|
||||
badge?: string; // Badge de top 3
|
||||
rankChange: number; // +5, -2, 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **XP no se puede perder:** Una vez ganado, permanece
|
||||
2. **Nivel no puede bajar:** Solo sube
|
||||
3. **Badges permanentes:** No se pueden perder
|
||||
4. **Leaderboard semanal:** Reset cada lunes 00:00 UTC
|
||||
5. **Anti-cheat:** Validar todas las acciones en backend
|
||||
6. **Rate limiting:** Máximo 1000 XP por hora para prevenir exploits
|
||||
7. **Recompensas únicas:** Algunos logros se pueden ganar solo una vez
|
||||
8. **Canje de recompensas:** Consume XP del balance, pero no baja nivel
|
||||
9. **Privacidad:** Usuario puede ocultar su perfil gamificado
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario gana XP al completar lección
|
||||
DADO que el usuario completa una lección de video
|
||||
CUANDO se marca como completada
|
||||
ENTONCES se otorgan 10 XP
|
||||
Y se muestra animación "+10 XP"
|
||||
Y se actualiza barra de progreso de nivel
|
||||
Y se guarda en historial de XP
|
||||
|
||||
Escenario: Usuario sube de nivel
|
||||
DADO que el usuario tiene 990 XP (nivel 9)
|
||||
Y necesita 1000 XP para nivel 10
|
||||
CUANDO completa un curso y gana 200 XP
|
||||
ENTONCES sube a nivel 10
|
||||
Y se muestra modal "¡Subiste de nivel!"
|
||||
Y se desbloquea badge de nivel 10
|
||||
Y se envía notificación por email
|
||||
|
||||
Escenario: Usuario obtiene badge
|
||||
DADO que el usuario completó 4 cursos
|
||||
CUANDO completa el 5to curso
|
||||
ENTONCES se otorga badge "Knowledge Seeker"
|
||||
Y se muestra toast notification
|
||||
Y el badge aparece en galería de perfil
|
||||
Y se suman 50 XP adicionales
|
||||
|
||||
Escenario: Usuario ve leaderboard
|
||||
DADO que el usuario está en posición #42
|
||||
CUANDO accede a /education/leaderboard
|
||||
ENTONCES se muestra tabla con top 100
|
||||
Y su posición está destacada
|
||||
Y se muestra su XP y nivel
|
||||
Y puede filtrar por: Semanal, Mensual, Histórico
|
||||
|
||||
Escenario: Usuario canjea recompensa
|
||||
DADO que el usuario tiene 5000 XP disponibles
|
||||
CUANDO canjea "Merch OrbiQuant" (5000 XP)
|
||||
ENTONCES se deduce 5000 XP de balance
|
||||
Y se registra el canje
|
||||
Y se envía email de confirmación
|
||||
Y nivel NO baja (XP acumulado permanece)
|
||||
|
||||
Escenario: Progreso hacia logro
|
||||
DADO que el usuario completó 7 de 10 cursos para logro
|
||||
CUANDO ve página de logros
|
||||
ENTONCES se muestra "7/10 cursos"
|
||||
Y barra de progreso al 70%
|
||||
Y descripción de lo que falta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PostgreSQL para gamification data
|
||||
- Redis para caché de leaderboards
|
||||
- Event system para otorgar XP en tiempo real
|
||||
- Notification service para celebraciones
|
||||
- Analytics para tracking de engagement
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Calcular leaderboard en background job cada 5 minutos
|
||||
- Usar Redis Sorted Sets para leaderboards rápidos
|
||||
- Implementar event handlers para cada acción que otorga XP
|
||||
- Crear índices en tablas de XP y badges para queries rápidas
|
||||
- Considerar rate limiting para prevenir farming de XP
|
||||
- Implementar audit log de XP ganado/gastado
|
||||
- Usar WebSockets para notificaciones en tiempo real
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/gamification.sql`
|
||||
- API: `/backend/src/modules/gamification/`
|
||||
- Frontend: `/frontend/src/pages/Leaderboard.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla gamification.user_xp: user_id, total_xp, level
|
||||
- [ ] Tabla gamification.badges: definición de badges
|
||||
- [ ] Tabla gamification.user_badges: user_id, badge_id, earned_at
|
||||
- [ ] Tabla gamification.achievements: definición de logros
|
||||
- [ ] Tabla gamification.user_achievements: progreso de usuario
|
||||
- [ ] Tabla gamification.xp_transactions: log de XP ganado/gastado
|
||||
- [ ] Tabla gamification.leaderboard: caché de rankings
|
||||
- [ ] Índices para queries de leaderboard
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /gamification/profile (stats del usuario)
|
||||
- [ ] Endpoint GET /gamification/leaderboard
|
||||
- [ ] Endpoint GET /gamification/badges
|
||||
- [ ] Endpoint GET /gamification/achievements
|
||||
- [ ] Endpoint POST /gamification/rewards/redeem
|
||||
- [ ] Implementar GamificationService.awardXP()
|
||||
- [ ] Implementar GamificationService.checkLevelUp()
|
||||
- [ ] Implementar GamificationService.checkAchievements()
|
||||
- [ ] Event handlers para todas las acciones que otorgan XP
|
||||
- [ ] Cron job para calcular leaderboards
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear LeaderboardPage.tsx
|
||||
- [ ] Crear BadgesGalleryPage.tsx
|
||||
- [ ] Crear AchievementsPage.tsx
|
||||
- [ ] Crear RewardsStorePage.tsx
|
||||
- [ ] Crear componente XPAnimation.tsx
|
||||
- [ ] Crear componente LevelUpModal.tsx
|
||||
- [ ] Crear componente BadgeToast.tsx
|
||||
- [ ] Crear componente ProgressBar.tsx para nivel
|
||||
- [ ] Integrar gamificación en perfil de usuario
|
||||
- [ ] Implementar gamificationStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test cálculo de nivel según XP
|
||||
- [ ] Test otorgamiento de badges automático
|
||||
- [ ] Test ranking en leaderboard
|
||||
- [ ] Test canje de recompensas
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
---
|
||||
id: "RF-EDU-006"
|
||||
title: "Sistema de Gamificacion"
|
||||
type: "Requirement"
|
||||
status: "Done"
|
||||
priority: "Media"
|
||||
module: "education"
|
||||
epic: "OQI-002"
|
||||
version: "1.0"
|
||||
created_date: "2025-12-05"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# RF-EDU-006: Sistema de Gamificación
|
||||
|
||||
**Versión:** 1.0.0
|
||||
**Fecha:** 2025-12-05
|
||||
**Épica:** OQI-002 - Módulo Educativo
|
||||
**Prioridad:** P2
|
||||
**Story Points:** 8
|
||||
|
||||
---
|
||||
|
||||
## Descripción
|
||||
|
||||
El sistema debe implementar mecánicas de gamificación que aumenten el engagement y motivación de los usuarios mediante puntos de experiencia (XP), niveles, badges, logros, leaderboards y recompensas, creando una experiencia de aprendizaje más inmersiva y competitiva.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos Funcionales
|
||||
|
||||
### RF-EDU-006.1: Sistema de XP (Puntos de Experiencia)
|
||||
|
||||
El sistema debe otorgar XP por las siguientes acciones:
|
||||
|
||||
| Acción | XP | Frecuencia |
|
||||
|--------|----|-----------|
|
||||
| Completar lección de video | 10 | Por lección |
|
||||
| Completar lección de artículo | 15 | Por lección |
|
||||
| Completar módulo | 50 | Por módulo |
|
||||
| Completar curso | 200 | Por curso |
|
||||
| Aprobar quiz (primera vez) | 30 | Por quiz |
|
||||
| Aprobar quiz con 100% | 50 | Por quiz |
|
||||
| Racha de 7 días consecutivos | 100 | Por milestone |
|
||||
| Racha de 30 días consecutivos | 500 | Por milestone |
|
||||
| Tomar notas en lección | 5 | Máx 1 por lección |
|
||||
| Descargar recursos | 2 | Máx 1 por lección |
|
||||
| Compartir certificado | 25 | Por certificado |
|
||||
| Referir a un amigo que se registre | 100 | Por referido |
|
||||
| Completar perfil 100% | 50 | Una vez |
|
||||
| Primera lección del día | 5 | Diario (bonus) |
|
||||
|
||||
**Bonificaciones:**
|
||||
- **Fin de semana:** +50% XP sábados y domingos
|
||||
- **Perfectionist:** +20% XP si completa curso con todos los quizzes al 100%
|
||||
- **Speed learner:** +30% XP si completa curso en <50% del tiempo estimado
|
||||
|
||||
### RF-EDU-006.2: Sistema de Niveles
|
||||
|
||||
Niveles del 1 al 100 con títulos temáticos:
|
||||
|
||||
| Nivel | XP Acumulado | Título | Descripción |
|
||||
|-------|--------------|--------|-------------|
|
||||
| 1-10 | 0 - 5,500 | Novice Trader | Iniciando el viaje |
|
||||
| 11-20 | 5,500 - 23,100 | Apprentice Analyst | Aprendiendo fundamentos |
|
||||
| 21-30 | 23,100 - 50,700 | Skilled Trader | Dominando estrategias |
|
||||
| 31-40 | 50,700 - 88,300 | Expert Strategist | Conocimiento avanzado |
|
||||
| 41-50 | 88,300 - 136,500 | Master Investor | Elite del trading |
|
||||
| 51-60 | 136,500 - 196,100 | Quant Analyst | Análisis cuantitativo |
|
||||
| 61-70 | 196,100 - 267,700 | Portfolio Manager | Gestión profesional |
|
||||
| 71-80 | 267,700 - 351,300 | Market Wizard | Dominio del mercado |
|
||||
| 81-90 | 351,300 - 447,900 | Trading Legend | Leyenda del trading |
|
||||
| 91-100 | 447,900 - 556,500 | Quant Master | Maestría absoluta |
|
||||
|
||||
Recompensas por nivel:
|
||||
- **Nivel 5:** Desbloquea tema oscuro premium
|
||||
- **Nivel 10:** Badge especial + Avatar frame
|
||||
- **Nivel 15:** Acceso a cursos exclusivos
|
||||
- **Nivel 20:** Descuento 10% en suscripción
|
||||
- **Nivel 25:** Prioridad en soporte
|
||||
- **Nivel 30:** Acceso a comunidad premium
|
||||
- **Nivel 50:** Certificado de "Elite Trader"
|
||||
- **Nivel 75:** Reunión 1-on-1 con instructor
|
||||
- **Nivel 100:** Trofeo físico + Lifetime discount 20%
|
||||
|
||||
### RF-EDU-006.3: Sistema de Badges (Insignias)
|
||||
|
||||
Categorías de badges:
|
||||
|
||||
**Logros de Curso:**
|
||||
- First Steps (completar primer curso)
|
||||
- Knowledge Seeker (completar 5 cursos)
|
||||
- Learning Machine (completar 10 cursos)
|
||||
- Master Scholar (completar 25 cursos)
|
||||
- Completionist (completar todos los cursos de una categoría)
|
||||
|
||||
**Logros de Velocidad:**
|
||||
- Fast Learner (completar curso en 1 día)
|
||||
- Speed Demon (completar 3 cursos en 1 semana)
|
||||
- Marathon Runner (completar curso de >10h)
|
||||
|
||||
**Logros de Calidad:**
|
||||
- Perfectionist (aprobar todos los quizzes al 100%)
|
||||
- Overachiever (superar 95% en todos los quizzes de un curso)
|
||||
- Note Taker (tomar notas en 50 lecciones)
|
||||
|
||||
**Logros de Racha:**
|
||||
- Week Warrior (racha de 7 días)
|
||||
- Month Master (racha de 30 días)
|
||||
- Unstoppable (racha de 100 días)
|
||||
- Year Legend (racha de 365 días)
|
||||
|
||||
**Logros Sociales:**
|
||||
- Influencer (compartir 5 certificados)
|
||||
- Recruiter (referir 10 usuarios)
|
||||
- Helper (responder 25 preguntas en foro)
|
||||
|
||||
**Logros Especiales:**
|
||||
- Early Bird (completar lección antes de las 6am)
|
||||
- Night Owl (completar lección después de las 11pm)
|
||||
- Weekend Warrior (completar 5 lecciones en fin de semana)
|
||||
- Category Master (completar todos los cursos de una categoría)
|
||||
|
||||
Cada badge tiene:
|
||||
- Nombre
|
||||
- Descripción
|
||||
- Icono (SVG/PNG)
|
||||
- Rareza: Común, Raro, Épico, Legendario
|
||||
- Fecha de obtención
|
||||
- Progreso hacia obtenerlo (si aplica)
|
||||
|
||||
### RF-EDU-006.4: Leaderboard (Tabla de Clasificación)
|
||||
|
||||
El sistema debe proveer leaderboards:
|
||||
|
||||
**Global:**
|
||||
- Top 100 usuarios por XP total
|
||||
- Actualización: Tiempo real
|
||||
|
||||
**Por Período:**
|
||||
- Esta semana (lunes a domingo)
|
||||
- Este mes
|
||||
- Este año
|
||||
- Histórico
|
||||
|
||||
**Por Categoría:**
|
||||
- Leaderboard por cada categoría de curso
|
||||
- Top learners de Análisis Técnico, etc.
|
||||
|
||||
**Por Métrica:**
|
||||
- Más cursos completados
|
||||
- Más racha consecutiva
|
||||
- Más badges obtenidos
|
||||
- Más rápido en completar curso X
|
||||
|
||||
Información mostrada:
|
||||
- Posición (#1, #2, ...)
|
||||
- Avatar del usuario
|
||||
- Nombre/username
|
||||
- XP total o métrica relevante
|
||||
- Badge de top 3 (oro, plata, bronce)
|
||||
- Indicador de subida/bajada de posición
|
||||
|
||||
Privacidad:
|
||||
- Usuario puede optar por salir del leaderboard
|
||||
- Por default, solo muestra username, no nombre completo
|
||||
- Top 10 siempre visible, resto opcional
|
||||
|
||||
### RF-EDU-006.5: Sistema de Logros (Achievements)
|
||||
|
||||
Logros son metas específicas que otorgan recompensas:
|
||||
|
||||
```typescript
|
||||
interface Achievement {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
category: 'course' | 'speed' | 'quality' | 'streak' | 'social';
|
||||
rarity: 'common' | 'rare' | 'epic' | 'legendary';
|
||||
xpReward: number;
|
||||
badgeReward?: string; // ID del badge que se otorga
|
||||
|
||||
requirements: {
|
||||
type: 'courses_completed' | 'quiz_score' | 'streak_days' | 'custom';
|
||||
target: number;
|
||||
metadata?: any;
|
||||
};
|
||||
|
||||
progress?: {
|
||||
current: number;
|
||||
target: number;
|
||||
percentage: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Ejemplos:
|
||||
- **"Trading Fundamentals Master":** Completar todos los cursos de categoría Fundamentos (200 XP + badge)
|
||||
- **"Quiz Perfectionist":** Obtener 100% en 20 quizzes (150 XP + badge épico)
|
||||
- **"Dedicated Learner":** Mantener racha de 30 días (500 XP + badge raro)
|
||||
|
||||
### RF-EDU-006.6: Recompensas y Premios
|
||||
|
||||
El sistema debe permitir canjear XP o logros por:
|
||||
- Descuentos en suscripción premium (1000 XP = 5% descuento)
|
||||
- Acceso early a nuevos cursos (500 XP)
|
||||
- Merch de OrbiQuant (camisetas, stickers) (5000 XP)
|
||||
- Consulta 1-on-1 con instructor (10,000 XP)
|
||||
- Features premium temporales (2,000 XP = 1 mes)
|
||||
|
||||
Tienda de recompensas:
|
||||
- Catálogo de items canjeables
|
||||
- Historial de canjes
|
||||
- Balance de XP disponible
|
||||
|
||||
### RF-EDU-006.7: Notificaciones y Celebraciones
|
||||
|
||||
El sistema debe mostrar animaciones/notificaciones para:
|
||||
- Subir de nivel (modal con confeti)
|
||||
- Obtener nuevo badge (toast notification)
|
||||
- Completar logro (modal con progreso)
|
||||
- Entrar al top 100 del leaderboard (email)
|
||||
- Alcanzar milestone de racha (confeti)
|
||||
- Obtener XP ("+10 XP" flotante en pantalla)
|
||||
|
||||
### RF-EDU-006.8: Perfil Gamificado
|
||||
|
||||
Página de perfil del usuario debe mostrar:
|
||||
- Avatar con marco según nivel
|
||||
- Nivel actual y barra de progreso
|
||||
- XP actual / XP para próximo nivel
|
||||
- Total de badges obtenidos
|
||||
- Galería de badges (destacar raros/épicos)
|
||||
- Logros recientes
|
||||
- Estadísticas: cursos, lecciones, quizzes, racha
|
||||
- Posición en leaderboard global
|
||||
- Gráfico de XP ganado por mes
|
||||
|
||||
---
|
||||
|
||||
## Datos de Salida
|
||||
|
||||
```typescript
|
||||
interface UserGamification {
|
||||
userId: string;
|
||||
totalXP: number;
|
||||
currentLevel: number;
|
||||
levelTitle: string;
|
||||
xpForCurrentLevel: number;
|
||||
xpForNextLevel: number;
|
||||
progressToNextLevel: number; // 0-100
|
||||
|
||||
badges: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
rarity: string;
|
||||
earnedAt: string;
|
||||
}[];
|
||||
|
||||
achievements: Achievement[];
|
||||
|
||||
leaderboard: {
|
||||
globalRank: number;
|
||||
weeklyRank: number;
|
||||
categoryRanks: {
|
||||
category: string;
|
||||
rank: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
stats: {
|
||||
coursesCompleted: number;
|
||||
quizzesPassed: number;
|
||||
currentStreak: number;
|
||||
longestStreak: number;
|
||||
totalBadges: number;
|
||||
rareBadges: number;
|
||||
epicBadges: number;
|
||||
legendaryBadges: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
userId: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
totalXP: number;
|
||||
level: number;
|
||||
badge?: string; // Badge de top 3
|
||||
rankChange: number; // +5, -2, 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Negocio
|
||||
|
||||
1. **XP no se puede perder:** Una vez ganado, permanece
|
||||
2. **Nivel no puede bajar:** Solo sube
|
||||
3. **Badges permanentes:** No se pueden perder
|
||||
4. **Leaderboard semanal:** Reset cada lunes 00:00 UTC
|
||||
5. **Anti-cheat:** Validar todas las acciones en backend
|
||||
6. **Rate limiting:** Máximo 1000 XP por hora para prevenir exploits
|
||||
7. **Recompensas únicas:** Algunos logros se pueden ganar solo una vez
|
||||
8. **Canje de recompensas:** Consume XP del balance, pero no baja nivel
|
||||
9. **Privacidad:** Usuario puede ocultar su perfil gamificado
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
```gherkin
|
||||
Escenario: Usuario gana XP al completar lección
|
||||
DADO que el usuario completa una lección de video
|
||||
CUANDO se marca como completada
|
||||
ENTONCES se otorgan 10 XP
|
||||
Y se muestra animación "+10 XP"
|
||||
Y se actualiza barra de progreso de nivel
|
||||
Y se guarda en historial de XP
|
||||
|
||||
Escenario: Usuario sube de nivel
|
||||
DADO que el usuario tiene 990 XP (nivel 9)
|
||||
Y necesita 1000 XP para nivel 10
|
||||
CUANDO completa un curso y gana 200 XP
|
||||
ENTONCES sube a nivel 10
|
||||
Y se muestra modal "¡Subiste de nivel!"
|
||||
Y se desbloquea badge de nivel 10
|
||||
Y se envía notificación por email
|
||||
|
||||
Escenario: Usuario obtiene badge
|
||||
DADO que el usuario completó 4 cursos
|
||||
CUANDO completa el 5to curso
|
||||
ENTONCES se otorga badge "Knowledge Seeker"
|
||||
Y se muestra toast notification
|
||||
Y el badge aparece en galería de perfil
|
||||
Y se suman 50 XP adicionales
|
||||
|
||||
Escenario: Usuario ve leaderboard
|
||||
DADO que el usuario está en posición #42
|
||||
CUANDO accede a /education/leaderboard
|
||||
ENTONCES se muestra tabla con top 100
|
||||
Y su posición está destacada
|
||||
Y se muestra su XP y nivel
|
||||
Y puede filtrar por: Semanal, Mensual, Histórico
|
||||
|
||||
Escenario: Usuario canjea recompensa
|
||||
DADO que el usuario tiene 5000 XP disponibles
|
||||
CUANDO canjea "Merch OrbiQuant" (5000 XP)
|
||||
ENTONCES se deduce 5000 XP de balance
|
||||
Y se registra el canje
|
||||
Y se envía email de confirmación
|
||||
Y nivel NO baja (XP acumulado permanece)
|
||||
|
||||
Escenario: Progreso hacia logro
|
||||
DADO que el usuario completó 7 de 10 cursos para logro
|
||||
CUANDO ve página de logros
|
||||
ENTONCES se muestra "7/10 cursos"
|
||||
Y barra de progreso al 70%
|
||||
Y descripción de lo que falta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
- PostgreSQL para gamification data
|
||||
- Redis para caché de leaderboards
|
||||
- Event system para otorgar XP en tiempo real
|
||||
- Notification service para celebraciones
|
||||
- Analytics para tracking de engagement
|
||||
|
||||
---
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- Calcular leaderboard en background job cada 5 minutos
|
||||
- Usar Redis Sorted Sets para leaderboards rápidos
|
||||
- Implementar event handlers para cada acción que otorga XP
|
||||
- Crear índices en tablas de XP y badges para queries rápidas
|
||||
- Considerar rate limiting para prevenir farming de XP
|
||||
- Implementar audit log de XP ganado/gastado
|
||||
- Usar WebSockets para notificaciones en tiempo real
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- Schema: `/backend/src/database/schemas/gamification.sql`
|
||||
- API: `/backend/src/modules/gamification/`
|
||||
- Frontend: `/frontend/src/pages/Leaderboard.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Tareas Técnicas
|
||||
|
||||
**Database:**
|
||||
- [ ] Tabla gamification.user_xp: user_id, total_xp, level
|
||||
- [ ] Tabla gamification.badges: definición de badges
|
||||
- [ ] Tabla gamification.user_badges: user_id, badge_id, earned_at
|
||||
- [ ] Tabla gamification.achievements: definición de logros
|
||||
- [ ] Tabla gamification.user_achievements: progreso de usuario
|
||||
- [ ] Tabla gamification.xp_transactions: log de XP ganado/gastado
|
||||
- [ ] Tabla gamification.leaderboard: caché de rankings
|
||||
- [ ] Índices para queries de leaderboard
|
||||
|
||||
**Backend:**
|
||||
- [ ] Endpoint GET /gamification/profile (stats del usuario)
|
||||
- [ ] Endpoint GET /gamification/leaderboard
|
||||
- [ ] Endpoint GET /gamification/badges
|
||||
- [ ] Endpoint GET /gamification/achievements
|
||||
- [ ] Endpoint POST /gamification/rewards/redeem
|
||||
- [ ] Implementar GamificationService.awardXP()
|
||||
- [ ] Implementar GamificationService.checkLevelUp()
|
||||
- [ ] Implementar GamificationService.checkAchievements()
|
||||
- [ ] Event handlers para todas las acciones que otorgan XP
|
||||
- [ ] Cron job para calcular leaderboards
|
||||
|
||||
**Frontend:**
|
||||
- [ ] Crear LeaderboardPage.tsx
|
||||
- [ ] Crear BadgesGalleryPage.tsx
|
||||
- [ ] Crear AchievementsPage.tsx
|
||||
- [ ] Crear RewardsStorePage.tsx
|
||||
- [ ] Crear componente XPAnimation.tsx
|
||||
- [ ] Crear componente LevelUpModal.tsx
|
||||
- [ ] Crear componente BadgeToast.tsx
|
||||
- [ ] Crear componente ProgressBar.tsx para nivel
|
||||
- [ ] Integrar gamificación en perfil de usuario
|
||||
- [ ] Implementar gamificationStore
|
||||
|
||||
**Tests:**
|
||||
- [ ] Test cálculo de nivel según XP
|
||||
- [ ] Test otorgamiento de badges automático
|
||||
- [ ] Test ranking en leaderboard
|
||||
- [ ] Test canje de recompensas
|
||||
|
||||
---
|
||||
|
||||
**Creado por:** Requirements-Analyst
|
||||
**Fecha:** 2025-12-05
|
||||
**Última actualización:** 2025-12-05
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,192 +1,200 @@
|
||||
# _MAP: OQI-003 - Trading y Charts
|
||||
|
||||
**Última actualización:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Versión:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Propósito
|
||||
|
||||
Esta épica implementa la plataforma de trading con charts profesionales usando Lightweight Charts, indicadores técnicos, watchlists personalizables y sistema de paper trading para simulación.
|
||||
|
||||
---
|
||||
|
||||
## Contenido del Directorio
|
||||
|
||||
```
|
||||
OQI-003-trading-charts/
|
||||
├── README.md # Resumen ejecutivo de la épica
|
||||
├── _MAP.md # Este archivo - índice
|
||||
├── requerimientos/ # Documentos de requerimientos funcionales
|
||||
│ ├── RF-TRD-001-charts.md # Charts y visualización
|
||||
│ ├── RF-TRD-002-indicadores.md # Indicadores técnicos
|
||||
│ ├── RF-TRD-003-watchlists.md # Gestión de watchlists
|
||||
│ ├── RF-TRD-004-paper-trading.md # Paper trading
|
||||
│ ├── RF-TRD-005-ordenes.md # Sistema de órdenes
|
||||
│ ├── RF-TRD-006-posiciones.md # Gestión de posiciones
|
||||
│ ├── RF-TRD-007-historial.md # Historial y trades
|
||||
│ └── RF-TRD-008-metricas.md # Métricas y estadísticas
|
||||
├── especificaciones/ # Especificaciones técnicas
|
||||
│ ├── ET-TRD-001-market-data.md # Obtención de datos de mercado
|
||||
│ ├── ET-TRD-002-websocket.md # Conexiones WebSocket
|
||||
│ ├── ET-TRD-003-database.md # Modelo de datos
|
||||
│ ├── ET-TRD-004-api.md # Endpoints REST
|
||||
│ ├── ET-TRD-005-frontend.md # Componentes React
|
||||
│ ├── ET-TRD-006-indicadores.md # Cálculo de indicadores
|
||||
│ ├── ET-TRD-007-paper-engine.md # Motor de paper trading
|
||||
│ └── ET-TRD-008-performance.md # Optimizaciones
|
||||
├── historias-usuario/ # User Stories
|
||||
│ ├── US-TRD-001-ver-chart.md
|
||||
│ ├── US-TRD-002-cambiar-timeframe.md
|
||||
│ ├── US-TRD-003-agregar-indicador.md
|
||||
│ ├── US-TRD-004-crear-watchlist.md
|
||||
│ ├── US-TRD-005-agregar-simbolo.md
|
||||
│ ├── US-TRD-006-crear-orden-market.md
|
||||
│ ├── US-TRD-007-crear-orden-limit.md
|
||||
│ ├── US-TRD-008-cerrar-posicion.md
|
||||
│ ├── US-TRD-009-ver-posiciones.md
|
||||
│ ├── US-TRD-010-ver-historial.md
|
||||
│ ├── US-TRD-011-ver-estadisticas.md
|
||||
│ ├── US-TRD-012-configurar-tp-sl.md
|
||||
│ ├── US-TRD-013-alertas-precio.md
|
||||
│ ├── US-TRD-014-reset-balance.md
|
||||
│ ├── US-TRD-015-exportar-trades.md
|
||||
│ ├── US-TRD-016-modo-oscuro-chart.md
|
||||
│ ├── US-TRD-017-zoom-pan-chart.md
|
||||
│ └── US-TRD-018-comparar-simbolos.md
|
||||
└── implementacion/ # Trazabilidad de implementación
|
||||
└── TRACEABILITY.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Funcionales
|
||||
|
||||
| ID | Nombre | Prioridad | SP | Estado |
|
||||
|----|--------|-----------|-----|--------|
|
||||
| RF-TRD-001 | Charts y Visualización | P0 | 8 | Pendiente |
|
||||
| RF-TRD-002 | Indicadores Técnicos | P1 | 5 | Pendiente |
|
||||
| RF-TRD-003 | Gestión de Watchlists | P1 | 5 | Pendiente |
|
||||
| RF-TRD-004 | Paper Trading | P0 | 13 | Pendiente |
|
||||
| RF-TRD-005 | Sistema de Órdenes | P0 | 8 | Pendiente |
|
||||
| RF-TRD-006 | Gestión de Posiciones | P0 | 5 | Pendiente |
|
||||
| RF-TRD-007 | Historial y Trades | P1 | 5 | Pendiente |
|
||||
| RF-TRD-008 | Métricas y Estadísticas | P2 | 6 | Pendiente |
|
||||
|
||||
**Total:** 55 SP
|
||||
|
||||
---
|
||||
|
||||
## Especificaciones Técnicas
|
||||
|
||||
| ID | Nombre | Componente | Estado |
|
||||
|----|--------|------------|--------|
|
||||
| ET-TRD-001 | Market Data | Backend | Pendiente |
|
||||
| ET-TRD-002 | WebSocket | Backend/Frontend | Pendiente |
|
||||
| ET-TRD-003 | Database | Database | ✅ Schema existe |
|
||||
| ET-TRD-004 | API REST | Backend | Pendiente |
|
||||
| ET-TRD-005 | Frontend | Frontend | Pendiente |
|
||||
| ET-TRD-006 | Indicadores | Backend/ML | Pendiente |
|
||||
| ET-TRD-007 | Paper Engine | Backend | Pendiente |
|
||||
| ET-TRD-008 | Performance | All | Pendiente |
|
||||
|
||||
---
|
||||
|
||||
## Historias de Usuario
|
||||
|
||||
| ID | Historia | Prioridad | SP | Estado |
|
||||
|----|----------|-----------|-----|--------|
|
||||
| US-TRD-001 | Ver chart de un símbolo | P0 | 5 | Pendiente |
|
||||
| US-TRD-002 | Cambiar timeframe | P0 | 2 | Pendiente |
|
||||
| US-TRD-003 | Agregar indicador al chart | P1 | 3 | Pendiente |
|
||||
| US-TRD-004 | Crear watchlist | P1 | 2 | Pendiente |
|
||||
| US-TRD-005 | Agregar símbolo a watchlist | P1 | 2 | Pendiente |
|
||||
| US-TRD-006 | Crear orden market | P0 | 5 | Pendiente |
|
||||
| US-TRD-007 | Crear orden limit | P1 | 3 | Pendiente |
|
||||
| US-TRD-008 | Cerrar posición | P0 | 3 | Pendiente |
|
||||
| US-TRD-009 | Ver posiciones abiertas | P0 | 3 | Pendiente |
|
||||
| US-TRD-010 | Ver historial de trades | P1 | 3 | Pendiente |
|
||||
| US-TRD-011 | Ver estadísticas de rendimiento | P1 | 3 | Pendiente |
|
||||
| US-TRD-012 | Configurar TP/SL | P1 | 3 | Pendiente |
|
||||
| US-TRD-013 | Configurar alertas de precio | P2 | 3 | Pendiente |
|
||||
| US-TRD-014 | Resetear balance paper | P2 | 1 | Pendiente |
|
||||
| US-TRD-015 | Exportar trades a CSV | P2 | 2 | Pendiente |
|
||||
| US-TRD-016 | Modo oscuro en chart | P2 | 2 | Pendiente |
|
||||
| US-TRD-017 | Zoom y pan en chart | P1 | 3 | Pendiente |
|
||||
| US-TRD-018 | Comparar múltiples símbolos | P2 | 5 | Pendiente |
|
||||
|
||||
**Total:** 55 SP
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de:
|
||||
|
||||
- **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado
|
||||
|
||||
### Bloquea:
|
||||
|
||||
- **OQI-006:** ML Signals (integración con charts)
|
||||
|
||||
---
|
||||
|
||||
## Stack Técnico
|
||||
|
||||
| Capa | Tecnología | Uso |
|
||||
|------|------------|-----|
|
||||
| Frontend | Lightweight Charts | Renderizado de velas |
|
||||
| Frontend | React + Zustand | Estado y componentes |
|
||||
| Backend | Express.js | API REST |
|
||||
| Backend | ws | WebSocket server |
|
||||
| Database | PostgreSQL | Persistencia |
|
||||
| External | Binance API | Datos de mercado |
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### Funcionales
|
||||
|
||||
- [ ] Charts renderizan correctamente con datos de Binance
|
||||
- [ ] 7 timeframes disponibles y funcionales
|
||||
- [ ] Mínimo 5 indicadores técnicos implementados
|
||||
- [ ] Watchlists CRUD completo
|
||||
- [ ] Paper trading ejecuta órdenes market y limit
|
||||
- [ ] Posiciones se actualizan en tiempo real
|
||||
- [ ] Historial muestra todos los trades cerrados
|
||||
- [ ] Métricas calculan win rate y P&L correctamente
|
||||
|
||||
### No Funcionales
|
||||
|
||||
- [ ] Chart carga en < 2 segundos
|
||||
- [ ] Updates en tiempo real < 500ms latencia
|
||||
- [ ] Soporta 1000+ velas sin lag
|
||||
- [ ] Mobile responsive
|
||||
|
||||
### Técnicos
|
||||
|
||||
- [ ] Cobertura de tests > 70%
|
||||
- [ ] Documentación API completa
|
||||
- [ ] Sin memory leaks en WebSocket
|
||||
|
||||
---
|
||||
|
||||
## Hitos
|
||||
|
||||
| Hito | Entregables | Target |
|
||||
|------|-------------|--------|
|
||||
| M1 | Charts básicos + timeframes | Sprint 3 |
|
||||
| M2 | Indicadores + watchlists | Sprint 3 |
|
||||
| M3 | Paper trading completo | Sprint 4 |
|
||||
| M4 | Métricas + polish | Sprint 4 |
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- [README Principal](./README.md)
|
||||
- [Vision del Producto](../../00-vision-general/VISION-PRODUCTO.md)
|
||||
- [Arquitectura](../../00-vision-general/ARQUITECTURA-GENERAL.md)
|
||||
- [_MAP Fase MVP](../_MAP.md)
|
||||
---
|
||||
id: "MAP-OQI-003-trading-charts"
|
||||
title: "Mapa de OQI-003-trading-charts"
|
||||
type: "Index"
|
||||
project: "trading-platform"
|
||||
updated_date: "2026-01-04"
|
||||
---
|
||||
|
||||
# _MAP: OQI-003 - Trading y Charts
|
||||
|
||||
**Última actualización:** 2025-12-05
|
||||
**Estado:** Pendiente
|
||||
**Versión:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Propósito
|
||||
|
||||
Esta épica implementa la plataforma de trading con charts profesionales usando Lightweight Charts, indicadores técnicos, watchlists personalizables y sistema de paper trading para simulación.
|
||||
|
||||
---
|
||||
|
||||
## Contenido del Directorio
|
||||
|
||||
```
|
||||
OQI-003-trading-charts/
|
||||
├── README.md # Resumen ejecutivo de la épica
|
||||
├── _MAP.md # Este archivo - índice
|
||||
├── requerimientos/ # Documentos de requerimientos funcionales
|
||||
│ ├── RF-TRD-001-charts.md # Charts y visualización
|
||||
│ ├── RF-TRD-002-indicadores.md # Indicadores técnicos
|
||||
│ ├── RF-TRD-003-watchlists.md # Gestión de watchlists
|
||||
│ ├── RF-TRD-004-paper-trading.md # Paper trading
|
||||
│ ├── RF-TRD-005-ordenes.md # Sistema de órdenes
|
||||
│ ├── RF-TRD-006-posiciones.md # Gestión de posiciones
|
||||
│ ├── RF-TRD-007-historial.md # Historial y trades
|
||||
│ └── RF-TRD-008-metricas.md # Métricas y estadísticas
|
||||
├── especificaciones/ # Especificaciones técnicas
|
||||
│ ├── ET-TRD-001-market-data.md # Obtención de datos de mercado
|
||||
│ ├── ET-TRD-002-websocket.md # Conexiones WebSocket
|
||||
│ ├── ET-TRD-003-database.md # Modelo de datos
|
||||
│ ├── ET-TRD-004-api.md # Endpoints REST
|
||||
│ ├── ET-TRD-005-frontend.md # Componentes React
|
||||
│ ├── ET-TRD-006-indicadores.md # Cálculo de indicadores
|
||||
│ ├── ET-TRD-007-paper-engine.md # Motor de paper trading
|
||||
│ └── ET-TRD-008-performance.md # Optimizaciones
|
||||
├── historias-usuario/ # User Stories
|
||||
│ ├── US-TRD-001-ver-chart.md
|
||||
│ ├── US-TRD-002-cambiar-timeframe.md
|
||||
│ ├── US-TRD-003-agregar-indicador.md
|
||||
│ ├── US-TRD-004-crear-watchlist.md
|
||||
│ ├── US-TRD-005-agregar-simbolo.md
|
||||
│ ├── US-TRD-006-crear-orden-market.md
|
||||
│ ├── US-TRD-007-crear-orden-limit.md
|
||||
│ ├── US-TRD-008-cerrar-posicion.md
|
||||
│ ├── US-TRD-009-ver-posiciones.md
|
||||
│ ├── US-TRD-010-ver-historial.md
|
||||
│ ├── US-TRD-011-ver-estadisticas.md
|
||||
│ ├── US-TRD-012-configurar-tp-sl.md
|
||||
│ ├── US-TRD-013-alertas-precio.md
|
||||
│ ├── US-TRD-014-reset-balance.md
|
||||
│ ├── US-TRD-015-exportar-trades.md
|
||||
│ ├── US-TRD-016-modo-oscuro-chart.md
|
||||
│ ├── US-TRD-017-zoom-pan-chart.md
|
||||
│ └── US-TRD-018-comparar-simbolos.md
|
||||
└── implementacion/ # Trazabilidad de implementación
|
||||
└── TRACEABILITY.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos Funcionales
|
||||
|
||||
| ID | Nombre | Prioridad | SP | Estado |
|
||||
|----|--------|-----------|-----|--------|
|
||||
| RF-TRD-001 | Charts y Visualización | P0 | 8 | Pendiente |
|
||||
| RF-TRD-002 | Indicadores Técnicos | P1 | 5 | Pendiente |
|
||||
| RF-TRD-003 | Gestión de Watchlists | P1 | 5 | Pendiente |
|
||||
| RF-TRD-004 | Paper Trading | P0 | 13 | Pendiente |
|
||||
| RF-TRD-005 | Sistema de Órdenes | P0 | 8 | Pendiente |
|
||||
| RF-TRD-006 | Gestión de Posiciones | P0 | 5 | Pendiente |
|
||||
| RF-TRD-007 | Historial y Trades | P1 | 5 | Pendiente |
|
||||
| RF-TRD-008 | Métricas y Estadísticas | P2 | 6 | Pendiente |
|
||||
|
||||
**Total:** 55 SP
|
||||
|
||||
---
|
||||
|
||||
## Especificaciones Técnicas
|
||||
|
||||
| ID | Nombre | Componente | Estado |
|
||||
|----|--------|------------|--------|
|
||||
| ET-TRD-001 | Market Data | Backend | Pendiente |
|
||||
| ET-TRD-002 | WebSocket | Backend/Frontend | Pendiente |
|
||||
| ET-TRD-003 | Database | Database | ✅ Schema existe |
|
||||
| ET-TRD-004 | API REST | Backend | Pendiente |
|
||||
| ET-TRD-005 | Frontend | Frontend | Pendiente |
|
||||
| ET-TRD-006 | Indicadores | Backend/ML | Pendiente |
|
||||
| ET-TRD-007 | Paper Engine | Backend | Pendiente |
|
||||
| ET-TRD-008 | Performance | All | Pendiente |
|
||||
|
||||
---
|
||||
|
||||
## Historias de Usuario
|
||||
|
||||
| ID | Historia | Prioridad | SP | Estado |
|
||||
|----|----------|-----------|-----|--------|
|
||||
| US-TRD-001 | Ver chart de un símbolo | P0 | 5 | Pendiente |
|
||||
| US-TRD-002 | Cambiar timeframe | P0 | 2 | Pendiente |
|
||||
| US-TRD-003 | Agregar indicador al chart | P1 | 3 | Pendiente |
|
||||
| US-TRD-004 | Crear watchlist | P1 | 2 | Pendiente |
|
||||
| US-TRD-005 | Agregar símbolo a watchlist | P1 | 2 | Pendiente |
|
||||
| US-TRD-006 | Crear orden market | P0 | 5 | Pendiente |
|
||||
| US-TRD-007 | Crear orden limit | P1 | 3 | Pendiente |
|
||||
| US-TRD-008 | Cerrar posición | P0 | 3 | Pendiente |
|
||||
| US-TRD-009 | Ver posiciones abiertas | P0 | 3 | Pendiente |
|
||||
| US-TRD-010 | Ver historial de trades | P1 | 3 | Pendiente |
|
||||
| US-TRD-011 | Ver estadísticas de rendimiento | P1 | 3 | Pendiente |
|
||||
| US-TRD-012 | Configurar TP/SL | P1 | 3 | Pendiente |
|
||||
| US-TRD-013 | Configurar alertas de precio | P2 | 3 | Pendiente |
|
||||
| US-TRD-014 | Resetear balance paper | P2 | 1 | Pendiente |
|
||||
| US-TRD-015 | Exportar trades a CSV | P2 | 2 | Pendiente |
|
||||
| US-TRD-016 | Modo oscuro en chart | P2 | 2 | Pendiente |
|
||||
| US-TRD-017 | Zoom y pan en chart | P1 | 3 | Pendiente |
|
||||
| US-TRD-018 | Comparar múltiples símbolos | P2 | 5 | Pendiente |
|
||||
|
||||
**Total:** 55 SP
|
||||
|
||||
---
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de:
|
||||
|
||||
- **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado
|
||||
|
||||
### Bloquea:
|
||||
|
||||
- **OQI-006:** ML Signals (integración con charts)
|
||||
|
||||
---
|
||||
|
||||
## Stack Técnico
|
||||
|
||||
| Capa | Tecnología | Uso |
|
||||
|------|------------|-----|
|
||||
| Frontend | Lightweight Charts | Renderizado de velas |
|
||||
| Frontend | React + Zustand | Estado y componentes |
|
||||
| Backend | Express.js | API REST |
|
||||
| Backend | ws | WebSocket server |
|
||||
| Database | PostgreSQL | Persistencia |
|
||||
| External | Binance API | Datos de mercado |
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
### Funcionales
|
||||
|
||||
- [ ] Charts renderizan correctamente con datos de Binance
|
||||
- [ ] 7 timeframes disponibles y funcionales
|
||||
- [ ] Mínimo 5 indicadores técnicos implementados
|
||||
- [ ] Watchlists CRUD completo
|
||||
- [ ] Paper trading ejecuta órdenes market y limit
|
||||
- [ ] Posiciones se actualizan en tiempo real
|
||||
- [ ] Historial muestra todos los trades cerrados
|
||||
- [ ] Métricas calculan win rate y P&L correctamente
|
||||
|
||||
### No Funcionales
|
||||
|
||||
- [ ] Chart carga en < 2 segundos
|
||||
- [ ] Updates en tiempo real < 500ms latencia
|
||||
- [ ] Soporta 1000+ velas sin lag
|
||||
- [ ] Mobile responsive
|
||||
|
||||
### Técnicos
|
||||
|
||||
- [ ] Cobertura de tests > 70%
|
||||
- [ ] Documentación API completa
|
||||
- [ ] Sin memory leaks en WebSocket
|
||||
|
||||
---
|
||||
|
||||
## Hitos
|
||||
|
||||
| Hito | Entregables | Target |
|
||||
|------|-------------|--------|
|
||||
| M1 | Charts básicos + timeframes | Sprint 3 |
|
||||
| M2 | Indicadores + watchlists | Sprint 3 |
|
||||
| M3 | Paper trading completo | Sprint 4 |
|
||||
| M4 | Métricas + polish | Sprint 4 |
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- [README Principal](./README.md)
|
||||
- [Vision del Producto](../../00-vision-general/VISION-PRODUCTO.md)
|
||||
- [Arquitectura](../../00-vision-general/ARQUITECTURA-GENERAL.md)
|
||||
- [_MAP Fase MVP](../_MAP.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user