Compare commits

..

No commits in common. "a7cca885f0961bf6ea8bb3d65a0c87c5cd3cc40d" and "bfda089f4e05c1ceb37234efb636665d01702128" have entirely different histories.

355 changed files with 85524 additions and 127676 deletions

339
AGENTS.md
View File

@ -1,339 +0,0 @@
# 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

View File

@ -1,52 +0,0 @@
# 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

View File

@ -1,31 +0,0 @@
# 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

View File

@ -1,57 +0,0 @@
# 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"]

View File

@ -1,345 +0,0 @@
# 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

View File

@ -1,54 +0,0 @@
{
"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"
}
}

View File

@ -1,159 +0,0 @@
/**
* 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,
};

View File

@ -1,332 +0,0 @@
/**
* 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;

View File

@ -1,209 +0,0 @@
/**
* 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,
};

View File

@ -1,471 +0,0 @@
/**
* 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;
}

View File

@ -1,265 +0,0 @@
/**
* 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}` }],
};
}

View File

@ -1,288 +0,0 @@
/**
* 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';
}

View File

@ -1,392 +0,0 @@
/**
* 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);
}

View File

@ -1,334 +0,0 @@
/**
* 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}` }],
};
}

View File

@ -1,67 +0,0 @@
/**
* 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;

View File

@ -1,23 +0,0 @@
{
"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"]
}

View File

@ -1,31 +0,0 @@
# 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

View File

@ -1,31 +0,0 @@
# 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/

View File

@ -1,277 +0,0 @@
# 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

View File

@ -1,272 +0,0 @@
# 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`

View File

@ -1,428 +0,0 @@
# 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 |

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +0,0 @@
{
"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"
}
}

View File

@ -1,291 +0,0 @@
/**
* 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);
});

View File

@ -1,375 +0,0 @@
/**
* 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;
}

View File

@ -1,143 +0,0 @@
/**
* 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}`,
},
],
};
}

View File

@ -1,212 +0,0 @@
/**
* 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,
};

View File

@ -1,315 +0,0 @@
/**
* 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}`,
},
],
};
}

View File

@ -1,193 +0,0 @@
/**
* 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;
}

View File

@ -1,402 +0,0 @@
/**
* 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}`,
},
],
};
}

View File

@ -1,23 +0,0 @@
{
"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"]
}

View File

@ -13,23 +13,23 @@ services:
# =========================================================================== # ===========================================================================
postgres: postgres:
image: postgres:16-alpine image: postgres:15-alpine
container_name: orbiquantia-postgres container_name: orbiquant-postgres
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_DB: orbiquantia_platform POSTGRES_DB: orbiquant_trading
POSTGRES_USER: orbiquantia POSTGRES_USER: orbiquant_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
POSTGRES_INITDB_ARGS: "-E UTF8" POSTGRES_INITDB_ARGS: "-E UTF8"
ports: ports:
- "${POSTGRES_PORT:-5433}:5432" - "${POSTGRES_PORT:-5432}:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
- ./apps/database/schemas:/docker-entrypoint-initdb.d:ro - ./apps/database/schemas:/docker-entrypoint-initdb.d:ro
networks: networks:
- orbiquant-network - orbiquant-network
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U orbiquantia -d orbiquantia_platform"] test: ["CMD-SHELL", "pg_isready -U orbiquant_user -d orbiquant_trading"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@ -66,9 +66,9 @@ services:
PORT: ${BACKEND_API_PORT:-3081} PORT: ${BACKEND_API_PORT:-3081}
DB_HOST: postgres DB_HOST: postgres
DB_PORT: 5432 DB_PORT: 5432
DB_NAME: orbiquantia_platform DB_NAME: orbiquant_trading
DB_USER: orbiquantia DB_USER: orbiquant_user
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025} DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
REDIS_HOST: redis REDIS_HOST: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
ML_ENGINE_URL: http://ml-engine:3083 ML_ENGINE_URL: http://ml-engine:3083
@ -131,9 +131,9 @@ services:
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
DB_HOST: postgres DB_HOST: postgres
DB_PORT: 5432 DB_PORT: 5432
DB_NAME: orbiquantia_platform DB_NAME: orbiquant_trading
DB_USER: orbiquantia DB_USER: orbiquant_user
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025} DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
REDIS_HOST: redis REDIS_HOST: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
PORT: ${ML_ENGINE_PORT:-3083} PORT: ${ML_ENGINE_PORT:-3083}
@ -163,9 +163,9 @@ services:
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
DB_HOST: postgres DB_HOST: postgres
DB_PORT: 5432 DB_PORT: 5432
DB_NAME: orbiquantia_platform DB_NAME: orbiquant_trading
DB_USER: orbiquantia DB_USER: orbiquant_user
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025} DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025}
POLYGON_API_KEY: ${POLYGON_API_KEY} POLYGON_API_KEY: ${POLYGON_API_KEY}
METAAPI_TOKEN: ${METAAPI_TOKEN} METAAPI_TOKEN: ${METAAPI_TOKEN}
METAAPI_ACCOUNT_ID: ${METAAPI_ACCOUNT_ID} METAAPI_ACCOUNT_ID: ${METAAPI_ACCOUNT_ID}

View File

@ -1,12 +1,3 @@
---
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 # NOTA: Discrepancia de Puertos Detectada
**Fecha:** 2025-12-08 **Fecha:** 2025-12-08

View File

@ -1,12 +1,3 @@
---
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 # Arquitectura General - OrbiQuant IA
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,414 +0,0 @@
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

View File

@ -1,12 +1,3 @@
---
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 # Stack Tecnologico - OrbiQuant IA
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,12 +1,3 @@
---
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 # Vision del Producto - OrbiQuant IA
**Version:** 1.0.0 **Version:** 1.0.0
@ -124,87 +115,6 @@ Empoderar a personas de todos los niveles de experiencia para que puedan inverti
**Objetivo:** Aprender de la comunidad y validar estrategias. **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 ## Modelo de Suscripcion
@ -319,6 +229,3 @@ Empoderar a personas de todos los niveles de experiencia para que puedan inverti
- [Arquitectura General](./ARQUITECTURA-GENERAL.md) - [Arquitectura General](./ARQUITECTURA-GENERAL.md)
- [Modelo de Negocio](./MODELO-NEGOCIO.md) - [Modelo de Negocio](./MODELO-NEGOCIO.md)
- [Stack Tecnologico](./STACK-TECNOLOGICO.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)

View File

@ -1,11 +1,3 @@
---
id: "MAP-00-vision-general"
title: "Mapa de 00-vision-general"
type: "Index"
project: "trading-platform"
updated_date: "2026-01-04"
---
# _MAP: Vision General # _MAP: Vision General
**Ultima actualizacion:** 2025-12-05 **Ultima actualizacion:** 2025-12-05

View File

@ -1,252 +0,0 @@
---
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*

View File

@ -1,12 +1,3 @@
---
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 # Arquitectura Multi-Agente MT4
**Fecha:** 2025-12-12 **Fecha:** 2025-12-12

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,3 @@
---
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 # Integracion API Massive - Pipeline de Datos
**Version:** 1.0.0 **Version:** 1.0.0

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,3 @@
---
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 # Integracion LLM Local - chatgpt-oss 16GB
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,12 +1,3 @@
---
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 # Integracion MetaTrader4 via MetaAPI
**Version:** 1.0.0 **Version:** 1.0.0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,3 @@
---
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 # OQI-001: Fundamentos y Autenticación
## Resumen Ejecutivo ## Resumen Ejecutivo

View File

@ -1,11 +1,3 @@
---
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 # _MAP: OQI-001 - Fundamentos y Autenticación
**Ultima actualizacion:** 2025-12-05 **Ultima actualizacion:** 2025-12-05

View File

@ -1,15 +1,3 @@
---
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 # ET-AUTH-001: Especificación Técnica - OAuth Providers
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,15 +1,3 @@
---
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 # ET-AUTH-002: Especificación Técnica - JWT Tokens
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,15 +1,3 @@
---
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 # ET-AUTH-003: Especificación Técnica - Esquema de Base de Datos
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,15 +1,3 @@
---
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 # ET-AUTH-004: Especificación Técnica - API Endpoints
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,15 +1,3 @@
---
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 # ET-AUTH-005: Especificación Técnica - Seguridad
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,15 +1,3 @@
---
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 # US-AUTH-001: Registro con Email
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,271 +1,259 @@
--- # US-AUTH-002: Login con Email
id: "US-AUTH-002"
title: "Login con Email" **Version:** 1.0.0
type: "User Story" **Fecha:** 2025-12-05
status: "To Do" **Estado:** Pendiente
priority: "Alta" **Story Points:** 3
epic: "OQI-001" **Prioridad:** P0 (Crítica)
story_points: 3 **Épica:** [OQI-001](../_MAP.md)
created_date: "2025-12-05"
updated_date: "2026-01-04" ---
---
## Historia de Usuario
# US-AUTH-002: Login con Email
**Como** usuario registrado de OrbiQuant
**Version:** 1.0.0 **Quiero** iniciar sesión con mi email y contraseña
**Fecha:** 2025-12-05 **Para** acceder a mi cuenta y utilizar la plataforma
**Estado:** Pendiente
**Story Points:** 3 ---
**Prioridad:** P0 (Crítica)
**Épica:** [OQI-001](../_MAP.md) ## Criterios de Aceptación
--- ### AC-001: Formulario de login
## Historia de Usuario **Dado** que soy un usuario registrado
**Cuando** accedo a la página de login
**Como** usuario registrado de OrbiQuant **Entonces** debería ver un formulario con:
**Quiero** iniciar sesión con mi email y contraseña - Campo de email
**Para** acceder a mi cuenta y utilizar la plataforma - Campo de contraseña
- Checkbox "Recordarme"
--- - Botón "Iniciar sesión"
- Link "¿Olvidaste tu contraseña?"
## Criterios de Aceptación - Opciones de OAuth
### AC-001: Formulario de login ### AC-002: Validación de campos
**Dado** que soy un usuario registrado **Dado** que estoy en el formulario de login
**Cuando** accedo a la página de login **Cuando** intento enviar el formulario con campos vacíos
**Entonces** debería ver un formulario con: **Entonces** debería ver mensajes de error:
- Campo de email - "El email es requerido"
- Campo de contraseña - "La contraseña es requerida"
- Checkbox "Recordarme"
- Botón "Iniciar sesión" ### AC-003: Login exitoso
- Link "¿Olvidaste tu contraseña?"
- Opciones de OAuth **Dado** que ingresé credenciales válidas
**Cuando** hago click en "Iniciar sesión"
### AC-002: Validación de campos **Entonces** debería:
1. Recibir un JWT token
**Dado** que estoy en el formulario de login 2. Ser redirigido al dashboard
**Cuando** intento enviar el formulario con campos vacíos 3. Ver mi nombre en el header
**Entonces** debería ver mensajes de error: 4. Tener la sesión activa
- "El email es requerido"
- "La contraseña es requerida" ### AC-004: Credenciales incorrectas
### AC-003: Login exitoso **Dado** que ingreso credenciales incorrectas
**Cuando** hago click en "Iniciar sesión"
**Dado** que ingresé credenciales válidas **Entonces** debería ver un mensaje:
**Cuando** hago click en "Iniciar sesión" - "Email o contraseña incorrectos"
**Entonces** debería: **Y** los campos no deberían limpiarse
1. Recibir un JWT token **Y** debería poder intentar de nuevo
2. Ser redirigido al dashboard
3. Ver mi nombre en el header ### AC-005: Email no verificado
4. Tener la sesión activa
**Dado** que mi cuenta no ha verificado el email
### AC-004: Credenciales incorrectas **Cuando** intento hacer login
**Entonces** debería ver un mensaje:
**Dado** que ingreso credenciales incorrectas - "Por favor verifica tu email antes de iniciar sesión"
**Cuando** hago click en "Iniciar sesión" **Y** debería ver un botón "Reenviar email de verificación"
**Entonces** debería ver un mensaje:
- "Email o contraseña incorrectos" ### AC-006: Cuenta bloqueada
**Y** los campos no deberían limpiarse
**Y** debería poder intentar de nuevo **Dado** que mi cuenta ha sido bloqueada por seguridad
**Cuando** intento hacer login
### AC-005: Email no verificado **Entonces** debería ver un mensaje:
- "Tu cuenta ha sido bloqueada. Contacta soporte."
**Dado** que mi cuenta no ha verificado el email
**Cuando** intento hacer login ### AC-007: Recordarme
**Entonces** debería ver un mensaje:
- "Por favor verifica tu email antes de iniciar sesión" **Dado** que marqué la opción "Recordarme"
**Y** debería ver un botón "Reenviar email de verificación" **Cuando** inicio sesión exitosamente
**Entonces** el token debería tener una duración de 30 días
### AC-006: Cuenta bloqueada **Y** cuando cierre el navegador y vuelva a abrir
**Entonces** debería seguir con sesión activa
**Dado** que mi cuenta ha sido bloqueada por seguridad
**Cuando** intento hacer login ### AC-008: Rate limiting
**Entonces** debería ver un mensaje:
- "Tu cuenta ha sido bloqueada. Contacta soporte." **Dado** que he fallado 5 intentos de login
**Cuando** intento iniciar sesión nuevamente
### AC-007: Recordarme **Entonces** debería ver un mensaje:
- "Demasiados intentos. Intenta en 15 minutos"
**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 ## Mockup
**Entonces** debería seguir con sesión activa
```
### AC-008: Rate limiting ┌─────────────────────────────────────────────────────────────┐
│ │
**Dado** que he fallado 5 intentos de login │ 🌟 Inicia sesión en OrbiQuant │
**Cuando** intento iniciar sesión nuevamente │ │
**Entonces** debería ver un mensaje: │ ┌─────────────────────────────────────────────────────┐ │
- "Demasiados intentos. Intenta en 15 minutos" │ │ Email │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
--- │ │ │ usuario@example.com │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
## Mockup │ └─────────────────────────────────────────────────────┘ │
│ │
``` │ ┌─────────────────────────────────────────────────────┐ │
┌─────────────────────────────────────────────────────────────┐ │ │ Contraseña │ │
│ │ │ │ ┌─────────────────────────────────────────────────┐ │ │
│ 🌟 Inicia sesión en OrbiQuant │ │ │ │ •••••••••••• 👁 │ │ │
│ │ │ │ └─────────────────────────────────────────────────┘ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ └─────────────────────────────────────────────────────┘ │
│ │ Email │ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │ │ ☐ Recordarme ¿Olvidaste tu contraseña? │
│ │ │ usuario@example.com │ │ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │ │ ┌─────────────────────────────────────────────────────┐ │
│ └─────────────────────────────────────────────────────┘ │ │ │ Iniciar sesión │ │
│ │ │ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ Contraseña │ │ │ ─────────────────── O continúa con ─────────────────── │
│ │ ┌─────────────────────────────────────────────────┐ │ │ │ │
│ │ │ •••••••••••• 👁 │ │ │ │ [Google] [Facebook] [X] [Apple] [GitHub] │
│ │ └─────────────────────────────────────────────────┘ │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ │ ¿No tienes cuenta? Regístrate │
│ │ │ │
│ ☐ Recordarme ¿Olvidaste tu contraseña? │ └─────────────────────────────────────────────────────────────┘
│ │ ```
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Iniciar sesión │ │ ---
│ └─────────────────────────────────────────────────────┘ │
│ │ ## Tareas Técnicas
│ ─────────────────── O continúa con ─────────────────── │
│ │ ### Database (DB)
│ [Google] [Facebook] [X] [Apple] [GitHub] │
│ │ - [ ] Crear tabla `login_attempts` para rate limiting
│ ¿No tienes cuenta? Regístrate │ ```sql
│ │ CREATE TABLE login_attempts (
└─────────────────────────────────────────────────────────────┘ id UUID PRIMARY KEY,
``` email VARCHAR(255),
ip_address VARCHAR(45),
--- attempt_time TIMESTAMP,
success BOOLEAN,
## Tareas Técnicas created_at TIMESTAMP DEFAULT NOW()
);
### Database (DB) ```
- [ ] Índice en `email` y `attempt_time`
- [ ] Crear tabla `login_attempts` para rate limiting
```sql ### Backend (BE)
CREATE TABLE login_attempts (
id UUID PRIMARY KEY, - [ ] Endpoint `POST /api/v1/auth/login`
email VARCHAR(255), - Validación de entrada con Zod
ip_address VARCHAR(45), - Verificar email verificado
attempt_time TIMESTAMP, - Verificar cuenta no bloqueada
success BOOLEAN, - Comparar hash con bcrypt
created_at TIMESTAMP DEFAULT NOW() - Generar JWT token
); - Rate limiting (5 intentos / 15 min)
``` - Logging de intentos
- [ ] Índice en `email` y `attempt_time` - [ ] Service `AuthService.login()`
- [ ] Middleware de rate limiting
### Backend (BE) - [ ] Tests unitarios (8 casos)
- [ ] Tests de integración (5 escenarios)
- [ ] Endpoint `POST /api/v1/auth/login`
- Validación de entrada con Zod ### Frontend (FE)
- Verificar email verificado
- Verificar cuenta no bloqueada - [ ] Componente `apps/frontend/src/modules/auth/pages/Login.tsx`
- Comparar hash con bcrypt - [ ] Form validation con React Hook Form + Zod
- Generar JWT token - [ ] Estado de loading durante autenticación
- Rate limiting (5 intentos / 15 min) - [ ] Manejo de errores específicos
- Logging de intentos - [ ] Almacenamiento de token en localStorage/sessionStorage
- [ ] Service `AuthService.login()` - [ ] Redirección post-login
- [ ] Middleware de rate limiting - [ ] Tests con React Testing Library (6 casos)
- [ ] Tests unitarios (8 casos)
- [ ] Tests de integración (5 escenarios) ### Testing (QA)
### Frontend (FE) - [ ] E2E: Login exitoso (Playwright)
- [ ] E2E: Credenciales incorrectas
- [ ] Componente `apps/frontend/src/modules/auth/pages/Login.tsx` - [ ] E2E: Email no verificado
- [ ] Form validation con React Hook Form + Zod - [ ] E2E: Rate limiting
- [ ] Estado de loading durante autenticación - [ ] E2E: Recordarme funcionalidad
- [ ] Manejo de errores específicos - [ ] Test de seguridad: SQL injection
- [ ] Almacenamiento de token en localStorage/sessionStorage - [ ] Test de seguridad: XSS
- [ ] Redirección post-login - [ ] Performance: < 500ms response time
- [ ] Tests con React Testing Library (6 casos)
---
### Testing (QA)
## Dependencias
- [ ] E2E: Login exitoso (Playwright)
- [ ] E2E: Credenciales incorrectas - **Bloqueantes:**
- [ ] E2E: Email no verificado - US-AUTH-001: Necesita usuarios registrados para poder hacer login
- [ ] E2E: Rate limiting
- [ ] E2E: Recordarme funcionalidad - **Deseables:**
- [ ] Test de seguridad: SQL injection - US-AUTH-011: Para el flujo de "¿Olvidaste tu contraseña?"
- [ ] Test de seguridad: XSS
- [ ] Performance: < 500ms response time ---
--- ## Definition of Ready (DoR)
## Dependencias - [ ] Mockups aprobados por UX
- [ ] Esquema de base de datos revisado
- **Bloqueantes:** - [ ] API contract definido
- US-AUTH-001: Necesita usuarios registrados para poder hacer login - [ ] Criterios de aceptación claros
- [ ] Estimación acordada por el equipo
- **Deseables:**
- US-AUTH-011: Para el flujo de "¿Olvidaste tu contraseña?" ---
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado y revisado (code review)
- [ ] Tests unitarios con 80%+ cobertura
- [ ] Mockups aprobados por UX - [ ] Tests de integración pasando
- [ ] Esquema de base de datos revisado - [ ] Tests E2E implementados
- [ ] API contract definido - [ ] Documentación API actualizada
- [ ] Criterios de aceptación claros - [ ] Rate limiting configurado
- [ ] Estimación acordada por el equipo - [ ] Logs implementados
- [ ] Seguridad validada (OWASP)
--- - [ ] QA aprobado en staging
- [ ] Deploy a producción exitoso
## Definition of Done (DoD)
---
- [ ] Código implementado y revisado (code review)
- [ ] Tests unitarios con 80%+ cobertura ## Notas Técnicas
- [ ] Tests de integración pasando
- [ ] Tests E2E implementados ### JWT Token Structure
- [ ] Documentación API actualizada
- [ ] Rate limiting configurado ```json
- [ ] Logs implementados {
- [ ] Seguridad validada (OWASP) "sub": "user-id",
- [ ] QA aprobado en staging "email": "user@example.com",
- [ ] Deploy a producción exitoso "role": "user",
"iat": 1234567890,
--- "exp": 1234567890
}
## Notas Técnicas ```
### JWT Token Structure ### Rate Limiting Strategy
```json - 5 intentos fallidos por email
{ - Ventana de 15 minutos
"sub": "user-id", - Reset después de login exitoso
"email": "user@example.com", - Bloqueo temporal, no permanente
"role": "user",
"iat": 1234567890, ### Security Considerations
"exp": 1234567890
} - HTTPS obligatorio
``` - Password no visible en logs
- Tokens con expiración
### Rate Limiting Strategy - CSRF protection
- Content-Security-Policy headers
- 5 intentos fallidos por email
- Ventana de 15 minutos ---
- Reset después de login exitoso
- Bloqueo temporal, no permanente ## Requerimientos Relacionados
### Security Considerations - [RF-AUTH-002: Autenticación por Email](../requerimientos/RF-AUTH-002-email.md)
- HTTPS obligatorio ## Especificaciones Relacionadas
- Password no visible en logs
- Tokens con expiración - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- CSRF protection - [ET-AUTH-003: Database](../especificaciones/ET-AUTH-003-database.md)
- 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)

View File

@ -1,15 +1,3 @@
---
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 # US-AUTH-003: Login con Google
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,305 +1,293 @@
--- # US-AUTH-004: OAuth Facebook
id: "US-AUTH-004"
title: "OAuth Facebook" **Version:** 1.0.0
type: "User Story" **Fecha:** 2025-12-05
status: "To Do" **Estado:** Pendiente
priority: "Alta" **Story Points:** 3
epic: "OQI-001" **Prioridad:** P1 (Alta)
story_points: 3 **Épica:** [OQI-001](../_MAP.md)
created_date: "2025-12-05"
updated_date: "2026-01-04" ---
---
## Historia de Usuario
# US-AUTH-004: OAuth Facebook
**Como** visitante o usuario de OrbiQuant
**Version:** 1.0.0 **Quiero** poder registrarme e iniciar sesión usando mi cuenta de Facebook
**Fecha:** 2025-12-05 **Para** tener un acceso rápido y sencillo sin crear una nueva contraseña
**Estado:** Pendiente
**Story Points:** 3 ---
**Prioridad:** P1 (Alta)
**Épica:** [OQI-001](../_MAP.md) ## Criterios de Aceptación
--- ### AC-001: Botón de Facebook visible
## Historia de Usuario **Dado** que estoy en la página de registro o login
**Cuando** veo las opciones de autenticación
**Como** visitante o usuario de OrbiQuant **Entonces** debería ver un botón "Continuar con Facebook"
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de Facebook **Y** debería tener el color y logo oficial de Facebook
**Para** tener un acceso rápido y sencillo sin crear una nueva contraseña
### AC-002: Flujo de OAuth
---
**Dado** que hago click en "Continuar con Facebook"
## Criterios de Aceptación **Cuando** se abre la ventana de Facebook
**Entonces** debería:
### AC-001: Botón de Facebook visible 1. Ver la pantalla de autorización de Facebook
2. Poder revisar los permisos solicitados
**Dado** que estoy en la página de registro o login 3. Poder autorizar o cancelar
**Cuando** veo las opciones de autenticación
**Entonces** debería ver un botón "Continuar con Facebook" ### AC-003: Permisos solicitados
**Y** debería tener el color y logo oficial de Facebook
**Dado** que estoy en la pantalla de autorización de Facebook
### AC-002: Flujo de OAuth **Cuando** reviso los permisos
**Entonces** la app debería solicitar únicamente:
**Dado** que hago click en "Continuar con Facebook" - Email
**Cuando** se abre la ventana de Facebook - Nombre público
**Entonces** debería: - Foto de perfil
1. Ver la pantalla de autorización de Facebook
2. Poder revisar los permisos solicitados ### AC-004: Primer registro exitoso
3. Poder autorizar o cancelar
**Dado** que es mi primera vez usando Facebook OAuth
### AC-003: Permisos solicitados **Cuando** autorizo los permisos
**Entonces** debería:
**Dado** que estoy en la pantalla de autorización de Facebook 1. Crear mi cuenta automáticamente
**Cuando** reviso los permisos 2. Recibir un JWT token
**Entonces** la app debería solicitar únicamente: 3. Ser redirigido al dashboard
- Email 4. Ver mi nombre y foto de Facebook
- Nombre público 5. NO necesitar verificación de email
- Foto de perfil
### AC-005: Login existente
### AC-004: Primer registro exitoso
**Dado** que ya tengo una cuenta vinculada con Facebook
**Dado** que es mi primera vez usando Facebook OAuth **Cuando** uso "Continuar con Facebook"
**Cuando** autorizo los permisos **Entonces** debería:
**Entonces** debería: 1. Iniciar sesión automáticamente
1. Crear mi cuenta automáticamente 2. Ser redirigido al dashboard
2. Recibir un JWT token 3. NO ver pantalla de registro
3. Ser redirigido al dashboard
4. Ver mi nombre y foto de Facebook ### AC-006: Email ya registrado con otro método
5. NO necesitar verificación de email
**Dado** que mi email de Facebook ya está registrado con email/password
### AC-005: Login existente **Cuando** intento usar Facebook OAuth
**Entonces** debería ver un mensaje:
**Dado** que ya tengo una cuenta vinculada con Facebook - "Este email ya está registrado. ¿Deseas vincular tu cuenta de Facebook?"
**Cuando** uso "Continuar con Facebook" **Y** debería poder vincular las cuentas
**Entonces** debería:
1. Iniciar sesión automáticamente ### AC-007: Cancelación del flujo
2. Ser redirigido al dashboard
3. NO ver pantalla de registro **Dado** que inicio el flujo de Facebook OAuth
**Cuando** cancelo en la ventana de Facebook
### AC-006: Email ya registrado con otro método **Entonces** debería:
1. Volver a la página de login/registro
**Dado** que mi email de Facebook ya está registrado con email/password 2. Ver un mensaje "Autenticación cancelada"
**Cuando** intento usar Facebook OAuth 3. Poder intentar con otro método
**Entonces** debería ver un mensaje:
- "Este email ya está registrado. ¿Deseas vincular tu cuenta de Facebook?" ### AC-008: Error de Facebook
**Y** debería poder vincular las cuentas
**Dado** que hay un error en el servicio de Facebook
### AC-007: Cancelación del flujo **Cuando** intento autenticarme
**Entonces** debería ver un mensaje:
**Dado** que inicio el flujo de Facebook OAuth - "Error al conectar con Facebook. Intenta más tarde"
**Cuando** cancelo en la ventana de Facebook **Y** debería poder usar otro método de autenticación
**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 ## Mockup
### AC-008: Error de Facebook ```
┌─────────────────────────────────────────────────────────────┐
**Dado** que hay un error en el servicio de Facebook │ │
**Cuando** intento autenticarme │ 🌟 Bienvenido a OrbiQuant │
**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 │ │ 📧 Email │ │
│ └─────────────────────────────────────────────────────┘ │
--- │ │
│ ┌─────────────────────────────────────────────────────┐ │
## Mockup │ │ 🔵 Continuar con Facebook │ │
│ └─────────────────────────────────────────────────────┘ │
``` │ │
┌─────────────────────────────────────────────────────────────┐ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ 🔴 Continuar con Google │ │
│ 🌟 Bienvenido a OrbiQuant │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ [Twitter/X] [Apple] [GitHub] │
│ │ 📧 Email │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
│ │
│ ┌─────────────────────────────────────────────────────┐ │ Ventana de Facebook OAuth:
│ │ 🔵 Continuar con Facebook │ │ ┌─────────────────────────────────────────────────────────────┐
│ └─────────────────────────────────────────────────────┘ │ │ facebook.com ✕ │
│ │ ├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ 🔴 Continuar con Google │ │ │ OrbiQuant desea acceder a: │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ │ │ ✓ Tu nombre y foto de perfil │
│ [Twitter/X] [Apple] [GitHub] │ │ ✓ Tu dirección de email │
│ │ │ │
└─────────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────┐ │
│ │ Continuar como Juan Pérez │ │
Ventana de Facebook OAuth: │ └─────────────────────────────────────────────────────┘ │
┌─────────────────────────────────────────────────────────────┐ │ │
│ facebook.com ✕ │ │ Cancelar │
├─────────────────────────────────────────────────────────────┤ │ │
│ │ └─────────────────────────────────────────────────────────────┘
│ OrbiQuant desea acceder a: │ ```
│ │
│ ✓ Tu nombre y foto de perfil │ ---
│ ✓ Tu dirección de email │
│ │ ## Tareas Técnicas
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Continuar como Juan Pérez │ │ ### Database (DB)
│ └─────────────────────────────────────────────────────┘ │
│ │ - [ ] Agregar campos a tabla `users`:
│ Cancelar │ ```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 (
## Tareas Técnicas id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
### Database (DB) provider VARCHAR(50), -- 'facebook', 'google', etc
provider_user_id VARCHAR(255),
- [ ] Agregar campos a tabla `users`: access_token TEXT,
```sql refresh_token TEXT,
ALTER TABLE users ADD COLUMN facebook_id VARCHAR(255) UNIQUE; token_expires_at TIMESTAMP,
ALTER TABLE users ADD COLUMN avatar_url TEXT; created_at TIMESTAMP DEFAULT NOW(),
``` updated_at TIMESTAMP DEFAULT NOW(),
- [ ] Tabla `oauth_connections`: UNIQUE(provider, provider_user_id)
```sql );
CREATE TABLE oauth_connections ( ```
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id), ### Backend (BE)
provider VARCHAR(50), -- 'facebook', 'google', etc
provider_user_id VARCHAR(255), - [ ] Configurar Facebook App en Meta Developers
access_token TEXT, - [ ] Obtener App ID y App Secret
refresh_token TEXT, - [ ] Endpoint `GET /api/v1/auth/facebook`
token_expires_at TIMESTAMP, - Redirige a Facebook OAuth
created_at TIMESTAMP DEFAULT NOW(), - [ ] Endpoint `GET /api/v1/auth/facebook/callback`
updated_at TIMESTAMP DEFAULT NOW(), - Recibe código de autorización
UNIQUE(provider, provider_user_id) - Intercambia por access token
); - Obtiene datos del usuario
``` - Crea o actualiza usuario
- Genera JWT token
### Backend (BE) - [ ] Service `FacebookOAuthService`
- `getAuthorizationUrl()`
- [ ] Configurar Facebook App en Meta Developers - `exchangeCodeForToken()`
- [ ] Obtener App ID y App Secret - `getUserProfile()`
- [ ] Endpoint `GET /api/v1/auth/facebook` - `linkAccount()`
- Redirige a Facebook OAuth - [ ] Manejo de refresh tokens
- [ ] Endpoint `GET /api/v1/auth/facebook/callback` - [ ] Tests unitarios (8 casos)
- Recibe código de autorización - [ ] Tests de integración con mock de Facebook API
- Intercambia por access token
- Obtiene datos del usuario ### Frontend (FE)
- Crea o actualiza usuario
- Genera JWT token - [ ] Botón "Continuar con Facebook"
- [ ] Service `FacebookOAuthService` - [ ] Manejo de popup o redirect de OAuth
- `getAuthorizationUrl()` - [ ] Recepción de callback
- `exchangeCodeForToken()` - [ ] Almacenamiento de token JWT
- `getUserProfile()` - [ ] Estado de loading durante OAuth
- `linkAccount()` - [ ] Manejo de errores
- [ ] Manejo de refresh tokens - [ ] Modal de vinculación de cuentas
- [ ] Tests unitarios (8 casos) - [ ] Tests con React Testing Library
- [ ] Tests de integración con mock de Facebook API
### Testing (QA)
### Frontend (FE)
- [ ] E2E: Registro nuevo con Facebook
- [ ] Botón "Continuar con Facebook" - [ ] E2E: Login existente con Facebook
- [ ] Manejo de popup o redirect de OAuth - [ ] E2E: Vinculación de cuentas
- [ ] Recepción de callback - [ ] E2E: Cancelación del flujo
- [ ] Almacenamiento de token JWT - [ ] E2E: Permisos rechazados
- [ ] Estado de loading durante OAuth - [ ] Test de seguridad: Validación de tokens
- [ ] Manejo de errores - [ ] Test de seguridad: CSRF protection
- [ ] Modal de vinculación de cuentas - [ ] Mock de Facebook API para tests
- [ ] Tests con React Testing Library
---
### Testing (QA)
## Dependencias
- [ ] E2E: Registro nuevo con Facebook
- [ ] E2E: Login existente con Facebook - **Bloqueantes:**
- [ ] E2E: Vinculación de cuentas - Cuenta de Facebook Developer
- [ ] E2E: Cancelación del flujo - Configuración de dominio verificado
- [ ] E2E: Permisos rechazados - SSL/HTTPS en producción
- [ ] Test de seguridad: Validación de tokens
- [ ] Test de seguridad: CSRF protection - **Deseables:**
- [ ] Mock de Facebook API para tests - US-AUTH-003: Para mantener consistencia con Google OAuth
--- ---
## Dependencias ## Definition of Ready (DoR)
- **Bloqueantes:** - [ ] Facebook App creada y configurada
- Cuenta de Facebook Developer - [ ] Credenciales de desarrollo disponibles
- Configuración de dominio verificado - [ ] Mockups aprobados
- SSL/HTTPS en producción - [ ] API contract definido
- [ ] Política de privacidad publicada (requerido por Facebook)
- **Deseables:**
- US-AUTH-003: Para mantener consistencia con Google OAuth ---
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura
- [ ] Facebook App creada y configurada - [ ] Tests de integración pasando
- [ ] Credenciales de desarrollo disponibles - [ ] Tests E2E implementados
- [ ] Mockups aprobados - [ ] Facebook App Review aprobado (para producción)
- [ ] API contract definido - [ ] Documentación actualizada
- [ ] Política de privacidad publicada (requerido por Facebook) - [ ] Manejo de errores completo
- [ ] Logs implementados
--- - [ ] QA aprobado en staging
- [ ] Deploy a producción exitoso
## Definition of Done (DoD)
---
- [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura ## Notas Técnicas
- [ ] Tests de integración pasando
- [ ] Tests E2E implementados ### Facebook OAuth Flow
- [ ] Facebook App Review aprobado (para producción)
- [ ] Documentación actualizada 1. Frontend redirige a `/api/v1/auth/facebook`
- [ ] Manejo de errores completo 2. Backend redirige a Facebook con:
- [ ] Logs implementados - `client_id`
- [ ] QA aprobado en staging - `redirect_uri`
- [ ] Deploy a producción exitoso - `scope=email,public_profile`
- `state` (CSRF token)
--- 3. Usuario autoriza en Facebook
4. Facebook redirige a `redirect_uri` con `code`
## Notas Técnicas 5. Backend intercambia `code` por `access_token`
6. Backend obtiene perfil del usuario
### Facebook OAuth Flow 7. Backend crea/actualiza usuario
8. Backend genera JWT y redirige a frontend
1. Frontend redirige a `/api/v1/auth/facebook`
2. Backend redirige a Facebook con: ### Facebook API Endpoints
- `client_id`
- `redirect_uri` - Authorization: `https://www.facebook.com/v18.0/dialog/oauth`
- `scope=email,public_profile` - Token exchange: `https://graph.facebook.com/v18.0/oauth/access_token`
- `state` (CSRF token) - User info: `https://graph.facebook.com/v18.0/me?fields=id,name,email,picture`
3. Usuario autoriza en Facebook
4. Facebook redirige a `redirect_uri` con `code` ### Environment Variables
5. Backend intercambia `code` por `access_token`
6. Backend obtiene perfil del usuario ```env
7. Backend crea/actualiza usuario FACEBOOK_APP_ID=your_app_id
8. Backend genera JWT y redirige a frontend FACEBOOK_APP_SECRET=your_app_secret
FACEBOOK_CALLBACK_URL=https://orbiquant.com/api/v1/auth/facebook/callback
### Facebook API Endpoints ```
- Authorization: `https://www.facebook.com/v18.0/dialog/oauth` ### Security Considerations
- 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` - Validar `state` parameter para prevenir CSRF
- No almacenar access tokens en localStorage
### Environment Variables - Usar refresh tokens cuando sea posible
- Validar que el email viene de Facebook
```env - Rate limiting en endpoints de OAuth
FACEBOOK_APP_ID=your_app_id
FACEBOOK_APP_SECRET=your_app_secret ---
FACEBOOK_CALLBACK_URL=https://orbiquant.com/api/v1/auth/facebook/callback
``` ## Requerimientos Relacionados
### Security Considerations - [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
- Validar `state` parameter para prevenir CSRF ## Especificaciones Relacionadas
- No almacenar access tokens en localStorage
- Usar refresh tokens cuando sea posible - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- Validar que el email viene de Facebook - [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
- 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)

View File

@ -1,328 +1,316 @@
--- # US-AUTH-005: OAuth Twitter/X
id: "US-AUTH-005"
title: "OAuth Twitter/X" **Version:** 1.0.0
type: "User Story" **Fecha:** 2025-12-05
status: "To Do" **Estado:** Pendiente
priority: "Alta" **Story Points:** 3
epic: "OQI-001" **Prioridad:** P1 (Alta)
story_points: 3 **Épica:** [OQI-001](../_MAP.md)
created_date: "2025-12-05"
updated_date: "2026-01-04" ---
---
## Historia de Usuario
# US-AUTH-005: OAuth Twitter/X
**Como** visitante o usuario de OrbiQuant
**Version:** 1.0.0 **Quiero** poder registrarme e iniciar sesión usando mi cuenta de Twitter/X
**Fecha:** 2025-12-05 **Para** tener un acceso rápido sin crear una nueva contraseña
**Estado:** Pendiente
**Story Points:** 3 ---
**Prioridad:** P1 (Alta)
**Épica:** [OQI-001](../_MAP.md) ## Criterios de Aceptación
--- ### AC-001: Botón de Twitter/X visible
## Historia de Usuario **Dado** que estoy en la página de registro o login
**Cuando** veo las opciones de autenticación
**Como** visitante o usuario de OrbiQuant **Entonces** debería ver un botón "Continuar con X"
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de Twitter/X **Y** debería tener el logo y estilo oficial de X (antes Twitter)
**Para** tener un acceso rápido sin crear una nueva contraseña
### AC-002: Flujo de OAuth
---
**Dado** que hago click en "Continuar con X"
## Criterios de Aceptación **Cuando** se abre la ventana de autorización
**Entonces** debería:
### AC-001: Botón de Twitter/X visible 1. Ver la pantalla de autorización de X
2. Poder revisar los permisos solicitados
**Dado** que estoy en la página de registro o login 3. Poder autorizar la aplicación
**Cuando** veo las opciones de autenticación
**Entonces** debería ver un botón "Continuar con X" ### AC-003: Permisos solicitados
**Y** debería tener el logo y estilo oficial de X (antes Twitter)
**Dado** que estoy en la pantalla de autorización de X
### AC-002: Flujo de OAuth **Cuando** reviso los permisos
**Entonces** la app debería solicitar:
**Dado** que hago click en "Continuar con X" - Leer información de perfil
**Cuando** se abre la ventana de autorización - Email (si está disponible)
**Entonces** debería:
1. Ver la pantalla de autorización de X ### AC-004: Primer registro exitoso
2. Poder revisar los permisos solicitados
3. Poder autorizar la aplicación **Dado** que es mi primera vez usando X OAuth
**Cuando** autorizo los permisos
### AC-003: Permisos solicitados **Entonces** debería:
1. Crear mi cuenta automáticamente
**Dado** que estoy en la pantalla de autorización de X 2. Recibir un JWT token
**Cuando** reviso los permisos 3. Ser redirigido al dashboard
**Entonces** la app debería solicitar: 4. Ver mi nombre y foto de X
- Leer información de perfil 5. Si X no proporciona email, solicitar email adicional
- Email (si está disponible)
### AC-005: Solicitud de email adicional
### AC-004: Primer registro exitoso
**Dado** que X no proporcionó mi email
**Dado** que es mi primera vez usando X OAuth **Cuando** completo la autorización
**Cuando** autorizo los permisos **Entonces** debería ver un formulario que solicita:
**Entonces** debería: - "Completa tu registro: ingresa tu email"
1. Crear mi cuenta automáticamente **Y** debería validar que el email no esté en uso
2. Recibir un JWT token **Y** debería enviar email de verificación
3. Ser redirigido al dashboard
4. Ver mi nombre y foto de X ### AC-006: Login existente
5. Si X no proporciona email, solicitar email adicional
**Dado** que ya tengo una cuenta vinculada con X
### AC-005: Solicitud de email adicional **Cuando** uso "Continuar con X"
**Entonces** debería:
**Dado** que X no proporcionó mi email 1. Iniciar sesión automáticamente
**Cuando** completo la autorización 2. Ser redirigido al dashboard
**Entonces** debería ver un formulario que solicita:
- "Completa tu registro: ingresa tu email" ### AC-007: Email ya registrado
**Y** debería validar que el email no esté en uso
**Y** debería enviar email de verificación **Dado** que mi email de X ya está registrado con otro método
**Cuando** intento usar X OAuth
### AC-006: Login existente **Entonces** debería ver opción de vincular cuentas
**Y** debería poder vincular después de autenticarme
**Dado** que ya tengo una cuenta vinculada con X
**Cuando** uso "Continuar con X" ### AC-008: Cancelación del flujo
**Entonces** debería:
1. Iniciar sesión automáticamente **Dado** que inicio el flujo de X OAuth
2. Ser redirigido al dashboard **Cuando** cancelo en la ventana de X
**Entonces** debería volver a login/registro
### AC-007: Email ya registrado **Y** ver mensaje "Autenticación cancelada"
**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 ## Mockup
**Y** debería poder vincular después de autenticarme
```
### AC-008: Cancelación del flujo ┌─────────────────────────────────────────────────────────────┐
│ │
**Dado** que inicio el flujo de X OAuth │ 🌟 Bienvenido a OrbiQuant │
**Cuando** cancelo en la ventana de X │ │
**Entonces** debería volver a login/registro │ ┌─────────────────────────────────────────────────────┐ │
**Y** ver mensaje "Autenticación cancelada" │ │ 📧 Email │ │
│ └─────────────────────────────────────────────────────┘ │
--- │ │
│ ┌─────────────────────────────────────────────────────┐ │
## Mockup │ │ 🔴 Continuar con Google │ │
│ └─────────────────────────────────────────────────────┘ │
``` │ │
┌─────────────────────────────────────────────────────────────┐ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ ⚫ Continuar con X │ │
│ 🌟 Bienvenido a OrbiQuant │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ [Facebook] [Apple] [GitHub] │
│ │ 📧 Email │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
│ │
│ ┌─────────────────────────────────────────────────────┐ │ Ventana de X OAuth:
│ │ 🔴 Continuar con Google │ │ ┌─────────────────────────────────────────────────────────────┐
│ └─────────────────────────────────────────────────────┘ │ │ x.com ✕ │
│ │ ├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ ⚫ Continuar con X │ │ │ Autorizar OrbiQuant │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ │ │ Esta aplicación podrá: │
│ [Facebook] [Apple] [GitHub] │ │ │
│ │ │ • Ver tu perfil y posts │
└─────────────────────────────────────────────────────────────┘ │ • Ver los perfiles que sigues │
│ • Actualizar tu perfil │
Ventana de X OAuth: │ │
┌─────────────────────────────────────────────────────────────┐ @juanperez
│ x.com ✕ │ │ │
├─────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ Autorizar aplicación │ │
│ Autorizar OrbiQuant │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ Esta aplicación podrá: │ │ Cancelar │
│ │ │ │
│ • Ver tu perfil y posts │ └─────────────────────────────────────────────────────────────┘
│ • Ver los perfiles que sigues │
│ • Actualizar tu perfil │ Formulario adicional si falta email:
│ │ ┌─────────────────────────────────────────────────────────────┐
@juanperez │ │
│ │ │ Un último paso para completar tu registro │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ Autorizar aplicación │ │ │ X no compartió tu email. Por favor ingrésalo: │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
│ Cancelar │ │ │ Email │ │
│ │ │ │ ┌─────────────────────────────────────────────────┐ │ │
└─────────────────────────────────────────────────────────────┘ │ │ │ tu@email.com │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
Formulario adicional si falta email: │ └─────────────────────────────────────────────────────┘ │
┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
│ Un último paso para completar tu registro │ │ │ Completar registro │ │
│ │ │ └─────────────────────────────────────────────────────┘ │
│ X no compartió tu email. Por favor ingrésalo: │ │ │
│ │ └─────────────────────────────────────────────────────────────┘
│ ┌─────────────────────────────────────────────────────┐ │ ```
│ │ Email │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │ ---
│ │ │ tu@email.com │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │ ## Tareas Técnicas
│ └─────────────────────────────────────────────────────┘ │
│ │ ### Database (DB)
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Completar registro │ │ - [ ] 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`
## Tareas Técnicas ### Backend (BE)
### Database (DB) - [ ] Configurar X Developer App
- [ ] Obtener API Key y API Secret
- [ ] Agregar campos a tabla `users`: - [ ] Endpoint `GET /api/v1/auth/twitter`
```sql - Redirige a X OAuth
ALTER TABLE users ADD COLUMN twitter_id VARCHAR(255) UNIQUE; - [ ] Endpoint `GET /api/v1/auth/twitter/callback`
ALTER TABLE users ADD COLUMN twitter_username VARCHAR(255); - Recibe código de autorización
``` - Intercambia por access token
- [ ] Usar tabla `oauth_connections` existente - Obtiene datos del usuario
- [ ] Índice en `twitter_id` - Verifica si email está disponible
- Crea o actualiza usuario
### Backend (BE) - Genera JWT token
- [ ] Endpoint `POST /api/v1/auth/twitter/complete-email`
- [ ] Configurar X Developer App - Para usuarios sin email de X
- [ ] Obtener API Key y API Secret - Valida email
- [ ] Endpoint `GET /api/v1/auth/twitter` - Envía verificación
- Redirige a X OAuth - [ ] Service `TwitterOAuthService`
- [ ] Endpoint `GET /api/v1/auth/twitter/callback` - `getAuthorizationUrl()`
- Recibe código de autorización - `exchangeCodeForToken()`
- Intercambia por access token - `getUserProfile()`
- Obtiene datos del usuario - `linkAccount()`
- Verifica si email está disponible - [ ] Tests unitarios (10 casos)
- Crea o actualiza usuario - [ ] Tests de integración con mock de X API
- Genera JWT token
- [ ] Endpoint `POST /api/v1/auth/twitter/complete-email` ### Frontend (FE)
- Para usuarios sin email de X
- Valida email - [ ] Botón "Continuar con X"
- Envía verificación - [ ] Manejo de popup/redirect OAuth
- [ ] Service `TwitterOAuthService` - [ ] Componente `CompleteEmailForm.tsx`
- `getAuthorizationUrl()` - [ ] Recepción de callback
- `exchangeCodeForToken()` - [ ] Almacenamiento de token JWT
- `getUserProfile()` - [ ] Estado de loading
- `linkAccount()` - [ ] Manejo de errores
- [ ] Tests unitarios (10 casos) - [ ] Tests con React Testing Library
- [ ] Tests de integración con mock de X API
### Testing (QA)
### Frontend (FE)
- [ ] E2E: Registro con X (con email)
- [ ] Botón "Continuar con X" - [ ] E2E: Registro con X (sin email, completar manualmente)
- [ ] Manejo de popup/redirect OAuth - [ ] E2E: Login existente con X
- [ ] Componente `CompleteEmailForm.tsx` - [ ] E2E: Vinculación de cuentas
- [ ] Recepción de callback - [ ] E2E: Cancelación del flujo
- [ ] Almacenamiento de token JWT - [ ] Test de seguridad: Validación de tokens
- [ ] Estado de loading - [ ] Test de seguridad: CSRF protection
- [ ] Manejo de errores - [ ] Mock de X API para tests
- [ ] Tests con React Testing Library
---
### Testing (QA)
## Dependencias
- [ ] E2E: Registro con X (con email)
- [ ] E2E: Registro con X (sin email, completar manualmente) - **Bloqueantes:**
- [ ] E2E: Login existente con X - Cuenta de X Developer (con nivel Elevated o superior)
- [ ] E2E: Vinculación de cuentas - Configuración de OAuth 2.0 en X Developer Portal
- [ ] E2E: Cancelación del flujo - SSL/HTTPS en producción
- [ ] Test de seguridad: Validación de tokens
- [ ] Test de seguridad: CSRF protection - **Deseables:**
- [ ] Mock de X API para tests - US-AUTH-003: Consistencia con otros OAuth
- US-AUTH-004: Patrón similar a Facebook OAuth
---
---
## Dependencias
## Definition of Ready (DoR)
- **Bloqueantes:**
- Cuenta de X Developer (con nivel Elevated o superior) - [ ] X Developer App creada
- Configuración de OAuth 2.0 en X Developer Portal - [ ] Credenciales OAuth 2.0 disponibles
- SSL/HTTPS en producción - [ ] Mockups aprobados
- [ ] API contract definido
- **Deseables:** - [ ] Flujo de email adicional diseñado
- US-AUTH-003: Consistencia con otros OAuth
- US-AUTH-004: Patrón similar a Facebook OAuth ---
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura
- [ ] X Developer App creada - [ ] Tests de integración pasando
- [ ] Credenciales OAuth 2.0 disponibles - [ ] Tests E2E implementados
- [ ] Mockups aprobados - [ ] Documentación actualizada
- [ ] API contract definido - [ ] Manejo de errores completo
- [ ] Flujo de email adicional diseñado - [ ] Flujo de email adicional funcional
- [ ] Logs implementados
--- - [ ] QA aprobado en staging
- [ ] Deploy a producción exitoso
## Definition of Done (DoD)
---
- [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura ## Notas Técnicas
- [ ] Tests de integración pasando
- [ ] Tests E2E implementados ### Twitter/X OAuth 2.0 Flow
- [ ] Documentación actualizada
- [ ] Manejo de errores completo 1. Frontend redirige a `/api/v1/auth/twitter`
- [ ] Flujo de email adicional funcional 2. Backend redirige a X con:
- [ ] Logs implementados - `client_id`
- [ ] QA aprobado en staging - `redirect_uri`
- [ ] Deploy a producción exitoso - `scope=tweet.read users.read offline.access`
- `state` (CSRF token)
--- - `code_challenge` (PKCE)
3. Usuario autoriza en X
## Notas Técnicas 4. X redirige a `redirect_uri` con `code`
5. Backend intercambia `code` por `access_token`
### Twitter/X OAuth 2.0 Flow 6. Backend obtiene perfil del usuario
7. Si no hay email, redirige a formulario adicional
1. Frontend redirige a `/api/v1/auth/twitter` 8. Backend crea/actualiza usuario
2. Backend redirige a X con: 9. Backend genera JWT
- `client_id`
- `redirect_uri` ### X API v2 Endpoints
- `scope=tweet.read users.read offline.access`
- `state` (CSRF token) - Authorization: `https://twitter.com/i/oauth2/authorize`
- `code_challenge` (PKCE) - Token exchange: `https://api.twitter.com/2/oauth2/token`
3. Usuario autoriza en X - User info: `https://api.twitter.com/2/users/me?user.fields=profile_image_url`
4. X redirige a `redirect_uri` con `code`
5. Backend intercambia `code` por `access_token` ### Environment Variables
6. Backend obtiene perfil del usuario
7. Si no hay email, redirige a formulario adicional ```env
8. Backend crea/actualiza usuario TWITTER_CLIENT_ID=your_client_id
9. Backend genera JWT TWITTER_CLIENT_SECRET=your_client_secret
TWITTER_CALLBACK_URL=https://orbiquant.com/api/v1/auth/twitter/callback
### X API v2 Endpoints ```
- Authorization: `https://twitter.com/i/oauth2/authorize` ### Consideraciones Especiales de X
- Token exchange: `https://api.twitter.com/2/oauth2/token`
- User info: `https://api.twitter.com/2/users/me?user.fields=profile_image_url` - X no siempre proporciona email del usuario
- Requiere OAuth 2.0 con PKCE
### Environment Variables - Necesita scope `offline.access` para refresh tokens
- Rate limits más estrictos que otros proveedores
```env - Requiere X Developer Account con nivel "Elevated" mínimo
TWITTER_CLIENT_ID=your_client_id
TWITTER_CLIENT_SECRET=your_client_secret ### Security Considerations
TWITTER_CALLBACK_URL=https://orbiquant.com/api/v1/auth/twitter/callback
``` - Implementar PKCE (Proof Key for Code Exchange)
- Validar `state` parameter (CSRF)
### Consideraciones Especiales de X - Validar email adicional si es requerido
- Rate limiting en endpoints
- X no siempre proporciona email del usuario - Logs de intentos de autenticación
- 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 ## Requerimientos Relacionados
### Security Considerations - [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
- Implementar PKCE (Proof Key for Code Exchange) ## Especificaciones Relacionadas
- Validar `state` parameter (CSRF)
- Validar email adicional si es requerido - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- Rate limiting en endpoints - [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
- 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)

View File

@ -1,349 +1,337 @@
--- # US-AUTH-006: OAuth Apple Sign In
id: "US-AUTH-006"
title: "OAuth Apple Sign In" **Version:** 1.0.0
type: "User Story" **Fecha:** 2025-12-05
status: "To Do" **Estado:** Pendiente
priority: "Alta" **Story Points:** 3
epic: "OQI-001" **Prioridad:** P1 (Alta)
story_points: 3 **Épica:** [OQI-001](../_MAP.md)
created_date: "2025-12-05"
updated_date: "2026-01-04" ---
---
## Historia de Usuario
# US-AUTH-006: OAuth Apple Sign In
**Como** visitante o usuario de OrbiQuant
**Version:** 1.0.0 **Quiero** poder registrarme e iniciar sesión usando Apple Sign In
**Fecha:** 2025-12-05 **Para** tener un acceso seguro y privado sin compartir mi email real
**Estado:** Pendiente
**Story Points:** 3 ---
**Prioridad:** P1 (Alta)
**Épica:** [OQI-001](../_MAP.md) ## Criterios de Aceptación
--- ### AC-001: Botón de Apple visible
## Historia de Usuario **Dado** que estoy en la página de registro o login
**Cuando** veo las opciones de autenticación
**Como** visitante o usuario de OrbiQuant **Entonces** debería ver un botón "Continuar con Apple"
**Quiero** poder registrarme e iniciar sesión usando Apple Sign In **Y** debería seguir las guías de diseño de Apple (botón negro con logo)
**Para** tener un acceso seguro y privado sin compartir mi email real
### AC-002: Flujo de OAuth
---
**Dado** que hago click en "Continuar con Apple"
## Criterios de Aceptación **Cuando** se abre la ventana de Apple
**Entonces** debería:
### AC-001: Botón de Apple visible 1. Ver la pantalla de autorización de Apple
2. Poder elegir compartir mi email real o ocultarlo
**Dado** que estoy en la página de registro o login 3. Poder usar Touch ID / Face ID si está disponible
**Cuando** veo las opciones de autenticación 4. Poder autorizar con mi Apple ID
**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-003: Opción de ocultar email
### AC-002: Flujo de OAuth **Dado** que estoy en la pantalla de Apple Sign In
**Cuando** elijo ocultar mi email
**Dado** que hago click en "Continuar con Apple" **Entonces** Apple debería generar un email relay privado
**Cuando** se abre la ventana de Apple **Y** ese email debería funcionar para comunicaciones
**Entonces** debería:
1. Ver la pantalla de autorización de Apple ### AC-004: Primer registro exitoso
2. Poder elegir compartir mi email real o ocultarlo
3. Poder usar Touch ID / Face ID si está disponible **Dado** que es mi primera vez usando Apple Sign In
4. Poder autorizar con mi Apple ID **Cuando** autorizo los permisos
**Entonces** debería:
### AC-003: Opción de ocultar email 1. Crear mi cuenta automáticamente
2. Recibir un JWT token
**Dado** que estoy en la pantalla de Apple Sign In 3. Ser redirigido al dashboard
**Cuando** elijo ocultar mi email 4. Ver mi nombre de Apple (si lo compartí)
**Entonces** Apple debería generar un email relay privado 5. Email verificado automáticamente
**Y** ese email debería funcionar para comunicaciones
### AC-005: Login existente
### AC-004: Primer registro exitoso
**Dado** que ya tengo una cuenta vinculada con Apple
**Dado** que es mi primera vez usando Apple Sign In **Cuando** uso "Continuar con Apple"
**Cuando** autorizo los permisos **Entonces** debería:
**Entonces** debería: 1. Iniciar sesión automáticamente
1. Crear mi cuenta automáticamente 2. Usar Touch ID / Face ID si está configurado
2. Recibir un JWT token 3. Ser redirigido al dashboard
3. Ser redirigido al dashboard
4. Ver mi nombre de Apple (si lo compartí) ### AC-006: Email relay de Apple
5. Email verificado automáticamente
**Dado** que usé la opción de ocultar mi email
### AC-005: Login existente **Cuando** la aplicación envía emails
**Entonces** debería enviarlos al email relay de Apple
**Dado** que ya tengo una cuenta vinculada con Apple **Y** Apple debería reenviarlos a mi email real
**Cuando** uso "Continuar con Apple" **Y** debería poder responder a través del relay
**Entonces** debería:
1. Iniciar sesión automáticamente ### AC-007: Datos mínimos recibidos
2. Usar Touch ID / Face ID si está configurado
3. Ser redirigido al dashboard **Dado** que autorizo Apple Sign In
**Cuando** completo el flujo
### AC-006: Email relay de Apple **Entonces** la app debería recibir:
- `user_id` único de Apple
**Dado** que usé la opción de ocultar mi email - Email (real o relay)
**Cuando** la aplicación envía emails - Nombre (opcional, solo primera vez)
**Entonces** debería enviarlos al email relay de Apple **Y** NO debería recibir otra información personal
**Y** Apple debería reenviarlos a mi email real
**Y** debería poder responder a través del relay ### AC-008: Revocación de acceso
### AC-007: Datos mínimos recibidos **Dado** que revoco el acceso desde configuración de Apple
**Cuando** intento hacer login nuevamente
**Dado** que autorizo Apple Sign In **Entonces** debería ver un error de autorización
**Cuando** completo el flujo **Y** debería poder re-autorizar la aplicación
**Entonces** la app debería recibir:
- `user_id` único de Apple ---
- Email (real o relay)
- Nombre (opcional, solo primera vez) ## Mockup
**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 │ 🌟 Bienvenido a OrbiQuant │
**Cuando** intento hacer login nuevamente │ │
**Entonces** debería ver un error de autorización │ ┌─────────────────────────────────────────────────────┐ │
**Y** debería poder re-autorizar la aplicación │ │ 📧 Email │ │
│ └─────────────────────────────────────────────────────┘ │
--- │ │
│ ┌─────────────────────────────────────────────────────┐ │
## Mockup │ │ 🔴 Continuar con Google │ │
│ └─────────────────────────────────────────────────────┘ │
``` │ │
┌─────────────────────────────────────────────────────────────┐ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ Continuar con Apple │ │
│ 🌟 Bienvenido a OrbiQuant │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ (Botón negro con logo de Apple blanco) │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ 📧 Email │ │ │ [Facebook] [Twitter/X] [GitHub] │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🔴 Continuar con Google │ │ Ventana de Apple Sign In:
│ └─────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────┐
│ │ │ appleid.apple.com ✕ │
│ ┌─────────────────────────────────────────────────────┐ │ ├─────────────────────────────────────────────────────────────┤
│ │ Continuar con Apple │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ (Botón negro con logo de Apple blanco) │ │ │
│ │ │ "OrbiQuant" desea usar tu Apple ID para iniciar sesión │
│ [Facebook] [Twitter/X] [GitHub] │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
└─────────────────────────────────────────────────────────────┘ │ │ Nombre │ │
│ │ ○ Compartir mi nombre │ │
Ventana de Apple Sign In: │ │ ○ No compartir │ │
┌─────────────────────────────────────────────────────────────┐ │ └─────────────────────────────────────────────────────┘ │
│ appleid.apple.com ✕ │ │ │
├─────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ Email │ │
│ │ │ │ ○ Compartir mi email (juan@icloud.com) │ │
│ │ │ │ ● Ocultar mi email │ │
│ "OrbiQuant" desea usar tu Apple ID para iniciar sesión │ │ │ (se usará: xyz123@privaterelay.appleid.com) │ │
│ │ │ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ Nombre │ │ │ Tu información será compartida según las políticas de │
│ │ ○ Compartir mi nombre │ │ │ privacidad de OrbiQuant. │
│ │ ○ No compartir │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ Continuar con Touch ID │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ └─────────────────────────────────────────────────────┘ │
│ │ Email │ │ │ │
│ │ ○ Compartir mi email (juan@icloud.com) │ │ │ Cancelar │
│ │ ● Ocultar mi email │ │ │ │
│ │ (se usará: xyz123@privaterelay.appleid.com) │ │ └─────────────────────────────────────────────────────────────┘
│ └─────────────────────────────────────────────────────┘ │ ```
│ │
│ Tu información será compartida según las políticas de │ ---
│ privacidad de OrbiQuant. │
│ │ ## Tareas Técnicas
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Continuar con Touch ID │ │ ### Database (DB)
│ └─────────────────────────────────────────────────────┘ │
│ │ - [ ] Agregar campos a tabla `users`:
│ Cancelar │ ```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`
## Tareas Técnicas ### Backend (BE)
### Database (DB) - [ ] Configurar Apple Developer Account
- [ ] Crear App ID y Service ID
- [ ] Agregar campos a tabla `users`: - [ ] Generar y configurar private key (.p8)
```sql - [ ] Endpoint `GET /api/v1/auth/apple`
ALTER TABLE users ADD COLUMN apple_id VARCHAR(255) UNIQUE; - Redirige a Apple Sign In
ALTER TABLE users ADD COLUMN is_private_email BOOLEAN DEFAULT false; - [ ] Endpoint `POST /api/v1/auth/apple/callback`
``` - Recibe ID Token (JWT)
- [ ] Usar tabla `oauth_connections` existente - Valida firma del token con Apple public key
- [ ] Índice en `apple_id` - Decodifica user info
- Maneja primera autorización (recibe nombre)
### Backend (BE) - Crea o actualiza usuario
- Genera JWT token
- [ ] Configurar Apple Developer Account - [ ] Service `AppleOAuthService`
- [ ] Crear App ID y Service ID - `getAuthorizationUrl()`
- [ ] Generar y configurar private key (.p8) - `validateIdToken()`
- [ ] Endpoint `GET /api/v1/auth/apple` - `decodeUserInfo()`
- Redirige a Apple Sign In - `linkAccount()`
- [ ] Endpoint `POST /api/v1/auth/apple/callback` - [ ] Librería: `apple-signin-auth` o similar
- Recibe ID Token (JWT) - [ ] Tests unitarios (8 casos)
- Valida firma del token con Apple public key - [ ] Tests de integración con mock
- Decodifica user info
- Maneja primera autorización (recibe nombre) ### Frontend (FE)
- Crea o actualiza usuario
- Genera JWT token - [ ] Botón "Sign in with Apple" (siguiendo guías de Apple)
- [ ] Service `AppleOAuthService` - [ ] Manejo de popup/redirect OAuth
- `getAuthorizationUrl()` - [ ] Recepción de callback
- `validateIdToken()` - [ ] Almacenamiento de token JWT
- `decodeUserInfo()` - [ ] Estado de loading
- `linkAccount()` - [ ] Manejo de errores
- [ ] Librería: `apple-signin-auth` o similar - [ ] Tests con React Testing Library
- [ ] Tests unitarios (8 casos)
- [ ] Tests de integración con mock ### Testing (QA)
### Frontend (FE) - [ ] E2E: Registro con Apple (email real)
- [ ] E2E: Registro con Apple (email oculto)
- [ ] Botón "Sign in with Apple" (siguiendo guías de Apple) - [ ] E2E: Login existente
- [ ] Manejo de popup/redirect OAuth - [ ] E2E: Revocación y re-autorización
- [ ] Recepción de callback - [ ] Test de validación de ID Token
- [ ] Almacenamiento de token JWT - [ ] Test de seguridad: Firma del token
- [ ] Estado de loading - [ ] Test de seguridad: CSRF protection
- [ ] Manejo de errores - [ ] Mock de Apple ID Token
- [ ] Tests con React Testing Library
---
### Testing (QA)
## Dependencias
- [ ] E2E: Registro con Apple (email real)
- [ ] E2E: Registro con Apple (email oculto) - **Bloqueantes:**
- [ ] E2E: Login existente - Apple Developer Account ($99/año)
- [ ] E2E: Revocación y re-autorización - Dominio verificado en Apple
- [ ] Test de validación de ID Token - Configuración de Service ID
- [ ] Test de seguridad: Firma del token - Private key (.p8) generada
- [ ] Test de seguridad: CSRF protection - SSL/HTTPS en producción
- [ ] Mock de Apple ID Token
- **Deseables:**
--- - US-AUTH-003: Consistencia con otros OAuth
## Dependencias ---
- **Bloqueantes:** ## Definition of Ready (DoR)
- Apple Developer Account ($99/año)
- Dominio verificado en Apple - [ ] Apple Developer Account activa
- Configuración de Service ID - [ ] Service ID configurado
- Private key (.p8) generada - [ ] Private key generada y segura
- SSL/HTTPS en producción - [ ] Mockups aprobados siguiendo Apple HIG
- [ ] API contract definido
- **Deseables:** - [ ] Dominio verificado en Apple
- US-AUTH-003: Consistencia con otros OAuth
---
---
## Definition of Done (DoD)
## Definition of Ready (DoR)
- [ ] Código implementado y revisado
- [ ] Apple Developer Account activa - [ ] Tests unitarios con 80%+ cobertura
- [ ] Service ID configurado - [ ] Tests de integración pasando
- [ ] Private key generada y segura - [ ] Tests E2E implementados
- [ ] Mockups aprobados siguiendo Apple HIG - [ ] Botón cumple con Apple guidelines
- [ ] API contract definido - [ ] Validación de ID Token implementada
- [ ] Dominio verificado en Apple - [ ] Manejo de email relay funcional
- [ ] Documentación actualizada
--- - [ ] Logs implementados
- [ ] QA aprobado en staging
## Definition of Done (DoD) - [ ] Deploy a producción exitoso
- [ ] Código implementado y revisado ---
- [ ] Tests unitarios con 80%+ cobertura
- [ ] Tests de integración pasando ## Notas Técnicas
- [ ] Tests E2E implementados
- [ ] Botón cumple con Apple guidelines ### Apple Sign In Flow
- [ ] Validación de ID Token implementada
- [ ] Manejo de email relay funcional 1. Frontend redirige a `/api/v1/auth/apple`
- [ ] Documentación actualizada 2. Backend redirige a Apple con:
- [ ] Logs implementados - `client_id` (Service ID)
- [ ] QA aprobado en staging - `redirect_uri`
- [ ] Deploy a producción exitoso - `response_type=code id_token`
- `response_mode=form_post`
--- - `scope=name email`
- `state` (CSRF token)
## Notas Técnicas 3. Usuario autoriza en Apple
4. Apple envía POST a `redirect_uri` con:
### Apple Sign In Flow - `id_token` (JWT firmado)
- `code` (authorization code)
1. Frontend redirige a `/api/v1/auth/apple` - `user` (solo primera vez, contiene nombre)
2. Backend redirige a Apple con: 5. Backend valida ID Token con Apple public key
- `client_id` (Service ID) 6. Backend decodifica user info
- `redirect_uri` 7. Backend crea/actualiza usuario
- `response_type=code id_token` 8. Backend genera JWT y redirige a frontend
- `response_mode=form_post`
- `scope=name email` ### Apple ID Token Structure
- `state` (CSRF token)
3. Usuario autoriza en Apple ```json
4. Apple envía POST a `redirect_uri` con: {
- `id_token` (JWT firmado) "iss": "https://appleid.apple.com",
- `code` (authorization code) "aud": "com.orbiquant.service",
- `user` (solo primera vez, contiene nombre) "exp": 1234567890,
5. Backend valida ID Token con Apple public key "iat": 1234567890,
6. Backend decodifica user info "sub": "001234.abc123...", // Apple User ID
7. Backend crea/actualiza usuario "email": "xyz@privaterelay.appleid.com",
8. Backend genera JWT y redirige a frontend "email_verified": true,
"is_private_email": true,
### Apple ID Token Structure "nonce_supported": true
}
```json ```
{
"iss": "https://appleid.apple.com", ### Environment Variables
"aud": "com.orbiquant.service",
"exp": 1234567890, ```env
"iat": 1234567890, APPLE_SERVICE_ID=com.orbiquant.service
"sub": "001234.abc123...", // Apple User ID APPLE_TEAM_ID=ABC123XYZ
"email": "xyz@privaterelay.appleid.com", APPLE_KEY_ID=KEY123
"email_verified": true, APPLE_PRIVATE_KEY_PATH=/path/to/AuthKey_KEY123.p8
"is_private_email": true, APPLE_CALLBACK_URL=https://orbiquant.com/api/v1/auth/apple/callback
"nonce_supported": true ```
}
``` ### Consideraciones Especiales de Apple
### Environment Variables - Apple solo envía nombre en la primera autorización
- Email relay de Apple es permanente por app
```env - ID Token está firmado con RS256
APPLE_SERVICE_ID=com.orbiquant.service - Requiere validar firma con Apple public keys
APPLE_TEAM_ID=ABC123XYZ - Response mode debe ser `form_post` (no query params)
APPLE_KEY_ID=KEY123 - Requiere HTTPS estricto
APPLE_PRIVATE_KEY_PATH=/path/to/AuthKey_KEY123.p8 - No hay refresh tokens en el flujo web
APPLE_CALLBACK_URL=https://orbiquant.com/api/v1/auth/apple/callback
``` ### Security Considerations
### Consideraciones Especiales de Apple - Validar firma del ID Token con Apple public key
- Verificar `aud` claim coincide con Service ID
- Apple solo envía nombre en la primera autorización - Validar `iss` es `https://appleid.apple.com`
- Email relay de Apple es permanente por app - Verificar `exp` no está expirado
- ID Token está firmado con RS256 - Validar `state` parameter (CSRF)
- Requiere validar firma con Apple public keys - Guardar nombre solo en primera autorización
- Response mode debe ser `form_post` (no query params) - Logs de autenticación
- Requiere HTTPS estricto
- No hay refresh tokens en el flujo web ### Apple Design Guidelines
### Security Considerations - Usar botón oficial "Sign in with Apple"
- Color negro en tema claro, blanco en tema oscuro
- Validar firma del ID Token con Apple public key - Logo de Apple siempre visible
- Verificar `aud` claim coincide con Service ID - Texto específico según contexto
- Validar `iss` es `https://appleid.apple.com` - Mismo tamaño que otros botones OAuth
- Verificar `exp` no está expirado
- Validar `state` parameter (CSRF) ---
- Guardar nombre solo en primera autorización
- Logs de autenticación ## Requerimientos Relacionados
### Apple Design Guidelines - [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
- Usar botón oficial "Sign in with Apple" ## Especificaciones Relacionadas
- Color negro en tema claro, blanco en tema oscuro
- Logo de Apple siempre visible - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- Texto específico según contexto - [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
- 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)

View File

@ -1,319 +1,307 @@
--- # US-AUTH-007: OAuth GitHub
id: "US-AUTH-007"
title: "OAuth GitHub" **Version:** 1.0.0
type: "User Story" **Fecha:** 2025-12-05
status: "To Do" **Estado:** Pendiente
priority: "Media" **Story Points:** 3
epic: "OQI-001" **Prioridad:** P2 (Media)
story_points: 3 **Épica:** [OQI-001](../_MAP.md)
created_date: "2025-12-05"
updated_date: "2026-01-04" ---
---
## Historia de Usuario
# US-AUTH-007: OAuth GitHub
**Como** desarrollador o usuario técnico de OrbiQuant
**Version:** 1.0.0 **Quiero** poder registrarme e iniciar sesión usando mi cuenta de GitHub
**Fecha:** 2025-12-05 **Para** tener un acceso rápido usando mis credenciales de desarrollador
**Estado:** Pendiente
**Story Points:** 3 ---
**Prioridad:** P2 (Media)
**Épica:** [OQI-001](../_MAP.md) ## Criterios de Aceptación
--- ### AC-001: Botón de GitHub visible
## Historia de Usuario **Dado** que estoy en la página de registro o login
**Cuando** veo las opciones de autenticación
**Como** desarrollador o usuario técnico de OrbiQuant **Entonces** debería ver un botón "Continuar con GitHub"
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de GitHub **Y** debería tener el logo oficial de GitHub
**Para** tener un acceso rápido usando mis credenciales de desarrollador
### AC-002: Flujo de OAuth
---
**Dado** que hago click en "Continuar con GitHub"
## Criterios de Aceptación **Cuando** se abre la ventana de GitHub
**Entonces** debería:
### AC-001: Botón de GitHub visible 1. Ver la pantalla de autorización de GitHub
2. Poder revisar los permisos solicitados
**Dado** que estoy en la página de registro o login 3. Poder autorizar la aplicación
**Cuando** veo las opciones de autenticación
**Entonces** debería ver un botón "Continuar con GitHub" ### AC-003: Permisos solicitados
**Y** debería tener el logo oficial de GitHub
**Dado** que estoy en la pantalla de autorización de GitHub
### AC-002: Flujo de OAuth **Cuando** reviso los permisos
**Entonces** la app debería solicitar únicamente:
**Dado** que hago click en "Continuar con GitHub" - `user:email` (para leer email)
**Cuando** se abre la ventana de GitHub - `read:user` (para leer perfil básico)
**Entonces** debería:
1. Ver la pantalla de autorización de GitHub ### AC-004: Primer registro exitoso
2. Poder revisar los permisos solicitados
3. Poder autorizar la aplicación **Dado** que es mi primera vez usando GitHub OAuth
**Cuando** autorizo los permisos
### AC-003: Permisos solicitados **Entonces** debería:
1. Crear mi cuenta automáticamente
**Dado** que estoy en la pantalla de autorización de GitHub 2. Recibir un JWT token
**Cuando** reviso los permisos 3. Ser redirigido al dashboard
**Entonces** la app debería solicitar únicamente: 4. Ver mi nombre y avatar de GitHub
- `user:email` (para leer email) 5. Usar mi email primario de GitHub
- `read:user` (para leer perfil básico)
### AC-005: Email primario privado
### AC-004: Primer registro exitoso
**Dado** que tengo mi email configurado como privado en GitHub
**Dado** que es mi primera vez usando GitHub OAuth **Cuando** autorizo la aplicación
**Cuando** autorizo los permisos **Entonces** debería usar mi email noreply de GitHub
**Entonces** debería: **O** solicitar un email alternativo
1. Crear mi cuenta automáticamente
2. Recibir un JWT token ### AC-006: Login existente
3. Ser redirigido al dashboard
4. Ver mi nombre y avatar de GitHub **Dado** que ya tengo una cuenta vinculada con GitHub
5. Usar mi email primario de GitHub **Cuando** uso "Continuar con GitHub"
**Entonces** debería:
### AC-005: Email primario privado 1. Iniciar sesión automáticamente
2. Ser redirigido al dashboard
**Dado** que tengo mi email configurado como privado en GitHub
**Cuando** autorizo la aplicación ### AC-007: Múltiples emails en GitHub
**Entonces** debería usar mi email noreply de GitHub
**O** solicitar un email alternativo **Dado** que tengo múltiples emails en mi cuenta de GitHub
**Cuando** autorizo la aplicación
### AC-006: Login existente **Entonces** debería usar el email marcado como primario
**Y** debería verificar que no esté ya registrado
**Dado** que ya tengo una cuenta vinculada con GitHub
**Cuando** uso "Continuar con GitHub" ### AC-008: Cancelación del flujo
**Entonces** debería:
1. Iniciar sesión automáticamente **Dado** que inicio el flujo de GitHub OAuth
2. Ser redirigido al dashboard **Cuando** cancelo en la ventana de GitHub
**Entonces** debería volver a login/registro
### AC-007: Múltiples emails en GitHub **Y** ver mensaje "Autenticación cancelada"
**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 ## Mockup
**Y** debería verificar que no esté ya registrado
```
### AC-008: Cancelación del flujo ┌─────────────────────────────────────────────────────────────┐
│ │
**Dado** que inicio el flujo de GitHub OAuth │ 🌟 Bienvenido a OrbiQuant │
**Cuando** cancelo en la ventana de GitHub │ │
**Entonces** debería volver a login/registro │ ┌─────────────────────────────────────────────────────┐ │
**Y** ver mensaje "Autenticación cancelada" │ │ 📧 Email │ │
│ └─────────────────────────────────────────────────────┘ │
--- │ │
│ ┌─────────────────────────────────────────────────────┐ │
## Mockup │ │ 🔴 Continuar con Google │ │
│ └─────────────────────────────────────────────────────┘ │
``` │ │
┌─────────────────────────────────────────────────────────────┐ │ [Facebook] [Twitter/X] [Apple] │
│ │ │ │
│ 🌟 Bienvenido a OrbiQuant │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ ⚫ Continuar con GitHub │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ └─────────────────────────────────────────────────────┘ │
│ │ 📧 Email │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
│ │
│ ┌─────────────────────────────────────────────────────┐ │ Ventana de GitHub OAuth:
│ │ 🔴 Continuar con Google │ │ ┌─────────────────────────────────────────────────────────────┐
│ └─────────────────────────────────────────────────────┘ │ │ github.com ✕ │
│ │ ├─────────────────────────────────────────────────────────────┤
│ [Facebook] [Twitter/X] [Apple] │ │ │
│ │ │ Authorize OrbiQuant │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ ⚫ Continuar con GitHub │ │ │ OrbiQuant by OrbiQuant Team │
│ └─────────────────────────────────────────────────────┘ │ │ wants to access your juanperez account │
│ │ │ │
└─────────────────────────────────────────────────────────────┘ │ This application will be able to: │
│ │
Ventana de GitHub OAuth: │ ✓ Verify your GitHub identity │
┌─────────────────────────────────────────────────────────────┐ │ ✓ Read your email addresses │
│ github.com ✕ │ │ ✓ Read your profile information │
├─────────────────────────────────────────────────────────────┤ │ │
│ │ │ Authorizing will redirect to: │
│ Authorize OrbiQuant │ │ https://orbiquant.com/api/v1/auth/github/callback │
│ │ │ │
│ OrbiQuant by OrbiQuant Team │ │ ┌─────────────────────────────────────────────────────┐ │
│ wants to access your juanperez account │ │ │ Authorize OrbiQuant │ │
│ │ │ └─────────────────────────────────────────────────────┘ │
│ This application will be able to: │ │ │
│ │ │ Cancel │
│ ✓ Verify your GitHub identity │ │ │
│ ✓ Read your email addresses │ └─────────────────────────────────────────────────────────────┘
│ ✓ Read your profile information │ ```
│ │
│ Authorizing will redirect to: │ ---
│ https://orbiquant.com/api/v1/auth/github/callback │
│ │ ## Tareas Técnicas
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Authorize OrbiQuant │ │ ### Database (DB)
│ └─────────────────────────────────────────────────────┘ │
│ │ - [ ] Agregar campos a tabla `users`:
│ Cancel │ ```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`
## Tareas Técnicas ### Backend (BE)
### Database (DB) - [ ] Crear GitHub OAuth App
- [ ] Obtener Client ID y Client Secret
- [ ] Agregar campos a tabla `users`: - [ ] Endpoint `GET /api/v1/auth/github`
```sql - Redirige a GitHub OAuth
ALTER TABLE users ADD COLUMN github_id VARCHAR(255) UNIQUE; - [ ] Endpoint `GET /api/v1/auth/github/callback`
ALTER TABLE users ADD COLUMN github_username VARCHAR(255); - Recibe código de autorización
``` - Intercambia por access token
- [ ] Usar tabla `oauth_connections` existente - Obtiene perfil del usuario
- [ ] Índice en `github_id` - Obtiene emails del usuario
- Selecciona email primario
### Backend (BE) - Crea o actualiza usuario
- Genera JWT token
- [ ] Crear GitHub OAuth App - [ ] Service `GitHubOAuthService`
- [ ] Obtener Client ID y Client Secret - `getAuthorizationUrl()`
- [ ] Endpoint `GET /api/v1/auth/github` - `exchangeCodeForToken()`
- Redirige a GitHub OAuth - `getUserProfile()`
- [ ] Endpoint `GET /api/v1/auth/github/callback` - `getUserEmails()`
- Recibe código de autorización - `selectPrimaryEmail()`
- Intercambia por access token - `linkAccount()`
- Obtiene perfil del usuario - [ ] Tests unitarios (8 casos)
- Obtiene emails del usuario - [ ] Tests de integración con mock de GitHub API
- Selecciona email primario
- Crea o actualiza usuario ### Frontend (FE)
- Genera JWT token
- [ ] Service `GitHubOAuthService` - [ ] Botón "Continuar con GitHub"
- `getAuthorizationUrl()` - [ ] Manejo de popup/redirect OAuth
- `exchangeCodeForToken()` - [ ] Recepción de callback
- `getUserProfile()` - [ ] Almacenamiento de token JWT
- `getUserEmails()` - [ ] Estado de loading
- `selectPrimaryEmail()` - [ ] Manejo de errores
- `linkAccount()` - [ ] Tests con React Testing Library
- [ ] Tests unitarios (8 casos)
- [ ] Tests de integración con mock de GitHub API ### Testing (QA)
### Frontend (FE) - [ ] E2E: Registro con GitHub
- [ ] E2E: Login existente
- [ ] Botón "Continuar con GitHub" - [ ] E2E: Email privado/noreply
- [ ] Manejo de popup/redirect OAuth - [ ] E2E: Múltiples emails
- [ ] Recepción de callback - [ ] E2E: Cancelación del flujo
- [ ] Almacenamiento de token JWT - [ ] Test de seguridad: Validación de tokens
- [ ] Estado de loading - [ ] Test de seguridad: CSRF protection
- [ ] Manejo de errores - [ ] Mock de GitHub API
- [ ] Tests con React Testing Library
---
### Testing (QA)
## Dependencias
- [ ] E2E: Registro con GitHub
- [ ] E2E: Login existente - **Bloqueantes:**
- [ ] E2E: Email privado/noreply - GitHub OAuth App creada
- [ ] E2E: Múltiples emails - Credenciales configuradas
- [ ] E2E: Cancelación del flujo - SSL/HTTPS en producción
- [ ] Test de seguridad: Validación de tokens
- [ ] Test de seguridad: CSRF protection - **Deseables:**
- [ ] Mock de GitHub API - US-AUTH-003: Consistencia con otros OAuth
--- ---
## Dependencias ## Definition of Ready (DoR)
- **Bloqueantes:** - [ ] GitHub OAuth App creada
- GitHub OAuth App creada - [ ] Client ID y Secret disponibles
- Credenciales configuradas - [ ] Mockups aprobados
- SSL/HTTPS en producción - [ ] API contract definido
- [ ] Callback URL configurada
- **Deseables:**
- US-AUTH-003: Consistencia con otros OAuth ---
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura
- [ ] GitHub OAuth App creada - [ ] Tests de integración pasando
- [ ] Client ID y Secret disponibles - [ ] Tests E2E implementados
- [ ] Mockups aprobados - [ ] Manejo de emails privados funcional
- [ ] API contract definido - [ ] Documentación actualizada
- [ ] Callback URL configurada - [ ] Logs implementados
- [ ] QA aprobado en staging
--- - [ ] Deploy a producción exitoso
## Definition of Done (DoD) ---
- [ ] Código implementado y revisado ## Notas Técnicas
- [ ] Tests unitarios con 80%+ cobertura
- [ ] Tests de integración pasando ### GitHub OAuth Flow
- [ ] Tests E2E implementados
- [ ] Manejo de emails privados funcional 1. Frontend redirige a `/api/v1/auth/github`
- [ ] Documentación actualizada 2. Backend redirige a GitHub con:
- [ ] Logs implementados - `client_id`
- [ ] QA aprobado en staging - `redirect_uri`
- [ ] Deploy a producción exitoso - `scope=user:email read:user`
- `state` (CSRF token)
--- 3. Usuario autoriza en GitHub
4. GitHub redirige a `redirect_uri` con `code`
## Notas Técnicas 5. Backend intercambia `code` por `access_token`
6. Backend obtiene perfil: `GET /user`
### GitHub OAuth Flow 7. Backend obtiene emails: `GET /user/emails`
8. Backend selecciona email primario y verificado
1. Frontend redirige a `/api/v1/auth/github` 9. Backend crea/actualiza usuario
2. Backend redirige a GitHub con: 10. Backend genera JWT
- `client_id`
- `redirect_uri` ### GitHub API Endpoints
- `scope=user:email read:user`
- `state` (CSRF token) - Authorization: `https://github.com/login/oauth/authorize`
3. Usuario autoriza en GitHub - Token exchange: `https://github.com/login/oauth/access_token`
4. GitHub redirige a `redirect_uri` con `code` - User profile: `https://api.github.com/user`
5. Backend intercambia `code` por `access_token` - User emails: `https://api.github.com/user/emails`
6. Backend obtiene perfil: `GET /user`
7. Backend obtiene emails: `GET /user/emails` ### Email Selection Logic
8. Backend selecciona email primario y verificado
9. Backend crea/actualiza usuario ```typescript
10. Backend genera JWT // Prioridad de selección de email:
1. Email primario + verificado
### GitHub API Endpoints 2. Email primario (aunque no esté verificado)
3. Primer email verificado
- Authorization: `https://github.com/login/oauth/authorize` 4. Solicitar email adicional al usuario
- Token exchange: `https://github.com/login/oauth/access_token` ```
- User profile: `https://api.github.com/user`
- User emails: `https://api.github.com/user/emails` ### Environment Variables
### Email Selection Logic ```env
GITHUB_CLIENT_ID=your_client_id
```typescript GITHUB_CLIENT_SECRET=your_client_secret
// Prioridad de selección de email: GITHUB_CALLBACK_URL=https://orbiquant.com/api/v1/auth/github/callback
1. Email primario + verificado ```
2. Email primario (aunque no esté verificado)
3. Primer email verificado ### Consideraciones Especiales de GitHub
4. Solicitar email adicional al usuario
``` - GitHub permite múltiples emails por cuenta
- Email puede ser privado (noreply@github.com)
### Environment Variables - Necesita dos llamadas: una para perfil, otra para emails
- Access tokens no expiran (a menos que sean revocados)
```env - Scopes mínimos: `user:email` y `read:user`
GITHUB_CLIENT_ID=your_client_id - Rate limit: 5000 requests/hour para usuarios autenticados
GITHUB_CLIENT_SECRET=your_client_secret
GITHUB_CALLBACK_URL=https://orbiquant.com/api/v1/auth/github/callback ### Security Considerations
```
- Validar `state` parameter (CSRF)
### Consideraciones Especiales de GitHub - Usar HTTPS en callbacks
- No almacenar access tokens sin encriptar
- GitHub permite múltiples emails por cuenta - Rate limiting en endpoints
- Email puede ser privado (noreply@github.com) - Logs de intentos de autenticación
- Necesita dos llamadas: una para perfil, otra para emails - Validar que el email sea verificado en GitHub
- 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
## Requerimientos Relacionados
### Security Considerations
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
- Validar `state` parameter (CSRF)
- Usar HTTPS en callbacks ## Especificaciones Relacionadas
- No almacenar access tokens sin encriptar
- Rate limiting en endpoints - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- Logs de intentos de autenticación - [ET-AUTH-004: OAuth Integration](../especificaciones/ET-AUTH-004-oauth.md)
- 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)

View File

@ -1,454 +1,442 @@
--- # US-AUTH-008: Autenticación con SMS (Twilio)
id: "US-AUTH-008"
title: "Autenticacion con SMS (Twilio)" **Version:** 1.0.0
type: "User Story" **Fecha:** 2025-12-05
status: "To Do" **Estado:** Pendiente
priority: "Alta" **Story Points:** 5
epic: "OQI-001" **Prioridad:** P1 (Alta)
story_points: 5 **Épica:** [OQI-001](../_MAP.md)
created_date: "2025-12-05"
updated_date: "2026-01-04" ---
---
## Historia de Usuario
# US-AUTH-008: Autenticación con SMS (Twilio)
**Como** usuario de OrbiQuant
**Version:** 1.0.0 **Quiero** poder registrarme e iniciar sesión usando mi número de teléfono y un código SMS
**Fecha:** 2025-12-05 **Para** tener un acceso rápido sin necesidad de recordar contraseñas
**Estado:** Pendiente
**Story Points:** 5 ---
**Prioridad:** P1 (Alta)
**Épica:** [OQI-001](../_MAP.md) ## Criterios de Aceptación
--- ### AC-001: Formulario de teléfono
## Historia de Usuario **Dado** que estoy en la página de registro/login
**Cuando** selecciono la opción de teléfono
**Como** usuario de OrbiQuant **Entonces** debería ver:
**Quiero** poder registrarme e iniciar sesión usando mi número de teléfono y un código SMS - Selector de país con banderas (+1, +52, +34, etc.)
**Para** tener un acceso rápido sin necesidad de recordar contraseñas - Campo para número de teléfono
- Formato visual según el país seleccionado
--- - Botón "Enviar código"
## Criterios de Aceptación ### AC-002: Validación de número
### AC-001: Formulario de teléfono **Dado** que ingreso un número de teléfono
**Cuando** el número no es válido para el país seleccionado
**Dado** que estoy en la página de registro/login **Entonces** debería ver un mensaje de error
**Cuando** selecciono la opción de teléfono **Y** el botón "Enviar código" debería estar deshabilitado
**Entonces** debería ver:
- Selector de país con banderas (+1, +52, +34, etc.) ### AC-003: Envío de código SMS
- Campo para número de teléfono
- Formato visual según el país seleccionado **Dado** que ingresé un número válido
- Botón "Enviar código" **Cuando** hago click en "Enviar código"
**Entonces** debería:
### AC-002: Validación de número 1. Ver un mensaje "Enviando código..."
2. Recibir un SMS con un código de 6 dígitos
**Dado** que ingreso un número de teléfono 3. Ver pantalla de verificación de código
**Cuando** el número no es válido para el país seleccionado 4. El código debería expirar en 10 minutos
**Entonces** debería ver un mensaje de error
**Y** el botón "Enviar código" debería estar deshabilitado ### AC-004: Formato del SMS
### AC-003: Envío de código SMS **Dado** que solicité un código
**Cuando** recibo el SMS
**Dado** que ingresé un número válido **Entonces** debería tener el formato:
**Cuando** hago click en "Enviar código" ```
**Entonces** debería: Tu código de OrbiQuant es: 123456
1. Ver un mensaje "Enviando código..."
2. Recibir un SMS con un código de 6 dígitos Válido por 10 minutos.
3. Ver pantalla de verificación de código No compartas este código.
4. El código debería expirar en 10 minutos ```
### AC-004: Formato del SMS ### AC-005: Ingreso de código
**Dado** que solicité un código **Dado** que recibí el código por SMS
**Cuando** recibo el SMS **Cuando** ingreso el código en la app
**Entonces** debería tener el formato: **Entonces** debería:
``` - Autoformatear con espacios (123 456)
Tu código de OrbiQuant es: 123456 - Auto-enviar al completar 6 dígitos
- Validar el código en tiempo real
Válido por 10 minutos.
No compartas este código. ### AC-006: Código correcto - Primer registro
```
**Dado** que es mi primera vez usando este número
### AC-005: Ingreso de código **Cuando** ingreso el código correcto
**Entonces** debería:
**Dado** que recibí el código por SMS 1. Ver formulario para completar perfil (nombre, apellido, email opcional)
**Cuando** ingreso el código en la app 2. Crear mi cuenta
**Entonces** debería: 3. Recibir un JWT token
- Autoformatear con espacios (123 456) 4. Ser redirigido al dashboard
- Auto-enviar al completar 6 dígitos
- Validar el código en tiempo real ### AC-007: Código correcto - Login existente
### AC-006: Código correcto - Primer registro **Dado** que ya tengo una cuenta con este número
**Cuando** ingreso el código correcto
**Dado** que es mi primera vez usando este número **Entonces** debería:
**Cuando** ingreso el código correcto 1. Iniciar sesión automáticamente
**Entonces** debería: 2. Recibir un JWT token
1. Ver formulario para completar perfil (nombre, apellido, email opcional) 3. Ser redirigido al dashboard
2. Crear mi cuenta
3. Recibir un JWT token ### AC-008: Código incorrecto
4. Ser redirigido al dashboard
**Dado** que ingreso un código incorrecto
### AC-007: Código correcto - Login existente **Cuando** envío el código
**Entonces** debería:
**Dado** que ya tengo una cuenta con este número - Ver mensaje "Código incorrecto"
**Cuando** ingreso el código correcto - Poder intentar nuevamente
**Entonces** debería: - Después de 3 intentos fallidos, invalidar el código
1. Iniciar sesión automáticamente - Poder solicitar un nuevo código
2. Recibir un JWT token
3. Ser redirigido al dashboard ### AC-009: Código expirado
### AC-008: Código incorrecto **Dado** que pasaron más de 10 minutos desde el envío
**Cuando** intento usar el código
**Dado** que ingreso un código incorrecto **Entonces** debería ver mensaje "Código expirado"
**Cuando** envío el código **Y** debería poder solicitar un nuevo código
**Entonces** debería:
- Ver mensaje "Código incorrecto" ### AC-010: Reenvío de código
- Poder intentar nuevamente
- Después de 3 intentos fallidos, invalidar el código **Dado** que no recibí el código o expiró
- Poder solicitar un nuevo código **Cuando** hago click en "Reenviar código"
**Entonces** debería:
### AC-009: Código expirado - Esperar 60 segundos antes de permitir reenvío
- Ver contador regresivo "Reenviar en 59s..."
**Dado** que pasaron más de 10 minutos desde el envío - Recibir un nuevo código (el anterior se invalida)
**Cuando** intento usar el código
**Entonces** debería ver mensaje "Código expirado" ### AC-011: Rate limiting
**Y** debería poder solicitar un nuevo código
**Dado** que solicité 5 códigos en 1 hora
### AC-010: Reenvío de código **Cuando** intento solicitar otro
**Entonces** debería ver mensaje:
**Dado** que no recibí el código o expiró - "Demasiados intentos. Intenta en 1 hora"
**Cuando** hago click en "Reenviar código"
**Entonces** debería: ### AC-012: Número ya registrado con email
- Esperar 60 segundos antes de permitir reenvío
- Ver contador regresivo "Reenviar en 59s..." **Dado** que mi número ya está vinculado a una cuenta de email
- Recibir un nuevo código (el anterior se invalida) **Cuando** completo la verificación SMS
**Entonces** debería iniciar sesión en esa cuenta
### AC-011: Rate limiting **Y** tener ambos métodos de autenticación disponibles
**Dado** que solicité 5 códigos en 1 hora ---
**Cuando** intento solicitar otro
**Entonces** debería ver mensaje: ## Mockup
- "Demasiados intentos. Intenta en 1 hora"
```
### AC-012: Número ya registrado con email Paso 1: Ingreso de teléfono
┌─────────────────────────────────────────────────────────────┐
**Dado** que mi número ya está vinculado a una cuenta de email │ │
**Cuando** completo la verificación SMS │ 🌟 Ingresa con tu número de teléfono │
**Entonces** debería iniciar sesión en esa cuenta │ │
**Y** tener ambos métodos de autenticación disponibles │ ┌─────────────────────────────────────────────────────┐ │
│ │ Número de teléfono │ │
--- │ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │
│ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │
## Mockup │ │ └────────┘ └──────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
``` │ │
Paso 1: Ingreso de teléfono │ ┌─────────────────────────────────────────────────────┐ │
┌─────────────────────────────────────────────────────────────┐ │ │ Enviar código │ │
│ │ │ └─────────────────────────────────────────────────────┘ │
│ 🌟 Ingresa con tu número de teléfono │ │ │
│ │ │ ─────────────────── O continúa con ─────────────────── │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ Número de teléfono │ │ │ [Email] [Google] [Facebook] [Apple] │
│ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │ │ │
│ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │ └─────────────────────────────────────────────────────────────┘
│ │ └────────┘ └──────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │ Paso 2: Verificación de código
│ │ ┌─────────────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ Enviar código │ │ │ 📱 Ingresa el código que enviamos │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ │ │ Enviamos un código a +1 (555) 123-4567 │
│ ─────────────────── O continúa con ─────────────────── │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
│ [Email] [Google] [Facebook] [Apple] │ │ │ │ │
│ │ │ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
└─────────────────────────────────────────────────────────────┘ │ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ │ │
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
Paso 2: Verificación de código │ │ │ │
┌─────────────────────────────────────────────────────────────┐ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ 📱 Ingresa el código que enviamos │ │ ¿No recibiste el código? │
│ │ │ Reenviar código (disponible en 58s) │
│ Enviamos un código a +1 (555) 123-4567 │ │ │
│ │ │ ← Cambiar número │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ └─────────────────────────────────────────────────────────────┘
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ │ │ Paso 3: Completar perfil (solo registro nuevo)
│ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │ ┌─────────────────────────────────────────────────────────────┐
│ │ │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ │ 🎉 ¡Bienvenido! Completa tu perfil │
│ │ │ │
│ ¿No recibiste el código? │ │ ┌────────────────────────┐ ┌────────────────────────┐ │
│ Reenviar código (disponible en 58s) │ │ │ Nombre │ │ Apellido │ │
│ │ │ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │
│ ← Cambiar número │ │ │ │ Juan │ │ │ │ Pérez │ │ │
│ │ │ │ └──────────────────┘ │ │ └──────────────────┘ │ │
└─────────────────────────────────────────────────────────────┘ │ └────────────────────────┘ └────────────────────────┘ │
│ │
Paso 3: Completar perfil (solo registro nuevo) │ ┌─────────────────────────────────────────────────────┐ │
┌─────────────────────────────────────────────────────────────┐ │ │ Email (opcional) │ │
│ │ │ │ ┌─────────────────────────────────────────────────┐ │ │
│ 🎉 ¡Bienvenido! Completa tu perfil │ │ │ │ juan@email.com │ │ │
│ │ │ │ └─────────────────────────────────────────────────┘ │ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │ │ └─────────────────────────────────────────────────────┘ │
│ │ Nombre │ │ Apellido │ │ │ │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │ │ ☑ Acepto los Términos de Servicio │
│ │ │ Juan │ │ │ │ Pérez │ │ │ │ │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │ │ ┌─────────────────────────────────────────────────────┐ │
│ └────────────────────────┘ └────────────────────────┘ │ │ │ Crear mi cuenta │ │
│ │ │ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ Email (opcional) │ │ └─────────────────────────────────────────────────────────────┘
│ │ ┌─────────────────────────────────────────────────┐ │ │ ```
│ │ │ juan@email.com │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │ ---
│ └─────────────────────────────────────────────────────┘ │
│ │ ## Tareas Técnicas
│ ☑ Acepto los Términos de Servicio │
│ │ ### Database (DB)
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Crear mi cuenta │ │ - [ ] 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
## Tareas Técnicas CREATE TABLE phone_verification_codes (
id UUID PRIMARY KEY,
### Database (DB) phone_number VARCHAR(20) NOT NULL,
code VARCHAR(6) NOT NULL,
- [ ] Agregar campos a tabla `users`: attempts INT DEFAULT 0,
```sql expires_at TIMESTAMP NOT NULL,
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) UNIQUE; used_at TIMESTAMP,
ALTER TABLE users ADD COLUMN phone_verified_at TIMESTAMP; created_at TIMESTAMP DEFAULT NOW(),
ALTER TABLE users ADD COLUMN phone_country_code VARCHAR(5); INDEX idx_phone_expires (phone_number, expires_at)
``` );
- [ ] Tabla `phone_verification_codes`: ```
```sql - [ ] Tabla `phone_rate_limits`:
CREATE TABLE phone_verification_codes ( ```sql
id UUID PRIMARY KEY, CREATE TABLE phone_rate_limits (
phone_number VARCHAR(20) NOT NULL, id UUID PRIMARY KEY,
code VARCHAR(6) NOT NULL, phone_number VARCHAR(20) NOT NULL,
attempts INT DEFAULT 0, ip_address VARCHAR(45),
expires_at TIMESTAMP NOT NULL, attempts INT DEFAULT 1,
used_at TIMESTAMP, window_start TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_phone_expires (phone_number, expires_at) INDEX idx_phone_window (phone_number, window_start)
); );
``` ```
- [ ] Tabla `phone_rate_limits`:
```sql ### Backend (BE)
CREATE TABLE phone_rate_limits (
id UUID PRIMARY KEY, - [ ] Configurar cuenta de Twilio
phone_number VARCHAR(20) NOT NULL, - [ ] Obtener Account SID y Auth Token
ip_address VARCHAR(45), - [ ] Configurar Twilio Phone Number
attempts INT DEFAULT 1, - [ ] Endpoint `POST /api/v1/auth/phone/send-code`
window_start TIMESTAMP DEFAULT NOW(), - Validar número con libphonenumber
created_at TIMESTAMP DEFAULT NOW(), - Rate limiting (5 códigos / hora)
INDEX idx_phone_window (phone_number, window_start) - 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`
### Backend (BE) - Validar código
- Verificar no expirado
- [ ] Configurar cuenta de Twilio - Verificar intentos < 3
- [ ] Obtener Account SID y Auth Token - Crear o actualizar usuario
- [ ] Configurar Twilio Phone Number - Generar JWT token
- [ ] Endpoint `POST /api/v1/auth/phone/send-code` - [ ] Endpoint `POST /api/v1/auth/phone/resend-code`
- Validar número con libphonenumber - Invalidar código anterior
- Rate limiting (5 códigos / hora) - Generar nuevo código
- Generar código aleatorio de 6 dígitos - Verificar cooldown de 60s
- Guardar en DB con expiración - [ ] Service `TwilioSMSService`
- Enviar SMS via Twilio - `sendVerificationCode()`
- [ ] Endpoint `POST /api/v1/auth/phone/verify-code` - `verifyCode()`
- Validar código - `formatPhoneNumber()`
- Verificar no expirado - [ ] Librería: `twilio` SDK
- Verificar intentos < 3 - [ ] Librería: `libphonenumber-js` para validación
- Crear o actualizar usuario - [ ] Tests unitarios (12 casos)
- Generar JWT token - [ ] Tests de integración con mock de Twilio
- [ ] Endpoint `POST /api/v1/auth/phone/resend-code`
- Invalidar código anterior ### Frontend (FE)
- Generar nuevo código
- Verificar cooldown de 60s - [ ] Componente `PhoneAuth.tsx`
- [ ] Service `TwilioSMSService` - [ ] Selector de país con banderas
- `sendVerificationCode()` - [ ] Input de teléfono con formato automático
- `verifyCode()` - [ ] Componente `CodeInput.tsx` (6 dígitos)
- `formatPhoneNumber()` - [ ] Componente `CompleteProfile.tsx`
- [ ] Librería: `twilio` SDK - [ ] Validación con React Hook Form
- [ ] Librería: `libphonenumber-js` para validación - [ ] Librería: `react-phone-number-input`
- [ ] Tests unitarios (12 casos) - [ ] Contador regresivo para reenvío
- [ ] Tests de integración con mock de Twilio - [ ] Tests con React Testing Library
### Frontend (FE) ### Testing (QA)
- [ ] Componente `PhoneAuth.tsx` - [ ] E2E: Registro con teléfono completo
- [ ] Selector de país con banderas - [ ] E2E: Login con teléfono existente
- [ ] Input de teléfono con formato automático - [ ] E2E: Código incorrecto (3 intentos)
- [ ] Componente `CodeInput.tsx` (6 dígitos) - [ ] E2E: Código expirado
- [ ] Componente `CompleteProfile.tsx` - [ ] E2E: Reenvío de código
- [ ] Validación con React Hook Form - [ ] E2E: Rate limiting
- [ ] Librería: `react-phone-number-input` - [ ] Test de integración con Twilio Test Credentials
- [ ] Contador regresivo para reenvío - [ ] Test de seguridad: Brute force protection
- [ ] Tests con React Testing Library - [ ] Performance: Envío de SMS < 2s
### Testing (QA) ---
- [ ] E2E: Registro con teléfono completo ## Dependencias
- [ ] E2E: Login con teléfono existente
- [ ] E2E: Código incorrecto (3 intentos) - **Bloqueantes:**
- [ ] E2E: Código expirado - Cuenta de Twilio activa
- [ ] E2E: Reenvío de código - Twilio Phone Number comprado
- [ ] E2E: Rate limiting - Presupuesto para SMS (aprox $0.0075 USD por SMS)
- [ ] Test de integración con Twilio Test Credentials
- [ ] Test de seguridad: Brute force protection - **Deseables:**
- [ ] Performance: Envío de SMS < 2s - US-AUTH-001: Para vinculación de cuentas
--- ---
## Dependencias ## Definition of Ready (DoR)
- **Bloqueantes:** - [ ] Cuenta de Twilio configurada
- Cuenta de Twilio activa - [ ] Twilio Phone Number asignado
- Twilio Phone Number comprado - [ ] Presupuesto aprobado para SMS
- Presupuesto para SMS (aprox $0.0075 USD por SMS) - [ ] Mockups aprobados
- [ ] API contract definido
- **Deseables:** - [ ] Estrategia de rate limiting definida
- US-AUTH-001: Para vinculación de cuentas
---
---
## Definition of Done (DoD)
## Definition of Ready (DoR)
- [ ] Código implementado y revisado
- [ ] Cuenta de Twilio configurada - [ ] Tests unitarios con 80%+ cobertura
- [ ] Twilio Phone Number asignado - [ ] Tests de integración pasando
- [ ] Presupuesto aprobado para SMS - [ ] Tests E2E implementados
- [ ] Mockups aprobados - [ ] Twilio configurado en todos los ambientes
- [ ] API contract definido - [ ] Rate limiting implementado
- [ ] Estrategia de rate limiting definida - [ ] Logs y monitoring de SMS
- [ ] Costos monitoreados
--- - [ ] Documentación actualizada
- [ ] QA aprobado en staging
## Definition of Done (DoD) - [ ] Deploy a producción exitoso
- [ ] Código implementado y revisado ---
- [ ] Tests unitarios con 80%+ cobertura
- [ ] Tests de integración pasando ## Notas Técnicas
- [ ] Tests E2E implementados
- [ ] Twilio configurado en todos los ambientes ### Twilio SMS Flow
- [ ] Rate limiting implementado
- [ ] Logs y monitoring de SMS 1. Usuario ingresa número de teléfono
- [ ] Costos monitoreados 2. Frontend valida formato
- [ ] Documentación actualizada 3. Frontend llama `POST /api/v1/auth/phone/send-code`
- [ ] QA aprobado en staging 4. Backend valida número con libphonenumber
- [ ] Deploy a producción exitoso 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
## Notas Técnicas 9. Usuario recibe SMS e ingresa código
10. Frontend llama `POST /api/v1/auth/phone/verify-code`
### Twilio SMS Flow 11. Backend valida código
12. Backend crea/actualiza usuario
1. Usuario ingresa número de teléfono 13. Backend genera JWT token
2. Frontend valida formato
3. Frontend llama `POST /api/v1/auth/phone/send-code` ### Environment Variables
4. Backend valida número con libphonenumber
5. Backend verifica rate limits ```env
6. Backend genera código aleatorio (6 dígitos) TWILIO_ACCOUNT_SID=AC...
7. Backend guarda código en DB (expira en 10 min) TWILIO_AUTH_TOKEN=your_auth_token
8. Backend envía SMS via Twilio API TWILIO_PHONE_NUMBER=+15551234567
9. Usuario recibe SMS e ingresa código TWILIO_VERIFY_SERVICE_SID=VA... # Opcional: usar Twilio Verify
10. Frontend llama `POST /api/v1/auth/phone/verify-code` ```
11. Backend valida código
12. Backend crea/actualiza usuario ### Twilio API Usage
13. Backend genera JWT token
```typescript
### Environment Variables import twilio from 'twilio';
```env const client = twilio(accountSid, authToken);
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=your_auth_token // Enviar SMS
TWILIO_PHONE_NUMBER=+15551234567 await client.messages.create({
TWILIO_VERIFY_SERVICE_SID=VA... # Opcional: usar Twilio Verify body: `Tu código de OrbiQuant es: ${code}\n\nVálido por 10 minutos.`,
``` from: twilioPhoneNumber,
to: userPhoneNumber
### Twilio API Usage });
```
```typescript
import twilio from 'twilio'; ### Phone Number Validation
const client = twilio(accountSid, authToken); ```typescript
import { parsePhoneNumber } from 'libphonenumber-js';
// Enviar SMS
await client.messages.create({ const phoneNumber = parsePhoneNumber(input, countryCode);
body: `Tu código de OrbiQuant es: ${code}\n\nVálido por 10 minutos.`, if (!phoneNumber || !phoneNumber.isValid()) {
from: twilioPhoneNumber, throw new Error('Invalid phone number');
to: userPhoneNumber }
}); ```
```
### Code Generation
### Phone Number Validation
```typescript
```typescript // Generar código de 6 dígitos
import { parsePhoneNumber } from 'libphonenumber-js'; const code = Math.floor(100000 + Math.random() * 900000).toString();
```
const phoneNumber = parsePhoneNumber(input, countryCode);
if (!phoneNumber || !phoneNumber.isValid()) { ### Rate Limiting Strategy
throw new Error('Invalid phone number');
} - 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
### Code Generation - Máximo 3 intentos de verificación por código
```typescript ### Security Considerations
// Generar código de 6 dígitos
const code = Math.floor(100000 + Math.random() * 900000).toString(); - 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 Strategy - Rate limiting estricto
- Logs de todos los intentos
- 5 códigos por número de teléfono por hora - Validación de número en backend
- 10 códigos por IP por hora - No devolver información si el número existe o no
- Cooldown de 60 segundos entre reenvíos
- Máximo 3 intentos de verificación por código ### Cost Optimization
### Security Considerations - SMS en USA: ~$0.0075 USD
- SMS internacional: $0.0075 - $0.10 USD
- Códigos de 6 dígitos (1 millón de combinaciones) - Usar Twilio Verify Service para mejor pricing
- Expiración de 10 minutos - Implementar captcha para prevenir abuso
- Invalidar después de 3 intentos fallidos - Alertas si se excede presupuesto mensual
- Rate limiting estricto
- Logs de todos los intentos ### Alternative: Twilio Verify Service
- Validación de número en backend
- No devolver información si el número existe o no En lugar de manejar códigos manualmente, considerar usar Twilio Verify:
- Manejo automático de códigos
### Cost Optimization - Rate limiting incluido
- Mejor pricing
- SMS en USA: ~$0.0075 USD - Reenvíos automáticos
- SMS internacional: $0.0075 - $0.10 USD - Múltiples canales (SMS, Voice, WhatsApp)
- Usar Twilio Verify Service para mejor pricing
- Implementar captcha para prevenir abuso ---
- Alertas si se excede presupuesto mensual
## Requerimientos Relacionados
### Alternative: Twilio Verify Service
- [RF-AUTH-004: Autenticación por Teléfono](../requerimientos/RF-AUTH-004-phone.md)
En lugar de manejar códigos manualmente, considerar usar Twilio Verify:
- Manejo automático de códigos ## Especificaciones Relacionadas
- Rate limiting incluido
- Mejor pricing - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- Reenvíos automáticos - [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md)
- 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)

View File

@ -1,404 +1,392 @@
--- # US-AUTH-009: Autenticación con WhatsApp
id: "US-AUTH-009"
title: "Autenticacion con WhatsApp" **Version:** 1.0.0
type: "User Story" **Fecha:** 2025-12-05
status: "To Do" **Estado:** Pendiente
priority: "Media" **Story Points:** 3
epic: "OQI-001" **Prioridad:** P2 (Media)
story_points: 3 **Épica:** [OQI-001](../_MAP.md)
created_date: "2025-12-05"
updated_date: "2026-01-04" ---
---
## Historia de Usuario
# US-AUTH-009: Autenticación con WhatsApp
**Como** usuario de OrbiQuant
**Version:** 1.0.0 **Quiero** poder registrarme e iniciar sesión recibiendo el código por WhatsApp
**Fecha:** 2025-12-05 **Para** usar una aplicación que ya tengo instalada y me resulta más familiar que SMS
**Estado:** Pendiente
**Story Points:** 3 ---
**Prioridad:** P2 (Media)
**Épica:** [OQI-001](../_MAP.md) ## Criterios de Aceptación
--- ### AC-001: Opción de WhatsApp
## Historia de Usuario **Dado** que estoy en la pantalla de ingreso de teléfono
**Cuando** ingreso mi número
**Como** usuario de OrbiQuant **Entonces** debería ver dos opciones:
**Quiero** poder registrarme e iniciar sesión recibiendo el código por WhatsApp - "Enviar por SMS"
**Para** usar una aplicación que ya tengo instalada y me resulta más familiar que SMS - "Enviar por WhatsApp"
**Y** debería poder seleccionar mi preferencia
---
### AC-002: Validación de WhatsApp
## Criterios de Aceptación
**Dado** que seleccioné la opción de WhatsApp
### AC-001: Opción de WhatsApp **Cuando** hago click en "Enviar código"
**Entonces** el sistema debería:
**Dado** que estoy en la pantalla de ingreso de teléfono 1. Verificar que el número tiene WhatsApp activo
**Cuando** ingreso mi número 2. Si no tiene WhatsApp, mostrar mensaje y ofrecer SMS
**Entonces** debería ver dos opciones: 3. Si tiene WhatsApp, enviar el código
- "Enviar por SMS"
- "Enviar por WhatsApp" ### AC-003: Mensaje de WhatsApp
**Y** debería poder seleccionar mi preferencia
**Dado** que solicité código por WhatsApp
### AC-002: Validación de WhatsApp **Cuando** recibo el mensaje
**Entonces** debería tener el formato:
**Dado** que seleccioné la opción de WhatsApp ```
**Cuando** hago click en "Enviar código" ¡Hola! 👋
**Entonces** el sistema debería:
1. Verificar que el número tiene WhatsApp activo Tu código de verificación de OrbiQuant es:
2. Si no tiene WhatsApp, mostrar mensaje y ofrecer SMS
3. Si tiene WhatsApp, enviar el código *123456*
### AC-003: Mensaje de WhatsApp Válido por 10 minutos.
No compartas este código con nadie.
**Dado** que solicité código por WhatsApp
**Cuando** recibo el mensaje OrbiQuant - Inversiones Inteligentes
**Entonces** debería tener el formato: ```
```
¡Hola! 👋 ### AC-004: Código recibido
Tu código de verificación de OrbiQuant es: **Dado** que recibí el código por WhatsApp
**Cuando** vuelvo a la app e ingreso el código
*123456* **Entonces** debería funcionar igual que con SMS
**Y** completar el registro o login
Válido por 10 minutos.
No compartas este código con nadie. ### AC-005: WhatsApp no disponible
OrbiQuant - Inversiones Inteligentes **Dado** que mi número no tiene WhatsApp activo
``` **Cuando** intento usar la opción de WhatsApp
**Entonces** debería ver un mensaje:
### AC-004: Código recibido - "Este número no tiene WhatsApp activo"
**Y** debería ver botón "Enviar por SMS"
**Dado** que recibí el código por WhatsApp
**Cuando** vuelvo a la app e ingreso el código ### AC-006: Fallback a SMS
**Entonces** debería funcionar igual que con SMS
**Y** completar el registro o login **Dado** que seleccioné WhatsApp pero el envío falló
**Cuando** ocurre un error en WhatsApp
### AC-005: WhatsApp no disponible **Entonces** debería:
1. Ver mensaje "No pudimos enviar por WhatsApp"
**Dado** que mi número no tiene WhatsApp activo 2. Ver opción "Enviar por SMS"
**Cuando** intento usar la opción de WhatsApp 3. Poder continuar con SMS sin reingresar el número
**Entonces** debería ver un mensaje:
- "Este número no tiene WhatsApp activo" ### AC-007: Preferencia guardada
**Y** debería ver botón "Enviar por SMS"
**Dado** que usé WhatsApp exitosamente
### AC-006: Fallback a SMS **Cuando** vuelvo a hacer login
**Entonces** WhatsApp debería ser la opción preseleccionada
**Dado** que seleccioné WhatsApp pero el envío falló
**Cuando** ocurre un error en WhatsApp ### AC-008: Rate limiting compartido
**Entonces** debería:
1. Ver mensaje "No pudimos enviar por WhatsApp" **Dado** que solicité códigos por SMS y WhatsApp
2. Ver opción "Enviar por SMS" **Cuando** sumo los intentos
3. Poder continuar con SMS sin reingresar el número **Entonces** debería contar ambos hacia el límite de 5 por hora
### AC-007: Preferencia guardada ---
**Dado** que usé WhatsApp exitosamente ## Mockup
**Cuando** vuelvo a hacer login
**Entonces** WhatsApp debería ser la opción preseleccionada ```
┌─────────────────────────────────────────────────────────────┐
### AC-008: Rate limiting compartido │ │
│ 🌟 Ingresa con tu número de teléfono │
**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 │ │ Número de teléfono │ │
│ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │
--- │ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │
│ │ └────────┘ └──────────────────────────────────────┘ │ │
## Mockup │ └─────────────────────────────────────────────────────┘ │
│ │
``` │ ¿Cómo quieres recibir el código? │
┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ 🌟 Ingresa con tu número de teléfono │ │ │ 📱 SMS │ │ 💬 WhatsApp │ │
│ │ │ └─────────────────────────┘ └─────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │ │ (Recomendado - más rápido) │
│ │ Número de teléfono │ │ │ │
│ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │ │ │ Enviar código │ │
│ │ └────────┘ └──────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘
│ ¿Cómo quieres recibir el código? │
│ │ Mensaje de WhatsApp:
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ ┌─────────────────────────────────────────────────────────────┐
│ │ 📱 SMS │ │ 💬 WhatsApp │ │ │ WhatsApp 🔍 ⋮ │
│ └─────────────────────────┘ └─────────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤
│ (Recomendado - más rápido) │ │ │
│ │ │ ◀ OrbiQuant ✓✓ │
│ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ Enviar código │ │ │ ┌──────────────────────────────────────────────────────┐ │
│ └─────────────────────────────────────────────────────┘ │ │ │ ¡Hola! 👋 │ │
│ │ │ │ │ │
└─────────────────────────────────────────────────────────────┘ │ │ Tu código de verificación de OrbiQuant es: │ │
│ │ │ │
Mensaje de WhatsApp: │ │ *123456* │ │
┌─────────────────────────────────────────────────────────────┐ │ │ │ │
│ WhatsApp 🔍 ⋮ │ │ │ Válido por 10 minutos. │ │
├─────────────────────────────────────────────────────────────┤ │ │ No compartas este código con nadie. │ │
│ │ │ │ │ │
│ ◀ OrbiQuant ✓✓ │ │ │ OrbiQuant - Inversiones Inteligentes │ │
│ │ │ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │ │ 15:42 │
│ │ ¡Hola! 👋 │ │ │ │
│ │ │ │ └─────────────────────────────────────────────────────────────┘
│ │ Tu código de verificación de OrbiQuant es: │ │
│ │ │ │ Pantalla si WhatsApp no disponible:
│ │ *123456* │ │ ┌─────────────────────────────────────────────────────────────┐
│ │ │ │ │ │
│ │ Válido por 10 minutos. │ │ │ ⚠️ WhatsApp no disponible │
│ │ No compartas este código con nadie. │ │ │ │
│ │ │ │ │ Este número no tiene WhatsApp activo. │
│ │ OrbiQuant - Inversiones Inteligentes │ │ │ ¿Quieres recibir el código por SMS? │
│ └──────────────────────────────────────────────────────┘ │ │ │
│ 15:42 │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ Enviar por SMS │ │
└─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ │
│ │
Pantalla si WhatsApp no disponible: │ ← Cambiar número │
┌─────────────────────────────────────────────────────────────┐ │ │
│ │ └─────────────────────────────────────────────────────────────┘
│ ⚠️ WhatsApp no disponible │ ```
│ │
│ Este número no tiene WhatsApp activo. │ ---
│ ¿Quieres recibir el código por SMS? │
│ │ ## Tareas Técnicas
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Enviar por SMS │ │ ### Database (DB)
│ └─────────────────────────────────────────────────────┘ │
│ │ - [ ] Agregar campo a tabla `users`:
│ ← Cambiar número │ ```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
## Tareas Técnicas ADD COLUMN channel VARCHAR(20) DEFAULT 'sms';
-- Valores: 'sms', 'whatsapp'
### Database (DB) ```
- [ ] Agregar campo a tabla `users`: ### Backend (BE)
```sql
ALTER TABLE users ADD COLUMN preferred_auth_channel VARCHAR(20) DEFAULT 'sms'; - [ ] Configurar WhatsApp Business API o Twilio WhatsApp
-- Valores: 'sms', 'whatsapp' - [ ] Endpoint `POST /api/v1/auth/phone/send-code` (modificar)
``` - Agregar parámetro `channel: 'sms' | 'whatsapp'`
- [ ] Agregar campo a `phone_verification_codes`: - Verificar si número tiene WhatsApp (usando Twilio Lookup)
```sql - Enviar por canal seleccionado
ALTER TABLE phone_verification_codes - [ ] Service `WhatsAppService`
ADD COLUMN channel VARCHAR(20) DEFAULT 'sms'; - `hasWhatsApp(phoneNumber)`
-- Valores: 'sms', 'whatsapp' - `sendVerificationCode(phoneNumber, code)`
``` - `formatMessage(code)`
- [ ] Librería: `twilio` SDK (WhatsApp support)
### Backend (BE) - [ ] Fallback automático a SMS si WhatsApp falla
- [ ] Tests unitarios (8 casos)
- [ ] Configurar WhatsApp Business API o Twilio WhatsApp - [ ] Tests de integración con mock
- [ ] Endpoint `POST /api/v1/auth/phone/send-code` (modificar)
- Agregar parámetro `channel: 'sms' | 'whatsapp'` ### Frontend (FE)
- Verificar si número tiene WhatsApp (usando Twilio Lookup)
- Enviar por canal seleccionado - [ ] Modificar `PhoneAuth.tsx`
- [ ] Service `WhatsAppService` - Agregar selector de canal (SMS/WhatsApp)
- `hasWhatsApp(phoneNumber)` - Mostrar logo de WhatsApp
- `sendVerificationCode(phoneNumber, code)` - Manejo de error si WhatsApp no disponible
- `formatMessage(code)` - [ ] Recordar preferencia del usuario
- [ ] Librería: `twilio` SDK (WhatsApp support) - [ ] Tests con React Testing Library
- [ ] Fallback automático a SMS si WhatsApp falla
- [ ] Tests unitarios (8 casos) ### Testing (QA)
- [ ] Tests de integración con mock
- [ ] E2E: Registro con WhatsApp
### Frontend (FE) - [ ] E2E: WhatsApp no disponible (fallback a SMS)
- [ ] E2E: Error en WhatsApp (fallback a SMS)
- [ ] Modificar `PhoneAuth.tsx` - [ ] E2E: Preferencia guardada
- Agregar selector de canal (SMS/WhatsApp) - [ ] Test de integración con Twilio WhatsApp Sandbox
- Mostrar logo de WhatsApp - [ ] Mock de Twilio Lookup API
- Manejo de error si WhatsApp no disponible
- [ ] Recordar preferencia del usuario ---
- [ ] Tests con React Testing Library
## Dependencias
### Testing (QA)
- **Bloqueantes:**
- [ ] E2E: Registro con WhatsApp - US-AUTH-008: Infraestructura de SMS ya implementada
- [ ] E2E: WhatsApp no disponible (fallback a SMS) - Twilio WhatsApp habilitado (requiere aprobación de Meta)
- [ ] E2E: Error en WhatsApp (fallback a SMS) - WhatsApp Business Profile aprobado
- [ ] E2E: Preferencia guardada
- [ ] Test de integración con Twilio WhatsApp Sandbox - **Alternativa:**
- [ ] Mock de Twilio Lookup API - Usar Twilio WhatsApp Sandbox para desarrollo
- Solicitar WhatsApp Business API access para producción
---
---
## Dependencias
## Definition of Ready (DoR)
- **Bloqueantes:**
- US-AUTH-008: Infraestructura de SMS ya implementada - [ ] Twilio WhatsApp configurado
- Twilio WhatsApp habilitado (requiere aprobación de Meta) - [ ] WhatsApp Business Profile creado
- WhatsApp Business Profile aprobado - [ ] Message templates aprobados por Meta (para producción)
- [ ] Mockups aprobados
- **Alternativa:** - [ ] API contract definido
- Usar Twilio WhatsApp Sandbox para desarrollo
- Solicitar WhatsApp Business API access para producción ---
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura
- [ ] Twilio WhatsApp configurado - [ ] Tests de integración pasando
- [ ] WhatsApp Business Profile creado - [ ] Tests E2E implementados
- [ ] Message templates aprobados por Meta (para producción) - [ ] Twilio WhatsApp configurado en todos los ambientes
- [ ] Mockups aprobados - [ ] Fallback a SMS funcional
- [ ] API contract definido - [ ] Documentación actualizada
- [ ] Logs y monitoring
--- - [ ] QA aprobado en staging
- [ ] Deploy a producción exitoso
## Definition of Done (DoD)
---
- [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura ## Notas Técnicas
- [ ] Tests de integración pasando
- [ ] Tests E2E implementados ### WhatsApp vs SMS
- [ ] Twilio WhatsApp configurado en todos los ambientes
- [ ] Fallback a SMS funcional **Ventajas de WhatsApp:**
- [ ] Documentación actualizada - Más familiar para usuarios
- [ ] Logs y monitoring - Gratis para el usuario
- [ ] QA aprobado en staging - Mayor tasa de apertura
- [ ] Deploy a producción exitoso - Confirmación de entrega y lectura
- Soporte para rich media
---
**Desventajas:**
## Notas Técnicas - Requiere número verificado de WhatsApp Business
- Proceso de aprobación de Meta
### WhatsApp vs SMS - Templates deben ser pre-aprobados (producción)
- No todos tienen WhatsApp
**Ventajas de WhatsApp:**
- Más familiar para usuarios ### Twilio WhatsApp Integration
- Gratis para el usuario
- Mayor tasa de apertura ```typescript
- Confirmación de entrega y lectura import twilio from 'twilio';
- Soporte para rich media
const client = twilio(accountSid, authToken);
**Desventajas:**
- Requiere número verificado de WhatsApp Business // Verificar si número tiene WhatsApp
- Proceso de aprobación de Meta const lookup = await client.lookups.v2
- Templates deben ser pre-aprobados (producción) .phoneNumbers(phoneNumber)
- No todos tienen WhatsApp .fetch({ fields: 'line_type_intelligence' });
### Twilio WhatsApp Integration const hasWhatsApp = lookup.lineTypeIntelligence?.type === 'whatsapp';
```typescript // Enviar mensaje por WhatsApp
import twilio from 'twilio'; 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`,
const client = twilio(accountSid, authToken); from: 'whatsapp:+14155238886', // Twilio WhatsApp number
to: `whatsapp:${phoneNumber}`
// Verificar si número tiene WhatsApp });
const lookup = await client.lookups.v2 ```
.phoneNumbers(phoneNumber)
.fetch({ fields: 'line_type_intelligence' }); ### Environment Variables
const hasWhatsApp = lookup.lineTypeIntelligence?.type === 'whatsapp'; ```env
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886
// Enviar mensaje por WhatsApp TWILIO_WHATSAPP_ENABLED=true
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 ### Development: WhatsApp Sandbox
to: `whatsapp:${phoneNumber}`
}); Para desarrollo, Twilio ofrece un Sandbox que no requiere aprobación:
```
1. Usuario debe enviar un mensaje join code a Twilio Sandbox
### Environment Variables 2. Luego puede recibir mensajes
3. Útil para testing pero no para producción
```env
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886 ### Production: WhatsApp Business API
TWILIO_WHATSAPP_ENABLED=true
``` Para producción:
### Development: WhatsApp Sandbox 1. Solicitar WhatsApp Business API access
2. Crear WhatsApp Business Profile
Para desarrollo, Twilio ofrece un Sandbox que no requiere aprobación: 3. Verificar número de teléfono
4. Crear y aprobar message templates
1. Usuario debe enviar un mensaje join code a Twilio Sandbox 5. Esperar aprobación de Meta (puede tardar días)
2. Luego puede recibir mensajes
3. Útil para testing pero no para producción ### Message Templates (Producción)
### Production: WhatsApp Business API En producción, WhatsApp requiere templates pre-aprobados:
Para producción: ```
Template Name: verification_code
1. Solicitar WhatsApp Business API access Category: AUTHENTICATION
2. Crear WhatsApp Business Profile Language: es
3. Verificar número de teléfono
4. Crear y aprobar message templates Body:
5. Esperar aprobación de Meta (puede tardar días) ¡Hola! 👋
### Message Templates (Producción) Tu código de verificación de OrbiQuant es:
En producción, WhatsApp requiere templates pre-aprobados: *{{1}}*
``` Válido por 10 minutos.
Template Name: verification_code No compartas este código con nadie.
Category: AUTHENTICATION
Language: es OrbiQuant - Inversiones Inteligentes
```
Body:
¡Hola! 👋 ### Fallback Strategy
Tu código de verificación de OrbiQuant es: ```typescript
async function sendVerificationCode(phone, code, channel) {
*{{1}}* try {
if (channel === 'whatsapp') {
Válido por 10 minutos. // Verificar si tiene WhatsApp
No compartas este código con nadie. const hasWhatsApp = await whatsappService.hasWhatsApp(phone);
OrbiQuant - Inversiones Inteligentes if (!hasWhatsApp) {
``` // Automáticamente usar SMS
return await smsService.sendCode(phone, code);
### Fallback Strategy }
```typescript // Intentar enviar por WhatsApp
async function sendVerificationCode(phone, code, channel) { return await whatsappService.sendCode(phone, code);
try { } else {
if (channel === 'whatsapp') { // Usar SMS
// Verificar si tiene WhatsApp return await smsService.sendCode(phone, code);
const hasWhatsApp = await whatsappService.hasWhatsApp(phone); }
} catch (error) {
if (!hasWhatsApp) { // Si WhatsApp falla, fallback a SMS
// Automáticamente usar SMS logger.warn('WhatsApp failed, falling back to SMS', { phone, error });
return await smsService.sendCode(phone, code); return await smsService.sendCode(phone, code);
} }
}
// Intentar enviar por WhatsApp ```
return await whatsappService.sendCode(phone, code);
} else { ### Cost Comparison
// Usar SMS
return await smsService.sendCode(phone, code); - SMS: ~$0.0075 USD por mensaje
} - WhatsApp: ~$0.005 USD por mensaje (más barato)
} catch (error) { - WhatsApp también tiene mejor deliverability
// Si WhatsApp falla, fallback a SMS
logger.warn('WhatsApp failed, falling back to SMS', { phone, error }); ### Security Considerations
return await smsService.sendCode(phone, code);
} - 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
### Cost Comparison - Fallback automático mantiene seguridad
- SMS: ~$0.0075 USD por mensaje ---
- WhatsApp: ~$0.005 USD por mensaje (más barato)
- WhatsApp también tiene mejor deliverability ## Requerimientos Relacionados
### Security Considerations - [RF-AUTH-004: Autenticación por Teléfono](../requerimientos/RF-AUTH-004-phone.md)
- Mismo código puede usarse para SMS o WhatsApp ## Especificaciones Relacionadas
- Rate limiting compartido entre canales
- Validar que el canal solicitado sea válido - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- Logs separados por canal para auditoría - [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md)
- 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)

View File

@ -1,15 +1,3 @@
---
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 # US-AUTH-010: Configurar 2FA
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,16 +1,3 @@
---
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 # RF-AUTH-001: OAuth Multi-proveedor
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,16 +1,3 @@
---
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 # RF-AUTH-002: Autenticación por Email
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,16 +1,3 @@
---
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 # RF-AUTH-003: Autenticación por Teléfono
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,16 +1,3 @@
---
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) # RF-AUTH-004: Two-Factor Authentication (2FA)
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,16 +1,3 @@
---
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 # RF-AUTH-005: Gestión de Sesiones
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,12 +1,3 @@
---
id: "README"
title: "Modulo Educativo"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# OQI-002: Modulo Educativo # OQI-002: Modulo Educativo
**Estado:** ✅ Implementado **Estado:** ✅ Implementado

View File

@ -1,243 +1,235 @@
--- # _MAP: OQI-002 - Módulo Educativo
id: "MAP-OQI-002-education"
title: "Mapa de OQI-002-education" **Última actualización:** 2025-12-05
type: "Index" **Estado:** Parcialmente Implementado
project: "trading-platform" **Versión:** 1.0.0
updated_date: "2026-01-04"
--- ---
# _MAP: OQI-002 - Módulo Educativo ## Propósito
**Última actualización:** 2025-12-05 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.
**Estado:** Parcialmente Implementado
**Versión:** 1.0.0 ---
--- ## Contenido del Directorio
## Propósito ```
OQI-002-education/
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. ├── 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
## Contenido del Directorio │ ├── RF-EDU-002-lecciones.md # Sistema de lecciones
│ ├── RF-EDU-003-progreso.md # Tracking de progreso
``` │ ├── RF-EDU-004-quizzes.md # Sistema de quizzes
OQI-002-education/ │ ├── RF-EDU-005-certificados.md # Certificaciones
├── README.md # Documentación técnica existente │ └── RF-EDU-006-gamificacion.md # XP y badges
├── _MAP.md # Este archivo - índice ├── especificaciones/ # Especificaciones técnicas
├── requerimientos/ # Documentos de requerimientos funcionales │ ├── ET-EDU-001-database.md # Modelo de datos
│ ├── RF-EDU-001-catalogo.md # Catálogo de cursos │ ├── ET-EDU-002-api.md # Endpoints REST
│ ├── RF-EDU-002-lecciones.md # Sistema de lecciones │ ├── ET-EDU-003-frontend.md # Componentes React
│ ├── RF-EDU-003-progreso.md # Tracking de progreso │ ├── ET-EDU-004-video.md # Streaming de video
│ ├── RF-EDU-004-quizzes.md # Sistema de quizzes │ ├── ET-EDU-005-quizzes.md # Motor de quizzes
│ ├── RF-EDU-005-certificados.md # Certificaciones │ └── ET-EDU-006-gamification.md # Sistema de gamificación
│ └── RF-EDU-006-gamificacion.md # XP y badges ├── historias-usuario/ # User Stories
├── especificaciones/ # Especificaciones técnicas │ ├── US-EDU-001-ver-catalogo.md
│ ├── ET-EDU-001-database.md # Modelo de datos │ ├── US-EDU-002-ver-curso.md
│ ├── ET-EDU-002-api.md # Endpoints REST │ ├── US-EDU-003-iniciar-leccion.md
│ ├── ET-EDU-003-frontend.md # Componentes React │ ├── US-EDU-004-ver-video.md
│ ├── ET-EDU-004-video.md # Streaming de video │ ├── US-EDU-005-completar-leccion.md
│ ├── ET-EDU-005-quizzes.md # Motor de quizzes │ ├── US-EDU-006-realizar-quiz.md
│ └── ET-EDU-006-gamification.md # Sistema de gamificación │ ├── US-EDU-007-ver-progreso.md
├── historias-usuario/ # User Stories │ ├── US-EDU-008-obtener-certificado.md
│ ├── US-EDU-001-ver-catalogo.md │ ├── US-EDU-009-buscar-cursos.md
│ ├── US-EDU-002-ver-curso.md │ ├── US-EDU-010-filtrar-categoria.md
│ ├── US-EDU-003-iniciar-leccion.md │ ├── US-EDU-011-marcar-favorito.md
│ ├── US-EDU-004-ver-video.md │ ├── US-EDU-012-dejar-review.md
│ ├── US-EDU-005-completar-leccion.md │ ├── US-EDU-013-ver-xp.md
│ ├── US-EDU-006-realizar-quiz.md │ ├── US-EDU-014-desbloquear-badge.md
│ ├── US-EDU-007-ver-progreso.md │ └── US-EDU-015-continuar-donde-deje.md
│ ├── US-EDU-008-obtener-certificado.md └── implementacion/ # Trazabilidad de implementación
│ ├── US-EDU-009-buscar-cursos.md └── TRACEABILITY.yml
│ ├── 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 ## Requerimientos Funcionales
│ └── US-EDU-015-continuar-donde-deje.md
└── implementacion/ # Trazabilidad de implementación | ID | Nombre | Prioridad | SP | Estado |
└── TRACEABILITY.yml |----|--------|-----------|-----|--------|
``` | 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 |
## Requerimientos Funcionales | RF-EDU-005 | Certificaciones | P2 | 5 | Pendiente |
| RF-EDU-006 | Gamificación | P2 | 8 | Pendiente |
| ID | Nombre | Prioridad | SP | Estado |
|----|--------|-----------|-----|--------| **Total:** 45 SP
| 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 | ## Especificaciones Técnicas
| RF-EDU-005 | Certificaciones | P2 | 5 | Pendiente |
| RF-EDU-006 | Gamificación | P2 | 8 | Pendiente | | ID | Nombre | Componente | Estado |
|----|--------|------------|--------|
**Total:** 45 SP | 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 |
## Especificaciones Técnicas | ET-EDU-005 | Quiz Engine | Backend | Pendiente |
| ET-EDU-006 | Gamification | Backend/Frontend | Pendiente |
| ID | Nombre | Componente | Estado |
|----|--------|------------|--------| ---
| ET-EDU-001 | Database | Database | ✅ Schema existe |
| ET-EDU-002 | API REST | Backend | ✅ Parcial | ## Historias de Usuario
| ET-EDU-003 | Frontend | Frontend | ✅ Parcial |
| ET-EDU-004 | Video Streaming | Backend | Pendiente | | ID | Historia | Prioridad | SP | Estado |
| ET-EDU-005 | Quiz Engine | Backend | Pendiente | |----|----------|-----------|-----|--------|
| ET-EDU-006 | Gamification | Backend/Frontend | Pendiente | | 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 |
## Historias de Usuario | US-EDU-005 | Completar lección | P0 | 3 | Pendiente |
| US-EDU-006 | Realizar quiz | P1 | 5 | Pendiente |
| ID | Historia | Prioridad | SP | Estado | | US-EDU-007 | Ver mi progreso | P0 | 3 | Pendiente |
|----|----------|-----------|-----|--------| | US-EDU-008 | Obtener certificado | P2 | 3 | Pendiente |
| US-EDU-001 | Ver catálogo de cursos | P0 | 3 | ✅ Implementado | | US-EDU-009 | Buscar cursos | P1 | 2 | Pendiente |
| US-EDU-002 | Ver detalle de curso | P0 | 3 | ✅ Implementado | | US-EDU-010 | Filtrar por categoría | P1 | 2 | Pendiente |
| US-EDU-003 | Iniciar una lección | P0 | 3 | Pendiente | | US-EDU-011 | Marcar favorito | P2 | 2 | Pendiente |
| US-EDU-004 | Ver video de lección | P0 | 3 | Pendiente | | US-EDU-012 | Dejar review | P2 | 3 | Pendiente |
| US-EDU-005 | Completar lección | P0 | 3 | Pendiente | | US-EDU-013 | Ver XP acumulado | P2 | 2 | Pendiente |
| US-EDU-006 | Realizar quiz | P1 | 5 | Pendiente | | US-EDU-014 | Desbloquear badge | P2 | 3 | Pendiente |
| US-EDU-007 | Ver mi progreso | P0 | 3 | Pendiente | | US-EDU-015 | Continuar donde dejé | P1 | 3 | Pendiente |
| US-EDU-008 | Obtener certificado | P2 | 3 | Pendiente |
| US-EDU-009 | Buscar cursos | P1 | 2 | Pendiente | **Total:** 45 SP
| 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 | ## Dependencias
| US-EDU-014 | Desbloquear badge | P2 | 3 | Pendiente |
| US-EDU-015 | Continuar donde dejé | P1 | 3 | Pendiente | ### Depende de:
**Total:** 45 SP - **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado
- **OQI-005:** Pagos (compra de cursos premium) - Pendiente
---
### Bloquea:
## Dependencias
- Ninguna
### Depende de:
---
- **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado
- **OQI-005:** Pagos (compra de cursos premium) - Pendiente ## Stack Técnico
### Bloquea: | Capa | Tecnología | Uso |
|------|------------|-----|
- Ninguna | Frontend | React + Zustand | UI y estado |
| Backend | Express.js | API REST |
--- | Database | PostgreSQL | Persistencia |
| Video | Cloudflare Stream / S3 | Hosting de videos |
## Stack Técnico | CDN | Cloudflare | Assets estáticos |
| Capa | Tecnología | Uso | ---
|------|------------|-----|
| Frontend | React + Zustand | UI y estado | ## Entidades Principales
| Backend | Express.js | API REST |
| Database | PostgreSQL | Persistencia | ### Category
| Video | Cloudflare Stream / S3 | Hosting de videos | - Categorías de cursos (Trading Básico, Análisis Técnico, etc.)
| CDN | Cloudflare | Assets estáticos |
### Course
--- - Curso con metadata, precio, nivel de dificultad
## Entidades Principales ### Module
- Agrupación de lecciones dentro de un curso
### Category
- Categorías de cursos (Trading Básico, Análisis Técnico, etc.) ### Lesson
- Contenido individual (video, artículo, quiz)
### Course
- Curso con metadata, precio, nivel de dificultad ### Enrollment
- Inscripción de usuario en curso
### Module
- Agrupación de lecciones dentro de un curso ### Progress
- Progreso del usuario por lección
### Lesson
- Contenido individual (video, artículo, quiz) ---
### Enrollment ## Niveles de Dificultad
- Inscripción de usuario en curso
| Nivel | Label | Color | Cursos típicos |
### Progress |-------|-------|-------|----------------|
- Progreso del usuario por lección | 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 |
## Niveles de Dificultad
---
| Nivel | Label | Color | Cursos típicos |
|-------|-------|-------|----------------| ## Gamificación (Fase 2)
| beginner | Principiante | Verde | Introducción al trading |
| intermediate | Intermedio | Azul | Análisis técnico | ### Sistema de XP
| advanced | Avanzado | Naranja | Estrategias avanzadas | - Completar lección: +10 XP
| expert | Experto | Rojo | Trading algorítmico | - Aprobar quiz: +25 XP
- Completar curso: +100 XP
--- - Streak diario: +5 XP
## Gamificación (Fase 2) ### Badges
- 🎓 "Primer Paso" - Completa tu primera lección
### Sistema de XP - 📚 "Estudiante Dedicado" - Completa 10 lecciones
- Completar lección: +10 XP - 🏆 "Graduado" - Completa tu primer curso
- Aprobar quiz: +25 XP - 🔥 "En Racha" - 7 días seguidos de estudio
- Completar curso: +100 XP - 💯 "Perfeccionista" - 100% en un quiz
- Streak diario: +5 XP
---
### Badges
- 🎓 "Primer Paso" - Completa tu primera lección ## Criterios de Aceptación
- 📚 "Estudiante Dedicado" - Completa 10 lecciones
- 🏆 "Graduado" - Completa tu primer curso ### Funcionales
- 🔥 "En Racha" - 7 días seguidos de estudio
- 💯 "Perfeccionista" - 100% en un quiz - [ ] Catálogo muestra cursos con filtros y búsqueda
- [ ] Usuarios pueden inscribirse en cursos
--- - [ ] Videos reproducen correctamente
- [ ] Progreso se guarda automáticamente
## Criterios de Aceptación - [ ] Quizzes validan respuestas correctamente
- [ ] Certificados se generan al completar curso
### Funcionales
### No Funcionales
- [ ] Catálogo muestra cursos con filtros y búsqueda
- [ ] Usuarios pueden inscribirse en cursos - [ ] Videos cargan en < 3 segundos
- [ ] Videos reproducen correctamente - [ ] Catálogo carga en < 1 segundo
- [ ] Progreso se guarda automáticamente - [ ] Responsive en mobile
- [ ] Quizzes validan respuestas correctamente - [ ] Accesible (WCAG 2.1 AA)
- [ ] Certificados se generan al completar curso
### Técnicos
### No Funcionales
- [ ] Cobertura de tests > 70%
- [ ] Videos cargan en < 3 segundos - [ ] Documentación API completa
- [ ] Catálogo carga en < 1 segundo - [ ] SEO optimizado para cursos públicos
- [ ] Responsive en mobile
- [ ] Accesible (WCAG 2.1 AA) ---
### Técnicos ## Hitos
- [ ] Cobertura de tests > 70% | Hito | Entregables | Target |
- [ ] Documentación API completa |------|-------------|--------|
- [ ] SEO optimizado para cursos públicos | M1 | Catálogo + detalle curso | Sprint 3 ✅ |
| M2 | Lecciones + videos | Sprint 3 |
--- | M3 | Progreso + quizzes | Sprint 4 |
| M4 | Gamificación + certificados | Sprint 4 |
## Hitos
---
| Hito | Entregables | Target |
|------|-------------|--------| ## Reutilización de GAMILIT
| M1 | Catálogo + detalle curso | Sprint 3 ✅ |
| M2 | Lecciones + videos | Sprint 3 | Esta épica reutiliza ~70% de la arquitectura del proyecto GAMILIT:
| M3 | Progreso + quizzes | Sprint 4 | - Estructura de cursos y lecciones
| M4 | Gamificación + certificados | Sprint 4 | - Sistema de progreso
- Motor de quizzes
--- - Sistema de gamificación (XP, badges)
## Reutilización de GAMILIT ---
Esta épica reutiliza ~70% de la arquitectura del proyecto GAMILIT: ## Referencias
- Estructura de cursos y lecciones
- Sistema de progreso - [README Técnico](./README.md)
- Motor de quizzes - [Vision del Producto](../../00-vision-general/VISION-PRODUCTO.md)
- Sistema de gamificación (XP, badges) - [_MAP Fase MVP](../_MAP.md)
---
## Referencias
- [README Técnico](./README.md)
- [Vision del Producto](../../00-vision-general/VISION-PRODUCTO.md)
- [_MAP Fase MVP](../_MAP.md)

View File

@ -1,238 +1,229 @@
--- # Especificaciones Técnicas - OQI-002 Módulo Educativo
id: "README"
title: "Especificaciones Técnicas - OQI-002 Módulo Educativo" Este directorio contiene las especificaciones técnicas detalladas para el módulo educativo de OrbiQuant IA.
type: "Documentation"
project: "trading-platform" ## Índice de Especificaciones
version: "1.0.0"
updated_date: "2026-01-04" ### [ET-EDU-001: Modelo de Datos - Schema Education](./ET-EDU-001-database.md)
--- **Componente:** Database
**Tamaño:** ~30KB
# Especificaciones Técnicas - OQI-002 Módulo Educativo
Define el schema completo de PostgreSQL para el módulo educativo:
Este directorio contiene las especificaciones técnicas detalladas para el módulo educativo de OrbiQuant IA. - 11 tablas principales (categories, courses, modules, lessons, enrollments, progress, quizzes, quiz_questions, quiz_attempts, certificates, user_achievements)
- 6 ENUMs personalizados
## Índice de Especificaciones - Triggers y funciones automáticas
- Vistas optimizadas
### [ET-EDU-001: Modelo de Datos - Schema Education](./ET-EDU-001-database.md) - Índices de performance
**Componente:** Database - Row Level Security (RLS)
**Tamaño:** ~30KB
**Contenido clave:**
Define el schema completo de PostgreSQL para el módulo educativo: - Schema education completo con relaciones
- 11 tablas principales (categories, courses, modules, lessons, enrollments, progress, quizzes, quiz_questions, quiz_attempts, certificates, user_achievements) - Triggers para auto-actualización de progreso
- 6 ENUMs personalizados - Vistas para queries complejas
- Triggers y funciones automáticas - Estrategias de indexación
- Vistas optimizadas
- Índices de performance ---
- Row Level Security (RLS)
### [ET-EDU-002: API REST - Endpoints del Módulo](./ET-EDU-002-api.md)
**Contenido clave:** **Componente:** Backend
- Schema education completo con relaciones **Tamaño:** ~42KB
- Triggers para auto-actualización de progreso
- Vistas para queries complejas Especificación completa de la API REST con Express.js + TypeScript:
- Estrategias de indexación - 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
### [ET-EDU-002: API REST - Endpoints del Módulo](./ET-EDU-002-api.md) - Validación con Zod
**Componente:** Backend - Manejo de errores estandarizado
**Tamaño:** ~42KB
**Contenido clave:**
Especificación completa de la API REST con Express.js + TypeScript: - ~60 endpoints documentados
- 10 grupos de endpoints (Categories, Courses, Modules, Lessons, Enrollments, Progress, Quizzes, Certificates, Achievements, Gamification) - Middleware stack completo
- Request/Response con TypeScript interfaces - Códigos de error estandarizados
- Autenticación JWT y autorización por roles - Ejemplos de tests con Supertest
- Rate limiting y paginación
- Validación con Zod ---
- Manejo de errores estandarizado
### [ET-EDU-003: Componentes Frontend - React + TypeScript](./ET-EDU-003-frontend.md)
**Contenido clave:** **Componente:** Frontend
- ~60 endpoints documentados **Tamaño:** ~46KB
- Middleware stack completo
- Códigos de error estandarizados Arquitectura frontend con React 18, TypeScript, Zustand y TailwindCSS:
- Ejemplos de tests con Supertest - 7 páginas principales (CoursesPage, CourseDetailPage, LessonPage, ProgressPage, QuizPage, CertificatesPage, AchievementsPage)
- 20+ componentes reutilizables
--- - Custom hooks para data fetching
- Stores Zustand para state management
### [ET-EDU-003: Componentes Frontend - React + TypeScript](./ET-EDU-003-frontend.md) - Integración con React Query
**Componente:** Frontend
**Tamaño:** ~46KB **Contenido clave:**
- Código completo de componentes principales
Arquitectura frontend con React 18, TypeScript, Zustand y TailwindCSS: - Hooks personalizados (useCourses, useEnrollment, useVideoProgress, etc.)
- 7 páginas principales (CoursesPage, CourseDetailPage, LessonPage, ProgressPage, QuizPage, CertificatesPage, AchievementsPage) - Stores con Zustand (courseStore, progressStore, gamificationStore)
- 20+ componentes reutilizables - Configuración de TailwindCSS
- Custom hooks para data fetching
- Stores Zustand para state management ---
- Integración con React Query
### [ET-EDU-004: Sistema de Streaming de Video](./ET-EDU-004-video.md)
**Contenido clave:** **Componente:** Backend/Infraestructura
- Código completo de componentes principales **Tamaño:** ~30KB
- Hooks personalizados (useCourses, useEnrollment, useVideoProgress, etc.)
- Stores con Zustand (courseStore, progressStore, gamificationStore) Integración de video streaming con Vimeo y AWS S3+CloudFront:
- Configuración de TailwindCSS - Configuración de Vimeo Pro/Business
- Upload y gestión de videos
--- - AWS S3 + CloudFront con signed URLs
- Transcoding HLS con FFmpeg
### [ET-EDU-004: Sistema de Streaming de Video](./ET-EDU-004-video.md) - Player configuration (Vimeo Player / Video.js)
**Componente:** Backend/Infraestructura - Tracking de progreso de video
**Tamaño:** ~30KB - Subtítulos WebVTT
Integración de video streaming con Vimeo y AWS S3+CloudFront: **Contenido clave:**
- Configuración de Vimeo Pro/Business - Servicios de upload a Vimeo
- Upload y gestión de videos - Generación de signed URLs en CloudFront
- AWS S3 + CloudFront con signed URLs - Pipeline de transcoding HLS multi-bitrate
- Transcoding HLS con FFmpeg - Componentes de video player (VimeoPlayer, HLSPlayer)
- Player configuration (Vimeo Player / Video.js) - Configuración de CloudFront Distribution
- Tracking de progreso de video
- Subtítulos WebVTT ---
**Contenido clave:** ### [ET-EDU-005: Motor de Evaluaciones y Quizzes](./ET-EDU-005-quizzes.md)
- Servicios de upload a Vimeo **Componente:** Backend/Frontend
- Generación de signed URLs en CloudFront **Tamaño:** ~33KB
- Pipeline de transcoding HLS multi-bitrate
- Componentes de video player (VimeoPlayer, HLSPlayer) Sistema completo de evaluaciones con múltiples tipos de preguntas:
- Configuración de CloudFront Distribution - 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
### [ET-EDU-005: Motor de Evaluaciones y Quizzes](./ET-EDU-005-quizzes.md) - Validación de respuestas (incluyendo fuzzy matching)
**Componente:** Backend/Frontend - Analytics de quizzes
**Tamaño:** ~33KB
**Contenido clave:**
Sistema completo de evaluaciones con múltiples tipos de preguntas: - QuizScoringService con algoritmos completos
- 5 tipos de preguntas (Multiple Choice, True/False, Multiple Select, Fill in the Blank, Code Challenge) - QuizAttemptService para flujo de quiz
- Algoritmo de scoring con crédito parcial - Componentes de UI (QuizQuestion, QuizTimer, QuizResults)
- Gestión de intentos con límites - Cálculo de dificultad de preguntas
- Timer con auto-submit - Distribución de puntajes
- Validación de respuestas (incluyendo fuzzy matching)
- Analytics de quizzes ---
**Contenido clave:** ### [ET-EDU-006: Sistema de Gamificación](./ET-EDU-006-gamification.md)
- QuizScoringService con algoritmos completos **Componente:** Backend/Frontend
- QuizAttemptService para flujo de quiz **Tamaño:** ~35KB
- Componentes de UI (QuizQuestion, QuizTimer, QuizResults)
- Cálculo de dificultad de preguntas Sistema de gamificación para aumentar engagement:
- Distribución de puntajes - 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
### [ET-EDU-006: Sistema de Gamificación](./ET-EDU-006-gamification.md) - Leaderboard global y por períodos
**Componente:** Backend/Frontend - Notificaciones de logros
**Tamaño:** ~35KB
**Contenido clave:**
Sistema de gamificación para aumentar engagement: - XPManagerService con cálculo de niveles
- Sistema de XP con múltiples fuentes - AchievementManagerService con verificación automática
- Fórmula de niveles: `Level = floor(sqrt(totalXP / 100))` - StreakManagerService para rachas diarias
- 15+ achievements predefinidos (common, uncommon, rare, epic, legendary) - LeaderboardManagerService con caching
- Sistema de rachas diarias con rewards - Configuración de achievements y recompensas
- Leaderboard global y por períodos - Componentes de UI (XPBar, LevelBadge, AchievementCard)
- Notificaciones de logros
---
**Contenido clave:**
- XPManagerService con cálculo de niveles ## Stack Tecnológico
- AchievementManagerService con verificación automática
- StreakManagerService para rachas diarias ### Backend
- LeaderboardManagerService con caching - **Runtime:** Node.js 18+
- Configuración de achievements y recompensas - **Framework:** Express.js
- Componentes de UI (XPBar, LevelBadge, AchievementCard) - **Language:** TypeScript 5.3+
- **Database:** PostgreSQL 15+
--- - **ORM:** Prisma / TypeORM (opcional)
- **Validation:** Zod
## Stack Tecnológico - **Auth:** JWT (jsonwebtoken)
- **Caching:** Redis 4+
### Backend - **Video Processing:** FFmpeg
- **Runtime:** Node.js 18+
- **Framework:** Express.js ### Frontend
- **Language:** TypeScript 5.3+ - **Framework:** React 18
- **Database:** PostgreSQL 15+ - **Language:** TypeScript 5.3+
- **ORM:** Prisma / TypeORM (opcional) - **State Management:** Zustand 4+
- **Validation:** Zod - **Data Fetching:** TanStack React Query 5+
- **Auth:** JWT (jsonwebtoken) - **Styling:** TailwindCSS 3+
- **Caching:** Redis 4+ - **Routing:** React Router 6+
- **Video Processing:** FFmpeg - **Video Player:** Vimeo Player / Video.js
- **Forms:** React Hook Form + Zod
### Frontend
- **Framework:** React 18 ### Infraestructura
- **Language:** TypeScript 5.3+ - **Video CDN:** Vimeo Pro/Business o AWS S3 + CloudFront
- **State Management:** Zustand 4+ - **Object Storage:** AWS S3
- **Data Fetching:** TanStack React Query 5+ - **Cache:** Redis
- **Styling:** TailwindCSS 3+ - **Monitoring:** (TBD)
- **Routing:** React Router 6+
- **Video Player:** Vimeo Player / Video.js ---
- **Forms:** React Hook Form + Zod
## Convenciones de Nomenclatura
### Infraestructura
- **Video CDN:** Vimeo Pro/Business o AWS S3 + CloudFront ### Archivos de Especificación
- **Object Storage:** AWS S3 ```
- **Cache:** Redis ET-EDU-XXX-{nombre}.md
- **Monitoring:** (TBD) ```
- **ET:** Especificación Técnica
--- - **EDU:** Módulo Education
- **XXX:** Número secuencial (001-999)
## Convenciones de Nomenclatura - **{nombre}:** Identificador descriptivo
### Archivos de Especificación ### Versiones
``` Todas las especificaciones están en versión **1.0.0** (2025-12-05)
ET-EDU-XXX-{nombre}.md
``` ---
- **ET:** Especificación Técnica
- **EDU:** Módulo Education ## Cómo Usar Este Documento
- **XXX:** Número secuencial (001-999)
- **{nombre}:** Identificador descriptivo 1. **Para Desarrolladores Backend:**
- Leer ET-EDU-001 (Database) y ET-EDU-002 (API)
### Versiones - Referencias: ET-EDU-004 (Video), ET-EDU-005 (Quizzes), ET-EDU-006 (Gamification)
Todas las especificaciones están en versión **1.0.0** (2025-12-05)
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)
## Cómo Usar Este Documento
3. **Para DevOps:**
1. **Para Desarrolladores Backend:** - Leer ET-EDU-001 (Database setup)
- Leer ET-EDU-001 (Database) y ET-EDU-002 (API) - Leer ET-EDU-004 (Video Infrastructure)
- Referencias: ET-EDU-004 (Video), ET-EDU-005 (Quizzes), ET-EDU-006 (Gamification) - Variables de entorno en cada especificación
2. **Para Desarrolladores Frontend:** 4. **Para Product Managers:**
- Leer ET-EDU-002 (API) y ET-EDU-003 (Frontend) - Todas las especificaciones contienen descripción y arquitectura
- Referencias: ET-EDU-004 (Video Player), ET-EDU-005 (Quiz UI), ET-EDU-006 (Gamification UI) - Ver sección de "Interfaces/Tipos" para data models
3. **Para DevOps:** ---
- Leer ET-EDU-001 (Database setup)
- Leer ET-EDU-004 (Video Infrastructure) ## Estado de Implementación
- Variables de entorno en cada especificación
| Especificación | Estado | Prioridad | Notas |
4. **Para Product Managers:** |---------------|--------|-----------|-------|
- Todas las especificaciones contienen descripción y arquitectura | ET-EDU-001 | Pendiente | Alta | Base de datos requerida primero |
- Ver sección de "Interfaces/Tipos" para data models | 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 |
## Estado de Implementación | ET-EDU-006 | Pendiente | Baja | Feature post-MVP |
| Especificación | Estado | Prioridad | Notas | ---
|---------------|--------|-----------|-------|
| ET-EDU-001 | Pendiente | Alta | Base de datos requerida primero | ## Próximos Pasos
| ET-EDU-002 | Pendiente | Alta | Depende de ET-EDU-001 |
| ET-EDU-003 | Pendiente | Alta | Depende de ET-EDU-002 | 1. **Revisión Técnica:** Validar especificaciones con el equipo de desarrollo
| ET-EDU-004 | Pendiente | Media | Puede iniciar en paralelo | 2. **Priorización:** Definir orden de implementación
| ET-EDU-005 | Pendiente | Media | Depende de ET-EDU-001, ET-EDU-002 | 3. **Estimación:** Calcular esfuerzo de desarrollo por especificación
| ET-EDU-006 | Pendiente | Baja | Feature post-MVP | 4. **Asignación:** Distribuir tareas entre el equipo
5. **Implementación:** Comenzar desarrollo siguiendo las especificaciones
---
---
## Próximos Pasos
## Contacto
1. **Revisión Técnica:** Validar especificaciones con el equipo de desarrollo
2. **Priorización:** Definir orden de implementación Para preguntas o aclaraciones sobre estas especificaciones, contactar al **Requirements Analyst** del proyecto.
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
**Última actualización:** 2025-12-05
--- **Versión del documento:** 1.0.0
## 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

View File

@ -1,326 +1,314 @@
--- # US-EDU-001: Ver Catálogo de Cursos
id: "US-EDU-001"
title: "Ver Catalogo de Cursos" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Alta" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-001 |
story_points: 3 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P0 |
--- | **Story Points** | 3 |
| **Sprint** | Sprint 3 |
# US-EDU-001: Ver Catálogo de Cursos | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-001 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario interesado en aprender trading,
| **Módulo** | education | **quiero** ver un catálogo completo de cursos disponibles con filtros y búsqueda,
| **Prioridad** | P0 | **para** descubrir contenido educativo relevante a mi nivel y áreas de interés.
| **Story Points** | 3 |
| **Sprint** | Sprint 3 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
┌─────────────────────────────────────────────────────────────────┐
**Como** usuario interesado en aprender trading, │ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
**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. │ │
│ CATÁLOGO DE CURSOS [🔍 Buscar cursos...] │
## 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. │ │ FILTROS │ │ Mostrando 24 de 47 cursos │ │
│ │ │ │ Ordenar: [Más recientes ▼] │ │
## Mockups/Wireframes │ │ Categorías │ │ │ │
│ │ □ Fundamentos │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│
``` │ │ ✓ Análisis T. │ │ │[IMG] │ │[IMG] │ │[IMG] ││
┌─────────────────────────────────────────────────────────────────┐ │ │ □ Gestión Riesgo│ │ │Fibonacci│ │Candlestk│ │Day Trad.││
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │ │ │ │ │ │básico │ │Avanzado │ │Pro ││
├─────────────────────────────────────────────────────────────────┤ │ │ Nivel │ │ │⭐ 4.8 │ │⭐ 4.9 │ │⭐ 4.7 ││
│ │ │ │ ✓ Principiante │ │ │2h 30m │ │4h 15m │ │6h 45m ││
│ CATÁLOGO DE CURSOS [🔍 Buscar cursos...] │ │ │ ✓ Intermedio │ │ │1,234 👥 │ │892 👥 │ │567 👥 ││
│ │ │ │ □ Avanzado │ │ │[Ver curso]│ │[60% ✓] │ │[Iniciar]││
│ ┌────────────────┐ ┌──────────────────────────────────────┐ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘│
│ │ FILTROS │ │ Mostrando 24 de 47 cursos │ │ │ │ Duración │ │ │ │
│ │ │ │ Ordenar: [Más recientes ▼] │ │ │ │ □ < 2 horas
│ │ Categorías │ │ │ │ │ │ ✓ 2-5 horas │ │ │ ...más cursos... ││
│ │ □ Fundamentos │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│ │ │ □ > 5 horas │ │ └─────────┘ └─────────┘ └─────────┘│
│ │ ✓ Análisis T. │ │ │[IMG] │ │[IMG] │ │[IMG] ││ │ │ │ │ │ │
│ │ □ Gestión Riesgo│ │ │Fibonacci│ │Candlestk│ │Day Trad.││ │ │ [Limpiar] │ │ [1] 2 3 4 ... 8 │ │
│ │ │ │ │básico │ │Avanzado │ │Pro ││ │ └────────────────┘ └──────────────────────────────────────┘ │
│ │ Nivel │ │ │⭐ 4.8 │ │⭐ 4.9 │ │⭐ 4.7 ││ │ │
│ │ ✓ Principiante │ │ │2h 30m │ │4h 15m │ │6h 45m ││ │ ┌──────────────────────────────────────────────────────────┐ │
│ │ ✓ Intermedio │ │ │1,234 👥 │ │892 👥 │ │567 👥 ││ │ │ 📚 Recomendado para ti │ │
│ │ □ Avanzado │ │ │[Ver curso]│ │[60% ✓] │ │[Iniciar]││ │ │ [Curso A] [Curso B] [Curso C] │ │
│ │ │ │ └─────────┘ └─────────┘ └─────────┘│ │ └──────────────────────────────────────────────────────────┘ │
│ │ Duración │ │ │ │ │ │
│ │ □ < 2 horas └─────────────────────────────────────────────────────────────────┘
│ │ ✓ 2-5 horas │ │ │ ...más cursos... ││ ```
│ │ □ > 5 horas │ │ └─────────┘ └─────────┘ └─────────┘│
│ │ │ │ │ │ ---
│ │ [Limpiar] │ │ [1] 2 3 4 ... 8 │ │
│ └────────────────┘ └──────────────────────────────────────┘ │ ## Criterios de Aceptación
│ │
│ ┌──────────────────────────────────────────────────────────┐ │ **Escenario 1: Ver catálogo completo**
│ │ 📚 Recomendado para ti │ │ ```gherkin
│ │ [Curso A] [Curso B] [Curso C] │ │ 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"
## Criterios de Aceptación ```
**Escenario 1: Ver catálogo completo** **Escenario 2: Filtrar por categoría**
```gherkin ```gherkin
DADO que el usuario está autenticado DADO que el usuario está en el catálogo
CUANDO navega a /education/courses CUANDO selecciona filtro "Análisis Técnico"
ENTONCES se muestra el catálogo de cursos ENTONCES solo se muestran cursos de esa categoría
Y se muestran 12 cursos por página Y el filtro se marca como activo (checkbox marcado)
Y cada curso muestra: imagen, título, instructor, duración, rating, estudiantes Y la URL se actualiza a ?category=technical-analysis
Y se muestran filtros en sidebar izquierdo Y el contador se actualiza "Mostrando X de Y cursos"
Y se muestra barra de búsqueda ```
Y se muestra contador "Mostrando X de Y cursos"
``` **Escenario 3: Filtrar por nivel**
```gherkin
**Escenario 2: Filtrar por categoría** DADO que el usuario está en el catálogo
```gherkin CUANDO selecciona "Principiante" e "Intermedio"
DADO que el usuario está en el catálogo ENTONCES solo se muestran cursos de esos niveles
CUANDO selecciona filtro "Análisis Técnico" Y se pueden combinar con otros filtros activos
ENTONCES solo se muestran cursos de esa categoría Y se muestra badge de nivel en cada curso
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 4: Buscar curso**
``` ```gherkin
DADO que el usuario está en el catálogo
**Escenario 3: Filtrar por nivel** CUANDO escribe "fibonacci" en el buscador
```gherkin ENTONCES se filtran cursos en tiempo real
DADO que el usuario está en el catálogo Y se muestran solo cursos que contengan "fibonacci" en título o descripción
CUANDO selecciona "Principiante" e "Intermedio" Y se resalta el término buscado en resultados
ENTONCES solo se muestran cursos de esos niveles Y se muestra "X resultados para 'fibonacci'"
Y se pueden combinar con otros filtros activos ```
Y se muestra badge de nivel en cada curso
``` **Escenario 5: Sin resultados**
```gherkin
**Escenario 4: Buscar curso** DADO que el usuario aplicó filtros
```gherkin Y no hay cursos que cumplan los criterios
DADO que el usuario está en el catálogo ENTONCES se muestra mensaje "No se encontraron cursos"
CUANDO escribe "fibonacci" en el buscador Y se sugiere "Intenta ajustar los filtros"
ENTONCES se filtran cursos en tiempo real Y se muestra botón "Limpiar filtros"
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 6: Ver progreso en curso inscrito**
``` ```gherkin
DADO que el usuario ya está inscrito en un curso
**Escenario 5: Sin resultados** CUANDO ve la tarjeta del curso en el catálogo
```gherkin ENTONCES se muestra barra de progreso (ej: "60% completado")
DADO que el usuario aplicó filtros Y el botón dice "Continuar" en lugar de "Ver curso"
Y no hay cursos que cumplan los criterios Y al hacer click, navega a la última lección vista
ENTONCES se muestra mensaje "No se encontraron cursos" ```
Y se sugiere "Intenta ajustar los filtros"
Y se muestra botón "Limpiar filtros" **Escenario 7: Ver recomendaciones**
``` ```gherkin
DADO que el usuario tiene cursos en progreso
**Escenario 6: Ver progreso en curso inscrito** CUANDO accede al catálogo
```gherkin ENTONCES se muestra sección "Recomendado para ti"
DADO que el usuario ya está inscrito en un curso Y aparecen máximo 6 cursos relacionados
CUANDO ve la tarjeta del curso en el catálogo Y se basa en: cursos en progreso, nivel del usuario, categorías de interés
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 8: Ordenar resultados**
``` ```gherkin
DADO que el usuario está viendo el catálogo
**Escenario 7: Ver recomendaciones** CUANDO selecciona ordenar por "Mejor valorados"
```gherkin ENTONCES los cursos se reordenan de mayor a menor rating
DADO que el usuario tiene cursos en progreso Y la paginación se mantiene
CUANDO accede al catálogo Y los filtros activos se mantienen
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 ## Criterios Adicionales
```
- [ ] Responsive design para móvil y tablet
**Escenario 8: Ordenar resultados** - [ ] Loading skeleton mientras cargan cursos
```gherkin - [ ] Infinite scroll opcional (además de paginación)
DADO que el usuario está viendo el catálogo - [ ] Animaciones suaves al filtrar
CUANDO selecciona ordenar por "Mejor valorados" - [ ] Badge "Nuevo" para cursos publicados hace < 30 días
ENTONCES los cursos se reordenan de mayor a menor rating - [ ] Badge "Popular" para cursos con > 1000 estudiantes
Y la paginación se mantiene - [ ] Guardar filtros en localStorage para próxima visita
Y los filtros activos se mantienen
``` ---
## Criterios Adicionales ## Tareas Técnicas
- [ ] Responsive design para móvil y tablet **Database:**
- [ ] Loading skeleton mientras cargan cursos - [ ] DB-EDU-001: Verificar schema education.courses
- [ ] Infinite scroll opcional (además de paginación) - [ ] DB-EDU-002: Verificar índices en category_id, level, published_at
- [ ] Animaciones suaves al filtrar - [ ] DB-EDU-003: Vista courses_catalog con joins optimizados
- [ ] Badge "Nuevo" para cursos publicados hace < 30 días
- [ ] Badge "Popular" para cursos con > 1000 estudiantes **Backend:**
- [ ] Guardar filtros en localStorage para próxima visita - [ ] 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
## Tareas Técnicas - [ ] BE-EDU-005: Implementar CourseService.getCatalog()
- [ ] BE-EDU-006: Implementar lógica de recomendaciones
**Database:** - [ ] BE-EDU-007: Caché de catálogo en Redis (5 min)
- [ ] DB-EDU-001: Verificar schema education.courses
- [ ] DB-EDU-002: Verificar índices en category_id, level, published_at **Frontend:**
- [ ] DB-EDU-003: Vista courses_catalog con joins optimizados - [ ] FE-EDU-001: Crear página CoursesPage.tsx
- [ ] FE-EDU-002: Crear componente CourseCard.tsx
**Backend:** - [ ] FE-EDU-003: Crear componente CourseFilters.tsx
- [ ] BE-EDU-001: Endpoint GET /education/courses (con paginación) - [ ] FE-EDU-004: Crear componente SearchBar.tsx
- [ ] BE-EDU-002: Implementar filtros: category, level, duration, search - [ ] FE-EDU-005: Crear componente Pagination.tsx
- [ ] BE-EDU-003: Implementar ordenamiento: recent, popular, rating - [ ] FE-EDU-006: Implementar coursesStore (Zustand)
- [ ] BE-EDU-004: Endpoint GET /education/categories - [ ] FE-EDU-007: Integrar con React Query para caché
- [ ] BE-EDU-005: Implementar CourseService.getCatalog() - [ ] FE-EDU-008: Skeleton loader para carga
- [ ] BE-EDU-006: Implementar lógica de recomendaciones
- [ ] BE-EDU-007: Caché de catálogo en Redis (5 min) **Tests:**
- [ ] TEST-EDU-001: Test unitario CourseService.getCatalog()
**Frontend:** - [ ] TEST-EDU-002: Test integración GET /courses con filtros
- [ ] FE-EDU-001: Crear página CoursesPage.tsx - [ ] TEST-EDU-003: Test E2E búsqueda y filtrado
- [ ] 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 ## Dependencias
- [ ] FE-EDU-006: Implementar coursesStore (Zustand)
- [ ] FE-EDU-007: Integrar con React Query para caché **Depende de:**
- [ ] FE-EDU-008: Skeleton loader para carga - [ ] US-AUTH-001: Sistema de autenticación - Estado: ✅ Completado
**Tests:** **Bloquea:**
- [ ] TEST-EDU-001: Test unitario CourseService.getCatalog() - [ ] US-EDU-002: Ver detalle de curso
- [ ] TEST-EDU-002: Test integración GET /courses con filtros - [ ] US-EDU-003: Iniciar lección
- [ ] TEST-EDU-003: Test E2E búsqueda y filtrado
---
---
## Notas Técnicas
## Dependencias
**Endpoints involucrados:**
**Depende de:** | Método | Endpoint | Descripción |
- [ ] US-AUTH-001: Sistema de autenticación - Estado: ✅ Completado |--------|----------|-------------|
| GET | /education/courses | Catálogo con filtros y paginación |
**Bloquea:** | GET | /education/categories | Listado de categorías |
- [ ] US-EDU-002: Ver detalle de curso
- [ ] US-EDU-003: Iniciar lección **Query params para GET /courses:**
```
--- ?page=1
&limit=12
## Notas Técnicas &category=technical-analysis,fundamentals
&level=beginner,intermediate
**Endpoints involucrados:** &search=fibonacci
| Método | Endpoint | Descripción | &sortBy=recent
|--------|----------|-------------| &sortOrder=desc
| GET | /education/courses | Catálogo con filtros y paginación | ```
| GET | /education/categories | Listado de categorías |
**Response GET /courses:**
**Query params para GET /courses:** ```typescript
``` {
?page=1 courses: [
&limit=12 {
&category=technical-analysis,fundamentals id: "uuid-1",
&level=beginner,intermediate title: "Fibonacci Retracement Básico",
&search=fibonacci slug: "fibonacci-retracement-basico",
&sortBy=recent shortDescription: "Aprende a usar Fibonacci...",
&sortOrder=desc thumbnail: "https://cdn.orbiquant.com/courses/fib.jpg",
``` category: {
id: "cat-1",
**Response GET /courses:** name: "Análisis Técnico",
```typescript slug: "technical-analysis",
{ icon: "📊"
courses: [ },
{ level: "beginner",
id: "uuid-1", duration: 150, // minutos
title: "Fibonacci Retracement Básico", moduleCount: 5,
slug: "fibonacci-retracement-basico", lessonCount: 23,
shortDescription: "Aprende a usar Fibonacci...", studentCount: 1234,
thumbnail: "https://cdn.orbiquant.com/courses/fib.jpg", rating: 4.8,
category: { reviewCount: 89,
id: "cat-1", instructor: {
name: "Análisis Técnico", id: "inst-1",
slug: "technical-analysis", name: "Carlos Mendoza",
icon: "📊" avatar: "https://...",
}, title: "Senior Trader"
level: "beginner", },
duration: 150, // minutos isPremium: false,
moduleCount: 5, publishedAt: "2025-11-15T10:00:00Z",
lessonCount: 23, userProgress: {
studentCount: 1234, enrolledAt: "2025-12-01T14:30:00Z",
rating: 4.8, progressPercent: 60,
reviewCount: 89, lastAccessedAt: "2025-12-04T18:20:00Z"
instructor: { }
id: "inst-1", }
name: "Carlos Mendoza", // ... más cursos
avatar: "https://...", ],
title: "Senior Trader" pagination: {
}, page: 1,
isPremium: false, limit: 12,
publishedAt: "2025-11-15T10:00:00Z", total: 47,
userProgress: { totalPages: 4
enrolledAt: "2025-12-01T14:30:00Z", },
progressPercent: 60, filters: {
lastAccessedAt: "2025-12-04T18:20:00Z" categories: [...],
} levels: ["beginner", "intermediate", "advanced", "expert"]
} }
// ... más cursos }
], ```
pagination: {
page: 1, **Entidades/Tablas:**
limit: 12, - `education.courses`: Catálogo de cursos
total: 47, - `education.categories`: Categorías
totalPages: 4 - `education.course_enrollments`: Inscripciones y progreso
}, - `education.instructors`: Información de instructores
filters: {
categories: [...], ---
levels: ["beginner", "intermediate", "advanced", "expert"]
} ## Definition of Ready (DoR)
}
``` - [x] Historia claramente escrita
- [x] Criterios de aceptación definidos
**Entidades/Tablas:** - [x] Story points estimados
- `education.courses`: Catálogo de cursos - [x] Dependencias identificadas
- `education.categories`: Categorías - [x] Sin bloqueadores
- `education.course_enrollments`: Inscripciones y progreso - [x] Diseño/mockup disponible
- `education.instructors`: Información de instructores - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,376 +1,364 @@
--- # US-EDU-002: Ver Detalle de Curso
id: "US-EDU-002"
title: "Ver Detalle de Curso" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Alta" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-002 |
story_points: 3 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P0 |
--- | **Story Points** | 3 |
| **Sprint** | Sprint 3 |
# US-EDU-002: Ver Detalle de Curso | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-002 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario interesado en un curso,
| **Módulo** | education | **quiero** ver información detallada del curso antes de inscribirme,
| **Prioridad** | P0 | **para** evaluar si el contenido se ajusta a mis objetivos de aprendizaje.
| **Story Points** | 3 |
| **Sprint** | Sprint 3 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
┌─────────────────────────────────────────────────────────────────┐
**Como** usuario interesado en un curso, │ ← Volver al catálogo 👤 │
**quiero** ver información detallada del curso antes de inscribirme, ├─────────────────────────────────────────────────────────────────┤
**para** evaluar si el contenido se ajusta a mis objetivos de aprendizaje. │ │
│ ┌────────────────────┐ FIBONACCI RETRACEMENT BÁSICO │
## Descripción Detallada │ │ │ ⭐ 4.8 (89 reseñas) | 1,234 estudiantes│
│ │ [VIDEO INTRO] │ Por: Carlos Mendoza | 2h 30m | 5 módul│
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. │ │ ▶ Preview │ │
│ │ │ [📥 Inscribirse gratis] │
## Mockups/Wireframes │ └────────────────────┘ │
│ │
``` │ ┌────────────────────────────────────────────────────────────┐ │
┌─────────────────────────────────────────────────────────────────┐ │ │ PESTAÑAS: [Descripción] [Temario] [Instructor] [Reseñas] │ │
│ ← Volver al catálogo 👤 │ │ └────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤ │ │
│ │ │ QUÉ APRENDERÁS │
│ ┌────────────────────┐ FIBONACCI RETRACEMENT BÁSICO │ │ ✓ Identificar niveles de Fibonacci en gráficos │
│ │ │ ⭐ 4.8 (89 reseñas) | 1,234 estudiantes│ │ ✓ Aplicar retrocesos en tendencias alcistas y bajistas │
│ │ [VIDEO INTRO] │ Por: Carlos Mendoza | 2h 30m | 5 módul│ │ ✓ Combinar Fibonacci con otros indicadores │
│ │ ▶ Preview │ │ │ ✓ Realizar entradas y salidas precisas │
│ │ │ [📥 Inscribirse gratis] │ │ │
│ └────────────────────┘ │ │ REQUISITOS │
│ │ │ • Conocimientos básicos de trading │
│ ┌────────────────────────────────────────────────────────────┐ │ │ • Familiaridad con gráficos de velas │
│ │ PESTAÑAS: [Descripción] [Temario] [Instructor] [Reseñas] │ │ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ DESCRIPCIÓN │
│ │ │ Fibonacci es una herramienta fundamental del análisis técnico...│
│ QUÉ APRENDERÁS │ │ [Texto completo de descripción del curso] │
│ ✓ Identificar niveles de Fibonacci en gráficos │ │ │
│ ✓ Aplicar retrocesos en tendencias alcistas y bajistas │ │ PARA QUIÉN ES ESTE CURSO │
│ ✓ Combinar Fibonacci con otros indicadores │ │ • Traders principiantes que quieren dominar Fibonacci │
│ ✓ Realizar entradas y salidas precisas │ │ • Analistas técnicos que buscan mejorar su precisión │
│ │ │ │
│ REQUISITOS │ └─────────────────────────────────────────────────────────────────┘
│ • Conocimientos básicos de trading │
│ • Familiaridad con gráficos de velas │ [PESTAÑA TEMARIO]
│ │ ┌─────────────────────────────────────────────────────────────────┐
│ DESCRIPCIÓN │ │ 📚 TEMARIO DEL CURSO 5 módulos | 23 lecciones │
│ Fibonacci es una herramienta fundamental del análisis técnico...│ │ │
│ [Texto completo de descripción del curso] │ │ ▼ Módulo 1: Introducción a Fibonacci (30 min) │
│ │ │ 1. ¿Qué es Fibonacci? (5:30) [🎬 Video] [Gratis] │
│ PARA QUIÉN ES ESTE CURSO │ │ 2. Historia y fundamentos (8:45) [🎬 Video] [Gratis] │
│ • Traders principiantes que quieren dominar Fibonacci │ │ 3. La secuencia de Fibonacci (10:20) [📄 Artículo] │
│ • Analistas técnicos que buscan mejorar su precisión │ │ 4. Quiz: Fundamentos (15 preguntas) [📝 Quiz] │
│ │ │ │
└─────────────────────────────────────────────────────────────────┘ │ ▼ Módulo 2: Niveles de Retroceso (45 min) │
│ 1. Niveles principales: 38.2%, 50%, 61.8% (12:15) │
[PESTAÑA TEMARIO] │ 2. Cómo dibujar Fibonacci en gráfico (18:30) │
┌─────────────────────────────────────────────────────────────────┐ │ 3. Práctica: Identificar retrocesos (20:00) │
│ 📚 TEMARIO DEL CURSO 5 módulos | 23 lecciones │ │ 4. Quiz: Niveles de retroceso │
│ │ │ │
│ ▼ Módulo 1: Introducción a Fibonacci (30 min) │ │ ▶ Módulo 3: Fibonacci en Tendencias (35 min) [🔒 Bloqueado] │
│ 1. ¿Qué es Fibonacci? (5:30) [🎬 Video] [Gratis] │ │ ▶ Módulo 4: Estrategias Avanzadas (25 min) [🔒 Bloqueado] │
│ 2. Historia y fundamentos (8:45) [🎬 Video] [Gratis] │ │ ▶ Módulo 5: Fibonacci + Otros Indicadores (15 min) [🔒] │
│ 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) │ ## Criterios de Aceptación
│ 4. Quiz: Niveles de retroceso │
│ │ **Escenario 1: Ver detalle de curso no inscrito**
│ ▶ Módulo 3: Fibonacci en Tendencias (35 min) [🔒 Bloqueado] │ ```gherkin
│ ▶ Módulo 4: Estrategias Avanzadas (25 min) [🔒 Bloqueado] │ DADO que el usuario está autenticado
│ ▶ Módulo 5: Fibonacci + Otros Indicadores (15 min) [🔒] │ 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
## Criterios de Aceptación Y se muestra temario desglosado por módulos
```
**Escenario 1: Ver detalle de curso no inscrito**
```gherkin **Escenario 2: Ver detalle de curso inscrito**
DADO que el usuario está autenticado ```gherkin
Y NO está inscrito en el curso DADO que el usuario está inscrito en el curso
CUANDO accede a /education/courses/fibonacci-retracement-basico Y tiene 60% de progreso
ENTONCES se muestra la página de detalle del curso CUANDO accede al detalle del curso
Y se muestra: título, rating, estudiantes, instructor, duración ENTONCES se muestra barra de progreso "60% completado"
Y se muestra botón "Inscribirse" o "Inscribirse gratis" Y el botón principal dice "Continuar curso"
Y se muestra video preview del curso (si existe) Y se destaca la última lección accedida
Y se muestra descripción completa Y se muestra resumen de progreso: "14 de 23 lecciones completadas"
Y se muestra temario desglosado por módulos ```
```
**Escenario 3: Ver temario completo**
**Escenario 2: Ver detalle de curso inscrito** ```gherkin
```gherkin DADO que el usuario está en la pestaña "Temario"
DADO que el usuario está inscrito en el curso ENTONCES se muestran todos los módulos del curso
Y tiene 60% de progreso Y cada módulo muestra: título, duración total, número de lecciones
CUANDO accede al detalle del curso Y cada lección muestra: título, duración, tipo (video/artículo/quiz)
ENTONCES se muestra barra de progreso "60% completado" Y se puede expandir/colapsar cada módulo
Y el botón principal dice "Continuar curso" Y lecciones gratuitas están marcadas como "Gratis"
Y se destaca la última lección accedida Y lecciones bloqueadas muestran candado 🔒
Y se muestra resumen de progreso: "14 de 23 lecciones completadas" ```
```
**Escenario 4: Preview de lección gratuita**
**Escenario 3: Ver temario completo** ```gherkin
```gherkin DADO que el usuario NO está inscrito
DADO que el usuario está en la pestaña "Temario" Y el curso tiene lecciones marcadas como "Gratis"
ENTONCES se muestran todos los módulos del curso CUANDO hace click en una lección gratuita
Y cada módulo muestra: título, duración total, número de lecciones ENTONCES puede ver el contenido sin inscribirse
Y cada lección muestra: título, duración, tipo (video/artículo/quiz) Y se muestra CTA "Inscríbete para acceder a todo el contenido"
Y se puede expandir/colapsar cada módulo ```
Y lecciones gratuitas están marcadas como "Gratis"
Y lecciones bloqueadas muestran candado 🔒 **Escenario 5: Ver información del instructor**
``` ```gherkin
DADO que el usuario está en la pestaña "Instructor"
**Escenario 4: Preview de lección gratuita** ENTONCES se muestra: foto, nombre, título, biografía
```gherkin Y se muestra lista de otros cursos del instructor
DADO que el usuario NO está inscrito Y se muestran estadísticas: total estudiantes, cursos, rating promedio
Y el curso tiene lecciones marcadas como "Gratis" Y se muestra botón "Ver perfil completo" (opcional)
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 6: Ver reseñas de estudiantes**
``` ```gherkin
DADO que el usuario está en la pestaña "Reseñas"
**Escenario 5: Ver información del instructor** ENTONCES se muestra rating promedio (ej: 4.8/5)
```gherkin Y se muestra distribución de ratings (5★: 60%, 4★: 30%, etc.)
DADO que el usuario está en la pestaña "Instructor" Y se muestran las últimas 10 reseñas
ENTONCES se muestra: foto, nombre, título, biografía Y cada reseña muestra: nombre del usuario, fecha, rating, comentario
Y se muestra lista de otros cursos del instructor Y se puede filtrar por: Todas, 5★, 4★, 3★, 2★, 1★
Y se muestran estadísticas: total estudiantes, cursos, rating promedio ```
Y se muestra botón "Ver perfil completo" (opcional)
``` **Escenario 7: Inscribirse en curso**
```gherkin
**Escenario 6: Ver reseñas de estudiantes** DADO que el usuario NO está inscrito
```gherkin Y el curso es gratuito
DADO que el usuario está en la pestaña "Reseñas" CUANDO hace click en "Inscribirse gratis"
ENTONCES se muestra rating promedio (ej: 4.8/5) ENTONCES se registra la inscripción en backend
Y se muestra distribución de ratings (5★: 60%, 4★: 30%, etc.) Y se muestra toast "¡Te has inscrito en el curso!"
Y se muestran las últimas 10 reseñas Y el botón cambia a "Comenzar curso"
Y cada reseña muestra: nombre del usuario, fecha, rating, comentario Y se navega a la primera lección al hacer click
Y se puede filtrar por: Todas, 5★, 4★, 3★, 2★, 1★ ```
```
**Escenario 8: Curso premium sin suscripción**
**Escenario 7: Inscribirse en curso** ```gherkin
```gherkin DADO que el curso es premium
DADO que el usuario NO está inscrito Y el usuario NO tiene suscripción activa
Y el curso es gratuito CUANDO intenta inscribirse
CUANDO hace click en "Inscribirse gratis" ENTONCES se muestra modal "Este curso requiere suscripción Premium"
ENTONCES se registra la inscripción en backend Y se muestra botón "Ver planes" que lleva a /pricing
Y se muestra toast "¡Te has inscrito en el curso!" Y NO se permite inscripción
Y el botón cambia a "Comenzar curso" ```
Y se navega a la primera lección al hacer click
``` ## Criterios Adicionales
**Escenario 8: Curso premium sin suscripción** - [ ] Video preview auto-play al cargar página (muted)
```gherkin - [ ] Compartir curso en redes sociales
DADO que el curso es premium - [ ] Agregar curso a "Guardados" (wishlist)
Y el usuario NO tiene suscripción activa - [ ] Mostrar badge "Bestseller" si es top 10 más vendido
CUANDO intenta inscribirse - [ ] Mostrar "Última actualización: DD/MM/YYYY"
ENTONCES se muestra modal "Este curso requiere suscripción Premium" - [ ] SEO optimizado con meta tags dinámicos
Y se muestra botón "Ver planes" que lleva a /pricing - [ ] Structured data (schema.org) para Google
Y NO se permite inscripción
``` ---
## Criterios Adicionales ## Tareas Técnicas
- [ ] Video preview auto-play al cargar página (muted) **Database:**
- [ ] Compartir curso en redes sociales - [ ] DB-EDU-004: Verificar relación courses -> modules -> lessons
- [ ] Agregar curso a "Guardados" (wishlist) - [ ] DB-EDU-005: Tabla course_reviews para reseñas
- [ ] Mostrar badge "Bestseller" si es top 10 más vendido - [ ] DB-EDU-006: Tabla course_enrollments para inscripciones
- [ ] Mostrar "Última actualización: DD/MM/YYYY"
- [ ] SEO optimizado con meta tags dinámicos **Backend:**
- [ ] Structured data (schema.org) para Google - [ ] 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()
## Tareas Técnicas - [ ] BE-EDU-012: Implementar EnrollmentService.enroll()
- [ ] BE-EDU-013: Verificar acceso Premium para cursos pagos
**Database:**
- [ ] DB-EDU-004: Verificar relación courses -> modules -> lessons **Frontend:**
- [ ] DB-EDU-005: Tabla course_reviews para reseñas - [ ] FE-EDU-009: Crear página CourseDetailPage.tsx
- [ ] DB-EDU-006: Tabla course_enrollments para inscripciones - [ ] FE-EDU-010: Crear componente CourseHeader.tsx
- [ ] FE-EDU-011: Crear componente CourseSyllabus.tsx (temario)
**Backend:** - [ ] FE-EDU-012: Crear componente InstructorCard.tsx
- [ ] BE-EDU-008: Endpoint GET /education/courses/:slug - [ ] FE-EDU-013: Crear componente CourseReviews.tsx
- [ ] BE-EDU-009: Endpoint POST /education/courses/:id/enroll - [ ] FE-EDU-014: Crear componente EnrollButton.tsx
- [ ] BE-EDU-010: Endpoint GET /education/courses/:id/reviews - [ ] FE-EDU-015: Implementar tabs de navegación
- [ ] BE-EDU-011: Implementar CourseService.getBySlug() - [ ] FE-EDU-016: Modal de confirmación de inscripción
- [ ] BE-EDU-012: Implementar EnrollmentService.enroll()
- [ ] BE-EDU-013: Verificar acceso Premium para cursos pagos **Tests:**
- [ ] TEST-EDU-004: Test inscripción en curso gratuito
**Frontend:** - [ ] TEST-EDU-005: Test bloqueo de curso premium
- [ ] FE-EDU-009: Crear página CourseDetailPage.tsx - [ ] TEST-EDU-006: Test E2E ver detalle e inscribirse
- [ ] 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 ## Dependencias
- [ ] FE-EDU-014: Crear componente EnrollButton.tsx
- [ ] FE-EDU-015: Implementar tabs de navegación **Depende de:**
- [ ] FE-EDU-016: Modal de confirmación de inscripción - [ ] US-EDU-001: Ver catálogo - Estado: Pendiente
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
**Tests:**
- [ ] TEST-EDU-004: Test inscripción en curso gratuito **Bloquea:**
- [ ] TEST-EDU-005: Test bloqueo de curso premium - [ ] US-EDU-003: Iniciar lección
- [ ] TEST-EDU-006: Test E2E ver detalle e inscribirse - [ ] US-EDU-007: Ver progreso
--- ---
## Dependencias ## Notas Técnicas
**Depende de:** **Endpoints involucrados:**
- [ ] US-EDU-001: Ver catálogo - Estado: Pendiente | Método | Endpoint | Descripción |
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado |--------|----------|-------------|
| GET | /education/courses/:slug | Detalle completo del curso |
**Bloquea:** | POST | /education/courses/:id/enroll | Inscribirse en curso |
- [ ] US-EDU-003: Iniciar lección | GET | /education/courses/:id/reviews | Reseñas del curso |
- [ ] US-EDU-007: Ver progreso
**Response GET /courses/:slug:**
--- ```typescript
{
## Notas Técnicas course: {
id: "uuid-1",
**Endpoints involucrados:** title: "Fibonacci Retracement Básico",
| Método | Endpoint | Descripción | slug: "fibonacci-retracement-basico",
|--------|----------|-------------| description: "Descripción completa del curso...",
| GET | /education/courses/:slug | Detalle completo del curso | learningObjectives: [
| POST | /education/courses/:id/enroll | Inscribirse en curso | "Identificar niveles de Fibonacci",
| GET | /education/courses/:id/reviews | Reseñas del curso | "Aplicar retrocesos en tendencias"
],
**Response GET /courses/:slug:** requirements: [
```typescript "Conocimientos básicos de trading",
{ "Familiaridad con gráficos"
course: { ],
id: "uuid-1", targetAudience: [
title: "Fibonacci Retracement Básico", "Traders principiantes",
slug: "fibonacci-retracement-basico", "Analistas técnicos"
description: "Descripción completa del curso...", ],
learningObjectives: [ category: {...},
"Identificar niveles de Fibonacci", level: "beginner",
"Aplicar retrocesos en tendencias" duration: 150,
], moduleCount: 5,
requirements: [ lessonCount: 23,
"Conocimientos básicos de trading", studentCount: 1234,
"Familiaridad con gráficos" rating: 4.8,
], reviewCount: 89,
targetAudience: [ instructor: {
"Traders principiantes", id: "inst-1",
"Analistas técnicos" name: "Carlos Mendoza",
], avatar: "...",
category: {...}, title: "Senior Trader",
level: "beginner", bio: "15 años de experiencia...",
duration: 150, coursesCount: 8,
moduleCount: 5, studentsCount: 12500,
lessonCount: 23, rating: 4.9
studentCount: 1234, },
rating: 4.8, isPremium: false,
reviewCount: 89, previewVideoUrl: "https://vimeo.com/...",
instructor: { updatedAt: "2025-11-20T10:00:00Z",
id: "inst-1", publishedAt: "2025-11-15T10:00:00Z",
name: "Carlos Mendoza",
avatar: "...", modules: [
title: "Senior Trader", {
bio: "15 años de experiencia...", id: "mod-1",
coursesCount: 8, title: "Introducción a Fibonacci",
studentsCount: 12500, order: 1,
rating: 4.9 duration: 30,
}, lessonCount: 4,
isPremium: false, lessons: [
previewVideoUrl: "https://vimeo.com/...", {
updatedAt: "2025-11-20T10:00:00Z", id: "les-1",
publishedAt: "2025-11-15T10:00:00Z", title: "¿Qué es Fibonacci?",
type: "video",
modules: [ duration: 5.5,
{ isFree: true,
id: "mod-1", isCompleted: false,
title: "Introducción a Fibonacci", order: 1
order: 1, }
duration: 30, // ... más lecciones
lessonCount: 4, ]
lessons: [ }
{ // ... más módulos
id: "les-1", ],
title: "¿Qué es Fibonacci?",
type: "video", userEnrollment: {
duration: 5.5, enrolledAt: "2025-12-01T14:30:00Z",
isFree: true, progressPercent: 60,
isCompleted: false, lessonsCompleted: 14,
order: 1 lastAccessedLesson: {
} id: "les-14",
// ... más lecciones title: "Fibonacci en tendencias bajistas"
] },
} lastAccessedAt: "2025-12-04T18:20:00Z"
// ... más módulos }
], }
}
userEnrollment: { ```
enrolledAt: "2025-12-01T14:30:00Z",
progressPercent: 60, **Entidades/Tablas:**
lessonsCompleted: 14, - `education.courses`
lastAccessedLesson: { - `education.modules`
id: "les-14", - `education.lessons`
title: "Fibonacci en tendencias bajistas" - `education.course_enrollments`
}, - `education.course_reviews`
lastAccessedAt: "2025-12-04T18:20:00Z" - `education.instructors`
}
} ---
}
``` ## Definition of Ready (DoR)
**Entidades/Tablas:** - [x] Historia claramente escrita
- `education.courses` - [x] Criterios de aceptación definidos
- `education.modules` - [x] Story points estimados
- `education.lessons` - [x] Dependencias identificadas
- `education.course_enrollments` - [x] Sin bloqueadores
- `education.course_reviews` - [x] Diseño/mockup disponible
- `education.instructors` - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,372 +1,360 @@
--- # US-EDU-003: Iniciar Lección
id: "US-EDU-003"
title: "Iniciar Leccion" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Alta" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-003 |
story_points: 3 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P0 |
--- | **Story Points** | 3 |
| **Sprint** | Sprint 3 |
# US-EDU-003: Iniciar Lección | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-003 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario inscrito en un curso,
| **Módulo** | education | **quiero** acceder y comenzar una lección específica,
| **Prioridad** | P0 | **para** consumir el contenido educativo y avanzar en mi aprendizaje.
| **Story Points** | 3 |
| **Sprint** | Sprint 3 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
┌─────────────────────────────────────────────────────────────────┐
**Como** usuario inscrito en un curso, │ ← Volver al curso [≡ Temario] 👤 │
**quiero** acceder y comenzar una lección específica, ├─────────────────────────────────────────────────────────────────┤
**para** consumir el contenido educativo y avanzar en mi aprendizaje. │ │
│ Módulo 2: Niveles de Retroceso │
## Descripción Detallada │ Lección 1/4: Niveles principales (38.2%, 50%, 61.8%) │
│ Progreso del curso: ████████░░░░░░░░ 45% │
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 │ │ │ │
│ │ [REPRODUCTOR DE VIDEO] │ │
``` │ │ │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ ▶ PLAY │ │
│ ← Volver al curso [≡ Temario] 👤 │ │ │ │ │
├─────────────────────────────────────────────────────────────────┤ │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12:15 │ │
│ │ │ │ ⏮ ⏸ ⏭ 🔊──── ⚙ 1x CC ⛶ │ │
│ Módulo 2: Niveles de Retroceso │ │ └────────────────────────────────────────────────────────────┘ │
│ Lección 1/4: Niveles principales (38.2%, 50%, 61.8%) │ │ │
│ Progreso del curso: ████████░░░░░░░░ 45% │ │ ┌─ PESTAÑAS ────────────────────────────────────────────────┐ │
│ │ │ │ [Descripción] [Recursos] [Mis Notas] [Q&A] │ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ └───────────────────────────────────────────────────────────┘ │
│ │ │ │ │ │
│ │ [REPRODUCTOR DE VIDEO] │ │ │ 📝 DESCRIPCIÓN │
│ │ │ │ │ En esta lección aprenderás los tres niveles principales de │
│ │ ▶ PLAY │ │ │ Fibonacci: 38.2%, 50% y 61.8%. Veremos cómo identificarlos... │
│ │ │ │ │ │
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12:15 │ │ │ 📥 RECURSOS (2) │
│ │ ⏮ ⏸ ⏭ 🔊──── ⚙ 1x CC ⛶ │ │ │ • Plantilla de Fibonacci.xlsx (45 KB) [Descargar] │
│ └────────────────────────────────────────────────────────────┘ │ │ • Cheatsheet de niveles.pdf (120 KB) [Descargar] │
│ │ │ │
│ ┌─ PESTAÑAS ────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ [Descripción] [Recursos] [Mis Notas] [Q&A] │ │ │ │ [← Anterior: Historia y fundamentos] │ │
│ └───────────────────────────────────────────────────────────┘ │ │ │ [Siguiente: Cómo dibujar Fibonacci →] │ │
│ │ │ │ [✓ Marcar como completada] │ │
│ 📝 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) │ [SIDEBAR TEMARIO - Desplegable]
│ • Plantilla de Fibonacci.xlsx (45 KB) [Descargar] │ ┌────────────────────────┐
│ • Cheatsheet de niveles.pdf (120 KB) [Descargar] │ │ TEMARIO │
│ │ │ ✓ Módulo 1 (4/4) ✓ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ▼ Módulo 2 (1/4) │
│ │ [← Anterior: Historia y fundamentos] │ │ │ ✓ 1. Niveles princ. │ ← Lección actual
│ │ [Siguiente: Cómo dibujar Fibonacci →] │ │ │ ○ 2. Dibujar Fib. │
│ │ [✓ Marcar como completada] │ │ │ 🔒 3. Práctica │
│ └────────────────────────────────────────────────────────────┘ │ │ 🔒 4. Quiz │
│ │ │ 🔒 Módulo 3 │
└─────────────────────────────────────────────────────────────────┘ │ 🔒 Módulo 4 │
│ 🔒 Módulo 5 │
[SIDEBAR TEMARIO - Desplegable] └────────────────────────┘
┌────────────────────────┐ ```
│ TEMARIO │
│ ✓ Módulo 1 (4/4) ✓ │ ---
│ ▼ Módulo 2 (1/4) │
│ ✓ 1. Niveles princ. │ ← Lección actual ## Criterios de Aceptación
│ ○ 2. Dibujar Fib. │
│ 🔒 3. Práctica │ **Escenario 1: Iniciar lección desbloqueada**
│ 🔒 4. Quiz │ ```gherkin
│ 🔒 Módulo 3 │ DADO que el usuario está inscrito en el curso
│ 🔒 Módulo 4 │ Y la lección está desbloqueada
│ 🔒 Módulo 5 │ 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
## Criterios de Aceptación ```
**Escenario 1: Iniciar lección desbloqueada** **Escenario 2: Intentar acceder a lección bloqueada**
```gherkin ```gherkin
DADO que el usuario está inscrito en el curso DADO que el curso requiere orden secuencial
Y la lección está desbloqueada Y el usuario NO ha completado la lección anterior
CUANDO hace click en la lección desde el temario CUANDO intenta acceder a una lección bloqueada
ENTONCES se navega a /education/courses/:slug/lessons/:lessonSlug ENTONCES se muestra modal "Debes completar lecciones anteriores"
Y se carga el contenido de la lección Y se sugiere la última lección disponible
Y se registra en backend que el usuario inició la lección Y NO se carga el contenido
Y se actualiza "última lección accedida" ```
Y se muestra sidebar con temario completo
``` **Escenario 3: Lección de video carga posición guardada**
```gherkin
**Escenario 2: Intentar acceder a lección bloqueada** DADO que el usuario vio 5:30 de un video de 12:15
```gherkin Y cerró la lección sin terminar
DADO que el curso requiere orden secuencial CUANDO vuelve a abrir la lección
Y el usuario NO ha completado la lección anterior ENTONCES el video se posiciona en 5:30
CUANDO intenta acceder a una lección bloqueada Y se muestra toast "Continuando desde 5:30"
ENTONCES se muestra modal "Debes completar lecciones anteriores" Y se puede resetear posición si el usuario lo desea
Y se sugiere la última lección disponible ```
Y NO se carga el contenido
``` **Escenario 4: Usuario no inscrito intenta acceder**
```gherkin
**Escenario 3: Lección de video carga posición guardada** DADO que el usuario NO está inscrito en el curso
```gherkin CUANDO intenta acceder directamente a una lección
DADO que el usuario vio 5:30 de un video de 12:15 ENTONCES se redirige a la página del curso
Y cerró la lección sin terminar Y se muestra mensaje "Inscríbete para acceder a este curso"
CUANDO vuelve a abrir la lección Y se muestra botón "Inscribirse"
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 5: Ver recursos de la lección**
``` ```gherkin
DADO que la lección tiene recursos descargables
**Escenario 4: Usuario no inscrito intenta acceder** CUANDO el usuario accede a la pestaña "Recursos"
```gherkin ENTONCES se muestran todos los archivos disponibles
DADO que el usuario NO está inscrito en el curso Y se muestra: nombre, tipo, tamaño
CUANDO intenta acceder directamente a una lección Y puede descargar cada archivo con un click
ENTONCES se redirige a la página del curso Y se registra la descarga en analytics
Y se muestra mensaje "Inscríbete para acceder a este curso" ```
Y se muestra botón "Inscribirse"
``` **Escenario 6: Tomar notas durante lección**
```gherkin
**Escenario 5: Ver recursos de la lección** DADO que el usuario está en una lección
```gherkin CUANDO accede a la pestaña "Mis Notas"
DADO que la lección tiene recursos descargables Y escribe texto en el editor
CUANDO el usuario accede a la pestaña "Recursos" ENTONCES las notas se guardan automáticamente cada 2s
ENTONCES se muestran todos los archivos disponibles Y se muestra indicador "Guardado" cuando se persiste
Y se muestra: nombre, tipo, tamaño Y para videos se guarda el timestamp actual
Y puede descargar cada archivo con un click ```
Y se registra la descarga en analytics
``` **Escenario 7: Navegar entre lecciones**
```gherkin
**Escenario 6: Tomar notas durante lección** DADO que el usuario está en una lección
```gherkin Y existe una lección siguiente
DADO que el usuario está en una lección CUANDO hace click en "Siguiente"
CUANDO accede a la pestaña "Mis Notas" ENTONCES navega a la siguiente lección del módulo
Y escribe texto en el editor Y se carga el nuevo contenido
ENTONCES las notas se guardan automáticamente cada 2s Y se actualiza el sidebar
Y se muestra indicador "Guardado" cuando se persiste ```
Y para videos se guarda el timestamp actual
``` **Escenario 8: Marcar lección como completada**
```gherkin
**Escenario 7: Navegar entre lecciones** DADO que el usuario vio la lección completa
```gherkin CUANDO hace click en "Marcar como completada"
DADO que el usuario está en una lección ENTONCES se actualiza el progreso en backend
Y existe una lección siguiente Y aparece checkmark ✓ en el sidebar
CUANDO hace click en "Siguiente" Y se actualiza la barra de progreso del curso
ENTONCES navega a la siguiente lección del módulo Y si es secuencial, se desbloquea siguiente lección
Y se carga el nuevo contenido ```
Y se actualiza el sidebar
``` ## Criterios Adicionales
**Escenario 8: Marcar lección como completada** - [ ] Auto-save de progreso de video cada 10s
```gherkin - [ ] Atajos de teclado: Espacio=play/pause, →=+10s, ←=-10s
DADO que el usuario vio la lección completa - [ ] Picture-in-Picture para videos
CUANDO hace click en "Marcar como completada" - [ ] Modo cine (ocultar sidebar)
ENTONCES se actualiza el progreso en backend - [ ] Velocidad de reproducción: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
Y aparece checkmark ✓ en el sidebar - [ ] Subtítulos si están disponibles
Y se actualiza la barra de progreso del curso - [ ] Analytics: tiempo en lección, pausas, rewinds
Y si es secuencial, se desbloquea siguiente lección
``` ---
## Criterios Adicionales ## Tareas Técnicas
- [ ] Auto-save de progreso de video cada 10s **Database:**
- [ ] Atajos de teclado: Espacio=play/pause, →=+10s, ←=-10s - [ ] DB-EDU-007: Tabla user_lesson_progress (started_at, completed_at, last_position)
- [ ] Picture-in-Picture para videos - [ ] DB-EDU-008: Tabla user_notes (lesson_id, content, timestamp)
- [ ] Modo cine (ocultar sidebar) - [ ] DB-EDU-009: Índice en user_id + lesson_id para queries rápidas
- [ ] Velocidad de reproducción: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
- [ ] Subtítulos si están disponibles **Backend:**
- [ ] Analytics: tiempo en lección, pausas, rewinds - [ ] 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
## Tareas Técnicas - [ ] BE-EDU-018: Endpoint GET/POST /education/lessons/:id/notes
- [ ] BE-EDU-019: Endpoint GET /education/lessons/:id/resources/:resourceId
**Database:** - [ ] BE-EDU-020: Implementar signed URLs para videos privados
- [ ] DB-EDU-007: Tabla user_lesson_progress (started_at, completed_at, last_position) - [ ] BE-EDU-021: Verificar orden secuencial si aplica
- [ ] DB-EDU-008: Tabla user_notes (lesson_id, content, timestamp)
- [ ] DB-EDU-009: Índice en user_id + lesson_id para queries rápidas **Frontend:**
- [ ] FE-EDU-017: Crear LessonPlayerPage.tsx
**Backend:** - [ ] FE-EDU-018: Crear componente VideoPlayer.tsx
- [ ] BE-EDU-014: Endpoint GET /education/courses/:id/lessons/:lessonId - [ ] FE-EDU-019: Crear componente LessonSidebar.tsx
- [ ] BE-EDU-015: Validar inscripción y acceso a lección - [ ] FE-EDU-020: Crear componente NotesEditor.tsx
- [ ] BE-EDU-016: Endpoint POST /education/lessons/:id/progress (guardar posición) - [ ] FE-EDU-021: Crear componente ResourcesList.tsx
- [ ] BE-EDU-017: Endpoint POST /education/lessons/:id/complete - [ ] FE-EDU-022: Implementar auto-save de posición (cada 10s)
- [ ] BE-EDU-018: Endpoint GET/POST /education/lessons/:id/notes - [ ] FE-EDU-023: Implementar lessonStore (Zustand)
- [ ] BE-EDU-019: Endpoint GET /education/lessons/:id/resources/:resourceId - [ ] FE-EDU-024: Navegación con teclas de flecha
- [ ] BE-EDU-020: Implementar signed URLs para videos privados - [ ] FE-EDU-025: Toast notifications para acciones
- [ ] BE-EDU-021: Verificar orden secuencial si aplica
**Tests:**
**Frontend:** - [ ] TEST-EDU-007: Test validación de acceso a lección
- [ ] FE-EDU-017: Crear LessonPlayerPage.tsx - [ ] TEST-EDU-008: Test bloqueo de lección secuencial
- [ ] FE-EDU-018: Crear componente VideoPlayer.tsx - [ ] TEST-EDU-009: Test guardar y restaurar posición de video
- [ ] FE-EDU-019: Crear componente LessonSidebar.tsx - [ ] TEST-EDU-010: Test E2E iniciar lección y marcar completada
- [ ] 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) ## Dependencias
- [ ] FE-EDU-024: Navegación con teclas de flecha
- [ ] FE-EDU-025: Toast notifications para acciones **Depende de:**
- [ ] US-EDU-002: Ver detalle de curso - Estado: Pendiente
**Tests:** - [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
- [ ] TEST-EDU-007: Test validación de acceso a lección
- [ ] TEST-EDU-008: Test bloqueo de lección secuencial **Bloquea:**
- [ ] TEST-EDU-009: Test guardar y restaurar posición de video - [ ] US-EDU-004: Ver video completo
- [ ] TEST-EDU-010: Test E2E iniciar lección y marcar completada - [ ] US-EDU-005: Completar lección
- [ ] US-EDU-007: Ver progreso
---
---
## Dependencias
## Notas Técnicas
**Depende de:**
- [ ] US-EDU-002: Ver detalle de curso - Estado: Pendiente **Endpoints involucrados:**
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado | Método | Endpoint | Descripción |
|--------|----------|-------------|
**Bloquea:** | GET | /education/courses/:courseId/lessons/:lessonId | Obtener lección |
- [ ] US-EDU-004: Ver video completo | POST | /education/lessons/:id/progress | Guardar posición |
- [ ] US-EDU-005: Completar lección | POST | /education/lessons/:id/complete | Marcar completada |
- [ ] US-EDU-007: Ver progreso | GET | /education/lessons/:id/notes | Obtener notas |
| POST | /education/lessons/:id/notes | Crear/actualizar notas |
---
**Response GET /lessons/:id:**
## Notas Técnicas ```typescript
{
**Endpoints involucrados:** lesson: {
| Método | Endpoint | Descripción | id: "les-5",
|--------|----------|-------------| courseId: "course-1",
| GET | /education/courses/:courseId/lessons/:lessonId | Obtener lección | moduleId: "mod-2",
| POST | /education/lessons/:id/progress | Guardar posición | title: "Niveles principales: 38.2%, 50%, 61.8%",
| POST | /education/lessons/:id/complete | Marcar completada | slug: "niveles-principales",
| GET | /education/lessons/:id/notes | Obtener notas | description: "En esta lección aprenderás...",
| POST | /education/lessons/:id/notes | Crear/actualizar notas | type: "video",
duration: 12.25, // minutos
**Response GET /lessons/:id:** order: 1,
```typescript isFree: false,
{
lesson: { // Video específico
id: "les-5", videoUrl: "https://vimeo.com/signed-url-12345",
courseId: "course-1", videoProvider: "vimeo",
moduleId: "mod-2", subtitles: [
title: "Niveles principales: 38.2%, 50%, 61.8%", { language: "es", url: "..." },
slug: "niveles-principales", { language: "en", url: "..." }
description: "En esta lección aprenderás...", ],
type: "video",
duration: 12.25, // minutos resources: [
order: 1, {
isFree: false, id: "res-1",
name: "Plantilla de Fibonacci.xlsx",
// Video específico type: "application/vnd.ms-excel",
videoUrl: "https://vimeo.com/signed-url-12345", size: 45120,
videoProvider: "vimeo", url: "https://s3.../signed-url"
subtitles: [ }
{ language: "es", url: "..." }, ],
{ language: "en", url: "..." }
], userProgress: {
startedAt: "2025-12-04T10:30:00Z",
resources: [ completedAt: null,
{ lastPosition: 5.5, // minutos
id: "res-1", timeSpent: 320, // segundos
name: "Plantilla de Fibonacci.xlsx", isCompleted: false
type: "application/vnd.ms-excel", },
size: 45120,
url: "https://s3.../signed-url" navigation: {
} previous: {
], id: "les-4",
title: "Historia y fundamentos",
userProgress: { slug: "historia-fundamentos"
startedAt: "2025-12-04T10:30:00Z", },
completedAt: null, next: {
lastPosition: 5.5, // minutos id: "les-6",
timeSpent: 320, // segundos title: "Cómo dibujar Fibonacci",
isCompleted: false slug: "como-dibujar-fibonacci",
}, isLocked: false
}
navigation: { },
previous: {
id: "les-4", module: {
title: "Historia y fundamentos", id: "mod-2",
slug: "historia-fundamentos" title: "Niveles de Retroceso",
}, lessonsCompleted: 1,
next: { totalLessons: 4
id: "les-6", },
title: "Cómo dibujar Fibonacci",
slug: "como-dibujar-fibonacci", course: {
isLocked: false id: "course-1",
} title: "Fibonacci Retracement Básico",
}, slug: "fibonacci-retracement-basico",
progressPercent: 45,
module: { isSequential: true
id: "mod-2", }
title: "Niveles de Retroceso", }
lessonsCompleted: 1, }
totalLessons: 4 ```
},
**Validaciones backend:**
course: { 1. Usuario autenticado
id: "course-1", 2. Usuario inscrito en el curso
title: "Fibonacci Retracement Básico", 3. Si curso es secuencial, validar lecciones anteriores completadas
slug: "fibonacci-retracement-basico", 4. Si curso es premium, validar suscripción activa
progressPercent: 45,
isSequential: true **Entidades/Tablas:**
} - `education.lessons`
} - `education.user_lesson_progress`
} - `education.user_notes`
``` - `education.lesson_resources`
**Validaciones backend:** ---
1. Usuario autenticado
2. Usuario inscrito en el curso ## Definition of Ready (DoR)
3. Si curso es secuencial, validar lecciones anteriores completadas
4. Si curso es premium, validar suscripción activa - [x] Historia claramente escrita
- [x] Criterios de aceptación definidos
**Entidades/Tablas:** - [x] Story points estimados
- `education.lessons` - [x] Dependencias identificadas
- `education.user_lesson_progress` - [x] Sin bloqueadores
- `education.user_notes` - [x] Diseño/mockup disponible
- `education.lesson_resources` - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,369 +1,357 @@
--- # US-EDU-004: Ver Video de Lección
id: "US-EDU-004"
title: "Ver Video de Leccion" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Alta" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-004 |
story_points: 3 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P0 |
--- | **Story Points** | 3 |
| **Sprint** | Sprint 3 |
# US-EDU-004: Ver Video de Lección | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-004 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario inscrito viendo una lección de video,
| **Módulo** | education | **quiero** reproducir el video con controles completos y funcionalidades avanzadas,
| **Prioridad** | P0 | **para** consumir el contenido educativo de manera cómoda y eficiente.
| **Story Points** | 3 |
| **Sprint** | Sprint 3 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
┌─────────────────────────────────────────────────────────────────┐
**Como** usuario inscrito viendo una lección de video, │ Lección 2.1: Niveles principales de Fibonacci │
**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. │ │ [VIDEO REPRODUCIÉNDOSE] │ │
│ │ │ │
## Mockups/Wireframes │ │ Carlos explicando │ │
│ │ niveles de Fibonacci │ │
``` │ │ │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ │ │
│ Lección 2.1: Niveles principales de Fibonacci │ │ │ ━━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━ 05:30 / 12:15 │ │
├─────────────────────────────────────────────────────────────────┤ │ │ ⏮ ⏸ ⏭ 🔊──────● ⚙ 1.25x [CC] [PIP] ⛶ │ │
│ │ │ │ │ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ │ Capítulos: │ │
│ │ │ │ │ │ • 0:00 - Introducción │ │
│ │ │ │ │ │ • 2:15 - Nivel 38.2% │ │
│ │ [VIDEO REPRODUCIÉNDOSE] │ │ │ │ • 5:30 - Nivel 50% ← Actual │ │
│ │ │ │ │ │ • 8:45 - Nivel 61.8% │ │
│ │ Carlos explicando │ │ │ │ • 11:00 - Conclusión │ │
│ │ niveles de Fibonacci │ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │ │
│ │ │ │ │ CONTROLES: │
│ │ ━━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━ 05:30 / 12:15 │ │ │ • Espacio: Play/Pause │
│ │ ⏮ ⏸ ⏭ 🔊──────● ⚙ 1.25x [CC] [PIP] ⛶ │ │ │ • →: Adelantar 10s │
│ │ │ │ │ • ←: Retroceder 10s │
│ │ Capítulos: │ │ │ • F: Pantalla completa │
│ │ • 0:00 - Introducción │ │ │ • M: Silenciar │
│ │ • 2:15 - Nivel 38.2% │ │ │ • 0-9: Saltar a ese % del video │
│ │ • 5:30 - Nivel 50% ← Actual │ │ │ │
│ │ • 8:45 - Nivel 61.8% │ │ └─────────────────────────────────────────────────────────────────┘
│ │ • 11:00 - Conclusión │ │
│ └────────────────────────────────────────────────────────────┘ │ [MENÚ DE VELOCIDAD]
│ │ ┌──────────────┐
│ CONTROLES: │ │ Velocidad │
│ • Espacio: Play/Pause │ │ ○ 0.5x │
│ • →: Adelantar 10s │ │ ○ 0.75x │
│ • ←: Retroceder 10s │ │ ● 1x │ ← Seleccionado
│ • F: Pantalla completa │ │ ○ 1.25x │
│ • M: Silenciar │ │ ○ 1.5x │
│ • 0-9: Saltar a ese % del video │ │ ○ 2x │
│ │ └──────────────┘
└─────────────────────────────────────────────────────────────────┘
[MENÚ DE CALIDAD]
[MENÚ DE VELOCIDAD] ┌──────────────┐
┌──────────────┐ │ Calidad │
│ Velocidad │ │ ● Auto │ ← Adaptativa
│ ○ 0.5x │ │ ○ 1080p │
│ ○ 0.75x │ │ ○ 720p │
│ ● 1x │ ← Seleccionado │ ○ 480p │
│ ○ 1.25x │ │ ○ 360p │
│ ○ 1.5x │ └──────────────┘
│ ○ 2x │
└──────────────┘ [SUBTÍTULOS]
┌──────────────┐
[MENÚ DE CALIDAD] │ Subtítulos │
┌──────────────┐ │ ● Desactivado│
│ Calidad │ │ ○ Español │
│ ● Auto │ ← Adaptativa │ ○ English │
│ ○ 1080p │ └──────────────┘
│ ○ 720p │ ```
│ ○ 480p │
│ ○ 360p │ ---
└──────────────┘
## Criterios de Aceptación
[SUBTÍTULOS]
┌──────────────┐ **Escenario 1: Reproducir video**
│ Subtítulos │ ```gherkin
│ ● Desactivado│ DADO que el usuario accedió a una lección de video
│ ○ Español │ CUANDO el reproductor carga
│ ○ English │ 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"
```
## Criterios de Aceptación
**Escenario 2: Controles básicos funcionan**
**Escenario 1: Reproducir video** ```gherkin
```gherkin DADO que el video está cargado
DADO que el usuario accedió a una lección de video CUANDO el usuario hace click en Play
CUANDO el reproductor carga ENTONCES el video se reproduce
ENTONCES se muestra el video con controles Y el botón cambia a Pause ⏸
Y el video está pausado inicialmente Y la barra de progreso avanza
Y se muestra duración total Y el tiempo actual se actualiza cada segundo
Y se carga en la última posición guardada (si existe) ```
Y se muestra toast "Continuando desde X:XX"
``` **Escenario 3: Cambiar velocidad de reproducción**
```gherkin
**Escenario 2: Controles básicos funcionan** DADO que el video se está reproduciendo a 1x
```gherkin CUANDO el usuario selecciona velocidad 1.5x
DADO que el video está cargado ENTONCES el video se reproduce 50% más rápido
CUANDO el usuario hace click en Play Y el audio se ajusta automáticamente (sin distorsión)
ENTONCES el video se reproduce Y se muestra indicador "1.5x" en el reproductor
Y el botón cambia a Pause ⏸ Y la configuración se guarda para próximos videos
Y la barra de progreso avanza ```
Y el tiempo actual se actualiza cada segundo
``` **Escenario 4: Activar subtítulos**
```gherkin
**Escenario 3: Cambiar velocidad de reproducción** DADO que el video tiene subtítulos en español
```gherkin CUANDO el usuario activa subtítulos
DADO que el video se está reproduciendo a 1x ENTONCES aparecen subtítulos sincronizados con el audio
CUANDO el usuario selecciona velocidad 1.5x Y se pueden personalizar tamaño y posición
ENTONCES el video se reproduce 50% más rápido Y la preferencia se guarda para próximos videos
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 5: Saltar a posición específica**
``` ```gherkin
DADO que el video se está reproduciendo
**Escenario 4: Activar subtítulos** CUANDO el usuario hace click en la barra de progreso
```gherkin ENTONCES el video salta a esa posición
DADO que el video tiene subtítulos en español Y se muestra preview al hacer hover sobre la barra
CUANDO el usuario activa subtítulos Y la nueva posición se guarda automáticamente
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 6: Auto-guardado de progreso**
``` ```gherkin
DADO que el usuario está viendo un video
**Escenario 5: Saltar a posición específica** Y el video alcanza la posición 7:30
```gherkin CUANDO pasan 10 segundos
DADO que el video se está reproduciendo ENTONCES se guarda la posición en backend
CUANDO el usuario hace click en la barra de progreso Y si el usuario cierra la página y vuelve
ENTONCES el video salta a esa posición ENTONCES el video se carga en 7:30
Y se muestra preview al hacer hover sobre la barra ```
Y la nueva posición se guarda automáticamente
``` **Escenario 7: Atajos de teclado**
```gherkin
**Escenario 6: Auto-guardado de progreso** DADO que el usuario está viendo un video
```gherkin CUANDO presiona la tecla →
DADO que el usuario está viendo un video ENTONCES el video avanza 10 segundos
Y el video alcanza la posición 7:30 Y se muestra indicador "+10s"
CUANDO pasan 10 segundos
ENTONCES se guarda la posición en backend CUANDO presiona la tecla ←
Y si el usuario cierra la página y vuelve ENTONCES el video retrocede 10 segundos
ENTONCES el video se carga en 7:30 Y se muestra indicador "-10s"
```
CUANDO presiona Espacio
**Escenario 7: Atajos de teclado** ENTONCES el video pausa/reanuda
```gherkin
DADO que el usuario está viendo un video CUANDO presiona F
CUANDO presiona la tecla → ENTONCES entra/sale de pantalla completa
ENTONCES el video avanza 10 segundos ```
Y se muestra indicador "+10s"
**Escenario 8: Picture-in-Picture**
CUANDO presiona la tecla ← ```gherkin
ENTONCES el video retrocede 10 segundos DADO que el video se está reproduciendo
Y se muestra indicador "-10s" CUANDO el usuario hace click en botón PIP
ENTONCES el video se minimiza en una ventana flotante
CUANDO presiona Espacio Y puede navegar a otras páginas mientras ve el video
ENTONCES el video pausa/reanuda Y los controles básicos están disponibles en PIP
Y al cerrar PIP, vuelve al reproductor normal
CUANDO presiona F ```
ENTONCES entra/sale de pantalla completa
``` **Escenario 9: Capítulos del video**
```gherkin
**Escenario 8: Picture-in-Picture** DADO que el video tiene capítulos definidos
```gherkin CUANDO el usuario hace click en un capítulo
DADO que el video se está reproduciendo ENTONCES el video salta a ese timestamp
CUANDO el usuario hace click en botón PIP Y se muestra marcador de capítulo en la barra de progreso
ENTONCES el video se minimiza en una ventana flotante Y al hacer hover en la barra, muestra nombre del capítulo
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 10: Completar video automáticamente**
``` ```gherkin
DADO que el usuario está viendo un video
**Escenario 9: Capítulos del video** CUANDO el video alcanza el 90% de reproducción
```gherkin ENTONCES la lección se marca automáticamente como completada
DADO que el video tiene capítulos definidos Y se muestra toast "Lección completada +10 XP"
CUANDO el usuario hace click en un capítulo Y se actualiza el progreso del curso
ENTONCES el video salta a ese timestamp Y se desbloquea siguiente lección (si es secuencial)
Y se muestra marcador de capítulo en la barra de progreso ```
Y al hacer hover en la barra, muestra nombre del capítulo
``` ## Criterios Adicionales
**Escenario 10: Completar video automáticamente** - [ ] Calidad adaptativa según ancho de banda
```gherkin - [ ] Buffer progresivo para evitar cortes
DADO que el usuario está viendo un video - [ ] Indicador de buffering cuando carga
CUANDO el video alcanza el 90% de reproducción - [ ] Manejo de errores (video no disponible, error de red)
ENTONCES la lección se marca automáticamente como completada - [ ] Analytics: pausas, rewinds, abandonos
Y se muestra toast "Lección completada +10 XP" - [ ] Thumbnail preview al hover sobre barra de progreso
Y se actualiza el progreso del curso - [ ] Continuar reproduciendo al cambiar de pestaña (background play)
Y se desbloquea siguiente lección (si es secuencial)
``` ---
## Criterios Adicionales ## Tareas Técnicas
- [ ] Calidad adaptativa según ancho de banda **Database:**
- [ ] Buffer progresivo para evitar cortes - [ ] DB-EDU-010: Tabla video_chapters (video_id, time, title)
- [ ] Indicador de buffering cuando carga - [ ] DB-EDU-011: Actualizar user_lesson_progress.last_position cada 10s
- [ ] Manejo de errores (video no disponible, error de red)
- [ ] Analytics: pausas, rewinds, abandonos **Backend:**
- [ ] Thumbnail preview al hover sobre barra de progreso - [ ] BE-EDU-022: Endpoint POST /education/lessons/:id/progress
- [ ] Continuar reproduciendo al cambiar de pestaña (background play) - [ ] 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
## Tareas Técnicas - [ ] BE-EDU-027: Rate limiting en auto-save (máx cada 5s)
**Database:** **Frontend:**
- [ ] DB-EDU-010: Tabla video_chapters (video_id, time, title) - [ ] FE-EDU-026: Implementar VideoPlayer.tsx con React Player
- [ ] DB-EDU-011: Actualizar user_lesson_progress.last_position cada 10s - [ ] FE-EDU-027: Custom controls overlay
- [ ] FE-EDU-028: Speed control menu
**Backend:** - [ ] FE-EDU-029: Subtitles toggle y customización
- [ ] BE-EDU-022: Endpoint POST /education/lessons/:id/progress - [ ] FE-EDU-030: Keyboard shortcuts (arrow keys, space, f, m)
- [ ] BE-EDU-023: Endpoint GET /education/videos/:id/chapters - [ ] FE-EDU-031: Picture-in-Picture implementation
- [ ] BE-EDU-024: Generar signed URLs para Vimeo/S3 - [ ] FE-EDU-032: Progress bar con preview thumbnail
- [ ] BE-EDU-025: Implementar validación de acceso a video - [ ] FE-EDU-033: Chapters navigation
- [ ] BE-EDU-026: Webhook de Vimeo para confirmar encoding completado - [ ] FE-EDU-034: Auto-save de posición cada 10s
- [ ] BE-EDU-027: Rate limiting en auto-save (máx cada 5s) - [ ] FE-EDU-035: Restore position on load
- [ ] FE-EDU-036: Analytics tracking (play, pause, seek, complete)
**Frontend:** - [ ] FE-EDU-037: Loading spinner y error states
- [ ] FE-EDU-026: Implementar VideoPlayer.tsx con React Player
- [ ] FE-EDU-027: Custom controls overlay **Tests:**
- [ ] FE-EDU-028: Speed control menu - [ ] TEST-EDU-011: Test auto-save de progreso
- [ ] FE-EDU-029: Subtitles toggle y customización - [ ] TEST-EDU-012: Test restaurar posición guardada
- [ ] FE-EDU-030: Keyboard shortcuts (arrow keys, space, f, m) - [ ] TEST-EDU-013: Test marcar completado al 90%
- [ ] FE-EDU-031: Picture-in-Picture implementation - [ ] TEST-EDU-014: Test E2E reproducir video completo
- [ ] 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 ## Dependencias
- [ ] FE-EDU-036: Analytics tracking (play, pause, seek, complete)
- [ ] FE-EDU-037: Loading spinner y error states **Depende de:**
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
**Tests:** - [ ] Video CDN: Vimeo Pro o AWS S3 + CloudFront
- [ ] TEST-EDU-011: Test auto-save de progreso
- [ ] TEST-EDU-012: Test restaurar posición guardada **Bloquea:**
- [ ] TEST-EDU-013: Test marcar completado al 90% - [ ] US-EDU-005: Completar lección
- [ ] TEST-EDU-014: Test E2E reproducir video completo - [ ] US-EDU-007: Ver progreso
--- ---
## Dependencias ## Notas Técnicas
**Depende de:** **CDN de Videos:**
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente - Opción 1: Vimeo Pro (recomendado para MVP)
- [ ] Video CDN: Vimeo Pro o AWS S3 + CloudFront - API robusta
- Encoding automático
**Bloquea:** - Streaming adaptativo HLS
- [ ] US-EDU-005: Completar lección - Subtítulos integrados
- [ ] US-EDU-007: Ver progreso - Analytics incluido
- Opción 2: AWS S3 + CloudFront + MediaConvert
--- - Más control
- Más setup inicial
## Notas Técnicas - Costos variables
**CDN de Videos:** **Librerías recomendadas:**
- Opción 1: Vimeo Pro (recomendado para MVP) - React Player: Wrapper para múltiples providers
- API robusta - Video.js: Player HTML5 completo y customizable
- Encoding automático - Plyr: Alternativa moderna y ligera
- Streaming adaptativo HLS
- Subtítulos integrados **Auto-save strategy:**
- Analytics incluido ```javascript
- Opción 2: AWS S3 + CloudFront + MediaConvert // Guardar posición cada 10s mientras reproduce
- Más control useEffect(() => {
- Más setup inicial const interval = setInterval(() => {
- Costos variables if (isPlaying) {
saveProgress(currentTime);
**Librerías recomendadas:** }
- React Player: Wrapper para múltiples providers }, 10000);
- Video.js: Player HTML5 completo y customizable return () => clearInterval(interval);
- Plyr: Alternativa moderna y ligera }, [isPlaying, currentTime]);
**Auto-save strategy:** // Guardar al pausar
```javascript const handlePause = () => {
// Guardar posición cada 10s mientras reproduce saveProgress(currentTime);
useEffect(() => { };
const interval = setInterval(() => {
if (isPlaying) { // Guardar antes de salir
saveProgress(currentTime); useEffect(() => {
} return () => {
}, 10000); saveProgress(currentTime);
return () => clearInterval(interval); };
}, [isPlaying, currentTime]); }, []);
```
// Guardar al pausar
const handlePause = () => { **Analytics events:**
saveProgress(currentTime); - `video_started`: Primera reproducción
}; - `video_played`: Cada vez que presiona play
- `video_paused`: Cada pausa
// Guardar antes de salir - `video_seeked`: Salto manual
useEffect(() => { - `video_completed`: Alcanza 90%
return () => { - `video_speed_changed`: Cambia velocidad
saveProgress(currentTime); - `video_quality_changed`: Cambia calidad
};
}, []); **Entidades/Tablas:**
``` - `education.lessons` (campo videoUrl)
- `education.video_chapters`
**Analytics events:** - `education.user_lesson_progress` (campo last_position)
- `video_started`: Primera reproducción
- `video_played`: Cada vez que presiona play ---
- `video_paused`: Cada pausa
- `video_seeked`: Salto manual ## Definition of Ready (DoR)
- `video_completed`: Alcanza 90%
- `video_speed_changed`: Cambia velocidad - [x] Historia claramente escrita
- `video_quality_changed`: Cambia calidad - [x] Criterios de aceptación definidos
- [x] Story points estimados
**Entidades/Tablas:** - [x] Dependencias identificadas
- `education.lessons` (campo videoUrl) - [x] Sin bloqueadores
- `education.video_chapters` - [x] Diseño/mockup disponible
- `education.user_lesson_progress` (campo last_position) - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,388 +1,376 @@
--- # US-EDU-005: Completar Lección
id: "US-EDU-005"
title: "Completar Leccion" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Alta" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-005 |
story_points: 3 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P0 |
--- | **Story Points** | 3 |
| **Sprint** | Sprint 3 |
# US-EDU-005: Completar Lección | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-005 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario viendo una lección,
| **Módulo** | education | **quiero** marcar la lección como completada y ganar recompensas,
| **Prioridad** | P0 | **para** registrar mi progreso, desbloquear siguiente contenido y acumular XP.
| **Story Points** | 3 |
| **Sprint** | Sprint 3 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
[VIDEO AL 90%]
**Como** usuario viendo una lección, ┌─────────────────────────────────────────────────────────────────┐
**quiero** marcar la lección como completada y ganar recompensas, │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● 11:00 / 12:15 │
**para** registrar mi progreso, desbloquear siguiente contenido y acumular XP. └─────────────────────────────────────────────────────────────────┘
## Descripción Detallada [ANIMACIÓN DE CELEBRACIÓN]
┌─────────────────────────────────────────────────────────────────┐
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 │ ✓ ¡LECCIÓN COMPLETADA! │
│ │
``` │ Niveles principales de Fibonacci │
[VIDEO AL 90%] │ │
┌─────────────────────────────────────────────────────────────────┐ │ +10 XP ganados │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● 11:00 / 12:15 │ │ │
└─────────────────────────────────────────────────────────────────┘ │ Progreso del curso: 50% │
│ ████████████░░░░░░░░ │
[ANIMACIÓN DE CELEBRACIÓN] │ │
┌─────────────────────────────────────────────────────────────────┐ │ ┌────────────────────────────────────┐ │
│ ✨ ✨ ✨ │ │ │ [Siguiente lección →] │ │
│ │ │ │ Cómo dibujar Fibonacci │ │
│ ✓ ¡LECCIÓN COMPLETADA! │ │ └────────────────────────────────────┘ │
│ │ │ │
│ Niveles principales de Fibonacci │ │ [Volver al curso] │
│ │ │ │
│ +10 XP ganados │ └─────────────────────────────────────────────────────────────────┘
│ │
│ Progreso del curso: 50% │ [ARTÍCULO - CHECKBOX AL FINAL]
│ ████████████░░░░░░░░ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ │ ... contenido del artículo ... │
│ ┌────────────────────────────────────┐ │ │ │
│ │ [Siguiente lección →] │ │ │ Conclusión │
│ │ Cómo dibujar Fibonacci │ │ │ En esta lección aprendiste los fundamentos de... │
│ └────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ [Volver al curso] │ │ │ ☐ Marcar como completada │ │
│ │ │ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘ │ │
│ [← Anterior] [Siguiente lección →] │
[ARTÍCULO - CHECKBOX AL FINAL] │ │
┌─────────────────────────────────────────────────────────────────┐ └─────────────────────────────────────────────────────────────────┘
│ ... contenido del artículo ... │
│ │ [SIDEBAR ACTUALIZADO]
│ Conclusión │ ┌────────────────────────┐
│ En esta lección aprendiste los fundamentos de... │ │ TEMARIO │
│ │ │ ✓ Módulo 1 (4/4) ✓ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ▼ Módulo 2 (2/4) │
│ │ ☐ Marcar como completada │ │ │ ✓ 1. Niveles princ. │ ← Completada
│ └────────────────────────────────────────────────────────────┘ │ │ ● 2. Dibujar Fib. │ ← Desbloqueada
│ │ │ 🔒 3. Práctica │
│ [← Anterior] [Siguiente lección →] │ │ 🔒 4. Quiz │
│ │ │ 🔒 Módulo 3 │
└─────────────────────────────────────────────────────────────────┘ └────────────────────────┘
```
[SIDEBAR ACTUALIZADO]
┌────────────────────────┐ ---
│ TEMARIO │
│ ✓ Módulo 1 (4/4) ✓ │ ## Criterios de Aceptación
│ ▼ Módulo 2 (2/4) │
│ ✓ 1. Niveles princ. │ ← Completada **Escenario 1: Completar video automáticamente**
│ ● 2. Dibujar Fib. │ ← Desbloqueada ```gherkin
│ 🔒 3. Práctica │ DADO que el usuario está viendo un video
│ 🔒 4. Quiz │ CUANDO el video alcanza el 90% de reproducción (10:48 de 12:00)
│ 🔒 Módulo 3 │ 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 ✓
## Criterios de Aceptación Y si curso es secuencial, se desbloquea siguiente lección
```
**Escenario 1: Completar video automáticamente**
```gherkin **Escenario 2: Completar artículo manualmente**
DADO que el usuario está viendo un video ```gherkin
CUANDO el video alcanza el 90% de reproducción (10:48 de 12:00) DADO que el usuario está leyendo un artículo
ENTONCES la lección se marca automáticamente como completada Y scrolleó hasta el final
Y se registra completed_at en backend CUANDO hace click en checkbox "Marcar como completada"
Y se muestra modal de celebración con confeti ENTONCES se marca la lección como completada
Y se muestra "+10 XP ganados" Y se registra en backend
Y se actualiza barra de progreso del curso Y se muestra toast "+15 XP ganados"
Y se actualiza sidebar con checkmark ✓ Y se habilita botón "Siguiente lección"
Y si curso es secuencial, se desbloquea siguiente lección Y checkbox cambia a checked ✓
``` ```
**Escenario 2: Completar artículo manualmente** **Escenario 3: Ganar XP por completar**
```gherkin ```gherkin
DADO que el usuario está leyendo un artículo DADO que el usuario completa una lección de video
Y scrolleó hasta el final ENTONCES se otorgan 10 XP
CUANDO hace click en checkbox "Marcar como completada" Y se actualiza totalXP del usuario
ENTONCES se marca la lección como completada Y se verifica si sube de nivel
Y se registra en backend Y si sube de nivel, se muestra animación adicional
Y se muestra toast "+15 XP ganados" Y se actualiza badge de nivel en UI
Y se habilita botón "Siguiente lección" ```
Y checkbox cambia a checked ✓
``` **Escenario 4: Desbloquear siguiente lección**
```gherkin
**Escenario 3: Ganar XP por completar** DADO que el curso es secuencial
```gherkin Y el usuario completa lección 3 del módulo 2
DADO que el usuario completa una lección de video ENTONCES la lección 4 del módulo 2 se desbloquea
ENTONCES se otorgan 10 XP Y el candado 🔒 se remueve del sidebar
Y se actualiza totalXP del usuario Y el usuario puede acceder a esa lección
Y se verifica si sube de nivel Y si intenta saltarse a lección 5, sigue bloqueada
Y si sube de nivel, se muestra animación adicional ```
Y se actualiza badge de nivel en UI
``` **Escenario 5: Completar módulo completo**
```gherkin
**Escenario 4: Desbloquear siguiente lección** DADO que el usuario completa la última lección de un módulo
```gherkin CUANDO se marca la lección como completada
DADO que el curso es secuencial ENTONCES también se completa el módulo
Y el usuario completa lección 3 del módulo 2 Y se otorgan +50 XP adicionales por módulo
ENTONCES la lección 4 del módulo 2 se desbloquea Y se muestra "¡Módulo completado!"
Y el candado 🔒 se remueve del sidebar Y se desbloquea el siguiente módulo (si es secuencial)
Y el usuario puede acceder a esa lección Y se actualiza contador "Módulo 2 (4/4) ✓"
Y si intenta saltarse a lección 5, sigue bloqueada ```
```
**Escenario 6: Completar curso completo**
**Escenario 5: Completar módulo completo** ```gherkin
```gherkin DADO que el usuario completa la última lección del último módulo
DADO que el usuario completa la última lección de un módulo CUANDO se marca como completada
CUANDO se marca la lección como completada ENTONCES se completa el curso
ENTONCES también se completa el módulo Y se otorgan +200 XP adicionales por curso
Y se otorgan +50 XP adicionales por módulo Y se muestra modal "¡CURSO COMPLETADO!"
Y se muestra "¡Módulo completado!" Y se genera certificado automáticamente
Y se desbloquea el siguiente módulo (si es secuencial) Y se muestra botón "Descargar certificado"
Y se actualiza contador "Módulo 2 (4/4) ✓" Y se envía email de felicitación
``` ```
**Escenario 6: Completar curso completo** **Escenario 7: Sugerir siguiente paso**
```gherkin ```gherkin
DADO que el usuario completa la última lección del último módulo DADO que el usuario completó una lección
CUANDO se marca como completada CUANDO se muestra el modal de celebración
ENTONCES se completa el curso ENTONCES se sugiere la siguiente lección
Y se otorgan +200 XP adicionales por curso Y se muestra título y duración de la siguiente
Y se muestra modal "¡CURSO COMPLETADO!" Y botón "Siguiente lección" navega directamente
Y se genera certificado automáticamente Y también hay opción "Volver al curso"
Y se muestra botón "Descargar certificado" Y si no hay siguiente lección, sugiere otro curso
Y se envía email de felicitación ```
```
**Escenario 8: Re-marcar como incompleta**
**Escenario 7: Sugerir siguiente paso** ```gherkin
```gherkin DADO que el usuario marcó una lección como completada
DADO que el usuario completó una lección Y quiere revisarla de nuevo
CUANDO se muestra el modal de celebración CUANDO desmarca el checkbox
ENTONCES se sugiere la siguiente lección ENTONCES la lección vuelve a estado incompleto
Y se muestra título y duración de la siguiente Y el progreso del curso se actualiza (disminuye)
Y botón "Siguiente lección" navega directamente Y NO se quita el XP ya ganado
Y también hay opción "Volver al curso" Y la lección puede volver a marcarse como completada
Y si no hay siguiente lección, sugiere otro curso ```
```
**Escenario 9: Actualizar racha diaria**
**Escenario 8: Re-marcar como incompleta** ```gherkin
```gherkin DADO que es el primer día del usuario en la plataforma
DADO que el usuario marcó una lección como completada CUANDO completa su primera lección
Y quiere revisarla de nuevo ENTONCES se inicia racha de 1 día
CUANDO desmarca el checkbox Y se muestra toast "¡Racha iniciada! 🔥 1 día"
ENTONCES la lección vuelve a estado incompleto
Y el progreso del curso se actualiza (disminuye) DADO que el usuario tiene racha de 5 días
Y NO se quita el XP ya ganado Y completa su primera lección del día
Y la lección puede volver a marcarse como completada ENTONCES la racha aumenta a 6 días
``` Y se muestra "¡Racha de 6 días! 🔥"
```
**Escenario 9: Actualizar racha diaria**
```gherkin **Escenario 10: Primera lección del día bonus**
DADO que es el primer día del usuario en la plataforma ```gherkin
CUANDO completa su primera lección DADO que el usuario NO completó lecciones hoy
ENTONCES se inicia racha de 1 día CUANDO completa su primera lección del día
Y se muestra toast "¡Racha iniciada! 🔥 1 día" ENTONCES recibe +5 XP adicionales de bonus
Y se muestra "+10 XP + 5 XP (bonus diario)"
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 ## Criterios Adicionales
Y se muestra "¡Racha de 6 días! 🔥"
``` - [ ] Animación de confeti al completar
- [ ] Sonido de celebración (opcional, con toggle)
**Escenario 10: Primera lección del día bonus** - [ ] Compartir logro en redes sociales
```gherkin - [ ] Badges especiales por milestones (10, 50, 100 lecciones)
DADO que el usuario NO completó lecciones hoy - [ ] Actualizar calendario de actividad
CUANDO completa su primera lección del día - [ ] Notificación push si está habilitado
ENTONCES recibe +5 XP adicionales de bonus
Y se muestra "+10 XP + 5 XP (bonus diario)" ---
```
## Tareas Técnicas
## Criterios Adicionales
**Database:**
- [ ] Animación de confeti al completar - [ ] DB-EDU-012: Campo completed_at en user_lesson_progress
- [ ] Sonido de celebración (opcional, con toggle) - [ ] DB-EDU-013: Tabla user_xp_transactions (log de XP ganado)
- [ ] Compartir logro en redes sociales - [ ] DB-EDU-014: Campo current_streak en users
- [ ] Badges especiales por milestones (10, 50, 100 lecciones)
- [ ] Actualizar calendario de actividad **Backend:**
- [ ] Notificación push si está habilitado - [ ] 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
## Tareas Técnicas - [ ] BE-EDU-032: Verificar si completa módulo
- [ ] BE-EDU-033: Verificar si completa curso
**Database:** - [ ] BE-EDU-034: Desbloquear siguiente lección/módulo
- [ ] DB-EDU-012: Campo completed_at en user_lesson_progress - [ ] BE-EDU-035: Actualizar racha diaria
- [ ] DB-EDU-013: Tabla user_xp_transactions (log de XP ganado) - [ ] BE-EDU-036: Verificar si sube de nivel
- [ ] DB-EDU-014: Campo current_streak en users - [ ] BE-EDU-037: Event handler para generar certificado
- [ ] BE-EDU-038: Enviar email de curso completado
**Backend:**
- [ ] BE-EDU-028: Endpoint POST /education/lessons/:id/complete **Frontend:**
- [ ] BE-EDU-029: Implementar LessonService.markAsCompleted() - [ ] FE-EDU-038: Componente LessonCompleteModal.tsx
- [ ] BE-EDU-030: Verificar si es primera del día (bonus XP) - [ ] FE-EDU-039: Animación de confeti con react-confetti
- [ ] BE-EDU-031: Otorgar XP según tipo de lección - [ ] FE-EDU-040: Actualizar sidebar en tiempo real
- [ ] BE-EDU-032: Verificar si completa módulo - [ ] FE-EDU-041: Actualizar barra de progreso
- [ ] BE-EDU-033: Verificar si completa curso - [ ] FE-EDU-042: Toast notification de XP ganado
- [ ] BE-EDU-034: Desbloquear siguiente lección/módulo - [ ] FE-EDU-043: Modal de curso completado
- [ ] BE-EDU-035: Actualizar racha diaria - [ ] FE-EDU-044: Checkbox para artículos
- [ ] BE-EDU-036: Verificar si sube de nivel - [ ] FE-EDU-045: Auto-complete al 90% en videos
- [ ] BE-EDU-037: Event handler para generar certificado - [ ] FE-EDU-046: Botón "Siguiente lección"
- [ ] BE-EDU-038: Enviar email de curso completado - [ ] FE-EDU-047: Integrar con progressStore
**Frontend:** **Tests:**
- [ ] FE-EDU-038: Componente LessonCompleteModal.tsx - [ ] TEST-EDU-015: Test completar lección otorga XP
- [ ] FE-EDU-039: Animación de confeti con react-confetti - [ ] TEST-EDU-016: Test desbloquear siguiente lección
- [ ] FE-EDU-040: Actualizar sidebar en tiempo real - [ ] TEST-EDU-017: Test completar módulo otorga bonus
- [ ] FE-EDU-041: Actualizar barra de progreso - [ ] TEST-EDU-018: Test completar curso genera certificado
- [ ] FE-EDU-042: Toast notification de XP ganado - [ ] TEST-EDU-019: Test actualizar racha diaria
- [ ] FE-EDU-043: Modal de curso completado - [ ] TEST-EDU-020: Test E2E completar lección end-to-end
- [ ] 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 ## Dependencias
**Tests:** **Depende de:**
- [ ] TEST-EDU-015: Test completar lección otorga XP - [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
- [ ] TEST-EDU-016: Test desbloquear siguiente lección - [ ] US-EDU-004: Ver video - Estado: Pendiente
- [ ] TEST-EDU-017: Test completar módulo otorga bonus - [ ] RF-EDU-003: Sistema de progreso
- [ ] TEST-EDU-018: Test completar curso genera certificado
- [ ] TEST-EDU-019: Test actualizar racha diaria **Bloquea:**
- [ ] TEST-EDU-020: Test E2E completar lección end-to-end - [ ] US-EDU-007: Ver progreso
- [ ] US-EDU-008: Obtener certificado
---
---
## Dependencias
## Notas Técnicas
**Depende de:**
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente **Endpoint POST /lessons/:id/complete:**
- [ ] US-EDU-004: Ver video - Estado: Pendiente ```typescript
- [ ] RF-EDU-003: Sistema de progreso // Request
POST /education/lessons/lesson-uuid-123/complete
**Bloquea:** {
- [ ] US-EDU-007: Ver progreso completedAt: "2025-12-05T15:30:00Z",
- [ ] US-EDU-008: Obtener certificado timeSpent: 720, // segundos que pasó en la lección
finalPosition: 12.15 // para videos
--- }
## Notas Técnicas // Response
{
**Endpoint POST /lessons/:id/complete:** success: true,
```typescript lesson: {
// Request id: "lesson-uuid-123",
POST /education/lessons/lesson-uuid-123/complete isCompleted: true,
{ completedAt: "2025-12-05T15:30:00Z"
completedAt: "2025-12-05T15:30:00Z", },
timeSpent: 720, // segundos que pasó en la lección rewards: {
finalPosition: 12.15 // para videos xpEarned: 10,
} bonusXP: 5, // Si es primera del día
totalXP: 15,
// Response newLevel: null, // Si subió de nivel, info del nuevo nivel
{ badges: [] // Nuevos badges ganados
success: true, },
lesson: { progress: {
id: "lesson-uuid-123", course: {
isCompleted: true, progressPercent: 50,
completedAt: "2025-12-05T15:30:00Z" lessonsCompleted: 12,
}, totalLessons: 23
rewards: { },
xpEarned: 10, module: {
bonusXP: 5, // Si es primera del día isCompleted: false,
totalXP: 15, lessonsCompleted: 2,
newLevel: null, // Si subió de nivel, info del nuevo nivel totalLessons: 4
badges: [] // Nuevos badges ganados }
}, },
progress: { nextLesson: {
course: { id: "lesson-uuid-124",
progressPercent: 50, title: "Cómo dibujar Fibonacci",
lessonsCompleted: 12, slug: "como-dibujar-fibonacci",
totalLessons: 23 isUnlocked: true
}, },
module: { streak: {
isCompleted: false, current: 6,
lessonsCompleted: 2, isNewDay: true
totalLessons: 4 },
} courseCompleted: false
}, }
nextLesson: { ```
id: "lesson-uuid-124",
title: "Cómo dibujar Fibonacci", **Reglas de XP:**
slug: "como-dibujar-fibonacci", - Lección de video: 10 XP
isUnlocked: true - Lección de artículo: 15 XP
}, - Completar módulo: +50 XP
streak: { - Completar curso: +200 XP
current: 6, - Primera lección del día: +5 XP
isNewDay: true - Aprobar quiz primera vez: +30 XP (ver US-EDU-006)
},
courseCompleted: false **Lógica de completitud:**
} ```javascript
``` // Video: 90% de duración
isVideoComplete = currentTime >= (duration * 0.9);
**Reglas de XP:**
- Lección de video: 10 XP // Artículo: Manual con checkbox
- Lección de artículo: 15 XP // Quiz: Aprobar con score >= passingScore
- Completar módulo: +50 XP ```
- Completar curso: +200 XP
- Primera lección del día: +5 XP **Entidades/Tablas:**
- Aprobar quiz primera vez: +30 XP (ver US-EDU-006) - `education.user_lesson_progress` (completed_at)
- `gamification.user_xp_transactions`
**Lógica de completitud:** - `gamification.user_stats` (current_streak, total_xp, level)
```javascript
// Video: 90% de duración ---
isVideoComplete = currentTime >= (duration * 0.9);
## Definition of Ready (DoR)
// Artículo: Manual con checkbox
// Quiz: Aprobar con score >= passingScore - [x] Historia claramente escrita
``` - [x] Criterios de aceptación definidos
- [x] Story points estimados
**Entidades/Tablas:** - [x] Dependencias identificadas
- `education.user_lesson_progress` (completed_at) - [x] Sin bloqueadores
- `gamification.user_xp_transactions` - [x] Diseño/mockup disponible
- `gamification.user_stats` (current_streak, total_xp, level) - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,485 +1,473 @@
--- # US-EDU-006: Realizar Quiz
id: "US-EDU-006"
title: "Realizar Quiz" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Alta" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-006 |
story_points: 5 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P1 |
--- | **Story Points** | 5 |
| **Sprint** | Sprint 4 |
# US-EDU-006: Realizar Quiz | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-006 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario inscrito en un curso,
| **Módulo** | education | **quiero** realizar quizzes interactivos al final de cada módulo,
| **Prioridad** | P1 | **para** validar mi conocimiento, recibir feedback y aprobar las evaluaciones requeridas.
| **Story Points** | 5 |
| **Sprint** | Sprint 4 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
[PANTALLA DE INTRODUCCIÓN AL QUIZ]
**Como** usuario inscrito en un curso, ┌─────────────────────────────────────────────────────────────────┐
**quiero** realizar quizzes interactivos al final de cada módulo, │ ← Volver al curso │
**para** validar mi conocimiento, recibir feedback y aprobar las evaluaciones requeridas. ├─────────────────────────────────────────────────────────────────┤
│ │
## Descripción Detallada │ 📝 QUIZ: FUNDAMENTOS DE FIBONACCI │
│ │
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). │ Módulo 1: Introducción a Fibonacci │
│ │
## Mockups/Wireframes │ ┌────────────────────────────────────────────────────────────┐ │
│ │ 📊 Información del Quiz │ │
``` │ │ │ │
[PANTALLA DE INTRODUCCIÓN AL QUIZ] │ │ Preguntas: 10 │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ Tiempo límite: 15 minutos │ │
│ ← Volver al curso │ │ │ Puntuación para aprobar: 70% │ │
├─────────────────────────────────────────────────────────────────┤ │ │ Intentos disponibles: 2 de 3 │ │
│ │ │ │ Tipo: Evaluación │ │
│ 📝 QUIZ: FUNDAMENTOS DE FIBONACCI │ │ │ │ │
│ │ │ │ ✓ Las preguntas están en orden aleatorio │ │
│ Módulo 1: Introducción a Fibonacci │ │ │ ✓ Verás tus respuestas al finalizar │ │
│ │ │ │ ✓ Puedes navegar libremente entre preguntas │ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ 📊 Información del Quiz │ │ │ │
│ │ │ │ │ [🚀 Comenzar Quiz] │
│ │ Preguntas: 10 │ │ │ │
│ │ Tiempo límite: 15 minutos │ │ │ Intentos anteriores: │
│ │ Puntuación para aprobar: 70% │ │ │ • Intento 1: 65% - Reprobado (hace 2 días) │
│ │ Intentos disponibles: 2 de 3 │ │ │ │
│ │ Tipo: Evaluación │ │ └─────────────────────────────────────────────────────────────────┘
│ │ │ │
│ │ ✓ Las preguntas están en orden aleatorio │ │ [PANTALLA DE PREGUNTA]
│ │ ✓ Verás tus respuestas al finalizar │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ ✓ Puedes navegar libremente entre preguntas │ │ │ Quiz: Fundamentos de Fibonacci ⏱ 12:45 restantes │
│ └────────────────────────────────────────────────────────────┘ │ │ Pregunta 3 de 10 ████████░░░░░░ 30% │
│ │ ├─────────────────────────────────────────────────────────────────┤
│ [🚀 Comenzar Quiz] │ │ │
│ │ │ ¿Cuál es el nivel de retroceso de Fibonacci más utilizado? │
│ Intentos anteriores: │ │ │
│ • Intento 1: 65% - Reprobado (hace 2 días) │ │ ○ 23.6% │
│ │ │ ○ 38.2% │
└─────────────────────────────────────────────────────────────────┘ │ ● 61.8% ← Seleccionado │
│ ○ 78.6% │
[PANTALLA DE PREGUNTA] │ │
┌─────────────────────────────────────────────────────────────────┐ │ ┌────────────────────────────────────────────────────────────┐ │
│ Quiz: Fundamentos de Fibonacci ⏱ 12:45 restantes │ │ │ [← Anterior] [🚩 Marcar] [Siguiente →] │ │
│ Pregunta 3 de 10 ████████░░░░░░ 30% │ │ └────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤ │ │
│ │ │ NAVEGADOR: │
│ ¿Cuál es el nivel de retroceso de Fibonacci más utilizado? │ │ [●1] [●2] [●3] [○4] [○5] [○6] [○7] [○8] [🚩9] [○10] │
│ │ │ ↑ ↑ ↑ Sin Sin Sin Sin Sin Marca Sin │
│ ○ 23.6% │ │ Resp Resp Actual resp resp resp resp resp da resp │
│ ○ 38.2% │ │ │
│ ● 61.8% ← Seleccionado │ │ [Finalizar Quiz] │
│ ○ 78.6% │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ [← Anterior] [🚩 Marcar] [Siguiente →] │ │ [CONFIRMACIÓN DE ENVÍO]
│ └────────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ │ ⚠ ¿Estás seguro? │
│ NAVEGADOR: │ │ │
│ [●1] [●2] [●3] [○4] [○5] [○6] [○7] [○8] [🚩9] [○10] │ │ Estás a punto de enviar tu quiz. │
│ ↑ ↑ ↑ Sin Sin Sin Sin Sin Marca Sin │ │ │
│ Resp Resp Actual resp resp resp resp resp da resp │ │ Resumen: │
│ │ │ • Preguntas respondidas: 9 de 10 │
│ [Finalizar Quiz] │ │ • Preguntas sin responder: 1 │
│ │ │ • Preguntas marcadas para revisar: 1 │
└─────────────────────────────────────────────────────────────────┘ │ │
│ ⚠ Una vez enviado, no podrás cambiar tus respuestas. │
[CONFIRMACIÓN DE ENVÍO] │ │
┌─────────────────────────────────────────────────────────────────┐ │ [Cancelar] [Enviar Quiz] │
│ ⚠ ¿Estás seguro? │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘
│ Estás a punto de enviar tu quiz. │
│ │ [RESULTADO - APROBADO]
│ Resumen: │ ┌─────────────────────────────────────────────────────────────────┐
│ • Preguntas respondidas: 9 de 10 │ │ ✨ ✨ ✨ │
│ • Preguntas sin responder: 1 │ │ │
│ • Preguntas marcadas para revisar: 1 │ │ ✅ ¡QUIZ APROBADO! │
│ │ │ │
│ ⚠ Una vez enviado, no podrás cambiar tus respuestas. │ │ Puntuación: 85% │
│ │ │ Preguntas correctas: 8.5/10 │
│ [Cancelar] [Enviar Quiz] │ │ │
│ │ │ +30 XP ganados │
└─────────────────────────────────────────────────────────────────┘ │ │
│ 🎯 Desglose: │
[RESULTADO - APROBADO] │ • Preguntas correctas: 8 │
┌─────────────────────────────────────────────────────────────────┐ │ • Preguntas incorrectas: 2 │
│ ✨ ✨ ✨ │ │ • Puntuación requerida: 70% │
│ │ │ │
│ ✅ ¡QUIZ APROBADO! │ │ Tiempo invertido: 10:32 de 15:00 │
│ │ │ Intento: 2 de 3 │
│ Puntuación: 85% │ │ │
│ Preguntas correctas: 8.5/10 │ │ [Ver respuestas y explicaciones] │
│ │ │ [Continuar al siguiente contenido →] │
│ +30 XP ganados │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘
│ 🎯 Desglose: │ ```
│ • Preguntas correctas: 8 │
│ • Preguntas incorrectas: 2 │ ---
│ • Puntuación requerida: 70% │
│ │ ## Criterios de Aceptación
│ Tiempo invertido: 10:32 de 15:00 │
│ Intento: 2 de 3 │ **Escenario 1: Ver introducción del quiz**
│ │ ```gherkin
│ [Ver respuestas y explicaciones] │ DADO que el usuario está inscrito en el curso
│ [Continuar al siguiente contenido →] │ 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"
```
## Criterios de Aceptación
**Escenario 2: Comenzar quiz**
**Escenario 1: Ver introducción del quiz** ```gherkin
```gherkin DADO que el usuario está en la introducción del quiz
DADO que el usuario está inscrito en el curso CUANDO hace click en "Comenzar Quiz"
CUANDO accede a una lección tipo quiz ENTONCES se registra el inicio del intento en backend
ENTONCES se muestra pantalla de introducción Y se navega a la primera pregunta
Y se muestra: número de preguntas, tiempo límite, puntuación para aprobar Y se inicia el timer countdown
Y se muestran intentos disponibles Y se carga el navegador de preguntas
Y se muestra historial de intentos anteriores (si existen) Y se registra timestamp de inicio
Y se muestra botón "Comenzar Quiz" ```
```
**Escenario 3: Responder pregunta de opción múltiple**
**Escenario 2: Comenzar quiz** ```gherkin
```gherkin DADO que el usuario está en una pregunta
DADO que el usuario está en la introducción del quiz CUANDO selecciona una opción
CUANDO hace click en "Comenzar Quiz" ENTONCES la opción se marca visualmente
ENTONCES se registra el inicio del intento en backend Y la pregunta se marca como "respondida" en el navegador
Y se navega a la primera pregunta Y puede cambiar su respuesta antes de enviar
Y se inicia el timer countdown Y la respuesta se guarda temporalmente en el estado
Y se carga el navegador de preguntas ```
Y se registra timestamp de inicio
``` **Escenario 4: Navegar entre preguntas**
```gherkin
**Escenario 3: Responder pregunta de opción múltiple** DADO que el usuario respondió la pregunta 3
```gherkin CUANDO hace click en "Siguiente"
DADO que el usuario está en una pregunta ENTONCES navega a la pregunta 4
CUANDO selecciona una opción Y su respuesta anterior se mantiene guardada
ENTONCES la opción se marca visualmente Y puede volver a pregunta 3 con "Anterior"
Y la pregunta se marca como "respondida" en el navegador Y puede saltar a cualquier pregunta desde el navegador
Y puede cambiar su respuesta antes de enviar ```
Y la respuesta se guarda temporalmente en el estado
``` **Escenario 5: Marcar pregunta para revisión**
```gherkin
**Escenario 4: Navegar entre preguntas** DADO que el usuario está en una pregunta
```gherkin Y tiene dudas sobre su respuesta
DADO que el usuario respondió la pregunta 3 CUANDO hace click en "🚩 Marcar"
CUANDO hace click en "Siguiente" ENTONCES la pregunta se marca con bandera en el navegador
ENTONCES navega a la pregunta 4 Y puede volver fácilmente a revisarla antes de enviar
Y su respuesta anterior se mantiene guardada Y puede desmarcarla con otro click
Y puede volver a pregunta 3 con "Anterior" ```
Y puede saltar a cualquier pregunta desde el navegador
``` **Escenario 6: Quiz con tiempo límite expira**
```gherkin
**Escenario 5: Marcar pregunta para revisión** DADO que el quiz tiene límite de 15 minutos
```gherkin Y el usuario está respondiendo
DADO que el usuario está en una pregunta CUANDO el timer llega a 0:00
Y tiene dudas sobre su respuesta ENTONCES el quiz se envía automáticamente
CUANDO hace click en "🚩 Marcar" Y se muestra "Tiempo agotado"
ENTONCES la pregunta se marca con bandera en el navegador Y se califica con las respuestas hasta el momento
Y puede volver fácilmente a revisarla antes de enviar Y preguntas sin responder cuentan como incorrectas
Y puede desmarcarla con otro click ```
```
**Escenario 7: Enviar quiz manualmente**
**Escenario 6: Quiz con tiempo límite expira** ```gherkin
```gherkin DADO que el usuario respondió todas las preguntas
DADO que el quiz tiene límite de 15 minutos CUANDO hace click en "Finalizar Quiz"
Y el usuario está respondiendo ENTONCES se muestra modal de confirmación
CUANDO el timer llega a 0:00 Y muestra resumen: respondidas, sin responder, marcadas
ENTONCES el quiz se envía automáticamente Y al confirmar, se envía al backend
Y se muestra "Tiempo agotado" Y se calcula la puntuación
Y se califica con las respuestas hasta el momento Y se muestra pantalla de resultados
Y preguntas sin responder cuentan como incorrectas ```
```
**Escenario 8: Aprobar quiz**
**Escenario 7: Enviar quiz manualmente** ```gherkin
```gherkin DADO que el usuario envió el quiz
DADO que el usuario respondió todas las preguntas Y obtuvo 85% de puntuación
CUANDO hace click en "Finalizar Quiz" Y la puntuación mínima es 70%
ENTONCES se muestra modal de confirmación ENTONCES se muestra "¡QUIZ APROBADO!"
Y muestra resumen: respondidas, sin responder, marcadas Y se otorgan +30 XP
Y al confirmar, se envía al backend Y se marca la lección quiz como completada
Y se calcula la puntuación Y se desbloquea siguiente contenido
Y se muestra pantalla de resultados Y se actualiza progreso del curso
``` ```
**Escenario 8: Aprobar quiz** **Escenario 9: Reprobar quiz con intentos disponibles**
```gherkin ```gherkin
DADO que el usuario envió el quiz DADO que el usuario obtuvo 65% (no aprobó)
Y obtuvo 85% de puntuación Y quedan 2 intentos disponibles
Y la puntuación mínima es 70% ENTONCES se muestra "Quiz Reprobado"
ENTONCES se muestra "¡QUIZ APROBADO!" Y se muestra puntuación obtenida
Y se otorgan +30 XP Y se muestra "Intentos restantes: 2"
Y se marca la lección quiz como completada Y se muestra botón "Reintentar"
Y se desbloquea siguiente contenido Y NO se desbloquea siguiente contenido
Y se actualiza progreso del curso Y NO se otorga XP
``` ```
**Escenario 9: Reprobar quiz con intentos disponibles** **Escenario 10: Reprobar quiz sin intentos**
```gherkin ```gherkin
DADO que el usuario obtuvo 65% (no aprobó) DADO que el usuario agotó todos los intentos
Y quedan 2 intentos disponibles Y no aprobó el quiz
ENTONCES se muestra "Quiz Reprobado" ENTONCES se muestra mensaje "Sin intentos disponibles"
Y se muestra puntuación obtenida Y se sugiere "Repasa las lecciones y contacta a soporte"
Y se muestra "Intentos restantes: 2" Y el siguiente contenido permanece bloqueado
Y se muestra botón "Reintentar" Y se registra el bloqueo para seguimiento
Y NO se desbloquea siguiente contenido ```
Y NO se otorga XP
``` **Escenario 11: Ver explicaciones de respuestas**
```gherkin
**Escenario 10: Reprobar quiz sin intentos** DADO que el usuario completó el quiz
```gherkin CUANDO hace click en "Ver respuestas y explicaciones"
DADO que el usuario agotó todos los intentos ENTONCES se muestran todas las preguntas
Y no aprobó el quiz Y se destacan respuestas correctas en verde
ENTONCES se muestra mensaje "Sin intentos disponibles" Y se destacan respuestas incorrectas en rojo
Y se sugiere "Repasa las lecciones y contacta a soporte" Y se muestra la respuesta correcta
Y el siguiente contenido permanece bloqueado Y se muestra explicación detallada de cada respuesta
Y se registra el bloqueo para seguimiento Y se sugieren lecciones relacionadas para repasar
``` ```
**Escenario 11: Ver explicaciones de respuestas** **Escenario 12: Reintentar quiz**
```gherkin ```gherkin
DADO que el usuario completó el quiz DADO que el usuario reprobó el quiz
CUANDO hace click en "Ver respuestas y explicaciones" Y tiene intentos disponibles
ENTONCES se muestran todas las preguntas CUANDO hace click en "Reintentar"
Y se destacan respuestas correctas en verde ENTONCES se inicia un nuevo intento
Y se destacan respuestas incorrectas en rojo Y las preguntas pueden estar en diferente orden
Y se muestra la respuesta correcta Y las opciones pueden estar en diferente orden
Y se muestra explicación detallada de cada respuesta Y sus respuestas anteriores NO están pre-seleccionadas
Y se sugieren lecciones relacionadas para repasar Y el contador de intentos se decrementa
``` ```
**Escenario 12: Reintentar quiz** ## Criterios Adicionales
```gherkin
DADO que el usuario reprobó el quiz - [ ] Auto-save de respuestas cada 30s (protección contra pérdida)
Y tiene intentos disponibles - [ ] Advertencia antes de salir de la página sin enviar
CUANDO hace click en "Reintentar" - [ ] Modo revisión: ver quiz aprobado sin poder cambiar respuestas
ENTONCES se inicia un nuevo intento - [ ] Estadísticas del quiz: % de aprobación, tiempo promedio
Y las preguntas pueden estar en diferente orden - [ ] Preguntas con imágenes embebidas
Y las opciones pueden estar en diferente orden - [ ] Tipo de pregunta: multiple select (varias correctas)
Y sus respuestas anteriores NO están pre-seleccionadas - [ ] Puntuación parcial en multiple select
Y el contador de intentos se decrementa
``` ---
## Criterios Adicionales ## Tareas Técnicas
- [ ] Auto-save de respuestas cada 30s (protección contra pérdida) **Database:**
- [ ] Advertencia antes de salir de la página sin enviar - [ ] DB-EDU-015: Tabla education.quizzes
- [ ] Modo revisión: ver quiz aprobado sin poder cambiar respuestas - [ ] DB-EDU-016: Tabla education.questions (FK a quiz)
- [ ] Estadísticas del quiz: % de aprobación, tiempo promedio - [ ] DB-EDU-017: Tabla education.question_options
- [ ] Preguntas con imágenes embebidas - [ ] DB-EDU-018: Tabla education.quiz_attempts
- [ ] Tipo de pregunta: multiple select (varias correctas) - [ ] DB-EDU-019: Tabla education.quiz_answers (respuestas del usuario)
- [ ] Puntuación parcial en multiple select
**Backend:**
--- - [ ] BE-EDU-039: Endpoint GET /education/quizzes/:id
- [ ] BE-EDU-040: Endpoint POST /education/quizzes/:id/start
## Tareas Técnicas - [ ] BE-EDU-041: Endpoint POST /education/quizzes/:id/submit
- [ ] BE-EDU-042: Endpoint GET /education/quizzes/:id/attempts
**Database:** - [ ] BE-EDU-043: Endpoint GET /education/quizzes/:id/results/:attemptId
- [ ] DB-EDU-015: Tabla education.quizzes - [ ] BE-EDU-044: Implementar QuizService.gradeAttempt()
- [ ] DB-EDU-016: Tabla education.questions (FK a quiz) - [ ] BE-EDU-045: Implementar shuffle de preguntas y opciones
- [ ] DB-EDU-017: Tabla education.question_options - [ ] BE-EDU-046: Validar intentos disponibles
- [ ] DB-EDU-018: Tabla education.quiz_attempts - [ ] BE-EDU-047: Implementar timer y auto-submit
- [ ] DB-EDU-019: Tabla education.quiz_answers (respuestas del usuario) - [ ] BE-EDU-048: NO devolver respuestas correctas en GET (seguridad)
**Backend:** **Frontend:**
- [ ] BE-EDU-039: Endpoint GET /education/quizzes/:id - [ ] FE-EDU-048: Crear QuizIntroPage.tsx
- [ ] BE-EDU-040: Endpoint POST /education/quizzes/:id/start - [ ] FE-EDU-049: Crear QuizPlayerPage.tsx
- [ ] BE-EDU-041: Endpoint POST /education/quizzes/:id/submit - [ ] FE-EDU-050: Crear componente QuestionRenderer.tsx
- [ ] BE-EDU-042: Endpoint GET /education/quizzes/:id/attempts - [ ] FE-EDU-051: Crear componente QuizNavigator.tsx (minimap)
- [ ] BE-EDU-043: Endpoint GET /education/quizzes/:id/results/:attemptId - [ ] FE-EDU-052: Crear componente QuizTimer.tsx
- [ ] BE-EDU-044: Implementar QuizService.gradeAttempt() - [ ] FE-EDU-053: Crear QuizResultsPage.tsx
- [ ] BE-EDU-045: Implementar shuffle de preguntas y opciones - [ ] FE-EDU-054: Crear componente AnswerExplanation.tsx
- [ ] BE-EDU-046: Validar intentos disponibles - [ ] FE-EDU-055: Modal de confirmación de envío
- [ ] BE-EDU-047: Implementar timer y auto-submit - [ ] FE-EDU-056: Auto-save de respuestas cada 30s
- [ ] BE-EDU-048: NO devolver respuestas correctas en GET (seguridad) - [ ] FE-EDU-057: Advertencia antes de salir (window.onbeforeunload)
- [ ] FE-EDU-058: Implementar quizStore (Zustand)
**Frontend:** - [ ] FE-EDU-059: Animación de celebración al aprobar
- [ ] FE-EDU-048: Crear QuizIntroPage.tsx
- [ ] FE-EDU-049: Crear QuizPlayerPage.tsx **Tests:**
- [ ] FE-EDU-050: Crear componente QuestionRenderer.tsx - [ ] TEST-EDU-021: Test calificación de quiz
- [ ] FE-EDU-051: Crear componente QuizNavigator.tsx (minimap) - [ ] TEST-EDU-022: Test aprobar quiz otorga XP
- [ ] FE-EDU-052: Crear componente QuizTimer.tsx - [ ] TEST-EDU-023: Test auto-submit cuando expira tiempo
- [ ] FE-EDU-053: Crear QuizResultsPage.tsx - [ ] TEST-EDU-024: Test límite de intentos
- [ ] FE-EDU-054: Crear componente AnswerExplanation.tsx - [ ] TEST-EDU-025: Test E2E realizar quiz completo
- [ ] 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) ## Dependencias
- [ ] FE-EDU-059: Animación de celebración al aprobar
**Depende de:**
**Tests:** - [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
- [ ] TEST-EDU-021: Test calificación de quiz - [ ] RF-EDU-004: Sistema de quizzes
- [ ] TEST-EDU-022: Test aprobar quiz otorga XP
- [ ] TEST-EDU-023: Test auto-submit cuando expira tiempo **Bloquea:**
- [ ] TEST-EDU-024: Test límite de intentos - [ ] US-EDU-005: Completar lección
- [ ] TEST-EDU-025: Test E2E realizar quiz completo - [ ] US-EDU-007: Ver progreso
--- ---
## Dependencias ## Notas Técnicas
**Depende de:** **Endpoint GET /quizzes/:id (SIN respuestas correctas):**
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente ```typescript
- [ ] RF-EDU-004: Sistema de quizzes {
quiz: {
**Bloquea:** id: "quiz-1",
- [ ] US-EDU-005: Completar lección title: "Fundamentos de Fibonacci",
- [ ] US-EDU-007: Ver progreso description: "Evalúa tu conocimiento...",
timeLimit: 15, // minutos
--- passingScore: 70, // 0-100
maxAttempts: 3,
## Notas Técnicas questionCount: 10,
totalPoints: 10,
**Endpoint GET /quizzes/:id (SIN respuestas correctas):** shuffleQuestions: true,
```typescript shuffleOptions: true,
{ mode: "assessment",
quiz: {
id: "quiz-1", questions: [
title: "Fundamentos de Fibonacci", {
description: "Evalúa tu conocimiento...", id: "q-1",
timeLimit: 15, // minutos question: "¿Cuál es el nivel más utilizado?",
passingScore: 70, // 0-100 type: "multiple_choice",
maxAttempts: 3, points: 1,
questionCount: 10, options: [
totalPoints: 10, { id: "opt-1", text: "23.6%" },
shuffleQuestions: true, { id: "opt-2", text: "38.2%" },
shuffleOptions: true, { id: "opt-3", text: "61.8%" },
mode: "assessment", { id: "opt-4", text: "78.6%" }
]
questions: [ // NO incluir isCorrect en GET
{ }
id: "q-1", ],
question: "¿Cuál es el nivel más utilizado?",
type: "multiple_choice", userAttempts: [
points: 1, {
options: [ attemptNumber: 1,
{ id: "opt-1", text: "23.6%" }, score: 65,
{ id: "opt-2", text: "38.2%" }, passed: false,
{ id: "opt-3", text: "61.8%" }, submittedAt: "2025-12-03T10:30:00Z"
{ id: "opt-4", text: "78.6%" } }
] ],
// NO incluir isCorrect en GET attemptsRemaining: 2
} }
], }
```
userAttempts: [
{ **Endpoint POST /quizzes/:id/submit:**
attemptNumber: 1, ```typescript
score: 65, // Request
passed: false, {
submittedAt: "2025-12-03T10:30:00Z" answers: {
} "q-1": "opt-3",
], "q-2": "opt-1",
attemptsRemaining: 2 "q-3": ["opt-2", "opt-4"], // Multiple select
} // ...
} },
``` timeSpent: 632 // segundos
}
**Endpoint POST /quizzes/:id/submit:**
```typescript // Response
// Request {
{ attempt: {
answers: { id: "attempt-uuid",
"q-1": "opt-3", quizId: "quiz-1",
"q-2": "opt-1", attemptNumber: 2,
"q-3": ["opt-2", "opt-4"], // Multiple select score: 85,
// ... passed: true,
}, pointsEarned: 8.5,
timeSpent: 632 // segundos totalPoints: 10,
} submittedAt: "2025-12-05T16:45:00Z"
},
// Response rewards: {
{ xpEarned: 30,
attempt: { bonusXP: 20, // Si es 100%
id: "attempt-uuid", totalXP: 50
quizId: "quiz-1", },
attemptNumber: 2, answers: [
score: 85, {
passed: true, questionId: "q-1",
pointsEarned: 8.5, userAnswer: "opt-3",
totalPoints: 10, correctAnswer: "opt-3",
submittedAt: "2025-12-05T16:45:00Z" isCorrect: true,
}, pointsEarned: 1,
rewards: { explanation: "61.8% es el nivel dorado..."
xpEarned: 30, }
bonusXP: 20, // Si es 100% // ...
totalXP: 50 ],
}, attemptsRemaining: 1,
answers: [ canRetake: true
{ }
questionId: "q-1", ```
userAnswer: "opt-3",
correctAnswer: "opt-3", **Reglas de calificación:**
isCorrect: true, - Multiple choice: 1 punto si es correcta, 0 si no
pointsEarned: 1, - Multiple select: puntos parciales (0.5 si acierta 2 de 4)
explanation: "61.8% es el nivel dorado..." - True/False: 1 punto si es correcta
} - Sin responder: 0 puntos
// ...
], **Entidades/Tablas:**
attemptsRemaining: 1, - `education.quizzes`
canRetake: true - `education.questions`
} - `education.question_options`
``` - `education.quiz_attempts`
- `education.quiz_answers`
**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 ## Definition of Ready (DoR)
- Sin responder: 0 puntos
- [x] Historia claramente escrita
**Entidades/Tablas:** - [x] Criterios de aceptación definidos
- `education.quizzes` - [x] Story points estimados
- `education.questions` - [x] Dependencias identificadas
- `education.question_options` - [x] Sin bloqueadores
- `education.quiz_attempts` - [x] Diseño/mockup disponible
- `education.quiz_answers` - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,434 +1,422 @@
--- # US-EDU-007: Ver Progreso Educativo
id: "US-EDU-007"
title: "Ver Progreso Educativo" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Alta" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-007 |
story_points: 3 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P0 |
--- | **Story Points** | 3 |
| **Sprint** | Sprint 4 |
# US-EDU-007: Ver Progreso Educativo | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-007 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario activo en la plataforma educativa,
| **Módulo** | education | **quiero** ver un dashboard con mi progreso de aprendizaje completo,
| **Prioridad** | P0 | **para** monitorear mi avance, mantenerme motivado y planificar mi siguiente paso.
| **Story Points** | 3 |
| **Sprint** | Sprint 4 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
┌─────────────────────────────────────────────────────────────────┐
**Como** usuario activo en la plataforma educativa, │ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
**quiero** ver un dashboard con mi progreso de aprendizaje completo, ├─────────────────────────────────────────────────────────────────┤
**para** monitorear mi avance, mantenerme motivado y planificar mi siguiente paso. │ │
│ MI APRENDIZAJE │
## 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. │ │ RESUMEN GENERAL │ │
│ │ │ │
## Mockups/Wireframes │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ 🔄 3 │ │ ✅ 12 │ │ 📚 156 │ │ ⏱ 42h │ │ │
``` │ │ │ En prog│ │ Comple │ │ Lecc. │ │ Estudio│ │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │ │ │ │ │
├─────────────────────────────────────────────────────────────────┤ │ │ ┌───────────────────┐ ┌─────────────────┐ │ │
│ │ │ │ │ 🔥 RACHA: 12 DÍAS │ │ ⭐ NIVEL 15 │ │ │
│ MI APRENDIZAJE │ │ │ │ ¡Sigue así! │ │ 2,450 / 3,000 XP│ │ │
│ │ │ │ │ 📅📅📅📅📅📅📅 │ │ ████████░░ 82% │ │ │
│ ┌──────────────────────────────────────────────────────────┐ │ │ │ └───────────────────┘ └─────────────────┘ │ │
│ │ RESUMEN GENERAL │ │ │ └──────────────────────────────────────────────────────────┘ │
│ │ │ │ │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ ┌──────────────────────────────────────────────────────────┐ │
│ │ │ 🔄 3 │ │ ✅ 12 │ │ 📚 156 │ │ ⏱ 42h │ │ │ │ │ CURSOS EN PROGRESO (3) │ │
│ │ │ En prog│ │ Comple │ │ Lecc. │ │ Estudio│ │ │ │ │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │ │ [IMG] Fibonacci Retracement Básico │ │ │
│ │ ┌───────────────────┐ ┌─────────────────┐ │ │ │ │ │ ████████████████░░░░ 75% completado │ │ │
│ │ │ 🔥 RACHA: 12 DÍAS │ │ ⭐ NIVEL 15 │ │ │ │ │ │ 18 de 23 lecciones | Módulo 4 de 5 │ │ │
│ │ │ ¡Sigue así! │ │ 2,450 / 3,000 XP│ │ │ │ │ │ Última vez: Hace 2 horas │ │ │
│ │ │ 📅📅📅📅📅📅📅 │ │ ████████░░ 82% │ │ │ │ │ │ [Continuar →] Lección: Fibonacci en tendencias │ │ │
│ │ └───────────────────┘ └─────────────────┘ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ ┌──────────────────────────────────────────────────────────┐ │ │ │ │ [IMG] Day Trading para Principiantes │ │ │
│ │ CURSOS EN PROGRESO (3) │ │ │ │ │ ██████░░░░░░░░░░░░░░ 30% completado │ │ │
│ │ │ │ │ │ │ 9 de 30 lecciones | Módulo 2 de 6 │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ │ Última vez: Hace 1 día │ │ │
│ │ │ [IMG] Fibonacci Retracement Básico │ │ │ │ │ │ [Continuar →] Lección: Análisis de volumen │ │ │
│ │ │ ████████████████░░░░ 75% completado │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ │ │ 18 de 23 lecciones | Módulo 4 de 5 │ │ │ │ │ │ │
│ │ │ Última vez: Hace 2 horas │ │ │ │ │ [Ver todos los cursos en progreso] │ │
│ │ │ [Continuar →] Lección: Fibonacci en tendencias │ │ │ │ └──────────────────────────────────────────────────────────┘ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │
│ │ │ │ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ ACTIVIDAD RECIENTE │ │ CALENDARIO DE APRENDIZAJE │ │
│ │ │ [IMG] Day Trading para Principiantes │ │ │ │ │ │ │ │ │
│ │ │ ██████░░░░░░░░░░░░░░ 30% completado │ │ │ │ │ Hoy, 15:30 │ │ L M X J V S D │ │
│ │ │ 9 de 30 lecciones | Módulo 2 de 6 │ │ │ │ │ ✓ Completaste │ │ ██ ██ ░░ ██ ██ ██ ░░ │ │
│ │ │ Última vez: Hace 1 día │ │ │ │ │ "Niveles de Fib."│ │ ██ ██ ██ ██ ██ ░░ ░░ │ │
│ │ │ [Continuar →] Lección: Análisis de volumen │ │ │ │ │ +10 XP │ │ ██ ██ ██ ░░ ░░ ░░ ░░ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │
│ │ │ │ │ │ Hoy, 14:20 │ │ Días activos este mes: 18 │ │
│ │ [Ver todos los cursos en progreso] │ │ │ │ ✅ Aprobaste │ │ │ │
│ └──────────────────────────────────────────────────────────┘ │ │ │ Quiz Módulo 3 │ │ │ │
│ │ │ │ +30 XP │ │ │ │
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │ │ │ │ │ │
│ │ ACTIVIDAD RECIENTE │ │ CALENDARIO DE APRENDIZAJE │ │ │ │ Ayer, 18:45 │ │ │ │
│ │ │ │ │ │ │ │ 🎓 Obtuviste │ │ │ │
│ │ Hoy, 15:30 │ │ L M X J V S D │ │ │ │ badge "Week │ │ │ │
│ │ ✓ Completaste │ │ ██ ██ ░░ ██ ██ ██ ░░ │ │ │ │ Warrior" │ │ │ │
│ │ "Niveles de Fib."│ │ ██ ██ ██ ██ ██ ░░ ░░ │ │ │ │ │ │ │ │
│ │ +10 XP │ │ ██ ██ ██ ░░ ░░ ░░ ░░ │ │ │ │ [Ver más] │ │ │ │
│ │ │ │ │ │ │ └─────────────────────┘ └─────────────────────────────────┘ │
│ │ Hoy, 14:20 │ │ Días activos este mes: 18 │ │ │ │
│ │ ✅ Aprobaste │ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │
│ │ Quiz Módulo 3 │ │ │ │ │ │ 📊 ESTADÍSTICAS │ │
│ │ +30 XP │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ Tiempo promedio por lección: 15 min │ │
│ │ Ayer, 18:45 │ │ │ │ │ │ Cursos completados este mes: 3 │ │
│ │ 🎓 Obtuviste │ │ │ │ │ │ Tasa de completitud: 80% (12 de 15 cursos iniciados) │ │
│ │ badge "Week │ │ │ │ │ │ Categoría favorita: Análisis Técnico (6 cursos) │ │
│ │ Warrior" │ │ │ │ │ │ Mejor día de la semana: Miércoles (25 lecciones) │ │
│ │ │ │ │ │ │ │ │ │
│ │ [Ver más] │ │ │ │ │ │ [📈 Ver estadísticas detalladas] │ │
│ └─────────────────────┘ └─────────────────────────────────┘ │ │ └──────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌──────────────────────────────────────────────────────────┐ │ └─────────────────────────────────────────────────────────────────┘
│ │ 📊 ESTADÍSTICAS │ │ ```
│ │ │ │
│ │ Tiempo promedio por lección: 15 min │ │ ---
│ │ Cursos completados este mes: 3 │ │
│ │ Tasa de completitud: 80% (12 de 15 cursos iniciados) │ │ ## Criterios de Aceptación
│ │ Categoría favorita: Análisis Técnico (6 cursos) │ │
│ │ Mejor día de la semana: Miércoles (25 lecciones) │ │ **Escenario 1: Ver dashboard de progreso**
│ │ │ │ ```gherkin
│ │ [📈 Ver estadísticas detalladas] │ │ 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
## Criterios de Aceptación ```
**Escenario 1: Ver dashboard de progreso** **Escenario 2: Ver cursos en progreso**
```gherkin ```gherkin
DADO que el usuario está autenticado DADO que el usuario tiene 3 cursos en progreso
CUANDO navega a /education/progress o /education/dashboard CUANDO ve la sección "Cursos en progreso"
ENTONCES se muestra dashboard completo de aprendizaje ENTONCES se muestran los 3 cursos ordenados por última actividad
Y se muestran métricas: cursos en progreso, completados, lecciones, horas Y cada curso muestra: título, imagen, porcentaje, lecciones completadas
Y se muestra racha actual con visualización de días Y se muestra "Última vez: hace X tiempo"
Y se muestra nivel actual y progreso a siguiente nivel Y se muestra botón "Continuar" con próxima lección
Y se muestran cursos en progreso con porcentajes Y al hacer click en "Continuar", navega a esa lección
Y se muestra actividad reciente ```
```
**Escenario 3: Ver racha activa**
**Escenario 2: Ver cursos en progreso** ```gherkin
```gherkin DADO que el usuario tiene racha de 12 días
DADO que el usuario tiene 3 cursos en progreso CUANDO ve el widget de racha
CUANDO ve la sección "Cursos en progreso" ENTONCES se muestra "🔥 RACHA: 12 DÍAS"
ENTONCES se muestran los 3 cursos ordenados por última actividad Y se muestra visualización de últimos 7 días
Y cada curso muestra: título, imagen, porcentaje, lecciones completadas Y se muestra mensaje motivacional "¡Sigue así!"
Y se muestra "Última vez: hace X tiempo" Y si no completó lección hoy, muestra "Completa 1 lección hoy para mantener racha"
Y se muestra botón "Continuar" con próxima lección ```
Y al hacer click en "Continuar", navega a esa lección
``` **Escenario 4: Ver nivel y XP**
```gherkin
**Escenario 3: Ver racha activa** DADO que el usuario es nivel 15 con 2450 XP
```gherkin Y necesita 3000 XP para nivel 16
DADO que el usuario tiene racha de 12 días CUANDO ve el widget de nivel
CUANDO ve el widget de racha ENTONCES se muestra "⭐ NIVEL 15"
ENTONCES se muestra "🔥 RACHA: 12 DÍAS" Y se muestra "2,450 / 3,000 XP"
Y se muestra visualización de últimos 7 días Y se muestra barra de progreso al 82%
Y se muestra mensaje motivacional "¡Sigue así!" Y se muestra cuántos XP faltan: "Faltan 550 XP para nivel 16"
Y si no completó lección hoy, muestra "Completa 1 lección hoy para mantener racha" ```
```
**Escenario 5: Ver calendario de actividad**
**Escenario 4: Ver nivel y XP** ```gherkin
```gherkin DADO que el usuario completó lecciones en 18 días este mes
DADO que el usuario es nivel 15 con 2450 XP CUANDO ve el calendario
Y necesita 3000 XP para nivel 16 ENTONCES se muestra grid estilo GitHub contributions
CUANDO ve el widget de nivel Y días con actividad están resaltados (cuadros llenos)
ENTONCES se muestra "⭐ NIVEL 15" Y días sin actividad están vacíos
Y se muestra "2,450 / 3,000 XP" Y al hacer hover sobre un día, muestra detalle: "3 lecciones, 45 min"
Y se muestra barra de progreso al 82% Y se muestra contador "Días activos este mes: 18"
Y se muestra cuántos XP faltan: "Faltan 550 XP para nivel 16" ```
```
**Escenario 6: Ver actividad reciente**
**Escenario 5: Ver calendario de actividad** ```gherkin
```gherkin DADO que el usuario tiene actividad reciente
DADO que el usuario completó lecciones en 18 días este mes CUANDO ve la sección "Actividad reciente"
CUANDO ve el calendario ENTONCES se muestran últimos 10 eventos
ENTONCES se muestra grid estilo GitHub contributions Y cada evento muestra: timestamp, tipo, descripción, XP ganado
Y días con actividad están resaltados (cuadros llenos) Y eventos incluyen: lección completada, quiz aprobado, badge obtenido
Y días sin actividad están vacíos Y se ordenan de más reciente a más antiguo
Y al hacer hover sobre un día, muestra detalle: "3 lecciones, 45 min" Y hay botón "Ver más" para ver historial completo
Y se muestra contador "Días activos este mes: 18" ```
```
**Escenario 7: Ver estadísticas**
**Escenario 6: Ver actividad reciente** ```gherkin
```gherkin DADO que el usuario tiene suficiente actividad
DADO que el usuario tiene actividad reciente CUANDO ve la sección "Estadísticas"
CUANDO ve la sección "Actividad reciente" ENTONCES se muestra tiempo promedio por lección
ENTONCES se muestran últimos 10 eventos Y se muestra cursos completados este mes
Y cada evento muestra: timestamp, tipo, descripción, XP ganado Y se muestra tasa de completitud (cursos finalizados / iniciados)
Y eventos incluyen: lección completada, quiz aprobado, badge obtenido Y se muestra categoría favorita
Y se ordenan de más reciente a más antiguo Y se muestra mejor día de la semana
Y hay botón "Ver más" para ver historial completo Y hay botón para ver estadísticas detalladas
``` ```
**Escenario 7: Ver estadísticas** **Escenario 8: Continuar curso desde dashboard**
```gherkin ```gherkin
DADO que el usuario tiene suficiente actividad DADO que el usuario tiene curso en progreso
CUANDO ve la sección "Estadísticas" Y la última lección accedida fue "Lección 18"
ENTONCES se muestra tiempo promedio por lección CUANDO hace click en "Continuar" del curso
Y se muestra cursos completados este mes ENTONCES navega directamente a la Lección 19 (siguiente)
Y se muestra tasa de completitud (cursos finalizados / iniciados) Y el reproductor se carga listo para comenzar
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 9: Usuario sin actividad reciente**
``` ```gherkin
DADO que el usuario no tiene actividad en últimos 7 días
**Escenario 8: Continuar curso desde dashboard** CUANDO accede al dashboard
```gherkin ENTONCES se muestra mensaje de bienvenida
DADO que el usuario tiene curso en progreso Y se sugieren cursos populares para comenzar
Y la última lección accedida fue "Lección 18" Y se muestra motivación: "¡Comienza hoy tu racha de aprendizaje!"
CUANDO hace click en "Continuar" del curso Y las métricas muestran valores cero elegantemente
ENTONCES navega directamente a la Lección 19 (siguiente) ```
Y el reproductor se carga listo para comenzar
``` **Escenario 10: Racha en riesgo**
```gherkin
**Escenario 9: Usuario sin actividad reciente** DADO que el usuario tiene racha de 15 días
```gherkin Y NO ha completado lecciones hoy
DADO que el usuario no tiene actividad en últimos 7 días Y es después de las 6pm hora local
CUANDO accede al dashboard CUANDO accede al dashboard
ENTONCES se muestra mensaje de bienvenida ENTONCES se muestra alerta "⚠ ¡Racha en riesgo!"
Y se sugieren cursos populares para comenzar Y se muestra "Completa 1 lección antes de medianoche"
Y se muestra motivación: "¡Comienza hoy tu racha de aprendizaje!" Y se sugiere lección corta: "Lección rápida (5 min): [título]"
Y las métricas muestran valores cero elegantemente ```
```
## Criterios Adicionales
**Escenario 10: Racha en riesgo**
```gherkin - [ ] Gráfico de XP ganado por semana/mes
DADO que el usuario tiene racha de 15 días - [ ] Comparación con usuarios similares (opcional)
Y NO ha completado lecciones hoy - [ ] Metas personales de aprendizaje
Y es después de las 6pm hora local - [ ] Exportar reporte de progreso en PDF
CUANDO accede al dashboard - [ ] Compartir logros en redes sociales
ENTONCES se muestra alerta "⚠ ¡Racha en riesgo!" - [ ] Widget de "Próximas recompensas" (badge a desbloquear)
Y se muestra "Completa 1 lección antes de medianoche"
Y se sugiere lección corta: "Lección rápida (5 min): [título]" ---
```
## Tareas Técnicas
## Criterios Adicionales
**Database:**
- [ ] Gráfico de XP ganado por semana/mes - [ ] DB-EDU-020: Vista materialized para stats de usuario
- [ ] Comparación con usuarios similares (opcional) - [ ] DB-EDU-021: Índices en user_activity_log por user_id + timestamp
- [ ] Metas personales de aprendizaje
- [ ] Exportar reporte de progreso en PDF **Backend:**
- [ ] Compartir logros en redes sociales - [ ] BE-EDU-049: Endpoint GET /education/progress/overview
- [ ] Widget de "Próximas recompensas" (badge a desbloquear) - [ ] 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
## Tareas Técnicas - [ ] BE-EDU-054: Implementar ProgressService.getOverview()
- [ ] BE-EDU-055: Job diario para calcular stats agregadas
**Database:** - [ ] BE-EDU-056: Caché de stats en Redis (15 min)
- [ ] DB-EDU-020: Vista materialized para stats de usuario
- [ ] DB-EDU-021: Índices en user_activity_log por user_id + timestamp **Frontend:**
- [ ] FE-EDU-060: Crear EducationDashboardPage.tsx
**Backend:** - [ ] FE-EDU-061: Crear componente ProgressOverview.tsx
- [ ] BE-EDU-049: Endpoint GET /education/progress/overview - [ ] FE-EDU-062: Crear componente CourseProgressCard.tsx
- [ ] BE-EDU-050: Endpoint GET /education/progress/courses - [ ] FE-EDU-063: Crear componente StreakWidget.tsx
- [ ] BE-EDU-051: Endpoint GET /education/progress/stats - [ ] FE-EDU-064: Crear componente LevelWidget.tsx
- [ ] BE-EDU-052: Endpoint GET /education/progress/activity - [ ] FE-EDU-065: Crear componente ActivityCalendar.tsx (GitHub style)
- [ ] BE-EDU-053: Endpoint GET /education/progress/calendar - [ ] FE-EDU-066: Crear componente RecentActivity.tsx
- [ ] BE-EDU-054: Implementar ProgressService.getOverview() - [ ] FE-EDU-067: Crear componente StatsPanel.tsx
- [ ] BE-EDU-055: Job diario para calcular stats agregadas - [ ] FE-EDU-068: Implementar progressStore (Zustand)
- [ ] BE-EDU-056: Caché de stats en Redis (15 min) - [ ] FE-EDU-069: Skeleton loaders para cada sección
**Frontend:** **Tests:**
- [ ] FE-EDU-060: Crear EducationDashboardPage.tsx - [ ] TEST-EDU-026: Test cálculo de tasa de completitud
- [ ] FE-EDU-061: Crear componente ProgressOverview.tsx - [ ] TEST-EDU-027: Test cálculo de racha
- [ ] FE-EDU-062: Crear componente CourseProgressCard.tsx - [ ] TEST-EDU-028: Test stats agregadas
- [ ] FE-EDU-063: Crear componente StreakWidget.tsx - [ ] TEST-EDU-029: Test E2E visualizar dashboard completo
- [ ] 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 ## Dependencias
- [ ] FE-EDU-068: Implementar progressStore (Zustand)
- [ ] FE-EDU-069: Skeleton loaders para cada sección **Depende de:**
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
**Tests:** - [ ] RF-EDU-003: Sistema de progreso
- [ ] TEST-EDU-026: Test cálculo de tasa de completitud - [ ] RF-EDU-006: Gamificación (para nivel y XP)
- [ ] TEST-EDU-027: Test cálculo de racha
- [ ] TEST-EDU-028: Test stats agregadas **Bloquea:**
- [ ] TEST-EDU-029: Test E2E visualizar dashboard completo - Ninguna (es página de visualización)
--- ---
## Dependencias ## Notas Técnicas
**Depende de:** **Endpoint GET /education/progress/overview:**
- [ ] US-EDU-005: Completar lección - Estado: Pendiente ```typescript
- [ ] RF-EDU-003: Sistema de progreso {
- [ ] RF-EDU-006: Gamificación (para nivel y XP) overview: {
coursesInProgress: 3,
**Bloquea:** coursesCompleted: 12,
- Ninguna (es página de visualización) coursesSaved: 5,
lessonsCompleted: 156,
--- totalLearningTime: 2520, // minutos (42h)
currentStreak: 12,
## Notas Técnicas longestStreak: 18,
totalXP: 2450,
**Endpoint GET /education/progress/overview:** currentLevel: 15,
```typescript xpToNextLevel: 550
{ },
overview: {
coursesInProgress: 3, coursesInProgress: [
coursesCompleted: 12, {
coursesSaved: 5, courseId: "course-1",
lessonsCompleted: 156, title: "Fibonacci Retracement Básico",
totalLearningTime: 2520, // minutos (42h) slug: "fibonacci-retracement-basico",
currentStreak: 12, thumbnail: "...",
longestStreak: 18, progressPercent: 75,
totalXP: 2450, lessonsCompleted: 18,
currentLevel: 15, totalLessons: 23,
xpToNextLevel: 550 modulesCompleted: 3,
}, totalModules: 5,
lastAccessedAt: "2025-12-05T13:30:00Z",
coursesInProgress: [ nextLesson: {
{ id: "les-19",
courseId: "course-1", title: "Fibonacci en tendencias bajistas",
title: "Fibonacci Retracement Básico", slug: "fibonacci-tendencias-bajistas",
slug: "fibonacci-retracement-basico", duration: 12
thumbnail: "...", }
progressPercent: 75, }
lessonsCompleted: 18, // ... más cursos
totalLessons: 23, ],
modulesCompleted: 3,
totalModules: 5, recentActivity: [
lastAccessedAt: "2025-12-05T13:30:00Z", {
nextLesson: { type: "lesson_completed",
id: "les-19", title: "Completaste 'Niveles de Fibonacci'",
title: "Fibonacci en tendencias bajistas", description: "Lección 2.1 del curso Fibonacci Básico",
slug: "fibonacci-tendencias-bajistas", timestamp: "2025-12-05T15:30:00Z",
duration: 12 xpEarned: 10,
} icon: "✓"
} },
// ... más cursos {
], type: "quiz_passed",
title: "Aprobaste Quiz Módulo 3",
recentActivity: [ description: "Puntuación: 85%",
{ timestamp: "2025-12-05T14:20:00Z",
type: "lesson_completed", xpEarned: 30,
title: "Completaste 'Niveles de Fibonacci'", icon: "✅"
description: "Lección 2.1 del curso Fibonacci Básico", }
timestamp: "2025-12-05T15:30:00Z", // ... más actividad
xpEarned: 10, ],
icon: "✓"
}, calendar: [
{ { date: "2025-12-01", lessonsCompleted: 3, minutesLearned: 45 },
type: "quiz_passed", { date: "2025-12-02", lessonsCompleted: 2, minutesLearned: 30 },
title: "Aprobaste Quiz Módulo 3", { date: "2025-12-03", lessonsCompleted: 0, minutesLearned: 0 },
description: "Puntuación: 85%", // ...
timestamp: "2025-12-05T14:20:00Z", ],
xpEarned: 30,
icon: "✅" stats: {
} avgTimePerLesson: 15,
// ... más actividad coursesThisMonth: 3,
], completionRate: 80, // 12 completados de 15 iniciados
activeDays: 18,
calendar: [ favoriteCategory: "Análisis Técnico",
{ date: "2025-12-01", lessonsCompleted: 3, minutesLearned: 45 }, bestDayOfWeek: "Wednesday",
{ date: "2025-12-02", lessonsCompleted: 2, minutesLearned: 30 }, preferredTimeOfDay: "Evening"
{ date: "2025-12-03", lessonsCompleted: 0, minutesLearned: 0 }, }
// ... }
], ```
stats: { **Cálculos importantes:**
avgTimePerLesson: 15, ```javascript
coursesThisMonth: 3, // Racha actual
completionRate: 80, // 12 completados de 15 iniciados currentStreak = countConsecutiveDaysWithActivity(today, lookback=365);
activeDays: 18,
favoriteCategory: "Análisis Técnico", // Tasa de completitud
bestDayOfWeek: "Wednesday", completionRate = (coursesCompleted / coursesStarted) * 100;
preferredTimeOfDay: "Evening"
} // Tiempo promedio por lección
} avgTimePerLesson = totalLearningTime / lessonsCompleted;
```
// Categoría favorita
**Cálculos importantes:** favoriteCategory = categoryWithMostCompletedCourses();
```javascript ```
// Racha actual
currentStreak = countConsecutiveDaysWithActivity(today, lookback=365); **Optimizaciones:**
- Usar materialized views para stats agregadas
// Tasa de completitud - Calcular stats en background job nocturno
completionRate = (coursesCompleted / coursesStarted) * 100; - Cachear overview en Redis (15 min)
- Lazy load de secciones con IntersectionObserver
// Tiempo promedio por lección - Implementar skeleton loading para mejor UX
avgTimePerLesson = totalLearningTime / lessonsCompleted;
**Entidades/Tablas:**
// Categoría favorita - `education.user_course_progress`
favoriteCategory = categoryWithMostCompletedCourses(); - `education.user_lesson_progress`
``` - `education.user_activity_log`
- `gamification.user_stats`
**Optimizaciones:**
- Usar materialized views para stats agregadas ---
- Calcular stats en background job nocturno
- Cachear overview en Redis (15 min) ## Definition of Ready (DoR)
- Lazy load de secciones con IntersectionObserver
- Implementar skeleton loading para mejor UX - [x] Historia claramente escrita
- [x] Criterios de aceptación definidos
**Entidades/Tablas:** - [x] Story points estimados
- `education.user_course_progress` - [x] Dependencias identificadas
- `education.user_lesson_progress` - [x] Sin bloqueadores
- `education.user_activity_log` - [x] Diseño/mockup disponible
- `gamification.user_stats` - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,449 +1,437 @@
--- # US-EDU-008: Obtener Certificado
id: "US-EDU-008"
title: "Obtener Certificado" ## Metadata
type: "User Story"
status: "Done" | Campo | Valor |
priority: "Media" |-------|-------|
epic: "OQI-002" | **ID** | US-EDU-008 |
story_points: 3 | **Épica** | OQI-002 - Módulo Educativo |
created_date: "2025-12-05" | **Módulo** | education |
updated_date: "2026-01-04" | **Prioridad** | P2 |
--- | **Story Points** | 3 |
| **Sprint** | Sprint 5 |
# US-EDU-008: Obtener Certificado | **Estado** | Pendiente |
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
|-------|-------| ## Historia de Usuario
| **ID** | US-EDU-008 |
| **Épica** | OQI-002 - Módulo Educativo | **Como** usuario que completó un curso,
| **Módulo** | education | **quiero** obtener un certificado digital verificable,
| **Prioridad** | P2 | **para** validar mi logro, agregarlo a mi perfil profesional y compartirlo en redes sociales.
| **Story Points** | 3 |
| **Sprint** | Sprint 5 | ## Descripción Detallada
| **Estado** | Pendiente |
| **Asignado a** | Por asignar | 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
## Historia de Usuario ```
[MODAL DE CURSO COMPLETADO]
**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. │ │
│ ¡FELICIDADES! CURSO COMPLETADO │
## Descripción Detallada │ │
│ Fibonacci Retracement Básico │
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. │ │
│ +200 XP ganados │
## Mockups/Wireframes │ │
│ ┌────────────────────────────────────────────────────────────┐ │
``` │ │ │ │
[MODAL DE CURSO COMPLETADO] │ │ [PREVIEW DEL CERTIFICADO] │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ │ │
│ ✨ 🎓 ✨ │ │ │ ┌──────────────────────────────────────┐ │ │
│ │ │ │ │ [LOGO ORBIQUANT] │ │ │
│ ¡FELICIDADES! CURSO COMPLETADO │ │ │ │ CERTIFICADO DE FINALIZACIÓN │ │ │
│ │ │ │ │ │ │ │
│ Fibonacci Retracement Básico │ │ │ │ Se certifica que │ │ │
│ │ │ │ │ JUAN PÉREZ │ │ │
│ +200 XP ganados │ │ │ │ Ha completado exitosamente │ │ │
│ │ │ │ │ "Fibonacci Retracement Básico" │ │ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │
│ │ │ │ │ │ │ 05/12/2025 │ │ │
│ │ [PREVIEW DEL CERTIFICADO] │ │ │ │ │ OQI-EDU-A3F8D291 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ┌──────────────────────────────────────┐ │ │ │ │ │ [Firma Instructor] [Firma OQI] │ │ │
│ │ │ [LOGO ORBIQUANT] │ │ │ │ │ │ [QR CODE] │ │ │
│ │ │ CERTIFICADO DE FINALIZACIÓN │ │ │ │ │ └──────────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Se certifica que │ │ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ JUAN PÉREZ │ │ │ │ │
│ │ │ Ha completado exitosamente │ │ │ │ [📥 Descargar PDF] [💼 Compartir en LinkedIn] [✕ Cerrar] │
│ │ │ "Fibonacci Retracement Básico" │ │ │ │ │
│ │ │ │ │ │ └─────────────────────────────────────────────────────────────────┘
│ │ │ 05/12/2025 │ │ │
│ │ │ OQI-EDU-A3F8D291 │ │ │ [PÁGINA DE CERTIFICADOS]
│ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ │ [Firma Instructor] [Firma OQI] │ │ │ │ MIS CERTIFICADOS [🔍] │
│ │ │ [QR CODE] │ │ │ │ │
│ │ └──────────────────────────────────────┘ │ │ │ Has obtenido 12 certificados │
│ │ │ │ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ Filtros: [Todos ▼] [Más recientes ▼] │
│ │ │ │
│ [📥 Descargar PDF] [💼 Compartir en LinkedIn] [✕ Cerrar] │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │
└─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │
│ │ Fibonacci │ │ Day Trading │ │ Candlestick │ │
[PÁGINA DE CERTIFICADOS] │ │ Básico │ │ Pro │ │ Patterns │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ │
│ MIS CERTIFICADOS [🔍] │ │ │ 05/12/2025 │ │ 28/11/2025 │ │ 15/11/2025 │ │
│ │ │ │ OQI-...-291 │ │ OQI-...-8F2 │ │ OQI-...-4A1 │ │
│ Has obtenido 12 certificados │ │ │ │ │ │ │ │ │
│ │ │ │ [Ver] [📥] │ │ [Ver] [📥] │ │ [Ver] [📥] │ │
│ Filtros: [Todos ▼] [Más recientes ▼] │ │ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │
│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ │ [1] 2 3 4 │
│ │ │ │ │ │ │ │ │ │
│ │ Fibonacci │ │ Day Trading │ │ Candlestick │ │ └─────────────────────────────────────────────────────────────────┘
│ │ Básico │ │ Pro │ │ Patterns │ │
│ │ │ │ │ │ │ │ [PÁGINA DE VERIFICACIÓN PÚBLICA]
│ │ 05/12/2025 │ │ 28/11/2025 │ │ 15/11/2025 │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ OQI-...-291 │ │ OQI-...-8F2 │ │ OQI-...-4A1 │ │ │ [LOGO] OrbiQuant IA │
│ │ │ │ │ │ │ │ │ │
│ │ [Ver] [📥] │ │ [Ver] [📥] │ │ [Ver] [📥] │ │ │ VERIFICACIÓN DE CERTIFICADO │
│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │ │ Certificado: OQI-EDU-A3F8D291 │
│ │ │ │
│ [1] 2 3 4 │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │ ✓ CERTIFICADO VÁLIDO │ │
└─────────────────────────────────────────────────────────────────┘ │ │ │ │
│ │ Otorgado a: Juan Pérez │ │
[PÁGINA DE VERIFICACIÓN PÚBLICA] │ │ Curso: Fibonacci Retracement Básico │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ Categoría: Análisis Técnico │ │
│ [LOGO] OrbiQuant IA │ │ │ Fecha de finalización: 05/12/2025 │ │
│ │ │ │ Duración del curso: 2.5 horas │ │
│ VERIFICACIÓN DE CERTIFICADO │ │ │ Módulos: 5 | Lecciones: 23 │ │
│ │ │ │ │ │
│ Certificado: OQI-EDU-A3F8D291 │ │ │ Instructor: Carlos Mendoza │ │
│ │ │ │ Institución: OrbiQuant IA │ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ │ │ │
│ │ ✓ CERTIFICADO VÁLIDO │ │ │ │ Estado: ✅ Activo │ │
│ │ │ │ │ │ Emitido: 05/12/2025 15:45:00 UTC │ │
│ │ Otorgado a: Juan Pérez │ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ Curso: Fibonacci Retracement Básico │ │ │ │
│ │ Categoría: Análisis Técnico │ │ │ Este certificado puede ser verificado en cualquier momento en: │
│ │ Fecha de finalización: 05/12/2025 │ │ │ orbiquant.com/verify/OQI-EDU-A3F8D291 │
│ │ Duración del curso: 2.5 horas │ │ │ │
│ │ Módulos: 5 | Lecciones: 23 │ │ └─────────────────────────────────────────────────────────────────┘
│ │ │ │ ```
│ │ Instructor: Carlos Mendoza │ │
│ │ Institución: OrbiQuant IA │ │ ---
│ │ │ │
│ │ Estado: ✅ Activo │ │ ## Criterios de Aceptación
│ │ Emitido: 05/12/2025 15:45:00 UTC │ │
│ └────────────────────────────────────────────────────────────┘ │ **Escenario 1: Completar curso genera certificado**
│ │ ```gherkin
│ Este certificado puede ser verificado en cualquier momento en: │ DADO que el usuario completó todas las lecciones de un curso
│ orbiquant.com/verify/OQI-EDU-A3F8D291 │ 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
## Criterios de Aceptación Y se envía email con certificado adjunto
```
**Escenario 1: Completar curso genera certificado**
```gherkin **Escenario 2: Ver certificado en modal**
DADO que el usuario completó todas las lecciones de un curso ```gherkin
Y aprobó todos los quizzes obligatorios DADO que se generó el certificado
CUANDO se marca la última lección como completada CUANDO se muestra el modal de curso completado
ENTONCES se genera automáticamente un certificado ENTONCES se muestra preview del certificado
Y se registra en backend con ID único (OQI-EDU-XXXXXXXX) Y se muestra botón "Descargar PDF"
Y se genera PDF con diseño profesional Y se muestra botón "Compartir en LinkedIn"
Y se almacena PDF en S3 o CDN Y se muestra "Ver todos mis certificados"
Y se muestra modal de felicitación ```
Y se envía email con certificado adjunto
``` **Escenario 3: Descargar certificado en PDF**
```gherkin
**Escenario 2: Ver certificado en modal** DADO que el usuario tiene un certificado
```gherkin CUANDO hace click en "Descargar PDF"
DADO que se generó el certificado ENTONCES se descarga archivo PDF
CUANDO se muestra el modal de curso completado Y el PDF contiene:
ENTONCES se muestra preview del certificado - Logo de OrbiQuant IA
Y se muestra botón "Descargar PDF" - Título "Certificado de Finalización"
Y se muestra botón "Compartir en LinkedIn" - Nombre completo del usuario
Y se muestra "Ver todos mis certificados" - Título del curso
``` - Fecha de finalización
- ID único del certificado
**Escenario 3: Descargar certificado en PDF** - Firmas digitales (instructor + plataforma)
```gherkin - QR code para verificación
DADO que el usuario tiene un certificado - Footer con URL de verificación
CUANDO hace click en "Descargar PDF" ```
ENTONCES se descarga archivo PDF
Y el PDF contiene: **Escenario 4: Compartir en LinkedIn**
- Logo de OrbiQuant IA ```gherkin
- Título "Certificado de Finalización" DADO que el usuario quiere compartir su certificado
- Nombre completo del usuario CUANDO hace click en "Compartir en LinkedIn"
- Título del curso ENTONCES se abre nueva pestaña de LinkedIn
- Fecha de finalización Y el formulario de certificación está pre-llenado con:
- ID único del certificado - Nombre: "Fibonacci Retracement Básico"
- Firmas digitales (instructor + plataforma) - Organización: "OrbiQuant IA"
- QR code para verificación - Fecha de emisión: "Diciembre 2025"
- Footer con URL de verificación - ID de certificado: "OQI-EDU-A3F8D291"
``` - URL de verificación: "orbiquant.com/verify/..."
```
**Escenario 4: Compartir en LinkedIn**
```gherkin **Escenario 5: Ver galería de certificados**
DADO que el usuario quiere compartir su certificado ```gherkin
CUANDO hace click en "Compartir en LinkedIn" DADO que el usuario tiene 12 certificados
ENTONCES se abre nueva pestaña de LinkedIn CUANDO accede a /education/certificates
Y el formulario de certificación está pre-llenado con: ENTONCES se muestra galería de todos los certificados
- Nombre: "Fibonacci Retracement Básico" Y cada certificado muestra: thumbnail, título del curso, fecha
- Organización: "OrbiQuant IA" Y se muestra contador "Has obtenido 12 certificados"
- Fecha de emisión: "Diciembre 2025" Y se pueden filtrar por: categoría, fecha
- ID de certificado: "OQI-EDU-A3F8D291" Y se pueden ordenar por: más reciente, alfabético
- URL de verificación: "orbiquant.com/verify/..." ```
```
**Escenario 6: Verificar certificado públicamente**
**Escenario 5: Ver galería de certificados** ```gherkin
```gherkin DADO que alguien tiene el ID de un certificado
DADO que el usuario tiene 12 certificados CUANDO accede a orbiquant.com/verify/OQI-EDU-A3F8D291
CUANDO accede a /education/certificates ENTONCES se muestra página de verificación pública
ENTONCES se muestra galería de todos los certificados Y NO requiere login
Y cada certificado muestra: thumbnail, título del curso, fecha Y se muestra:
Y se muestra contador "Has obtenido 12 certificados" - Estado: ✅ Válido
Y se pueden filtrar por: categoría, fecha - Nombre del usuario
Y se pueden ordenar por: más reciente, alfabético - Título del curso
``` - Fecha de finalización
- Detalles del curso
**Escenario 6: Verificar certificado públicamente** Y se confirma autenticidad del certificado
```gherkin ```
DADO que alguien tiene el ID de un certificado
CUANDO accede a orbiquant.com/verify/OQI-EDU-A3F8D291 **Escenario 7: Verificar certificado inválido**
ENTONCES se muestra página de verificación pública ```gherkin
Y NO requiere login DADO que alguien accede con ID inválido
Y se muestra: CUANDO accede a /verify/INVALID-ID-123
- Estado: ✅ Válido ENTONCES se muestra "Certificado no encontrado"
- Nombre del usuario Y se muestra sugerencia "Verifica el ID ingresado"
- Título del curso Y se muestra link "¿Cómo verificar un certificado?"
- Fecha de finalización ```
- Detalles del curso
Y se confirma autenticidad del certificado **Escenario 8: Email de certificado**
``` ```gherkin
DADO que se generó un certificado
**Escenario 7: Verificar certificado inválido** CUANDO se envía el email
```gherkin ENTONCES el email contiene:
DADO que alguien accede con ID inválido - Asunto: "¡Felicidades! Certificado de [Curso]"
CUANDO accede a /verify/INVALID-ID-123 - Mensaje de felicitación personalizado
ENTONCES se muestra "Certificado no encontrado" - Estadísticas: duración, lecciones completadas
Y se muestra sugerencia "Verifica el ID ingresado" - PDF adjunto del certificado
Y se muestra link "¿Cómo verificar un certificado?" - Botones: Ver certificado, Compartir en LinkedIn
``` - Sugerencias de próximos cursos relacionados
```
**Escenario 8: Email de certificado**
```gherkin **Escenario 9: Curso sin certificado disponible**
DADO que se generó un certificado ```gherkin
CUANDO se envía el email DADO que un curso está marcado como "no certifiable"
ENTONCES el email contiene: CUANDO el usuario completa el curso
- Asunto: "¡Felicidades! Certificado de [Curso]" ENTONCES NO se genera certificado
- Mensaje de felicitación personalizado Y se muestra "Curso completado" sin opción de certificado
- Estadísticas: duración, lecciones completadas Y se explica "Este curso no otorga certificado"
- PDF adjunto del certificado ```
- Botones: Ver certificado, Compartir en LinkedIn
- Sugerencias de próximos cursos relacionados **Escenario 10: Certificado con requisitos adicionales**
``` ```gherkin
DADO que un curso requiere quiz final aprobado
**Escenario 9: Curso sin certificado disponible** Y el usuario completó todas las lecciones
```gherkin PERO no aprobó el quiz final
DADO que un curso está marcado como "no certifiable" CUANDO intenta obtener certificado
CUANDO el usuario completa el curso ENTONCES se muestra "Debes aprobar el quiz final"
ENTONCES NO se genera certificado Y se muestra score actual del quiz
Y se muestra "Curso completado" sin opción de certificado Y se muestra "Intentos restantes: X"
Y se explica "Este curso no otorga certificado" Y el certificado NO se genera hasta aprobar
``` ```
**Escenario 10: Certificado con requisitos adicionales** ## Criterios Adicionales
```gherkin
DADO que un curso requiere quiz final aprobado - [ ] Watermark en PDF para evitar falsificación
Y el usuario completó todas las lecciones - [ ] Blockchain verification (opcional, fase 2)
PERO no aprobó el quiz final - [ ] Traducción del certificado a inglés
CUANDO intenta obtener certificado - [ ] Certificado físico por correo (premium)
ENTONCES se muestra "Debes aprobar el quiz final" - [ ] Badge de LinkedIn auto-agregado via API
Y se muestra score actual del quiz - [ ] Opción de hacer certificado público/privado
Y se muestra "Intentos restantes: X" - [ ] Perfil público con todos los certificados del usuario
Y el certificado NO se genera hasta aprobar
``` ---
## Criterios Adicionales ## Tareas Técnicas
- [ ] Watermark en PDF para evitar falsificación **Database:**
- [ ] Blockchain verification (opcional, fase 2) - [ ] DB-EDU-022: Tabla education.certificates
- [ ] Traducción del certificado a inglés - [ ] DB-EDU-023: Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status
- [ ] Certificado físico por correo (premium) - [ ] DB-EDU-024: Tabla certificate_verifications (log de verificaciones)
- [ ] Badge de LinkedIn auto-agregado via API - [ ] DB-EDU-025: Índice único en certificate_number
- [ ] Opción de hacer certificado público/privado
- [ ] Perfil público con todos los certificados del usuario **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
## Tareas Técnicas - [ ] BE-EDU-060: Endpoint GET /api/public/certificates/verify/:number
- [ ] BE-EDU-061: Implementar CertificateService.generate()
**Database:** - [ ] BE-EDU-062: Generar PDF con Puppeteer o PDFKit
- [ ] DB-EDU-022: Tabla education.certificates - [ ] BE-EDU-063: Generar QR code con qrcode library
- [ ] DB-EDU-023: Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status - [ ] BE-EDU-064: Upload de PDF a S3 con signed URL
- [ ] DB-EDU-024: Tabla certificate_verifications (log de verificaciones) - [ ] BE-EDU-065: Event handler en course completion
- [ ] DB-EDU-025: Índice único en certificate_number - [ ] BE-EDU-066: Email service para enviar certificado
- [ ] BE-EDU-067: Rate limiting en verificación (100/hora por IP)
**Backend:**
- [ ] BE-EDU-057: Endpoint POST /education/certificates/generate **Frontend:**
- [ ] BE-EDU-058: Endpoint GET /education/certificates (del usuario) - [ ] FE-EDU-070: Modal CourseCompletedModal.tsx con preview
- [ ] BE-EDU-059: Endpoint GET /education/certificates/:id - [ ] FE-EDU-071: Crear CertificatesPage.tsx
- [ ] BE-EDU-060: Endpoint GET /api/public/certificates/verify/:number - [ ] FE-EDU-072: Crear componente CertificateCard.tsx
- [ ] BE-EDU-061: Implementar CertificateService.generate() - [ ] FE-EDU-073: Crear VerifyCertificatePage.tsx (pública)
- [ ] BE-EDU-062: Generar PDF con Puppeteer o PDFKit - [ ] FE-EDU-074: Botón "Compartir en LinkedIn" con pre-fill
- [ ] BE-EDU-063: Generar QR code con qrcode library - [ ] FE-EDU-075: Preview de PDF en modal
- [ ] BE-EDU-064: Upload de PDF a S3 con signed URL - [ ] FE-EDU-076: Galería con filtros y búsqueda
- [ ] BE-EDU-065: Event handler en course completion - [ ] FE-EDU-077: Implementar certificatesStore
- [ ] BE-EDU-066: Email service para enviar certificado
- [ ] BE-EDU-067: Rate limiting en verificación (100/hora por IP) **Tests:**
- [ ] TEST-EDU-030: Test generación de certificado
**Frontend:** - [ ] TEST-EDU-031: Test validación de certificado válido
- [ ] FE-EDU-070: Modal CourseCompletedModal.tsx con preview - [ ] TEST-EDU-032: Test verificación de certificado inválido
- [ ] FE-EDU-071: Crear CertificatesPage.tsx - [ ] TEST-EDU-033: Test E2E completar curso y obtener certificado
- [ ] 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 ## Dependencias
- [ ] FE-EDU-076: Galería con filtros y búsqueda
- [ ] FE-EDU-077: Implementar certificatesStore **Depende de:**
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
**Tests:** - [ ] RF-EDU-005: Sistema de certificados
- [ ] TEST-EDU-030: Test generación de certificado - [ ] PDF generation library (Puppeteer/PDFKit)
- [ ] TEST-EDU-031: Test validación de certificado válido - [ ] S3 bucket para almacenar PDFs
- [ ] TEST-EDU-032: Test verificación de certificado inválido - [ ] Email service
- [ ] TEST-EDU-033: Test E2E completar curso y obtener certificado
**Bloquea:**
--- - Ninguna (es funcionalidad final)
## Dependencias ---
**Depende de:** ## Notas Técnicas
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
- [ ] RF-EDU-005: Sistema de certificados **Generación del certificado:**
- [ ] PDF generation library (Puppeteer/PDFKit) ```javascript
- [ ] S3 bucket para almacenar PDFs // Triggered on course completion
- [ ] Email service async function onCourseCompleted(userId, courseId) {
// 1. Validar requisitos
**Bloquea:** const isEligible = await validateCertificateEligibility(userId, courseId);
- Ninguna (es funcionalidad final) if (!isEligible) return;
--- // 2. Generar ID único
const certificateNumber = generateCertificateId(); // OQI-EDU-A3F8D291
## Notas Técnicas
// 3. Generar PDF
**Generación del certificado:** const pdfBuffer = await generateCertificatePDF({
```javascript userName,
// Triggered on course completion courseName,
async function onCourseCompleted(userId, courseId) { completedDate,
// 1. Validar requisitos certificateNumber
const isEligible = await validateCertificateEligibility(userId, courseId); });
if (!isEligible) return;
// 4. Upload a S3
// 2. Generar ID único const pdfUrl = await uploadToS3(pdfBuffer, certificateNumber);
const certificateNumber = generateCertificateId(); // OQI-EDU-A3F8D291
// 5. Guardar en DB
// 3. Generar PDF await saveCertificate({
const pdfBuffer = await generateCertificatePDF({ userId,
userName, courseId,
courseName, certificateNumber,
completedDate, pdfUrl
certificateNumber });
});
// 6. Enviar email
// 4. Upload a S3 await sendCertificateEmail(userId, pdfUrl);
const pdfUrl = await uploadToS3(pdfBuffer, certificateNumber);
// 7. Otorgar XP bonus
// 5. Guardar en DB await awardXP(userId, 100, 'certificate_earned');
await saveCertificate({ }
userId, ```
courseId,
certificateNumber, **Endpoint GET /api/public/certificates/verify/:number:**
pdfUrl ```typescript
}); // Response para certificado válido
{
// 6. Enviar email valid: true,
await sendCertificateEmail(userId, pdfUrl); certificate: {
certificateNumber: "OQI-EDU-A3F8D291",
// 7. Otorgar XP bonus recipientName: "Juan Pérez",
await awardXP(userId, 100, 'certificate_earned'); courseTitle: "Fibonacci Retracement Básico",
} courseCategory: "Análisis Técnico",
``` completedAt: "2025-12-05T15:45:00Z",
issuedAt: "2025-12-05T15:45:00Z",
**Endpoint GET /api/public/certificates/verify/:number:** courseDuration: 150, // minutos
```typescript moduleCount: 5,
// Response para certificado válido lessonCount: 23,
{ instructor: "Carlos Mendoza",
valid: true, status: "active"
certificate: { }
certificateNumber: "OQI-EDU-A3F8D291", }
recipientName: "Juan Pérez",
courseTitle: "Fibonacci Retracement Básico", // Response para certificado inválido
courseCategory: "Análisis Técnico", {
completedAt: "2025-12-05T15:45:00Z", valid: false,
issuedAt: "2025-12-05T15:45:00Z", error: "Certificate not found"
courseDuration: 150, // minutos }
moduleCount: 5, ```
lessonCount: 23,
instructor: "Carlos Mendoza", **Template del PDF:**
status: "active" - 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
// Response para certificado inválido - Firmas como imágenes PNG embebidas
{
valid: false, **LinkedIn pre-fill URL:**
error: "Certificate not found" ```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}`;
``` ```
**Template del PDF:** **Seguridad:**
- Usar HTML + CSS para diseño - Rate limiting en endpoint de verificación
- Puppeteer para generar PDF desde HTML - Signed URLs de S3 con expiración de 1 hora para descargas
- Incluir logo en base64 para evitar carga externa - No exponer lista de todos los certificados (solo del usuario logueado)
- QR code generado con library qrcode.js - Validar que usuario solo puede descargar sus propios certificados
- Firmas como imágenes PNG embebidas
**Entidades/Tablas:**
**LinkedIn pre-fill URL:** - `education.certificates`
```javascript - `education.certificate_verifications` (log)
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:** ## Definition of Ready (DoR)
- Rate limiting en endpoint de verificación
- Signed URLs de S3 con expiración de 1 hora para descargas - [x] Historia claramente escrita
- No exponer lista de todos los certificados (solo del usuario logueado) - [x] Criterios de aceptación definidos
- Validar que usuario solo puede descargar sus propios certificados - [x] Story points estimados
- [x] Dependencias identificadas
**Entidades/Tablas:** - [x] Sin bloqueadores
- `education.certificates` - [x] Diseño/mockup disponible
- `education.certificate_verifications` (log) - [x] API spec disponible
--- ## Definition of Done (DoD)
## Definition of Ready (DoR) - [ ] Código implementado según criterios
- [ ] Tests unitarios escritos y pasando
- [x] Historia claramente escrita - [ ] Tests de integración pasando
- [x] Criterios de aceptación definidos - [ ] Code review aprobado
- [x] Story points estimados - [ ] Documentación actualizada
- [x] Dependencias identificadas - [ ] QA aprobado
- [x] Sin bloqueadores - [ ] Desplegado en ambiente de pruebas
- [x] Diseño/mockup disponible
- [x] API spec disponible ---
## Definition of Done (DoD) ## Historial de Cambios
- [ ] Código implementado según criterios | Fecha | Cambio | Autor |
- [ ] Tests unitarios escritos y pasando |-------|--------|-------|
- [ ] Tests de integración pasando | 2025-12-05 | Creación | Requirements-Analyst |
- [ ] Code review aprobado
- [ ] Documentación actualizada ---
- [ ] QA aprobado
- [ ] Desplegado en ambiente de pruebas **Creada por:** Requirements-Analyst
**Fecha:** 2025-12-05
--- **Última actualización:** 2025-12-05
## 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

View File

@ -1,11 +1,11 @@
# TRACEABILITY.yml - OQI-002 Módulo Educativo # TRACEABILITY.yml - OQI-002 Módulo Educativo
# Mapeo de requerimientos a implementación # Mapeo de requerimientos a implementación
version: "1.1.0" version: "1.0.0"
epic: OQI-002 epic: OQI-002
name: "Módulo Educativo - Cursos de Trading" name: "Módulo Educativo - Cursos de Trading"
updated: "2026-01-04" updated: "2025-12-05"
status: in_progress status: pending
# Resumen de trazabilidad # Resumen de trazabilidad
summary: summary:
@ -71,14 +71,7 @@ requirements:
RF-EDU-002: RF-EDU-002:
name: "Sistema de Lecciones" name: "Sistema de Lecciones"
status: implemented status: pending
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: specs:
- ET-EDU-001 - ET-EDU-001
- ET-EDU-004 - ET-EDU-004
@ -171,15 +164,7 @@ requirements:
RF-EDU-004: RF-EDU-004:
name: "Sistema de Quizzes" name: "Sistema de Quizzes"
status: implemented status: pending
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: specs:
- ET-EDU-005 - ET-EDU-005
user_stories: user_stories:
@ -477,67 +462,3 @@ notes:
- "Certificados con QR de verificación único" - "Certificados con QR de verificación único"
- "Quizzes con intentos ilimitados pero score máximo registrado" - "Quizzes con intentos ilimitados pero score máximo registrado"
- "Sistema de puntos: 10 por lección, 50 por quiz aprobado, 100 por certificado" - "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"

View File

@ -1,298 +1,285 @@
--- # RF-EDU-001: Catálogo de Cursos
id: "RF-EDU-001"
title: "Catalogo de Cursos" **Versión:** 1.0.0
type: "Requirement" **Fecha:** 2025-12-05
status: "Done" **Épica:** OQI-002 - Módulo Educativo
priority: "Alta" **Prioridad:** P0
module: "education" **Story Points:** 8
epic: "OQI-002"
version: "1.0" ---
created_date: "2025-12-05"
updated_date: "2026-01-04" ## 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.
# RF-EDU-001: Catálogo de Cursos
---
**Versión:** 1.0.0
**Fecha:** 2025-12-05 ## Requisitos Funcionales
**Épica:** OQI-002 - Módulo Educativo
**Prioridad:** P0 ### RF-EDU-001.1: Listado de Cursos
**Story Points:** 8
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
## Descripción - Mostrar badge de "Nuevo" para cursos publicados en últimos 30 días
- Mostrar badge de "En curso" para cursos iniciados por el usuario
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. - Mostrar badge de "Completado" con porcentaje para cursos en progreso
- Implementar paginación (12 cursos por página)
---
### RF-EDU-001.2: Categorías
## Requisitos Funcionales
El sistema debe soportar las siguientes categorías:
### RF-EDU-001.1: Listado de Cursos
| Categoría | Slug | Descripción |
El sistema debe: |-----------|------|-------------|
- Mostrar todos los cursos activos en formato de tarjetas (cards) | Fundamentos | fundamentals | Conceptos básicos de trading |
- Incluir para cada curso: título, descripción breve, imagen, nivel, duración, módulos, progreso | Análisis Técnico | technical-analysis | Indicadores y patrones |
- Mostrar badge de "Nuevo" para cursos publicados en últimos 30 días | Análisis Fundamental | fundamental-analysis | Valoración de activos |
- Mostrar badge de "En curso" para cursos iniciados por el usuario | Gestión de Riesgo | risk-management | Money management |
- Mostrar badge de "Completado" con porcentaje para cursos en progreso | Psicología del Trading | trading-psychology | Control emocional |
- Implementar paginación (12 cursos por página) | Estrategias Avanzadas | advanced-strategies | Sistemas complejos |
| Criptomonedas | crypto | Trading de cripto |
### RF-EDU-001.2: Categorías | IA y Trading | ai-trading | Machine Learning aplicado |
El sistema debe soportar las siguientes categorías: ### RF-EDU-001.3: Niveles de Dificultad
| Categoría | Slug | Descripción | El sistema debe clasificar cursos en:
|-----------|------|-------------| - **Principiante:** Sin conocimientos previos requeridos
| Fundamentos | fundamentals | Conceptos básicos de trading | - **Intermedio:** Requiere conocimientos básicos
| Análisis Técnico | technical-analysis | Indicadores y patrones | - **Avanzado:** Para traders experimentados
| Análisis Fundamental | fundamental-analysis | Valoración de activos | - **Experto:** Contenido especializado
| Gestión de Riesgo | risk-management | Money management |
| Psicología del Trading | trading-psychology | Control emocional | ### RF-EDU-001.4: Filtros
| Estrategias Avanzadas | advanced-strategies | Sistemas complejos |
| Criptomonedas | crypto | Trading de cripto | El sistema debe permitir filtrar por:
| IA y Trading | ai-trading | Machine Learning aplicado | - Categoría (múltiple selección)
- Nivel de dificultad (múltiple selección)
### RF-EDU-001.3: Niveles de Dificultad - Duración (rangos: <2h, 2-5h, 5-10h, >10h)
- Estado: Nuevos, En curso, Completados, No iniciados
El sistema debe clasificar cursos en: - Instructor
- **Principiante:** Sin conocimientos previos requeridos - Gratuitos vs Premium
- **Intermedio:** Requiere conocimientos básicos
- **Avanzado:** Para traders experimentados ### RF-EDU-001.5: Búsqueda
- **Experto:** Contenido especializado
El sistema debe:
### RF-EDU-001.4: Filtros - Implementar barra de búsqueda en tiempo real
- Buscar en: título, descripción, tags, nombre de instructor
El sistema debe permitir filtrar por: - Mostrar resultados mientras el usuario escribe (debounce 300ms)
- Categoría (múltiple selección) - Resaltar términos coincidentes en resultados
- Nivel de dificultad (múltiple selección) - Mostrar sugerencias de búsqueda basadas en términos populares
- Duración (rangos: <2h, 2-5h, 5-10h, >10h) - Guardar historial de búsquedas del usuario
- Estado: Nuevos, En curso, Completados, No iniciados
- Instructor ### RF-EDU-001.6: Ordenamiento
- Gratuitos vs Premium
El sistema debe permitir ordenar por:
### RF-EDU-001.5: Búsqueda - Más recientes
- Más populares (por número de estudiantes)
El sistema debe: - Mejor valorados (rating)
- Implementar barra de búsqueda en tiempo real - Duración (ascendente/descendente)
- Buscar en: título, descripción, tags, nombre de instructor - Alfabético (A-Z, Z-A)
- Mostrar resultados mientras el usuario escribe (debounce 300ms) - Progreso del usuario (para cursos iniciados)
- Resaltar términos coincidentes en resultados
- Mostrar sugerencias de búsqueda basadas en términos populares ### RF-EDU-001.7: Recomendaciones
- Guardar historial de búsquedas del usuario
El sistema debe:
### RF-EDU-001.6: Ordenamiento - Mostrar sección "Recomendado para ti" basado en:
- Cursos en progreso del usuario
El sistema debe permitir ordenar por: - Nivel de experiencia del perfil
- Más recientes - Cursos completados previamente
- Más populares (por número de estudiantes) - Categorías de interés
- Mejor valorados (rating) - Mostrar sección "Continuar aprendiendo" con cursos incompletos
- Duración (ascendente/descendente) - Mostrar "Cursos relacionados" al ver detalle de curso
- Alfabético (A-Z, Z-A)
- Progreso del usuario (para cursos iniciados) ---
### RF-EDU-001.7: Recomendaciones ## Datos de Entrada
El sistema debe: | Campo | Tipo | Descripción |
- Mostrar sección "Recomendado para ti" basado en: |-------|------|-------------|
- Cursos en progreso del usuario | page | number | Número de página (default: 1) |
- Nivel de experiencia del perfil | limit | number | Elementos por página (default: 12, max: 50) |
- Cursos completados previamente | category | string[] | IDs de categorías a filtrar |
- Categorías de interés | level | string[] | Niveles de dificultad |
- Mostrar sección "Continuar aprendiendo" con cursos incompletos | search | string | Término de búsqueda |
- Mostrar "Cursos relacionados" al ver detalle de curso | sortBy | string | Campo de ordenamiento |
| sortOrder | asc/desc | Dirección del ordenamiento |
---
---
## Datos de Entrada
## Datos de Salida
| Campo | Tipo | Descripción |
|-------|------|-------------| ```typescript
| page | number | Número de página (default: 1) | interface Course {
| limit | number | Elementos por página (default: 12, max: 50) | id: string;
| category | string[] | IDs de categorías a filtrar | title: string;
| level | string[] | Niveles de dificultad | slug: string;
| search | string | Término de búsqueda | description: string;
| sortBy | string | Campo de ordenamiento | shortDescription: string;
| sortOrder | asc/desc | Dirección del ordenamiento | thumbnail: string;
category: {
--- id: string;
name: string;
## Datos de Salida slug: string;
icon: string;
```typescript };
interface Course { level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
id: string; duration: number; // minutos
title: string; moduleCount: number;
slug: string; lessonCount: number;
description: string; studentCount: number;
shortDescription: string; rating: number; // 0-5
thumbnail: string; reviewCount: number;
category: { instructor: {
id: string; id: string;
name: string; name: string;
slug: string; avatar: string;
icon: string; title: string;
}; };
level: 'beginner' | 'intermediate' | 'advanced' | 'expert'; tags: string[];
duration: number; // minutos isPremium: boolean;
moduleCount: number; publishedAt: string;
lessonCount: number; userProgress?: {
studentCount: number; enrolledAt: string;
rating: number; // 0-5 progressPercent: number;
reviewCount: number; lastAccessedAt: string;
instructor: { isCompleted: boolean;
id: string; };
name: string; }
avatar: string;
title: string; interface CatalogResponse {
}; courses: Course[];
tags: string[]; pagination: {
isPremium: boolean; page: number;
publishedAt: string; limit: number;
userProgress?: { total: number;
enrolledAt: string; totalPages: number;
progressPercent: number; };
lastAccessedAt: string; filters: {
isCompleted: boolean; categories: Category[];
}; levels: string[];
} };
}
interface CatalogResponse { ```
courses: Course[];
pagination: { ---
page: number;
limit: number; ## Reglas de Negocio
total: number;
totalPages: number; 1. **Cursos activos:** Solo mostrar cursos con status 'published'
}; 2. **Acceso Premium:** Cursos premium requieren suscripción activa
filters: { 3. **Visibilidad:** Cursos draft solo visibles para instructores y admins
categories: Category[]; 4. **Límite de paginación:** Máximo 50 cursos por página
levels: string[]; 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
--- ---
## Reglas de Negocio ## Criterios de Aceptación
1. **Cursos activos:** Solo mostrar cursos con status 'published' ```gherkin
2. **Acceso Premium:** Cursos premium requieren suscripción activa Escenario: Usuario visualiza catálogo de cursos
3. **Visibilidad:** Cursos draft solo visibles para instructores y admins DADO que el usuario está autenticado
4. **Límite de paginación:** Máximo 50 cursos por página Y está en la página de educación
5. **Caché:** Catálogo se cachea por 5 minutos CUANDO accede a /education/courses
6. **Búsqueda mínima:** Al menos 2 caracteres para búsqueda ENTONCES se muestra un listado de cursos
7. **Recomendaciones:** Máximo 6 cursos en sección recomendados Y se muestran 12 cursos por página
8. **Orden por defecto:** Más recientes primero para usuarios nuevos 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
## Criterios de Aceptación Escenario: Usuario filtra por categoría
DADO que el usuario está en el catálogo
```gherkin CUANDO selecciona la categoría "Análisis Técnico"
Escenario: Usuario visualiza catálogo de cursos ENTONCES se muestran solo cursos de esa categoría
DADO que el usuario está autenticado Y el filtro se marca como activo
Y está en la página de educación Y la URL se actualiza con ?category=technical-analysis
CUANDO accede a /education/courses Y se mantienen otros filtros activos
ENTONCES se muestra un listado de cursos
Y se muestran 12 cursos por página Escenario: Usuario busca curso
Y cada curso muestra: título, imagen, nivel, duración, rating DADO que el usuario está en el catálogo
Y se muestran filtros en sidebar izquierdo CUANDO escribe "fibonacci" en la búsqueda
Y se muestra barra de búsqueda en header ENTONCES se muestran resultados en tiempo real
Y se resalta el término "fibonacci" en resultados
Escenario: Usuario filtra por categoría Y se muestra contador "X resultados para 'fibonacci'"
DADO que el usuario está en el catálogo
CUANDO selecciona la categoría "Análisis Técnico" Escenario: Usuario sin resultados
ENTONCES se muestran solo cursos de esa categoría DADO que el usuario busca "xyz123"
Y el filtro se marca como activo Y no hay cursos que coincidan
Y la URL se actualiza con ?category=technical-analysis ENTONCES se muestra mensaje "No se encontraron cursos"
Y se mantienen otros filtros activos Y se sugieren búsquedas alternativas
Y se muestran cursos populares como alternativa
Escenario: Usuario busca curso
DADO que el usuario está en el catálogo Escenario: Ver cursos recomendados
CUANDO escribe "fibonacci" en la búsqueda DADO que el usuario tiene cursos en progreso
ENTONCES se muestran resultados en tiempo real CUANDO accede al catálogo
Y se resalta el término "fibonacci" en resultados ENTONCES se muestra sección "Recomendado para ti"
Y se muestra contador "X resultados para 'fibonacci'" Y aparecen máximo 6 cursos relacionados
Y se muestra sección "Continuar aprendiendo"
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 ## Dependencias
Y se muestran cursos populares como alternativa
- Education API para datos de cursos
Escenario: Ver cursos recomendados - PostgreSQL schema education
DADO que el usuario tiene cursos en progreso - Redis para caché de catálogo
CUANDO accede al catálogo - Elasticsearch para búsqueda (opcional, mejora performance)
ENTONCES se muestra sección "Recomendado para ti"
Y aparecen máximo 6 cursos relacionados ---
Y se muestra sección "Continuar aprendiendo"
``` ## Notas Técnicas
--- - Implementar virtual scrolling para listas largas
- Usar React Query para caché de frontend
## Dependencias - Implementar skeleton loading durante carga
- Optimizar imágenes con lazy loading
- Education API para datos de cursos - Considerar SSR para mejor SEO
- PostgreSQL schema education - Implementar analytics de búsquedas para mejorar recomendaciones
- Redis para caché de catálogo
- Elasticsearch para búsqueda (opcional, mejora performance) ---
--- ## Referencias
## Notas Técnicas - Schema database: `/backend/src/database/schemas/education.sql`
- API endpoints: `/backend/src/modules/courses/courses.routes.ts`
- Implementar virtual scrolling para listas largas - Frontend: `/frontend/src/pages/Courses.tsx`
- Usar React Query para caché de frontend
- Implementar skeleton loading durante carga ---
- Optimizar imágenes con lazy loading
- Considerar SSR para mejor SEO ## Tareas Técnicas
- Implementar analytics de búsquedas para mejorar recomendaciones
**Database:**
--- - [ ] Verificar índices en education.courses (title, category_id, level, published_at)
- [ ] Crear vista courses_catalog con joins pre-calculados
## Referencias - [ ] Implementar full-text search en PostgreSQL
- Schema database: `/backend/src/database/schemas/education.sql` **Backend:**
- API endpoints: `/backend/src/modules/courses/courses.routes.ts` - [ ] Endpoint GET /education/courses con paginación y filtros
- Frontend: `/frontend/src/pages/Courses.tsx` - [ ] Endpoint GET /education/categories
- [ ] Implementar CourseService.getCatalog()
--- - [ ] Implementar sistema de recomendaciones básico
- [ ] Agregar rate limiting a búsqueda
## Tareas Técnicas - [ ] Implementar caché Redis para catálogo
**Database:** **Frontend:**
- [ ] Verificar índices en education.courses (title, category_id, level, published_at) - [ ] Crear página CoursesPage.tsx
- [ ] Crear vista courses_catalog con joins pre-calculados - [ ] Crear componente CourseCard.tsx
- [ ] Implementar full-text search en PostgreSQL - [ ] Crear componente CourseFilters.tsx
- [ ] Crear componente SearchBar.tsx
**Backend:** - [ ] Implementar coursesStore (Zustand)
- [ ] Endpoint GET /education/courses con paginación y filtros - [ ] Implementar infinite scroll opcional
- [ ] Endpoint GET /education/categories - [ ] Agregar analytics de búsqueda
- [ ] Implementar CourseService.getCatalog()
- [ ] Implementar sistema de recomendaciones básico **Tests:**
- [ ] Agregar rate limiting a búsqueda - [ ] Test unitario CourseService
- [ ] Implementar caché Redis para catálogo - [ ] Test integración GET /courses con filtros
- [ ] Test E2E navegación y búsqueda
**Frontend:**
- [ ] Crear página CoursesPage.tsx ---
- [ ] Crear componente CourseCard.tsx
- [ ] Crear componente CourseFilters.tsx **Creado por:** Requirements-Analyst
- [ ] Crear componente SearchBar.tsx **Fecha:** 2025-12-05
- [ ] Implementar coursesStore (Zustand) **Última actualización:** 2025-12-05
- [ ] 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

View File

@ -1,334 +1,321 @@
--- # RF-EDU-002: Sistema de Lecciones
id: "RF-EDU-002"
title: "Sistema de Lecciones" **Versión:** 1.0.0
type: "Requirement" **Fecha:** 2025-12-05
status: "Done" **Épica:** OQI-002 - Módulo Educativo
priority: "Alta" **Prioridad:** P0
module: "education" **Story Points:** 8
epic: "OQI-002"
version: "1.0" ---
created_date: "2025-12-05"
updated_date: "2026-01-04" ## 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.
# RF-EDU-002: Sistema de Lecciones
---
**Versión:** 1.0.0
**Fecha:** 2025-12-05 ## Requisitos Funcionales
**Épica:** OQI-002 - Módulo Educativo
**Prioridad:** P0 ### RF-EDU-002.1: Tipos de Lecciones
**Story Points:** 8
El sistema debe soportar:
---
| Tipo | Descripción | Características |
## Descripción |------|-------------|-----------------|
| **Video** | Contenido en video | Reproductor, subtítulos, velocidad |
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. | **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 |
## Requisitos Funcionales
### RF-EDU-002.2: Reproductor de Video
### RF-EDU-002.1: Tipos de Lecciones
El sistema debe:
El sistema debe soportar: - Reproducir videos desde CDN (Vimeo/YouTube/S3)
- Controles: play/pause, volumen, pantalla completa
| Tipo | Descripción | Características | - Velocidades: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
|------|-------------|-----------------| - Subtítulos en español e inglés (opcional)
| **Video** | Contenido en video | Reproductor, subtítulos, velocidad | - Recordar posición de reproducción
| **Artículo** | Contenido de texto | Markdown, imágenes, código | - Saltar 10s adelante/atrás con teclas de flecha
| **Quiz** | Evaluación interactiva | Preguntas, feedback inmediato | - Atajos de teclado (espacio=play/pause, f=fullscreen, m=mute)
| **Código** | Ejercicio práctico | Editor, ejecución, validación | - Barra de progreso con preview al hover
| **Recursos** | Descargables | PDFs, hojas de cálculo, código | - Marcadores de secciones importantes
- Calidad adaptativa según ancho de banda
### RF-EDU-002.2: Reproductor de Video
### RF-EDU-002.3: Lecciones de Artículo
El sistema debe:
- Reproducir videos desde CDN (Vimeo/YouTube/S3) El sistema debe:
- Controles: play/pause, volumen, pantalla completa - Renderizar Markdown con syntax highlighting
- Velocidades: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x - Soportar: headers, listas, tablas, imágenes, videos embebidos
- Subtítulos en español e inglés (opcional) - Mostrar tabla de contenidos (TOC) para artículos largos
- Recordar posición de reproducción - Estimación de tiempo de lectura
- Saltar 10s adelante/atrás con teclas de flecha - Resaltar código con Prism.js o similar
- Atajos de teclado (espacio=play/pause, f=fullscreen, m=mute) - Copiar código con un click
- Barra de progreso con preview al hover - Modo oscuro/claro para lectura
- Marcadores de secciones importantes - Marcar artículo como completado con checkbox al final
- Calidad adaptativa según ancho de banda
### RF-EDU-002.4: Navegación entre Lecciones
### RF-EDU-002.3: Lecciones de Artículo
El sistema debe:
El sistema debe: - Mostrar sidebar con estructura del curso (módulos > lecciones)
- Renderizar Markdown con syntax highlighting - Indicar lección actual destacada
- Soportar: headers, listas, tablas, imágenes, videos embebidos - Mostrar checkmarks en lecciones completadas
- Mostrar tabla de contenidos (TOC) para artículos largos - Mostrar progreso en módulos (X/Y lecciones)
- Estimación de tiempo de lectura - Botones "Anterior" y "Siguiente" lección
- Resaltar código con Prism.js o similar - Bloquear lecciones futuras si curso es secuencial
- Copiar código con un click - Permitir saltar libremente si curso es no-secuencial
- Modo oscuro/claro para lectura - Collapse/expand de módulos en sidebar
- Marcar artículo como completado con checkbox al final
### RF-EDU-002.5: Recursos Descargables
### RF-EDU-002.4: Navegación entre Lecciones
El sistema debe:
El sistema debe: - Listar recursos disponibles para la lección
- Mostrar sidebar con estructura del curso (módulos > lecciones) - Mostrar: nombre, tipo de archivo, tamaño
- Indicar lección actual destacada - Permitir descargar con un click
- Mostrar checkmarks en lecciones completadas - Trackear descargas para analytics
- Mostrar progreso en módulos (X/Y lecciones) - Validar acceso antes de descargar
- Botones "Anterior" y "Siguiente" lección - Soportar: PDF, XLSX, CSV, ZIP, código fuente
- Bloquear lecciones futuras si curso es secuencial
- Permitir saltar libremente si curso es no-secuencial ### RF-EDU-002.6: Notas del Usuario
- Collapse/expand de módulos en sidebar
El sistema debe:
### RF-EDU-002.5: Recursos Descargables - Permitir tomar notas durante lección
- Editor de texto enriquecido (bold, italic, listas)
El sistema debe: - Guardar automáticamente (debounce 2s)
- Listar recursos disponibles para la lección - Timestamp de la nota (para videos)
- Mostrar: nombre, tipo de archivo, tamaño - Listar todas las notas del curso
- Permitir descargar con un click - Buscar en notas
- Trackear descargas para analytics - Exportar notas a PDF/Markdown
- Validar acceso antes de descargar
- Soportar: PDF, XLSX, CSV, ZIP, código fuente ### RF-EDU-002.7: Marcadores y Favoritos
### RF-EDU-002.6: Notas del Usuario El sistema debe:
- Permitir marcar timestamp en videos
El sistema debe: - Agregar comentario al marcador
- Permitir tomar notas durante lección - Listar marcadores en sidebar
- Editor de texto enriquecido (bold, italic, listas) - Saltar a marcador con click
- Guardar automáticamente (debounce 2s) - Exportar marcadores
- Timestamp de la nota (para videos)
- Listar todas las notas del curso ---
- Buscar en notas
- Exportar notas a PDF/Markdown ## Datos de Entrada
### RF-EDU-002.7: Marcadores y Favoritos | Campo | Tipo | Descripción |
|-------|------|-------------|
El sistema debe: | courseId | string | UUID del curso |
- Permitir marcar timestamp en videos | lessonId | string | UUID de la lección |
- Agregar comentario al marcador | timestamp | number | Posición en video (segundos) |
- Listar marcadores en sidebar
- Saltar a marcador con click ---
- Exportar marcadores
## Datos de Salida
---
```typescript
## Datos de Entrada interface Lesson {
id: string;
| Campo | Tipo | Descripción | moduleId: string;
|-------|------|-------------| title: string;
| courseId | string | UUID del curso | slug: string;
| lessonId | string | UUID de la lección | description: string;
| timestamp | number | Posición en video (segundos) | type: 'video' | 'article' | 'quiz' | 'code' | 'resource';
order: number;
--- duration: number; // minutos
isFree: boolean;
## Datos de Salida isCompleted: boolean;
```typescript // Video específico
interface Lesson { videoUrl?: string;
id: string; videoProvider?: 'vimeo' | 'youtube' | 's3';
moduleId: string; videoId?: string;
title: string; subtitles?: {
slug: string; language: string;
description: string; url: string;
type: 'video' | 'article' | 'quiz' | 'code' | 'resource'; }[];
order: number;
duration: number; // minutos // Artículo específico
isFree: boolean; content?: string; // Markdown
isCompleted: boolean; readingTime?: number; // minutos
// Video específico // Quiz específico
videoUrl?: string; quizId?: string;
videoProvider?: 'vimeo' | 'youtube' | 's3'; questionsCount?: number;
videoId?: string; passingScore?: number;
subtitles?: {
language: string; // Recursos
url: string; resources?: {
}[]; id: string;
name: string;
// Artículo específico type: string;
content?: string; // Markdown url: string;
readingTime?: number; // minutos size: number;
}[];
// Quiz específico
quizId?: string; // Progreso del usuario
questionsCount?: number; userProgress?: {
passingScore?: number; startedAt: string;
completedAt?: string;
// Recursos lastPosition: number; // Para videos
resources?: { timeSpent: number; // segundos
id: string; notes?: string;
name: string; };
type: string; }
url: string;
size: number; interface LessonNavigation {
}[]; currentLesson: Lesson;
previousLesson?: {
// Progreso del usuario id: string;
userProgress?: { title: string;
startedAt: string; slug: string;
completedAt?: string; };
lastPosition: number; // Para videos nextLesson?: {
timeSpent: number; // segundos id: string;
notes?: string; title: string;
}; slug: string;
} };
module: {
interface LessonNavigation { id: string;
currentLesson: Lesson; title: string;
previousLesson?: { lessons: {
id: string; id: string;
title: string; title: string;
slug: string; isCompleted: boolean;
}; isLocked: boolean;
nextLesson?: { }[];
id: string; };
title: string; }
slug: string; ```
};
module: { ---
id: string;
title: string; ## Reglas de Negocio
lessons: {
id: string; 1. **Orden secuencial:** Si curso requiere orden, solo lección actual y anteriores están desbloqueadas
title: string; 2. **Completar lección:** Video: ver 90%, Artículo: scroll al final, Quiz: aprobar
isCompleted: boolean; 3. **Acceso Premium:** Lecciones no-free requieren suscripción activa
isLocked: boolean; 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
--- ---
## Reglas de Negocio ## Criterios de Aceptación
1. **Orden secuencial:** Si curso requiere orden, solo lección actual y anteriores están desbloqueadas ```gherkin
2. **Completar lección:** Video: ver 90%, Artículo: scroll al final, Quiz: aprobar Escenario: Usuario visualiza lección de video
3. **Acceso Premium:** Lecciones no-free requieren suscripción activa DADO que el usuario está inscrito en curso
4. **Auto-save progreso:** Guardar posición cada 10 segundos Y está en /education/courses/:slug/lessons/:lessonSlug
5. **Marcado manual:** Usuario puede marcar completado manualmente CUANDO la lección es tipo video
6. **Recursos solo para enrollados:** No se pueden descargar recursos sin estar inscrito ENTONCES se muestra reproductor de video
7. **Notas privadas:** Solo visibles para el usuario que las creó Y se muestran controles de reproducción
8. **Tiempo mínimo:** Video debe reproducirse al menos 30s para contar progreso Y se muestra sidebar con estructura del curso
Y se carga la posición guardada anteriormente
---
Escenario: Usuario completa lección de video
## Criterios de Aceptación DADO que el usuario está viendo un video
CUANDO el video alcanza el 90% de reproducción
```gherkin ENTONCES la lección se marca como completada
Escenario: Usuario visualiza lección de video Y se muestra checkmark en sidebar
DADO que el usuario está inscrito en curso Y se actualiza barra de progreso del curso
Y está en /education/courses/:slug/lessons/:lessonSlug Y se habilita siguiente lección si estaba bloqueada
CUANDO la lección es tipo video
ENTONCES se muestra reproductor de video Escenario: Usuario lee artículo
Y se muestran controles de reproducción DADO que la lección es tipo artículo
Y se muestra sidebar con estructura del curso CUANDO el usuario accede a la lección
Y se carga la posición guardada anteriormente ENTONCES se muestra contenido renderizado desde Markdown
Y se muestra tabla de contenidos si artículo >500 palabras
Escenario: Usuario completa lección de video Y se muestra tiempo estimado de lectura
DADO que el usuario está viendo un video Y se muestra checkbox "Marcar como completado"
CUANDO el video alcanza el 90% de reproducción
ENTONCES la lección se marca como completada Escenario: Usuario toma notas
Y se muestra checkmark en sidebar DADO que el usuario está en una lección
Y se actualiza barra de progreso del curso CUANDO hace click en pestaña "Mis notas"
Y se habilita siguiente lección si estaba bloqueada ENTONCES se muestra editor de texto
Y puede escribir notas
Escenario: Usuario lee artículo Y las notas se guardan automáticamente
DADO que la lección es tipo artículo Y para videos se guarda timestamp actual
CUANDO el usuario accede a la lección
ENTONCES se muestra contenido renderizado desde Markdown Escenario: Navegación entre lecciones
Y se muestra tabla de contenidos si artículo >500 palabras DADO que el usuario completó una lección
Y se muestra tiempo estimado de lectura CUANDO hace click en "Siguiente lección"
Y se muestra checkbox "Marcar como completado" ENTONCES navega a la siguiente lección
Y se carga el contenido correspondiente
Escenario: Usuario toma notas Y se actualiza sidebar destacando nueva lección
DADO que el usuario está en una lección
CUANDO hace click en pestaña "Mis notas" Escenario: Descargar recursos
ENTONCES se muestra editor de texto DADO que la lección tiene recursos descargables
Y puede escribir notas CUANDO el usuario hace click en "Descargar"
Y las notas se guardan automáticamente ENTONCES se descarga el archivo
Y para videos se guarda timestamp actual Y se registra la descarga en analytics
```
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 ## Dependencias
Y se carga el contenido correspondiente
Y se actualiza sidebar destacando nueva lección - Video CDN (Vimeo/YouTube/AWS S3 + CloudFront)
- PostgreSQL para metadata de lecciones
Escenario: Descargar recursos - Redis para caché de progreso
DADO que la lección tiene recursos descargables - S3 para archivos descargables
CUANDO el usuario hace click en "Descargar"
ENTONCES se descarga el archivo ---
Y se registra la descarga en analytics
``` ## Notas Técnicas
--- - Usar React Player o Video.js para reproductor
- Implementar PIP (Picture-in-Picture) para videos
## Dependencias - Considerar HLS para streaming adaptativo
- Implementar lazy loading de módulos en sidebar
- Video CDN (Vimeo/YouTube/AWS S3 + CloudFront) - Guardar progreso en IndexedDB local como backup
- PostgreSQL para metadata de lecciones - Usar Web Workers para procesamiento de Markdown pesado
- Redis para caché de progreso - Implementar analytics de engagement (pausas, rewinds, abandono)
- S3 para archivos descargables
---
---
## Referencias
## Notas Técnicas
- Schema: `/backend/src/database/schemas/education.sql`
- Usar React Player o Video.js para reproductor - API: `/backend/src/modules/courses/lessons.routes.ts`
- Implementar PIP (Picture-in-Picture) para videos - Frontend: `/frontend/src/pages/LessonPlayer.tsx`
- 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 ## Tareas Técnicas
- Implementar analytics de engagement (pausas, rewinds, abandono)
**Database:**
--- - [ ] Verificar schema education.lessons
- [ ] Tabla user_lesson_progress con campos: started_at, completed_at, last_position
## Referencias - [ ] Tabla user_notes con FK a lesson
- [ ] Tabla user_bookmarks para marcadores
- Schema: `/backend/src/database/schemas/education.sql`
- API: `/backend/src/modules/courses/lessons.routes.ts` **Backend:**
- Frontend: `/frontend/src/pages/LessonPlayer.tsx` - [ ] 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
## Tareas Técnicas - [ ] Endpoint GET /education/lessons/:id/resources/:resourceId/download
- [ ] Implementar signed URLs para videos privados
**Database:** - [ ] Rate limiting en download de recursos
- [ ] Verificar schema education.lessons
- [ ] Tabla user_lesson_progress con campos: started_at, completed_at, last_position **Frontend:**
- [ ] Tabla user_notes con FK a lesson - [ ] Crear LessonPlayerPage.tsx
- [ ] Tabla user_bookmarks para marcadores - [ ] Crear componente VideoPlayer.tsx
- [ ] Crear componente ArticleViewer.tsx
**Backend:** - [ ] Crear componente LessonSidebar.tsx
- [ ] Endpoint GET /education/courses/:id/lessons/:lessonId - [ ] Crear componente NotesEditor.tsx
- [ ] Endpoint POST /education/lessons/:id/progress (guardar posición) - [ ] Crear componente ResourcesList.tsx
- [ ] Endpoint POST /education/lessons/:id/complete - [ ] Implementar lessonStore para progreso
- [ ] Endpoint GET/POST/PUT/DELETE /education/lessons/:id/notes - [ ] Auto-save de posición cada 10s
- [ ] Endpoint GET /education/lessons/:id/resources/:resourceId/download - [ ] Atajos de teclado para navegación
- [ ] Implementar signed URLs para videos privados
- [ ] Rate limiting en download de recursos **Tests:**
- [ ] Test unitario LessonService
**Frontend:** - [ ] Test integración actualización de progreso
- [ ] Crear LessonPlayerPage.tsx - [ ] Test E2E completar lección y desbloquear siguiente
- [ ] Crear componente VideoPlayer.tsx
- [ ] Crear componente ArticleViewer.tsx ---
- [ ] Crear componente LessonSidebar.tsx
- [ ] Crear componente NotesEditor.tsx **Creado por:** Requirements-Analyst
- [ ] Crear componente ResourcesList.tsx **Fecha:** 2025-12-05
- [ ] Implementar lessonStore para progreso **Última actualización:** 2025-12-05
- [ ] 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

View File

@ -1,368 +1,355 @@
--- # RF-EDU-003: Tracking de Progreso
id: "RF-EDU-003"
title: "Tracking de Progreso" **Versión:** 1.0.0
type: "Requirement" **Fecha:** 2025-12-05
status: "Done" **Épica:** OQI-002 - Módulo Educativo
priority: "Alta" **Prioridad:** P0
module: "education" **Story Points:** 8
epic: "OQI-002"
version: "1.0" ---
created_date: "2025-12-05"
updated_date: "2026-01-04" ## 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.
# RF-EDU-003: Tracking de Progreso
---
**Versión:** 1.0.0
**Fecha:** 2025-12-05 ## Requisitos Funcionales
**Épica:** OQI-002 - Módulo Educativo
**Prioridad:** P0 ### RF-EDU-003.1: Dashboard de Progreso
**Story Points:** 8
El sistema debe mostrar:
--- - Resumen general de aprendizaje del usuario
- Total de cursos: En progreso, Completados, Guardados
## Descripción - Total de lecciones completadas
- Total de horas de aprendizaje
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. - Racha actual (días consecutivos de actividad)
- Racha más larga histórica
--- - XP total acumulado
- Nivel actual del usuario
## Requisitos Funcionales - Gráfico de actividad semanal/mensual
### RF-EDU-003.1: Dashboard de Progreso ### RF-EDU-003.2: Progreso por Curso
El sistema debe mostrar: El sistema debe mostrar para cada curso:
- Resumen general de aprendizaje del usuario - Porcentaje de completitud (0-100%)
- Total de cursos: En progreso, Completados, Guardados - Lecciones completadas / Total de lecciones
- Total de lecciones completadas - Módulos completados / Total de módulos
- Total de horas de aprendizaje - Tiempo invertido en el curso
- Racha actual (días consecutivos de actividad) - Última vez que accedió al curso
- Racha más larga histórica - Fecha de inscripción
- XP total acumulado - Fecha de finalización (si completó)
- Nivel actual del usuario - Próxima lección sugerida
- Gráfico de actividad semanal/mensual - Barra de progreso visual
### RF-EDU-003.2: Progreso por Curso ### RF-EDU-003.3: Historial de Actividad
El sistema debe mostrar para cada curso: El sistema debe registrar:
- Porcentaje de completitud (0-100%) - Timeline de actividades del usuario
- Lecciones completadas / Total de lecciones - Eventos: inscripción, lección completada, quiz aprobado, certificado obtenido
- Módulos completados / Total de módulos - Fecha y hora de cada evento
- Tiempo invertido en el curso - Filtros por tipo de evento y rango de fechas
- Última vez que accedió al curso - Exportar historial a CSV
- Fecha de inscripción
- Fecha de finalización (si completó) Tipos de eventos:
- Próxima lección sugerida ```typescript
- Barra de progreso visual enum ActivityType {
COURSE_ENROLLED = 'course_enrolled',
### RF-EDU-003.3: Historial de Actividad LESSON_STARTED = 'lesson_started',
LESSON_COMPLETED = 'lesson_completed',
El sistema debe registrar: MODULE_COMPLETED = 'module_completed',
- Timeline de actividades del usuario COURSE_COMPLETED = 'course_completed',
- Eventos: inscripción, lección completada, quiz aprobado, certificado obtenido QUIZ_PASSED = 'quiz_passed',
- Fecha y hora de cada evento QUIZ_FAILED = 'quiz_failed',
- Filtros por tipo de evento y rango de fechas CERTIFICATE_EARNED = 'certificate_earned',
- Exportar historial a CSV NOTE_CREATED = 'note_created',
RESOURCE_DOWNLOADED = 'resource_downloaded',
Tipos de eventos: }
```typescript ```
enum ActivityType {
COURSE_ENROLLED = 'course_enrolled', ### RF-EDU-003.4: Estadísticas de Aprendizaje
LESSON_STARTED = 'lesson_started',
LESSON_COMPLETED = 'lesson_completed', El sistema debe calcular y mostrar:
MODULE_COMPLETED = 'module_completed', - **Tiempo promedio por lección:** Total minutos / lecciones completadas
COURSE_COMPLETED = 'course_completed', - **Cursos por mes:** Cursos completados en último mes
QUIZ_PASSED = 'quiz_passed', - **Tasa de completitud:** % de cursos iniciados que fueron completados
QUIZ_FAILED = 'quiz_failed', - **Días activos:** Días con al menos 1 lección completada
CERTIFICATE_EARNED = 'certificate_earned', - **Mejor día de la semana:** Día con más actividad
NOTE_CREATED = 'note_created', - **Hora preferida:** Franja horaria con más actividad
RESOURCE_DOWNLOADED = 'resource_downloaded', - **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)
### RF-EDU-003.4: Estadísticas de Aprendizaje
El sistema debe:
El sistema debe calcular y mostrar: - Calcular racha actual: días consecutivos con actividad
- **Tiempo promedio por lección:** Total minutos / lecciones completadas - Definir actividad como: completar al menos 1 lección
- **Cursos por mes:** Cursos completados en último mes - Resetear racha si pasa 1 día sin actividad
- **Tasa de completitud:** % de cursos iniciados que fueron completados - Guardar racha más larga histórica
- **Días activos:** Días con al menos 1 lección completada - Mostrar calendario de actividad (estilo GitHub contributions)
- **Mejor día de la semana:** Día con más actividad - Enviar notificación si racha está en riesgo (no actividad hoy)
- **Hora preferida:** Franja horaria con más actividad - Otorgar badges especiales por rachas: 7, 30, 100, 365 días
- **Categoría favorita:** Categoría con más cursos completados
- **Velocidad de aprendizaje:** Comparación con promedio de usuarios ### RF-EDU-003.6: Sistema de Niveles
### RF-EDU-003.5: Racha de Aprendizaje (Streak) El sistema debe:
- Asignar nivel al usuario basado en XP acumulado
El sistema debe: - XP se gana por:
- Calcular racha actual: días consecutivos con actividad - Completar lección: 10 XP
- Definir actividad como: completar al menos 1 lección - Completar módulo: 50 XP
- Resetear racha si pasa 1 día sin actividad - Completar curso: 200 XP
- Guardar racha más larga histórica - Aprobar quiz primera vez: 30 XP
- Mostrar calendario de actividad (estilo GitHub contributions) - Obtener certificado: 100 XP
- Enviar notificación si racha está en riesgo (no actividad hoy) - Racha de 7 días: 100 XP
- Otorgar badges especiales por rachas: 7, 30, 100, 365 días - Niveles del 1 al 50
- XP requerido por nivel aumenta progresivamente
### RF-EDU-003.6: Sistema de Niveles
Fórmula XP por nivel:
El sistema debe: ```
- Asignar nivel al usuario basado en XP acumulado XP_needed(level) = 100 * level * (level + 1) / 2
- XP se gana por: ```
- Completar lección: 10 XP
- Completar módulo: 50 XP | Nivel | XP Requerido | XP Acumulado |
- Completar curso: 200 XP |-------|--------------|--------------|
- Aprobar quiz primera vez: 30 XP | 1 | 0 | 0 |
- Obtener certificado: 100 XP | 2 | 100 | 100 |
- Racha de 7 días: 100 XP | 3 | 200 | 300 |
- Niveles del 1 al 50 | 5 | 400 | 1000 |
- XP requerido por nivel aumenta progresivamente | 10 | 900 | 5500 |
| 20 | 1900 | 21000 |
Fórmula XP por nivel: | 50 | 4900 | 127500 |
```
XP_needed(level) = 100 * level * (level + 1) / 2 ### RF-EDU-003.7: Reportes de Progreso
```
El sistema debe generar:
| Nivel | XP Requerido | XP Acumulado | - Reporte semanal por email (opcional)
|-------|--------------|--------------| - Reporte mensual con estadísticas
| 1 | 0 | 0 | - Exportar progreso a PDF
| 2 | 100 | 100 | - Comparación mes a mes
| 3 | 200 | 300 | - Metas vs realidad
| 5 | 400 | 1000 |
| 10 | 900 | 5500 | ### RF-EDU-003.8: Metas de Aprendizaje
| 20 | 1900 | 21000 |
| 50 | 4900 | 127500 | El sistema debe permitir:
- Establecer meta de lecciones por semana
### RF-EDU-003.7: Reportes de Progreso - Establecer meta de cursos por mes
- Establecer meta de minutos de estudio por día
El sistema debe generar: - Visualizar progreso hacia metas
- Reporte semanal por email (opcional) - Notificaciones si está rezagado
- Reporte mensual con estadísticas - Celebración al cumplir meta
- Exportar progreso a PDF
- Comparación mes a mes ---
- Metas vs realidad
## Datos de Salida
### RF-EDU-003.8: Metas de Aprendizaje
```typescript
El sistema debe permitir: interface UserProgress {
- Establecer meta de lecciones por semana userId: string;
- Establecer meta de cursos por mes overview: {
- Establecer meta de minutos de estudio por día coursesInProgress: number;
- Visualizar progreso hacia metas coursesCompleted: number;
- Notificaciones si está rezagado coursesSaved: number;
- Celebración al cumplir meta lessonsCompleted: number;
totalLearningTime: number; // minutos
--- currentStreak: number;
longestStreak: number;
## Datos de Salida totalXP: number;
currentLevel: number;
```typescript xpToNextLevel: number;
interface UserProgress { };
userId: string;
overview: { courses: {
coursesInProgress: number; courseId: string;
coursesCompleted: number; courseTitle: string;
coursesSaved: number; thumbnail: string;
lessonsCompleted: number; progress: {
totalLearningTime: number; // minutos percent: number;
currentStreak: number; lessonsCompleted: number;
longestStreak: number; totalLessons: number;
totalXP: number; modulesCompleted: number;
currentLevel: number; totalModules: number;
xpToNextLevel: number; timeSpent: number;
}; enrolledAt: string;
completedAt?: string;
courses: { lastAccessedAt: string;
courseId: string; nextLesson?: {
courseTitle: string; id: string;
thumbnail: string; title: string;
progress: { };
percent: number; };
lessonsCompleted: number; }[];
totalLessons: number;
modulesCompleted: number; stats: {
totalModules: number; avgTimePerLesson: number;
timeSpent: number; coursesThisMonth: number;
enrolledAt: string; completionRate: number; // 0-100
completedAt?: string; activeDays: number;
lastAccessedAt: string; favoriteCategory: string;
nextLesson?: { bestDayOfWeek: string;
id: string; preferredTimeOfDay: string;
title: string; };
};
}; recentActivity: {
}[]; type: ActivityType;
title: string;
stats: { description: string;
avgTimePerLesson: number; timestamp: string;
coursesThisMonth: number; metadata?: any;
completionRate: number; // 0-100 }[];
activeDays: number;
favoriteCategory: string; calendar: {
bestDayOfWeek: string; date: string; // YYYY-MM-DD
preferredTimeOfDay: string; lessonsCompleted: number;
}; minutesLearned: number;
}[];
recentActivity: { }
type: ActivityType;
title: string; interface LevelInfo {
description: string; currentLevel: number;
timestamp: string; currentXP: number;
metadata?: any; xpForCurrentLevel: number;
}[]; xpForNextLevel: number;
progressToNextLevel: number; // 0-100
calendar: { title: string; // "Novice Trader", "Advanced Analyst", etc.
date: string; // YYYY-MM-DD }
lessonsCompleted: number; ```
minutesLearned: number;
}[]; ---
}
## Reglas de Negocio
interface LevelInfo {
currentLevel: number; 1. **Actividad mínima:** 1 lección completada para contar como día activo
currentXP: number; 2. **Racha:** Se resetea si pasan >24h sin actividad
xpForCurrentLevel: number; 3. **XP no se pierde:** Una vez ganado, el XP es permanente
xpForNextLevel: number; 4. **Nivel no baja:** Los niveles solo suben, nunca bajan
progressToNextLevel: number; // 0-100 5. **Tiempo de aprendizaje:** Solo cuenta tiempo activo (reproducción de video, scroll en artículo)
title: string; // "Novice Trader", "Advanced Analyst", etc. 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
---
---
## Reglas de Negocio
## Criterios de Aceptación
1. **Actividad mínima:** 1 lección completada para contar como día activo
2. **Racha:** Se resetea si pasan >24h sin actividad ```gherkin
3. **XP no se pierde:** Una vez ganado, el XP es permanente Escenario: Usuario visualiza dashboard de progreso
4. **Nivel no baja:** Los niveles solo suben, nunca bajan DADO que el usuario está autenticado
5. **Tiempo de aprendizaje:** Solo cuenta tiempo activo (reproducción de video, scroll en artículo) CUANDO accede a /education/progress
6. **Caché de stats:** Estadísticas se calculan cada hora, no en tiempo real ENTONCES se muestra resumen general de aprendizaje
7. **Zona horaria:** Racha se calcula según timezone del usuario Y se muestran estadísticas: cursos, lecciones, horas
8. **Reporte semanal:** Se envía lunes a las 8am hora local Y se muestra racha actual y más larga
9. **Completitud de curso:** 100% cuando todas las lecciones están completas Y se muestra nivel y XP
Y se muestra gráfico de actividad reciente
---
Escenario: Usuario visualiza progreso de curso
## Criterios de Aceptación DADO que el usuario está inscrito en un curso
CUANDO ve la tarjeta del curso en el dashboard
```gherkin ENTONCES se muestra barra de progreso con porcentaje
Escenario: Usuario visualiza dashboard de progreso Y se muestra "X/Y lecciones completadas"
DADO que el usuario está autenticado Y se muestra tiempo invertido
CUANDO accede a /education/progress Y se muestra botón "Continuar" que lleva a próxima lección
ENTONCES se muestra resumen general de aprendizaje
Y se muestran estadísticas: cursos, lecciones, horas Escenario: Usuario mantiene racha activa
Y se muestra racha actual y más larga DADO que el usuario tiene racha de 5 días
Y se muestra nivel y XP CUANDO completa 1 lección hoy
Y se muestra gráfico de actividad reciente ENTONCES la racha aumenta a 6 días
Y se muestra animación de celebración
Escenario: Usuario visualiza progreso de curso Y se actualiza calendario de actividad
DADO que el usuario está inscrito en un curso
CUANDO ve la tarjeta del curso en el dashboard Escenario: Usuario rompe racha
ENTONCES se muestra barra de progreso con porcentaje DADO que el usuario tiene racha de 10 días
Y se muestra "X/Y lecciones completadas" Y no completó lecciones ayer
Y se muestra tiempo invertido CUANDO accede hoy
Y se muestra botón "Continuar" que lleva a próxima lección ENTONCES la racha se resetea a 0 o 1 (según actividad de hoy)
Y se muestra mensaje "Racha reiniciada"
Escenario: Usuario mantiene racha activa Y se guarda racha anterior como "mejor racha"
DADO que el usuario tiene racha de 5 días
CUANDO completa 1 lección hoy Escenario: Usuario sube de nivel
ENTONCES la racha aumenta a 6 días DADO que el usuario tiene 950 XP (nivel 9)
Y se muestra animación de celebración CUANDO completa un curso y gana 200 XP
Y se actualiza calendario de actividad ENTONCES sube a nivel 10
Y se muestra animación "¡Subiste de nivel!"
Escenario: Usuario rompe racha Y se desbloquea nuevo badge
DADO que el usuario tiene racha de 10 días Y se envía notificación
Y no completó lecciones ayer
CUANDO accede hoy Escenario: Ver historial de actividad
ENTONCES la racha se resetea a 0 o 1 (según actividad de hoy) DADO que el usuario accede a historial
Y se muestra mensaje "Racha reiniciada" CUANDO filtra por "últimos 7 días"
Y se guarda racha anterior como "mejor racha" ENTONCES se muestran todas las actividades de la semana
Y se agrupan por día
Escenario: Usuario sube de nivel Y se muestra timeline visual
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 ## Dependencias
Y se envía notificación
- PostgreSQL para almacenar progreso
Escenario: Ver historial de actividad - Redis para caché de estadísticas
DADO que el usuario accede a historial - Cron jobs para calcular stats diarias
CUANDO filtra por "últimos 7 días" - Email service para reportes semanales
ENTONCES se muestran todas las actividades de la semana - Analytics para tracking de eventos
Y se agrupan por día
Y se muestra timeline visual ---
```
## Notas Técnicas
---
- Calcular estadísticas agregadas en background jobs (no en request)
## Dependencias - Usar materialized views para queries pesadas
- Implementar cache warming para stats de usuarios activos
- PostgreSQL para almacenar progreso - Considerar Event Sourcing para historial de actividades
- Redis para caché de estadísticas - Optimizar queries con índices en user_id + timestamp
- Cron jobs para calcular stats diarias - Implementar rate limiting en export de reportes
- Email service para reportes semanales
- Analytics para tracking de eventos ---
--- ## Referencias
## Notas Técnicas - Schema: `/backend/src/database/schemas/education.sql`
- API: `/backend/src/modules/courses/progress.routes.ts`
- Calcular estadísticas agregadas en background jobs (no en request) - Frontend: `/frontend/src/pages/EducationDashboard.tsx`
- 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 ## Tareas Técnicas
- Implementar rate limiting en export de reportes
**Database:**
--- - [ ] Tabla user_course_progress: percent, lessons_completed, time_spent
- [ ] Tabla user_activity_log: tipo, timestamp, metadata
## Referencias - [ ] Tabla user_stats: nivel, xp, racha, cache de stats
- [ ] Tabla user_goals: meta, progreso, fecha
- Schema: `/backend/src/database/schemas/education.sql` - [ ] Índices en user_id + timestamp para queries rápidas
- API: `/backend/src/modules/courses/progress.routes.ts` - [ ] Materialized view para stats agregadas
- Frontend: `/frontend/src/pages/EducationDashboard.tsx`
**Backend:**
--- - [ ] Endpoint GET /education/progress (dashboard completo)
- [ ] Endpoint GET /education/progress/stats
## Tareas Técnicas - [ ] Endpoint GET /education/progress/activity (historial)
- [ ] Endpoint POST /education/goals (crear meta)
**Database:** - [ ] Implementar ProgressService.calculateLevel()
- [ ] Tabla user_course_progress: percent, lessons_completed, time_spent - [ ] Implementar ProgressService.updateStreak() (cron daily)
- [ ] Tabla user_activity_log: tipo, timestamp, metadata - [ ] Job para generar reportes semanales
- [ ] Tabla user_stats: nivel, xp, racha, cache de stats - [ ] Event handlers para actualizar XP en actividades
- [ ] Tabla user_goals: meta, progreso, fecha
- [ ] Índices en user_id + timestamp para queries rápidas **Frontend:**
- [ ] Materialized view para stats agregadas - [ ] Crear EducationDashboardPage.tsx
- [ ] Crear componente ProgressOverview.tsx
**Backend:** - [ ] Crear componente CourseProgressCard.tsx
- [ ] Endpoint GET /education/progress (dashboard completo) - [ ] Crear componente ActivityCalendar.tsx (estilo GitHub)
- [ ] Endpoint GET /education/progress/stats - [ ] Crear componente LevelProgress.tsx
- [ ] Endpoint GET /education/progress/activity (historial) - [ ] Crear componente ActivityTimeline.tsx
- [ ] Endpoint POST /education/goals (crear meta) - [ ] Crear componente StatsCharts.tsx
- [ ] Implementar ProgressService.calculateLevel() - [ ] Animaciones para level up y racha
- [ ] Implementar ProgressService.updateStreak() (cron daily) - [ ] Implementar progressStore
- [ ] Job para generar reportes semanales
- [ ] Event handlers para actualizar XP en actividades **Tests:**
- [ ] Test cálculo de nivel según XP
**Frontend:** - [ ] Test cálculo de racha con diferentes escenarios
- [ ] Crear EducationDashboardPage.tsx - [ ] Test E2E completar lección y ver progreso actualizado
- [ ] Crear componente ProgressOverview.tsx
- [ ] Crear componente CourseProgressCard.tsx ---
- [ ] Crear componente ActivityCalendar.tsx (estilo GitHub)
- [ ] Crear componente LevelProgress.tsx **Creado por:** Requirements-Analyst
- [ ] Crear componente ActivityTimeline.tsx **Fecha:** 2025-12-05
- [ ] Crear componente StatsCharts.tsx **Última actualización:** 2025-12-05
- [ ] 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

View File

@ -1,417 +1,404 @@
--- # RF-EDU-004: Sistema de Quizzes
id: "RF-EDU-004"
title: "Sistema de Quizzes" **Versión:** 1.0.0
type: "Requirement" **Fecha:** 2025-12-05
status: "Done" **Épica:** OQI-002 - Módulo Educativo
priority: "Alta" **Prioridad:** P1
module: "education" **Story Points:** 8
epic: "OQI-002"
version: "1.0" ---
created_date: "2025-12-05"
updated_date: "2026-01-04" ## 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.
# RF-EDU-004: Sistema de Quizzes
---
**Versión:** 1.0.0
**Fecha:** 2025-12-05 ## Requisitos Funcionales
**Épica:** OQI-002 - Módulo Educativo
**Prioridad:** P1 ### RF-EDU-004.1: Tipos de Preguntas
**Story Points:** 8
El sistema debe soportar:
---
| Tipo | Descripción | Características |
## Descripción |------|-------------|-----------------|
| **Multiple Choice** | Una respuesta correcta | 2-6 opciones |
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. | **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 |
## Requisitos Funcionales | **Ordering** | Ordenar elementos | Secuencia correcta |
### RF-EDU-004.1: Tipos de Preguntas ### RF-EDU-004.2: Estructura de Quiz
El sistema debe soportar: Cada quiz debe tener:
- Título y descripción
| Tipo | Descripción | Características | - Tiempo límite (opcional)
|------|-------------|-----------------| - Número de preguntas
| **Multiple Choice** | Una respuesta correcta | 2-6 opciones | - Puntuación mínima para aprobar (% o puntos)
| **Multiple Select** | Varias respuestas correctas | Checkbox, puntuación parcial | - Número de intentos permitidos (ilimitado, 1, 2, 3...)
| **True/False** | Verdadero o falso | 2 opciones | - Modo: Práctica (sin límite) o Evaluación (formal)
| **Fill in the Blank** | Completar espacios | Input de texto, validación | - Mostrar respuestas correctas: Inmediato, Al finalizar, Nunca
| **Matching** | Emparejar elementos | Drag & drop opcional | - Barajear preguntas (randomizar orden)
| **Ordering** | Ordenar elementos | Secuencia correcta | - Barajear opciones de respuesta
### RF-EDU-004.2: Estructura de Quiz ### RF-EDU-004.3: Interfaz de Quiz
Cada quiz debe tener: El sistema debe mostrar:
- Título y descripción - Contador de preguntas (Pregunta 1 de 10)
- Tiempo límite (opcional) - Barra de progreso del quiz
- Número de preguntas - Timer countdown si hay límite de tiempo
- Puntuación mínima para aprobar (% o puntos) - Pregunta actual con opciones
- Número de intentos permitidos (ilimitado, 1, 2, 3...) - Botones: "Anterior", "Siguiente", "Marcar para revisión"
- Modo: Práctica (sin límite) o Evaluación (formal) - Navegador de preguntas (minimap con estado: respondida, marcada, pendiente)
- Mostrar respuestas correctas: Inmediato, Al finalizar, Nunca - Botón "Finalizar quiz" (requiere confirmación)
- Barajear preguntas (randomizar orden) - Auto-submit cuando expira el tiempo
- Barajear opciones de respuesta
### RF-EDU-004.4: Navegación y Estados
### RF-EDU-004.3: Interfaz de Quiz
Estados de pregunta:
El sistema debe mostrar: - **No respondida:** Sin respuesta seleccionada
- Contador de preguntas (Pregunta 1 de 10) - **Respondida:** Respuesta seleccionada
- Barra de progreso del quiz - **Marcada:** Flagged para revisión posterior
- Timer countdown si hay límite de tiempo - **Correcta:** Solo visible después de submit (si configurado)
- Pregunta actual con opciones - **Incorrecta:** Solo visible después de submit (si configurado)
- Botones: "Anterior", "Siguiente", "Marcar para revisión"
- Navegador de preguntas (minimap con estado: respondida, marcada, pendiente) El usuario debe poder:
- Botón "Finalizar quiz" (requiere confirmación) - Navegar libremente entre preguntas antes de submit
- Auto-submit cuando expira el tiempo - Cambiar respuestas antes de finalizar
- Marcar preguntas para revisar después
### RF-EDU-004.4: Navegación y Estados - Ver resumen antes de enviar
Estados de pregunta: ### RF-EDU-004.5: Calificación y Resultados
- **No respondida:** Sin respuesta seleccionada
- **Respondida:** Respuesta seleccionada Al finalizar el quiz, mostrar:
- **Marcada:** Flagged para revisión posterior - Puntuación obtenida (X/Y puntos o %)
- **Correcta:** Solo visible después de submit (si configurado) - Estado: Aprobado / Reprobado
- **Incorrecta:** Solo visible después de submit (si configurado) - Tiempo invertido
- Feedback general basado en score
El usuario debe poder: - Desglose por pregunta (si configurado):
- Navegar libremente entre preguntas antes de submit - Pregunta
- Cambiar respuestas antes de finalizar - Tu respuesta
- Marcar preguntas para revisar después - Respuesta correcta
- Ver resumen antes de enviar - Explicación
- Intentos restantes
### RF-EDU-004.5: Calificación y Resultados - Botón "Reintentar" si aplica
- Botón "Continuar al siguiente contenido"
Al finalizar el quiz, mostrar:
- Puntuación obtenida (X/Y puntos o %) ### RF-EDU-004.6: Historial de Intentos
- Estado: Aprobado / Reprobado
- Tiempo invertido El sistema debe:
- Feedback general basado en score - Guardar todos los intentos del usuario
- Desglose por pregunta (si configurado): - Mostrar tabla con: fecha, puntuación, tiempo, estado
- Pregunta - Permitir ver detalle de intento anterior
- Tu respuesta - Mostrar mejor intento destacado
- Respuesta correcta - Calcular promedio de intentos
- Explicación - Guardar última puntuación como oficial
- Intentos restantes
- Botón "Reintentar" si aplica ### RF-EDU-004.7: Feedback y Explicaciones
- Botón "Continuar al siguiente contenido"
El sistema debe permitir:
### RF-EDU-004.6: Historial de Intentos - Explicación de respuesta correcta (markdown)
- Explicación de por qué otras opciones son incorrectas
El sistema debe: - Links a recursos relacionados
- Guardar todos los intentos del usuario - Video explicativo opcional
- Mostrar tabla con: fecha, puntuación, tiempo, estado - Sugerencias de lecciones para repasar
- Permitir ver detalle de intento anterior
- Mostrar mejor intento destacado ### RF-EDU-004.8: Analítica de Quiz
- Calcular promedio de intentos
- Guardar última puntuación como oficial Para cada pregunta, rastrear:
- Número de veces respondida
### RF-EDU-004.7: Feedback y Explicaciones - Número de respuestas correctas
- Número de respuestas incorrectas
El sistema debe permitir: - Tasa de éxito global (%)
- Explicación de respuesta correcta (markdown) - Tiempo promedio de respuesta
- Explicación de por qué otras opciones son incorrectas - Opción más elegida (para detectar confusión)
- Links a recursos relacionados
- Video explicativo opcional Para cada quiz, rastrear:
- Sugerencias de lecciones para repasar - Número de intentos totales
- Tasa de aprobación (%)
### RF-EDU-004.8: Analítica de Quiz - Puntuación promedio
- Tiempo promedio de completitud
Para cada pregunta, rastrear: - Pregunta más difícil (menor % acierto)
- Número de veces respondida - Pregunta más fácil (mayor % acierto)
- Número de respuestas correctas
- Número de respuestas incorrectas ---
- Tasa de éxito global (%)
- Tiempo promedio de respuesta ## Datos de Entrada
- Opción más elegida (para detectar confusión)
| Campo | Tipo | Descripción |
Para cada quiz, rastrear: |-------|------|-------------|
- Número de intentos totales | quizId | string | UUID del quiz |
- Tasa de aprobación (%) | answers | object | Mapa de questionId -> respuesta |
- Puntuación promedio
- Tiempo promedio de completitud ---
- Pregunta más difícil (menor % acierto)
- Pregunta más fácil (mayor % acierto) ## Datos de Salida
--- ```typescript
interface Quiz {
## Datos de Entrada id: string;
title: string;
| Campo | Tipo | Descripción | description: string;
|-------|------|-------------| lessonId?: string;
| quizId | string | UUID del quiz | courseId: string;
| answers | object | Mapa de questionId -> respuesta | timeLimit?: number; // minutos
passingScore: number; // 0-100
--- maxAttempts: number; // 0 = ilimitado
questionCount: number;
## Datos de Salida totalPoints: number;
shuffleQuestions: boolean;
```typescript shuffleOptions: boolean;
interface Quiz { showAnswers: 'immediate' | 'after_submit' | 'never';
id: string; mode: 'practice' | 'assessment';
title: string; }
description: string;
lessonId?: string; interface Question {
courseId: string; id: string;
timeLimit?: number; // minutos quizId: string;
passingScore: number; // 0-100 type: 'multiple_choice' | 'multiple_select' | 'true_false' | 'fill_blank' | 'matching' | 'ordering';
maxAttempts: number; // 0 = ilimitado question: string; // Markdown
questionCount: number; points: number;
totalPoints: number; order: number;
shuffleQuestions: boolean;
shuffleOptions: boolean; // Para multiple choice/select
showAnswers: 'immediate' | 'after_submit' | 'never'; options?: {
mode: 'practice' | 'assessment'; id: string;
} text: string;
isCorrect: boolean;
interface Question { explanation?: string;
id: string; }[];
quizId: string;
type: 'multiple_choice' | 'multiple_select' | 'true_false' | 'fill_blank' | 'matching' | 'ordering'; // Para fill in the blank
question: string; // Markdown correctAnswers?: string[];
points: number; caseSensitive?: boolean;
order: number;
// Para matching
// Para multiple choice/select pairs?: {
options?: { left: string;
id: string; right: string;
text: string; }[];
isCorrect: boolean;
explanation?: string; // Para ordering
}[]; correctOrder?: string[];
// Para fill in the blank explanation?: string; // Explicación general
correctAnswers?: string[]; hint?: string;
caseSensitive?: boolean; relatedResources?: {
type: 'lesson' | 'article' | 'video';
// Para matching id: string;
pairs?: { title: string;
left: string; }[];
right: string; }
}[];
interface QuizAttempt {
// Para ordering id: string;
correctOrder?: string[]; quizId: string;
userId: string;
explanation?: string; // Explicación general attemptNumber: number;
hint?: string; startedAt: string;
relatedResources?: { submittedAt?: string;
type: 'lesson' | 'article' | 'video'; timeSpent: number; // segundos
id: string;
title: string; answers: {
}[]; questionId: string;
} userAnswer: any;
isCorrect: boolean;
interface QuizAttempt { pointsEarned: number;
id: string; }[];
quizId: string;
userId: string; score: number; // 0-100
attemptNumber: number; pointsEarned: number;
startedAt: string; totalPoints: number;
submittedAt?: string; passed: boolean;
timeSpent: number; // segundos
analytics: {
answers: { questionsCorrect: number;
questionId: string; questionsIncorrect: number;
userAnswer: any; questionsSkipped: number;
isCorrect: boolean; avgTimePerQuestion: number;
pointsEarned: number; };
}[]; }
score: number; // 0-100 interface QuizResults {
pointsEarned: number; attempt: QuizAttempt;
totalPoints: number; quiz: Quiz;
passed: boolean; questions: (Question & {
userAnswer: any;
analytics: { isCorrect: boolean;
questionsCorrect: number; pointsEarned: number;
questionsIncorrect: number; })[];
questionsSkipped: number; feedback: {
avgTimePerQuestion: number; title: string;
}; message: string;
} suggestions?: string[];
};
interface QuizResults { attemptsRemaining: number;
attempt: QuizAttempt; canRetake: boolean;
quiz: Quiz; nextContent?: {
questions: (Question & { type: 'lesson' | 'quiz' | 'module';
userAnswer: any; id: string;
isCorrect: boolean; title: string;
pointsEarned: number; };
})[]; }
feedback: { ```
title: string;
message: string; ---
suggestions?: string[];
}; ## Reglas de Negocio
attemptsRemaining: number;
canRetake: boolean; 1. **Puntuación mínima:** Default 70% para aprobar
nextContent?: { 2. **Intentos:** Si se agotan intentos, usuario debe esperar 24h o contactar soporte
type: 'lesson' | 'quiz' | 'module'; 3. **Tiempo límite:** Si expira, se auto-submit con respuestas actuales
id: string; 4. **Preguntas obligatorias:** No se puede submit sin responder todas (modo evaluación)
title: string; 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
---
---
## Reglas de Negocio
## Criterios de Aceptación
1. **Puntuación mínima:** Default 70% para aprobar
2. **Intentos:** Si se agotan intentos, usuario debe esperar 24h o contactar soporte ```gherkin
3. **Tiempo límite:** Si expira, se auto-submit con respuestas actuales Escenario: Usuario inicia quiz
4. **Preguntas obligatorias:** No se puede submit sin responder todas (modo evaluación) DADO que el usuario está en una lección con quiz
5. **Modo práctica:** Sin límite de intentos, sin guardar en historial oficial CUANDO hace click en "Iniciar quiz"
6. **Partial credit:** Multiple select otorga puntos parciales (50% si elige 2/4 correctas) ENTONCES se muestra pantalla de introducción del quiz
7. **Shuffle:** Si está activado, orden diferente en cada intento Y se muestra título, descripción, número de preguntas
8. **Feedback inmediato:** Solo en modo práctica Y se muestra tiempo límite si aplica
9. **Certificación:** Quiz final de curso debe aprobarse para certificado Y se muestra intentos disponibles
Y se muestra puntuación requerida para aprobar
--- Y se muestra botón "Comenzar"
## Criterios de Aceptación Escenario: Usuario responde preguntas
DADO que el usuario comenzó el quiz
```gherkin CUANDO selecciona una respuesta
Escenario: Usuario inicia quiz ENTONCES la opción se marca como seleccionada
DADO que el usuario está en una lección con quiz Y la pregunta se marca como "respondida"
CUANDO hace click en "Iniciar quiz" Y puede navegar a siguiente pregunta
ENTONCES se muestra pantalla de introducción del quiz Y puede volver a preguntas anteriores
Y se muestra título, descripción, número de preguntas Y puede cambiar respuesta antes de submit
Y se muestra tiempo límite si aplica
Y se muestra intentos disponibles Escenario: Usuario finaliza quiz exitosamente
Y se muestra puntuación requerida para aprobar DADO que el usuario respondió todas las preguntas
Y se muestra botón "Comenzar" CUANDO hace click en "Finalizar quiz"
Y confirma en el modal
Escenario: Usuario responde preguntas ENTONCES se calcula la puntuación
DADO que el usuario comenzó el quiz Y se muestra pantalla de resultados
CUANDO selecciona una respuesta Y se muestra "Aprobado" si score >= passing score
ENTONCES la opción se marca como seleccionada Y se desbloquea siguiente contenido
Y la pregunta se marca como "respondida" Y se otorga XP por aprobar
Y puede navegar a siguiente pregunta
Y puede volver a preguntas anteriores Escenario: Usuario reprueba quiz
Y puede cambiar respuesta antes de submit DADO que el usuario envió el quiz
Y la puntuación es < passing score
Escenario: Usuario finaliza quiz exitosamente ENTONCES se muestra pantalla de resultados
DADO que el usuario respondió todas las preguntas Y se muestra "Reprobado"
CUANDO hace click en "Finalizar quiz" Y se muestra feedback con áreas a mejorar
Y confirma en el modal Y se muestra "Intentos restantes: X"
ENTONCES se calcula la puntuación Y se muestra botón "Reintentar"
Y se muestra pantalla de resultados Y siguiente contenido permanece bloqueado
Y se muestra "Aprobado" si score >= passing score
Y se desbloquea siguiente contenido Escenario: Quiz con tiempo límite expira
Y se otorga XP por aprobar DADO que el quiz tiene tiempo límite de 30 minutos
Y el usuario está en la pregunta 5 de 10
Escenario: Usuario reprueba quiz CUANDO el tiempo llega a 0
DADO que el usuario envió el quiz ENTONCES el quiz se envía automáticamente
Y la puntuación es < passing score Y se califica con respuestas hasta el momento
ENTONCES se muestra pantalla de resultados Y preguntas sin responder cuentan como incorrectas
Y se muestra "Reprobado"
Y se muestra feedback con áreas a mejorar Escenario: Ver explicación de respuestas
Y se muestra "Intentos restantes: X" DADO que el quiz permite ver respuestas
Y se muestra botón "Reintentar" Y el usuario envió el quiz
Y siguiente contenido permanece bloqueado CUANDO ve los resultados
ENTONCES se muestran todas las preguntas
Escenario: Quiz con tiempo límite expira Y se destacan respuestas correctas en verde
DADO que el quiz tiene tiempo límite de 30 minutos Y se destacan respuestas incorrectas en rojo
Y el usuario está en la pregunta 5 de 10 Y se muestra explicación de cada respuesta
CUANDO el tiempo llega a 0 Y se muestran recursos relacionados
ENTONCES el quiz se envía automáticamente
Y se califica con respuestas hasta el momento Escenario: Reintentar quiz
Y preguntas sin responder cuentan como incorrectas DADO que el usuario reprobó un quiz
Y tiene intentos disponibles
Escenario: Ver explicación de respuestas CUANDO hace click en "Reintentar"
DADO que el quiz permite ver respuestas ENTONCES se inicia nuevo intento
Y el usuario envió el quiz Y preguntas pueden estar en diferente orden
CUANDO ve los resultados Y respuestas anteriores no están pre-seleccionadas
ENTONCES se muestran todas las preguntas Y contador de intentos se decrementa
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
## Dependencias
Escenario: Reintentar quiz
DADO que el usuario reprobó un quiz - PostgreSQL para quizzes y resultados
Y tiene intentos disponibles - Redis para cachear quizzes activos
CUANDO hace click en "Reintentar" - WebSocket para timer en tiempo real (opcional)
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 ## 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
## Dependencias - Validar respuestas en backend (nunca confiar en frontend)
- Implementar rate limiting para prevenir brute force
- PostgreSQL para quizzes y resultados - Usar optimistic updates para mejor UX
- Redis para cachear quizzes activos - Considerar adaptive quizzes (ajustar dificultad según respuestas)
- WebSocket para timer en tiempo real (opcional)
---
---
## Referencias
## Notas Técnicas
- Schema: `/backend/src/database/schemas/education.sql`
- Implementar auto-save cada 30s para evitar pérdida de progreso - API: `/backend/src/modules/courses/quizzes.routes.ts`
- Usar WebSockets para sincronizar timer entre tabs - Frontend: `/frontend/src/pages/QuizPlayer.tsx`
- 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 ## Tareas Técnicas
- Considerar adaptive quizzes (ajustar dificultad según respuestas)
**Database:**
--- - [ ] Tabla education.quizzes
- [ ] Tabla education.questions con FK a quiz
## Referencias - [ ] Tabla education.question_options
- [ ] Tabla education.quiz_attempts
- Schema: `/backend/src/database/schemas/education.sql` - [ ] Tabla education.quiz_answers
- API: `/backend/src/modules/courses/quizzes.routes.ts` - [ ] Índices para queries por usuario y quiz
- Frontend: `/frontend/src/pages/QuizPlayer.tsx`
**Backend:**
--- - [ ] Endpoint GET /education/quizzes/:id (sin respuestas correctas)
- [ ] Endpoint POST /education/quizzes/:id/start
## Tareas Técnicas - [ ] Endpoint POST /education/quizzes/:id/submit
- [ ] Endpoint GET /education/quizzes/:id/attempts (historial)
**Database:** - [ ] Endpoint GET /education/quizzes/:id/results/:attemptId
- [ ] Tabla education.quizzes - [ ] Implementar QuizService.gradeAttempt()
- [ ] Tabla education.questions con FK a quiz - [ ] Implementar shuffle de preguntas y opciones
- [ ] Tabla education.question_options - [ ] Rate limiting en submit
- [ ] Tabla education.quiz_attempts
- [ ] Tabla education.quiz_answers **Frontend:**
- [ ] Índices para queries por usuario y quiz - [ ] Crear QuizIntroPage.tsx
- [ ] Crear QuizPlayerPage.tsx
**Backend:** - [ ] Crear componente QuestionRenderer.tsx (soporta todos los tipos)
- [ ] Endpoint GET /education/quizzes/:id (sin respuestas correctas) - [ ] Crear componente QuizNavigator.tsx
- [ ] Endpoint POST /education/quizzes/:id/start - [ ] Crear componente QuizTimer.tsx
- [ ] Endpoint POST /education/quizzes/:id/submit - [ ] Crear QuizResultsPage.tsx
- [ ] Endpoint GET /education/quizzes/:id/attempts (historial) - [ ] Crear componente QuestionExplanation.tsx
- [ ] Endpoint GET /education/quizzes/:id/results/:attemptId - [ ] Auto-save de respuestas cada 30s
- [ ] Implementar QuizService.gradeAttempt() - [ ] Implementar quizStore
- [ ] Implementar shuffle de preguntas y opciones - [ ] Confirmación antes de salir (window.onbeforeunload)
- [ ] Rate limiting en submit
**Tests:**
**Frontend:** - [ ] Test calificación de quiz con diferentes tipos de preguntas
- [ ] Crear QuizIntroPage.tsx - [ ] Test partial credit en multiple select
- [ ] Crear QuizPlayerPage.tsx - [ ] Test expiración de tiempo
- [ ] Crear componente QuestionRenderer.tsx (soporta todos los tipos) - [ ] Test E2E completar quiz y aprobar
- [ ] Crear componente QuizNavigator.tsx
- [ ] Crear componente QuizTimer.tsx ---
- [ ] Crear QuizResultsPage.tsx
- [ ] Crear componente QuestionExplanation.tsx **Creado por:** Requirements-Analyst
- [ ] Auto-save de respuestas cada 30s **Fecha:** 2025-12-05
- [ ] Implementar quizStore **Última actualización:** 2025-12-05
- [ ] 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

View File

@ -1,336 +1,323 @@
--- # RF-EDU-005: Sistema de Certificados
id: "RF-EDU-005"
title: "Sistema de Certificados" **Versión:** 1.0.0
type: "Requirement" **Fecha:** 2025-12-05
status: "Done" **Épica:** OQI-002 - Módulo Educativo
priority: "Media" **Prioridad:** P2
module: "education" **Story Points:** 5
epic: "OQI-002"
version: "1.0" ---
created_date: "2025-12-05"
updated_date: "2026-01-04" ## 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.
# RF-EDU-005: Sistema de Certificados
---
**Versión:** 1.0.0
**Fecha:** 2025-12-05 ## Requisitos Funcionales
**Épica:** OQI-002 - Módulo Educativo
**Prioridad:** P2 ### RF-EDU-005.1: Generación de Certificados
**Story Points:** 5
El sistema debe:
--- - Generar certificado automáticamente al completar 100% de un curso
- Validar que todos los quizzes obligatorios estén aprobados
## Descripción - Validar que todas las lecciones estén marcadas como completadas
- Asignar ID único de certificado (formato: OQI-EDU-XXXXXXXX)
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. - Generar PDF con diseño profesional
- Almacenar PDF en S3 o similar
--- - Registrar en blockchain para verificación (opcional, fase 2)
## Requisitos Funcionales ### RF-EDU-005.2: Contenido del Certificado
### RF-EDU-005.1: Generación de Certificados Cada certificado debe incluir:
- Logo de OrbiQuant IA
El sistema debe: - Título: "Certificado de Finalización"
- Generar certificado automáticamente al completar 100% de un curso - Nombre completo del usuario
- Validar que todos los quizzes obligatorios estén aprobados - Título del curso completado
- Validar que todas las lecciones estén marcadas como completadas - Fecha de finalización
- Asignar ID único de certificado (formato: OQI-EDU-XXXXXXXX) - ID único del certificado
- Generar PDF con diseño profesional - Firma digital del instructor (imagen)
- Almacenar PDF en S3 o similar - Firma digital de la plataforma
- Registrar en blockchain para verificación (opcional, fase 2) - QR code para verificación online
- Footer: "Verifica este certificado en orbiquant.com/verify/{certificateId}"
### RF-EDU-005.2: Contenido del Certificado
Template:
Cada certificado debe incluir: ```
- Logo de OrbiQuant IA ┌─────────────────────────────────────────────────────────┐
- Título: "Certificado de Finalización" │ │
- Nombre completo del usuario │ [LOGO ORBIQUANT] │
- Título del curso completado │ │
- Fecha de finalización │ CERTIFICADO DE FINALIZACIÓN │
- ID único del certificado │ │
- Firma digital del instructor (imagen) │ Se certifica que │
- Firma digital de la plataforma │ │
- QR code para verificación online │ [NOMBRE USUARIO] │
- Footer: "Verifica este certificado en orbiquant.com/verify/{certificateId}" │ │
│ Ha completado exitosamente el curso │
Template: │ │
``` │ "[TÍTULO DEL CURSO]" │
┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Fecha: [DD/MM/YYYY] │
│ [LOGO ORBIQUANT] │ │ Certificado: OQI-EDU-XXXXXXXX │
│ │ │ │
│ CERTIFICADO DE FINALIZACIÓN │ ___________________ ___________________
│ │ │ [Firma Instructor] [Firma Plataforma] │
│ Se certifica que │ │ │
│ │ │ [QR CODE] │
│ [NOMBRE USUARIO] │ │ Verifica en orbiquant.com/verify/XXXX │
│ │ │ │
│ Ha completado exitosamente el curso │ └─────────────────────────────────────────────────────────┘
│ │ ```
│ "[TÍTULO DEL CURSO]" │
│ │ ### RF-EDU-005.3: Verificación de Certificados
│ Fecha: [DD/MM/YYYY] │
│ Certificado: OQI-EDU-XXXXXXXX │ El sistema debe:
│ │ - Proveer página pública /verify/:certificateId
___________________ ___________________ - Mostrar información del certificado sin login
│ [Firma Instructor] [Firma Plataforma] │ - Validar que el ID existe en base de datos
│ │ - Mostrar: nombre, curso, fecha, estado (válido/revocado)
│ [QR CODE] │ - Proteger contra scraping (rate limiting, captcha)
│ Verifica en orbiquant.com/verify/XXXX │ - API pública GET /api/certificates/verify/:id
│ │ - Responder en JSON para integraciones
└─────────────────────────────────────────────────────────┘
``` ### RF-EDU-005.4: Galería de Certificados del Usuario
### RF-EDU-005.3: Verificación de Certificados El sistema debe:
- Página /education/certificates con todos los certificados del usuario
El sistema debe: - Mostrar: thumbnail, título del curso, fecha
- Proveer página pública /verify/:certificateId - Filtrar por: fecha, curso, categoría
- Mostrar información del certificado sin login - Buscar por nombre de curso
- Validar que el ID existe en base de datos - Ordenar por: más reciente, alfabético, categoría
- Mostrar: nombre, curso, fecha, estado (válido/revocado) - Vista de cuadrícula o lista
- Proteger contra scraping (rate limiting, captcha) - Contador: "Has obtenido X certificados"
- API pública GET /api/certificates/verify/:id
- Responder en JSON para integraciones ### RF-EDU-005.5: Descarga y Compartir
### RF-EDU-005.4: Galería de Certificados del Usuario El sistema debe permitir:
- Descargar PDF del certificado
El sistema debe: - Botón "Compartir en LinkedIn" (pre-rellenado)
- Página /education/certificates con todos los certificados del usuario - Botón "Compartir en Twitter/X"
- Mostrar: thumbnail, título del curso, fecha - Botón "Copiar link de verificación"
- Filtrar por: fecha, curso, categoría - Generar imagen social (Open Graph) para compartir
- Buscar por nombre de curso - Agregar a perfil público del usuario (opcional)
- Ordenar por: más reciente, alfabético, categoría
- Vista de cuadrícula o lista Integración LinkedIn:
- Contador: "Has obtenido X certificados" ```javascript
// Pre-llenar certificación en LinkedIn
### RF-EDU-005.5: Descarga y Compartir 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}`;
```
El sistema debe permitir:
- Descargar PDF del certificado ### RF-EDU-005.6: Perfil Público de Certificados
- Botón "Compartir en LinkedIn" (pre-rellenado)
- Botón "Compartir en Twitter/X" El sistema debe:
- Botón "Copiar link de verificación" - Permitir al usuario crear perfil público opcional
- Generar imagen social (Open Graph) para compartir - URL: orbiquant.com/u/:username/certificates
- Agregar a perfil público del usuario (opcional) - Mostrar solo certificados que el usuario hizo públicos
- Galería visual de certificados
Integración LinkedIn: - Bio del usuario
```javascript - Enlaces a redes sociales
// Pre-llenar certificación en LinkedIn - No requiere login para ver
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.7: Revocación de Certificados
### RF-EDU-005.6: Perfil Público de Certificados El sistema debe permitir (solo admins):
- Revocar certificado por fraude
El sistema debe: - Agregar motivo de revocación
- Permitir al usuario crear perfil público opcional - Notificar al usuario por email
- URL: orbiquant.com/u/:username/certificates - Marcar certificado como "REVOKED" en verificación
- Mostrar solo certificados que el usuario hizo públicos - Mantener historial de revocaciones
- Galería visual de certificados
- Bio del usuario ### RF-EDU-005.8: Plantillas de Certificados
- Enlaces a redes sociales
- No requiere login para ver El sistema debe soportar:
- Múltiples plantillas (por categoría o nivel)
### RF-EDU-005.7: Revocación de Certificados - Plantilla estándar para todos los cursos
- Plantilla especial para cursos premium
El sistema debe permitir (solo admins): - Plantilla con colores de marca
- Revocar certificado por fraude - Editor de plantillas para admins (fase 2)
- Agregar motivo de revocación
- Notificar al usuario por email ---
- Marcar certificado como "REVOKED" en verificación
- Mantener historial de revocaciones ## Datos de Salida
### RF-EDU-005.8: Plantillas de Certificados ```typescript
interface Certificate {
El sistema debe soportar: id: string;
- Múltiples plantillas (por categoría o nivel) certificateNumber: string; // OQI-EDU-XXXXXXXX
- Plantilla estándar para todos los cursos userId: string;
- Plantilla especial para cursos premium userName: string;
- Plantilla con colores de marca courseId: string;
- Editor de plantillas para admins (fase 2) courseTitle: string;
courseCategory: string;
--- completedAt: string;
issuedAt: string;
## Datos de Salida pdfUrl: string;
verifyUrl: string;
```typescript qrCodeUrl: string;
interface Certificate { status: 'active' | 'revoked';
id: string; revocationReason?: string;
certificateNumber: string; // OQI-EDU-XXXXXXXX instructorSignature: string;
userId: string; metadata: {
userName: string; duration: number; // horas del curso
courseId: string; moduleCount: number;
courseTitle: string; lessonCount: number;
courseCategory: string; finalScore?: number; // Si hay quiz final
completedAt: string; };
issuedAt: string; }
pdfUrl: string;
verifyUrl: string; interface VerificationResult {
qrCodeUrl: string; valid: boolean;
status: 'active' | 'revoked'; certificate?: {
revocationReason?: string; certificateNumber: string;
instructorSignature: string; recipientName: string;
metadata: { courseTitle: string;
duration: number; // horas del curso completedAt: string;
moduleCount: number; status: 'active' | 'revoked';
lessonCount: number; };
finalScore?: number; // Si hay quiz final error?: string;
}; }
} ```
interface VerificationResult { ---
valid: boolean;
certificate?: { ## Reglas de Negocio
certificateNumber: string;
recipientName: string; 1. **Requisitos para certificado:**
courseTitle: string; - 100% de lecciones completadas
completedAt: string; - Todos los quizzes aprobados (si aplica)
status: 'active' | 'revoked'; - Curso debe estar marcado como "completable" (algunos cursos no otorgan certificado)
}; 2. **ID único:** Formato OQI-EDU-{8 dígitos hex aleatorios}
error?: string; 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
## Reglas de Negocio
---
1. **Requisitos para certificado:**
- 100% de lecciones completadas ## Criterios de Aceptación
- Todos los quizzes aprobados (si aplica)
- Curso debe estar marcado como "completable" (algunos cursos no otorgan certificado) ```gherkin
2. **ID único:** Formato OQI-EDU-{8 dígitos hex aleatorios} Escenario: Usuario completa curso y obtiene certificado
3. **Nombre en certificado:** Se usa nombre completo del perfil del usuario DADO que el usuario completó todas las lecciones de un curso
4. **Fecha:** Fecha de finalización del curso (última lección completada) Y aprobó todos los quizzes obligatorios
5. **PDF inmutable:** Una vez generado, el PDF no se regenera aunque el usuario cambie su nombre CUANDO se marca la última lección como completada
6. **Caducidad:** Los certificados no caducan ENTONCES se genera automáticamente un certificado
7. **Límite de verificaciones:** 100 verificaciones por IP por hora Y se muestra modal de felicitación
8. **Perfil público:** Opt-in, deshabilitado por default Y se envía email con el certificado adjunto
Y se muestra botón "Ver certificado"
---
Escenario: Usuario descarga certificado
## Criterios de Aceptación DADO que el usuario tiene un certificado
CUANDO accede a /education/certificates
```gherkin Y hace click en "Descargar PDF"
Escenario: Usuario completa curso y obtiene certificado ENTONCES se descarga archivo PDF con el certificado
DADO que el usuario completó todas las lecciones de un curso Y el PDF contiene: nombre, curso, fecha, ID, firmas, QR
Y aprobó todos los quizzes obligatorios
CUANDO se marca la última lección como completada Escenario: Usuario comparte en LinkedIn
ENTONCES se genera automáticamente un certificado DADO que el usuario está viendo su certificado
Y se muestra modal de felicitación CUANDO hace click en "Compartir en LinkedIn"
Y se envía email con el certificado adjunto ENTONCES se abre LinkedIn en nueva pestaña
Y se muestra botón "Ver certificado" Y el formulario está pre-llenado con:
- Nombre del curso
Escenario: Usuario descarga certificado - Organización: OrbiQuant IA
DADO que el usuario tiene un certificado - Fecha de emisión
CUANDO accede a /education/certificates - URL de verificación
Y hace click en "Descargar PDF" - ID del certificado
ENTONCES se descarga archivo PDF con el certificado
Y el PDF contiene: nombre, curso, fecha, ID, firmas, QR Escenario: Tercero verifica certificado
DADO que alguien tiene el ID de un certificado
Escenario: Usuario comparte en LinkedIn CUANDO accede a orbiquant.com/verify/OQI-EDU-12345678
DADO que el usuario está viendo su certificado ENTONCES se muestra página de verificación
CUANDO hace click en "Compartir en LinkedIn" Y se muestra: nombre del usuario, curso, fecha
ENTONCES se abre LinkedIn en nueva pestaña Y se muestra badge "✓ Certificado Válido"
Y el formulario está pre-llenado con: Y NO requiere login para ver
- Nombre del curso
- Organización: OrbiQuant IA Escenario: Verificar certificado inválido
- Fecha de emisión DADO que alguien accede a /verify/INVALID-ID
- URL de verificación CUANDO el ID no existe en la base de datos
- ID del certificado ENTONCES se muestra "Certificado no encontrado"
Y se sugiere verificar el ID ingresado
Escenario: Tercero verifica certificado
DADO que alguien tiene el ID de un certificado Escenario: Ver certificado revocado
CUANDO accede a orbiquant.com/verify/OQI-EDU-12345678 DADO que un certificado fue revocado por admin
ENTONCES se muestra página de verificación CUANDO alguien intenta verificarlo
Y se muestra: nombre del usuario, curso, fecha ENTONCES se muestra "Certificado Revocado"
Y se muestra badge "✓ Certificado Válido" Y se muestra motivo de revocación
Y NO requiere login para ver Y se marca en rojo como inválido
```
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" ## Dependencias
Y se sugiere verificar el ID ingresado
- PDF generation library (PDFKit, Puppeteer, o similar)
Escenario: Ver certificado revocado - S3 para almacenar PDFs
DADO que un certificado fue revocado por admin - QR code generator
CUANDO alguien intenta verificarlo - Email service para enviar certificados
ENTONCES se muestra "Certificado Revocado" - LinkedIn API para integración
Y se muestra motivo de revocación
Y se marca en rojo como inválido ---
```
## Notas Técnicas
---
- Usar Puppeteer para generar PDFs desde HTML template
## Dependencias - Almacenar PDFs con nombre: certificates/{userId}/{certificateId}.pdf
- Generar QR codes con librería qrcode.js
- PDF generation library (PDFKit, Puppeteer, o similar) - Implementar caché de verificaciones (Redis) para reducir load
- S3 para almacenar PDFs - Considerar watermark en PDFs para prevenir falsificación
- QR code generator - Usar signed URLs de S3 para descargas seguras
- Email service para enviar certificados - Implementar rate limiting agresivo en endpoint de verificación
- LinkedIn API para integración - Para blockchain: Guardar hash del certificado en Ethereum/Polygon
--- ---
## Notas Técnicas ## Referencias
- Usar Puppeteer para generar PDFs desde HTML template - Schema: `/backend/src/database/schemas/education.sql`
- Almacenar PDFs con nombre: certificates/{userId}/{certificateId}.pdf - API: `/backend/src/modules/courses/certificates.routes.ts`
- Generar QR codes con librería qrcode.js - Frontend: `/frontend/src/pages/Certificates.tsx`
- Implementar caché de verificaciones (Redis) para reducir load - Templates: `/backend/src/templates/certificate-template.html`
- 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 ## Tareas Técnicas
--- **Database:**
- [ ] Tabla education.certificates
## Referencias - [ ] Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status
- [ ] Tabla certificate_verifications (log de verificaciones)
- Schema: `/backend/src/database/schemas/education.sql` - [ ] Índice único en certificate_number
- API: `/backend/src/modules/courses/certificates.routes.ts`
- Frontend: `/frontend/src/pages/Certificates.tsx` **Backend:**
- Templates: `/backend/src/templates/certificate-template.html` - [ ] 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)
## Tareas Técnicas - [ ] Endpoint POST /admin/certificates/:id/revoke (admin only)
- [ ] Implementar CertificateService.generatePDF()
**Database:** - [ ] Implementar generación de QR code
- [ ] Tabla education.certificates - [ ] Event handler en course completion
- [ ] Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status - [ ] Rate limiting en verificación
- [ ] Tabla certificate_verifications (log de verificaciones)
- [ ] Índice único en certificate_number **Frontend:**
- [ ] Crear CertificatesPage.tsx
**Backend:** - [ ] Crear componente CertificateCard.tsx
- [ ] Endpoint POST /education/certificates/generate (triggered on course completion) - [ ] Crear CertificateDetailPage.tsx
- [ ] Endpoint GET /education/certificates (listar del usuario) - [ ] Crear VerifyCertificatePage.tsx (pública)
- [ ] Endpoint GET /education/certificates/:id - [ ] Crear modal de celebración al obtener certificado
- [ ] Endpoint GET /api/public/certificates/verify/:number (público) - [ ] Botones de compartir social media
- [ ] Endpoint POST /admin/certificates/:id/revoke (admin only) - [ ] Preview de PDF en modal
- [ ] Implementar CertificateService.generatePDF() - [ ] Implementar certificatesStore
- [ ] Implementar generación de QR code
- [ ] Event handler en course completion **Tests:**
- [ ] Rate limiting en verificación - [ ] Test generación de PDF
- [ ] Test verificación de certificado válido/inválido
**Frontend:** - [ ] Test E2E completar curso y obtener certificado
- [ ] Crear CertificatesPage.tsx
- [ ] Crear componente CertificateCard.tsx ---
- [ ] Crear CertificateDetailPage.tsx
- [ ] Crear VerifyCertificatePage.tsx (pública) **Creado por:** Requirements-Analyst
- [ ] Crear modal de celebración al obtener certificado **Fecha:** 2025-12-05
- [ ] Botones de compartir social media **Última actualización:** 2025-12-05
- [ ] 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

View File

@ -1,444 +1,431 @@
--- # RF-EDU-006: Sistema de Gamificación
id: "RF-EDU-006"
title: "Sistema de Gamificacion" **Versión:** 1.0.0
type: "Requirement" **Fecha:** 2025-12-05
status: "Done" **Épica:** OQI-002 - Módulo Educativo
priority: "Media" **Prioridad:** P2
module: "education" **Story Points:** 8
epic: "OQI-002"
version: "1.0" ---
created_date: "2025-12-05"
updated_date: "2026-01-04" ## 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.
# RF-EDU-006: Sistema de Gamificación
---
**Versión:** 1.0.0
**Fecha:** 2025-12-05 ## Requisitos Funcionales
**Épica:** OQI-002 - Módulo Educativo
**Prioridad:** P2 ### RF-EDU-006.1: Sistema de XP (Puntos de Experiencia)
**Story Points:** 8
El sistema debe otorgar XP por las siguientes acciones:
---
| Acción | XP | Frecuencia |
## Descripción |--------|----|-----------|
| Completar lección de video | 10 | Por lecció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. | 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 |
## Requisitos Funcionales | Aprobar quiz con 100% | 50 | Por quiz |
| Racha de 7 días consecutivos | 100 | Por milestone |
### RF-EDU-006.1: Sistema de XP (Puntos de Experiencia) | Racha de 30 días consecutivos | 500 | Por milestone |
| Tomar notas en lección | 5 | Máx 1 por lección |
El sistema debe otorgar XP por las siguientes acciones: | Descargar recursos | 2 | Máx 1 por lección |
| Compartir certificado | 25 | Por certificado |
| Acción | XP | Frecuencia | | Referir a un amigo que se registre | 100 | Por referido |
|--------|----|-----------| | Completar perfil 100% | 50 | Una vez |
| Completar lección de video | 10 | Por lección | | Primera lección del día | 5 | Diario (bonus) |
| Completar lección de artículo | 15 | Por lección |
| Completar módulo | 50 | Por módulo | **Bonificaciones:**
| Completar curso | 200 | Por curso | - **Fin de semana:** +50% XP sábados y domingos
| Aprobar quiz (primera vez) | 30 | Por quiz | - **Perfectionist:** +20% XP si completa curso con todos los quizzes al 100%
| Aprobar quiz con 100% | 50 | Por quiz | - **Speed learner:** +30% XP si completa curso en <50% del tiempo estimado
| Racha de 7 días consecutivos | 100 | Por milestone |
| Racha de 30 días consecutivos | 500 | Por milestone | ### RF-EDU-006.2: Sistema de Niveles
| Tomar notas en lección | 5 | Máx 1 por lección |
| Descargar recursos | 2 | Máx 1 por lección | Niveles del 1 al 100 con títulos temáticos:
| Compartir certificado | 25 | Por certificado |
| Referir a un amigo que se registre | 100 | Por referido | | Nivel | XP Acumulado | Título | Descripción |
| Completar perfil 100% | 50 | Una vez | |-------|--------------|--------|-------------|
| Primera lección del día | 5 | Diario (bonus) | | 1-10 | 0 - 5,500 | Novice Trader | Iniciando el viaje |
| 11-20 | 5,500 - 23,100 | Apprentice Analyst | Aprendiendo fundamentos |
**Bonificaciones:** | 21-30 | 23,100 - 50,700 | Skilled Trader | Dominando estrategias |
- **Fin de semana:** +50% XP sábados y domingos | 31-40 | 50,700 - 88,300 | Expert Strategist | Conocimiento avanzado |
- **Perfectionist:** +20% XP si completa curso con todos los quizzes al 100% | 41-50 | 88,300 - 136,500 | Master Investor | Elite del trading |
- **Speed learner:** +30% XP si completa curso en <50% del tiempo estimado | 51-60 | 136,500 - 196,100 | Quant Analyst | Análisis cuantitativo |
| 61-70 | 196,100 - 267,700 | Portfolio Manager | Gestión profesional |
### RF-EDU-006.2: Sistema de Niveles | 71-80 | 267,700 - 351,300 | Market Wizard | Dominio del mercado |
| 81-90 | 351,300 - 447,900 | Trading Legend | Leyenda del trading |
Niveles del 1 al 100 con títulos temáticos: | 91-100 | 447,900 - 556,500 | Quant Master | Maestría absoluta |
| Nivel | XP Acumulado | Título | Descripción | Recompensas por nivel:
|-------|--------------|--------|-------------| - **Nivel 5:** Desbloquea tema oscuro premium
| 1-10 | 0 - 5,500 | Novice Trader | Iniciando el viaje | - **Nivel 10:** Badge especial + Avatar frame
| 11-20 | 5,500 - 23,100 | Apprentice Analyst | Aprendiendo fundamentos | - **Nivel 15:** Acceso a cursos exclusivos
| 21-30 | 23,100 - 50,700 | Skilled Trader | Dominando estrategias | - **Nivel 20:** Descuento 10% en suscripción
| 31-40 | 50,700 - 88,300 | Expert Strategist | Conocimiento avanzado | - **Nivel 25:** Prioridad en soporte
| 41-50 | 88,300 - 136,500 | Master Investor | Elite del trading | - **Nivel 30:** Acceso a comunidad premium
| 51-60 | 136,500 - 196,100 | Quant Analyst | Análisis cuantitativo | - **Nivel 50:** Certificado de "Elite Trader"
| 61-70 | 196,100 - 267,700 | Portfolio Manager | Gestión profesional | - **Nivel 75:** Reunión 1-on-1 con instructor
| 71-80 | 267,700 - 351,300 | Market Wizard | Dominio del mercado | - **Nivel 100:** Trofeo físico + Lifetime discount 20%
| 81-90 | 351,300 - 447,900 | Trading Legend | Leyenda del trading |
| 91-100 | 447,900 - 556,500 | Quant Master | Maestría absoluta | ### RF-EDU-006.3: Sistema de Badges (Insignias)
Recompensas por nivel: Categorías de badges:
- **Nivel 5:** Desbloquea tema oscuro premium
- **Nivel 10:** Badge especial + Avatar frame **Logros de Curso:**
- **Nivel 15:** Acceso a cursos exclusivos - First Steps (completar primer curso)
- **Nivel 20:** Descuento 10% en suscripción - Knowledge Seeker (completar 5 cursos)
- **Nivel 25:** Prioridad en soporte - Learning Machine (completar 10 cursos)
- **Nivel 30:** Acceso a comunidad premium - Master Scholar (completar 25 cursos)
- **Nivel 50:** Certificado de "Elite Trader" - Completionist (completar todos los cursos de una categoría)
- **Nivel 75:** Reunión 1-on-1 con instructor
- **Nivel 100:** Trofeo físico + Lifetime discount 20% **Logros de Velocidad:**
- Fast Learner (completar curso en 1 día)
### RF-EDU-006.3: Sistema de Badges (Insignias) - Speed Demon (completar 3 cursos en 1 semana)
- Marathon Runner (completar curso de >10h)
Categorías de badges:
**Logros de Calidad:**
**Logros de Curso:** - Perfectionist (aprobar todos los quizzes al 100%)
- First Steps (completar primer curso) - Overachiever (superar 95% en todos los quizzes de un curso)
- Knowledge Seeker (completar 5 cursos) - Note Taker (tomar notas en 50 lecciones)
- Learning Machine (completar 10 cursos)
- Master Scholar (completar 25 cursos) **Logros de Racha:**
- Completionist (completar todos los cursos de una categoría) - Week Warrior (racha de 7 días)
- Month Master (racha de 30 días)
**Logros de Velocidad:** - Unstoppable (racha de 100 días)
- Fast Learner (completar curso en 1 día) - Year Legend (racha de 365 días)
- Speed Demon (completar 3 cursos en 1 semana)
- Marathon Runner (completar curso de >10h) **Logros Sociales:**
- Influencer (compartir 5 certificados)
**Logros de Calidad:** - Recruiter (referir 10 usuarios)
- Perfectionist (aprobar todos los quizzes al 100%) - Helper (responder 25 preguntas en foro)
- Overachiever (superar 95% en todos los quizzes de un curso)
- Note Taker (tomar notas en 50 lecciones) **Logros Especiales:**
- Early Bird (completar lección antes de las 6am)
**Logros de Racha:** - Night Owl (completar lección después de las 11pm)
- Week Warrior (racha de 7 días) - Weekend Warrior (completar 5 lecciones en fin de semana)
- Month Master (racha de 30 días) - Category Master (completar todos los cursos de una categoría)
- Unstoppable (racha de 100 días)
- Year Legend (racha de 365 días) Cada badge tiene:
- Nombre
**Logros Sociales:** - Descripción
- Influencer (compartir 5 certificados) - Icono (SVG/PNG)
- Recruiter (referir 10 usuarios) - Rareza: Común, Raro, Épico, Legendario
- Helper (responder 25 preguntas en foro) - Fecha de obtención
- Progreso hacia obtenerlo (si aplica)
**Logros Especiales:**
- Early Bird (completar lección antes de las 6am) ### RF-EDU-006.4: Leaderboard (Tabla de Clasificación)
- Night Owl (completar lección después de las 11pm)
- Weekend Warrior (completar 5 lecciones en fin de semana) El sistema debe proveer leaderboards:
- Category Master (completar todos los cursos de una categoría)
**Global:**
Cada badge tiene: - Top 100 usuarios por XP total
- Nombre - Actualización: Tiempo real
- Descripción
- Icono (SVG/PNG) **Por Período:**
- Rareza: Común, Raro, Épico, Legendario - Esta semana (lunes a domingo)
- Fecha de obtención - Este mes
- Progreso hacia obtenerlo (si aplica) - Este año
- Histórico
### RF-EDU-006.4: Leaderboard (Tabla de Clasificación)
**Por Categoría:**
El sistema debe proveer leaderboards: - Leaderboard por cada categoría de curso
- Top learners de Análisis Técnico, etc.
**Global:**
- Top 100 usuarios por XP total **Por Métrica:**
- Actualización: Tiempo real - Más cursos completados
- Más racha consecutiva
**Por Período:** - Más badges obtenidos
- Esta semana (lunes a domingo) - Más rápido en completar curso X
- Este mes
- Este año Información mostrada:
- Histórico - Posición (#1, #2, ...)
- Avatar del usuario
**Por Categoría:** - Nombre/username
- Leaderboard por cada categoría de curso - XP total o métrica relevante
- Top learners de Análisis Técnico, etc. - Badge de top 3 (oro, plata, bronce)
- Indicador de subida/bajada de posición
**Por Métrica:**
- Más cursos completados Privacidad:
- Más racha consecutiva - Usuario puede optar por salir del leaderboard
- Más badges obtenidos - Por default, solo muestra username, no nombre completo
- Más rápido en completar curso X - Top 10 siempre visible, resto opcional
Información mostrada: ### RF-EDU-006.5: Sistema de Logros (Achievements)
- Posición (#1, #2, ...)
- Avatar del usuario Logros son metas específicas que otorgan recompensas:
- Nombre/username
- XP total o métrica relevante ```typescript
- Badge de top 3 (oro, plata, bronce) interface Achievement {
- Indicador de subida/bajada de posición id: string;
name: string;
Privacidad: description: string;
- Usuario puede optar por salir del leaderboard icon: string;
- Por default, solo muestra username, no nombre completo category: 'course' | 'speed' | 'quality' | 'streak' | 'social';
- Top 10 siempre visible, resto opcional rarity: 'common' | 'rare' | 'epic' | 'legendary';
xpReward: number;
### RF-EDU-006.5: Sistema de Logros (Achievements) badgeReward?: string; // ID del badge que se otorga
Logros son metas específicas que otorgan recompensas: requirements: {
type: 'courses_completed' | 'quiz_score' | 'streak_days' | 'custom';
```typescript target: number;
interface Achievement { metadata?: any;
id: string; };
name: string;
description: string; progress?: {
icon: string; current: number;
category: 'course' | 'speed' | 'quality' | 'streak' | 'social'; target: number;
rarity: 'common' | 'rare' | 'epic' | 'legendary'; percentage: number;
xpReward: number; };
badgeReward?: string; // ID del badge que se otorga }
```
requirements: {
type: 'courses_completed' | 'quiz_score' | 'streak_days' | 'custom'; Ejemplos:
target: number; - **"Trading Fundamentals Master":** Completar todos los cursos de categoría Fundamentos (200 XP + badge)
metadata?: any; - **"Quiz Perfectionist":** Obtener 100% en 20 quizzes (150 XP + badge épico)
}; - **"Dedicated Learner":** Mantener racha de 30 días (500 XP + badge raro)
progress?: { ### RF-EDU-006.6: Recompensas y Premios
current: number;
target: number; El sistema debe permitir canjear XP o logros por:
percentage: number; - 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)
Ejemplos:
- **"Trading Fundamentals Master":** Completar todos los cursos de categoría Fundamentos (200 XP + badge) Tienda de recompensas:
- **"Quiz Perfectionist":** Obtener 100% en 20 quizzes (150 XP + badge épico) - Catálogo de items canjeables
- **"Dedicated Learner":** Mantener racha de 30 días (500 XP + badge raro) - Historial de canjes
- Balance de XP disponible
### RF-EDU-006.6: Recompensas y Premios
### RF-EDU-006.7: Notificaciones y Celebraciones
El sistema debe permitir canjear XP o logros por:
- Descuentos en suscripción premium (1000 XP = 5% descuento) El sistema debe mostrar animaciones/notificaciones para:
- Acceso early a nuevos cursos (500 XP) - Subir de nivel (modal con confeti)
- Merch de OrbiQuant (camisetas, stickers) (5000 XP) - Obtener nuevo badge (toast notification)
- Consulta 1-on-1 con instructor (10,000 XP) - Completar logro (modal con progreso)
- Features premium temporales (2,000 XP = 1 mes) - Entrar al top 100 del leaderboard (email)
- Alcanzar milestone de racha (confeti)
Tienda de recompensas: - Obtener XP ("+10 XP" flotante en pantalla)
- Catálogo de items canjeables
- Historial de canjes ### RF-EDU-006.8: Perfil Gamificado
- Balance de XP disponible
Página de perfil del usuario debe mostrar:
### RF-EDU-006.7: Notificaciones y Celebraciones - Avatar con marco según nivel
- Nivel actual y barra de progreso
El sistema debe mostrar animaciones/notificaciones para: - XP actual / XP para próximo nivel
- Subir de nivel (modal con confeti) - Total de badges obtenidos
- Obtener nuevo badge (toast notification) - Galería de badges (destacar raros/épicos)
- Completar logro (modal con progreso) - Logros recientes
- Entrar al top 100 del leaderboard (email) - Estadísticas: cursos, lecciones, quizzes, racha
- Alcanzar milestone de racha (confeti) - Posición en leaderboard global
- Obtener XP ("+10 XP" flotante en pantalla) - Gráfico de XP ganado por mes
### RF-EDU-006.8: Perfil Gamificado ---
Página de perfil del usuario debe mostrar: ## Datos de Salida
- Avatar con marco según nivel
- Nivel actual y barra de progreso ```typescript
- XP actual / XP para próximo nivel interface UserGamification {
- Total de badges obtenidos userId: string;
- Galería de badges (destacar raros/épicos) totalXP: number;
- Logros recientes currentLevel: number;
- Estadísticas: cursos, lecciones, quizzes, racha levelTitle: string;
- Posición en leaderboard global xpForCurrentLevel: number;
- Gráfico de XP ganado por mes xpForNextLevel: number;
progressToNextLevel: number; // 0-100
---
badges: {
## Datos de Salida id: string;
name: string;
```typescript description: string;
interface UserGamification { icon: string;
userId: string; rarity: string;
totalXP: number; earnedAt: string;
currentLevel: number; }[];
levelTitle: string;
xpForCurrentLevel: number; achievements: Achievement[];
xpForNextLevel: number;
progressToNextLevel: number; // 0-100 leaderboard: {
globalRank: number;
badges: { weeklyRank: number;
id: string; categoryRanks: {
name: string; category: string;
description: string; rank: number;
icon: string; }[];
rarity: string; };
earnedAt: string;
}[]; stats: {
coursesCompleted: number;
achievements: Achievement[]; quizzesPassed: number;
currentStreak: number;
leaderboard: { longestStreak: number;
globalRank: number; totalBadges: number;
weeklyRank: number; rareBadges: number;
categoryRanks: { epicBadges: number;
category: string; legendaryBadges: number;
rank: number; };
}[]; }
};
interface LeaderboardEntry {
stats: { rank: number;
coursesCompleted: number; userId: string;
quizzesPassed: number; username: string;
currentStreak: number; avatar: string;
longestStreak: number; totalXP: number;
totalBadges: number; level: number;
rareBadges: number; badge?: string; // Badge de top 3
epicBadges: number; rankChange: number; // +5, -2, 0
legendaryBadges: number; }
}; ```
}
---
interface LeaderboardEntry {
rank: number; ## Reglas de Negocio
userId: string;
username: string; 1. **XP no se puede perder:** Una vez ganado, permanece
avatar: string; 2. **Nivel no puede bajar:** Solo sube
totalXP: number; 3. **Badges permanentes:** No se pueden perder
level: number; 4. **Leaderboard semanal:** Reset cada lunes 00:00 UTC
badge?: string; // Badge de top 3 5. **Anti-cheat:** Validar todas las acciones en backend
rankChange: number; // +5, -2, 0 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
---
---
## Reglas de Negocio
## Criterios de Aceptación
1. **XP no se puede perder:** Una vez ganado, permanece
2. **Nivel no puede bajar:** Solo sube ```gherkin
3. **Badges permanentes:** No se pueden perder Escenario: Usuario gana XP al completar lección
4. **Leaderboard semanal:** Reset cada lunes 00:00 UTC DADO que el usuario completa una lección de video
5. **Anti-cheat:** Validar todas las acciones en backend CUANDO se marca como completada
6. **Rate limiting:** Máximo 1000 XP por hora para prevenir exploits ENTONCES se otorgan 10 XP
7. **Recompensas únicas:** Algunos logros se pueden ganar solo una vez Y se muestra animación "+10 XP"
8. **Canje de recompensas:** Consume XP del balance, pero no baja nivel Y se actualiza barra de progreso de nivel
9. **Privacidad:** Usuario puede ocultar su perfil gamificado Y se guarda en historial de XP
--- Escenario: Usuario sube de nivel
DADO que el usuario tiene 990 XP (nivel 9)
## Criterios de Aceptación Y necesita 1000 XP para nivel 10
CUANDO completa un curso y gana 200 XP
```gherkin ENTONCES sube a nivel 10
Escenario: Usuario gana XP al completar lección Y se muestra modal "¡Subiste de nivel!"
DADO que el usuario completa una lección de video Y se desbloquea badge de nivel 10
CUANDO se marca como completada Y se envía notificación por email
ENTONCES se otorgan 10 XP
Y se muestra animación "+10 XP" Escenario: Usuario obtiene badge
Y se actualiza barra de progreso de nivel DADO que el usuario completó 4 cursos
Y se guarda en historial de XP CUANDO completa el 5to curso
ENTONCES se otorga badge "Knowledge Seeker"
Escenario: Usuario sube de nivel Y se muestra toast notification
DADO que el usuario tiene 990 XP (nivel 9) Y el badge aparece en galería de perfil
Y necesita 1000 XP para nivel 10 Y se suman 50 XP adicionales
CUANDO completa un curso y gana 200 XP
ENTONCES sube a nivel 10 Escenario: Usuario ve leaderboard
Y se muestra modal "¡Subiste de nivel!" DADO que el usuario está en posición #42
Y se desbloquea badge de nivel 10 CUANDO accede a /education/leaderboard
Y se envía notificación por email ENTONCES se muestra tabla con top 100
Y su posición está destacada
Escenario: Usuario obtiene badge Y se muestra su XP y nivel
DADO que el usuario completó 4 cursos Y puede filtrar por: Semanal, Mensual, Histórico
CUANDO completa el 5to curso
ENTONCES se otorga badge "Knowledge Seeker" Escenario: Usuario canjea recompensa
Y se muestra toast notification DADO que el usuario tiene 5000 XP disponibles
Y el badge aparece en galería de perfil CUANDO canjea "Merch OrbiQuant" (5000 XP)
Y se suman 50 XP adicionales ENTONCES se deduce 5000 XP de balance
Y se registra el canje
Escenario: Usuario ve leaderboard Y se envía email de confirmación
DADO que el usuario está en posición #42 Y nivel NO baja (XP acumulado permanece)
CUANDO accede a /education/leaderboard
ENTONCES se muestra tabla con top 100 Escenario: Progreso hacia logro
Y su posición está destacada DADO que el usuario completó 7 de 10 cursos para logro
Y se muestra su XP y nivel CUANDO ve página de logros
Y puede filtrar por: Semanal, Mensual, Histórico ENTONCES se muestra "7/10 cursos"
Y barra de progreso al 70%
Escenario: Usuario canjea recompensa Y descripción de lo que falta
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 ## Dependencias
Y nivel NO baja (XP acumulado permanece)
- PostgreSQL para gamification data
Escenario: Progreso hacia logro - Redis para caché de leaderboards
DADO que el usuario completó 7 de 10 cursos para logro - Event system para otorgar XP en tiempo real
CUANDO ve página de logros - Notification service para celebraciones
ENTONCES se muestra "7/10 cursos" - Analytics para tracking de engagement
Y barra de progreso al 70%
Y descripción de lo que falta ---
```
## Notas Técnicas
---
- Calcular leaderboard en background job cada 5 minutos
## Dependencias - Usar Redis Sorted Sets para leaderboards rápidos
- Implementar event handlers para cada acción que otorga XP
- PostgreSQL para gamification data - Crear índices en tablas de XP y badges para queries rápidas
- Redis para caché de leaderboards - Considerar rate limiting para prevenir farming de XP
- Event system para otorgar XP en tiempo real - Implementar audit log de XP ganado/gastado
- Notification service para celebraciones - Usar WebSockets para notificaciones en tiempo real
- Analytics para tracking de engagement
---
---
## Referencias
## Notas Técnicas
- Schema: `/backend/src/database/schemas/gamification.sql`
- Calcular leaderboard en background job cada 5 minutos - API: `/backend/src/modules/gamification/`
- Usar Redis Sorted Sets para leaderboards rápidos - Frontend: `/frontend/src/pages/Leaderboard.tsx`
- 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 ## Tareas Técnicas
- Usar WebSockets para notificaciones en tiempo real
**Database:**
--- - [ ] Tabla gamification.user_xp: user_id, total_xp, level
- [ ] Tabla gamification.badges: definición de badges
## Referencias - [ ] Tabla gamification.user_badges: user_id, badge_id, earned_at
- [ ] Tabla gamification.achievements: definición de logros
- Schema: `/backend/src/database/schemas/gamification.sql` - [ ] Tabla gamification.user_achievements: progreso de usuario
- API: `/backend/src/modules/gamification/` - [ ] Tabla gamification.xp_transactions: log de XP ganado/gastado
- Frontend: `/frontend/src/pages/Leaderboard.tsx` - [ ] Tabla gamification.leaderboard: caché de rankings
- [ ] Índices para queries de leaderboard
---
**Backend:**
## Tareas Técnicas - [ ] Endpoint GET /gamification/profile (stats del usuario)
- [ ] Endpoint GET /gamification/leaderboard
**Database:** - [ ] Endpoint GET /gamification/badges
- [ ] Tabla gamification.user_xp: user_id, total_xp, level - [ ] Endpoint GET /gamification/achievements
- [ ] Tabla gamification.badges: definición de badges - [ ] Endpoint POST /gamification/rewards/redeem
- [ ] Tabla gamification.user_badges: user_id, badge_id, earned_at - [ ] Implementar GamificationService.awardXP()
- [ ] Tabla gamification.achievements: definición de logros - [ ] Implementar GamificationService.checkLevelUp()
- [ ] Tabla gamification.user_achievements: progreso de usuario - [ ] Implementar GamificationService.checkAchievements()
- [ ] Tabla gamification.xp_transactions: log de XP ganado/gastado - [ ] Event handlers para todas las acciones que otorgan XP
- [ ] Tabla gamification.leaderboard: caché de rankings - [ ] Cron job para calcular leaderboards
- [ ] Índices para queries de leaderboard
**Frontend:**
**Backend:** - [ ] Crear LeaderboardPage.tsx
- [ ] Endpoint GET /gamification/profile (stats del usuario) - [ ] Crear BadgesGalleryPage.tsx
- [ ] Endpoint GET /gamification/leaderboard - [ ] Crear AchievementsPage.tsx
- [ ] Endpoint GET /gamification/badges - [ ] Crear RewardsStorePage.tsx
- [ ] Endpoint GET /gamification/achievements - [ ] Crear componente XPAnimation.tsx
- [ ] Endpoint POST /gamification/rewards/redeem - [ ] Crear componente LevelUpModal.tsx
- [ ] Implementar GamificationService.awardXP() - [ ] Crear componente BadgeToast.tsx
- [ ] Implementar GamificationService.checkLevelUp() - [ ] Crear componente ProgressBar.tsx para nivel
- [ ] Implementar GamificationService.checkAchievements() - [ ] Integrar gamificación en perfil de usuario
- [ ] Event handlers para todas las acciones que otorgan XP - [ ] Implementar gamificationStore
- [ ] Cron job para calcular leaderboards
**Tests:**
**Frontend:** - [ ] Test cálculo de nivel según XP
- [ ] Crear LeaderboardPage.tsx - [ ] Test otorgamiento de badges automático
- [ ] Crear BadgesGalleryPage.tsx - [ ] Test ranking en leaderboard
- [ ] Crear AchievementsPage.tsx - [ ] Test canje de recompensas
- [ ] Crear RewardsStorePage.tsx
- [ ] Crear componente XPAnimation.tsx ---
- [ ] Crear componente LevelUpModal.tsx
- [ ] Crear componente BadgeToast.tsx **Creado por:** Requirements-Analyst
- [ ] Crear componente ProgressBar.tsx para nivel **Fecha:** 2025-12-05
- [ ] Integrar gamificación en perfil de usuario **Última actualización:** 2025-12-05
- [ ] 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

View File

@ -1,200 +1,192 @@
--- # _MAP: OQI-003 - Trading y Charts
id: "MAP-OQI-003-trading-charts"
title: "Mapa de OQI-003-trading-charts" **Última actualización:** 2025-12-05
type: "Index" **Estado:** Pendiente
project: "trading-platform" **Versión:** 1.0.0
updated_date: "2026-01-04"
--- ---
# _MAP: OQI-003 - Trading y Charts ## Propósito
**Última actualización:** 2025-12-05 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.
**Estado:** Pendiente
**Versión:** 1.0.0 ---
--- ## Contenido del Directorio
## Propósito ```
OQI-003-trading-charts/
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. ├── 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
## Contenido del Directorio │ ├── 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
OQI-003-trading-charts/ │ ├── RF-TRD-005-ordenes.md # Sistema de órdenes
├── README.md # Resumen ejecutivo de la épica │ ├── RF-TRD-006-posiciones.md # Gestión de posiciones
├── _MAP.md # Este archivo - índice │ ├── RF-TRD-007-historial.md # Historial y trades
├── requerimientos/ # Documentos de requerimientos funcionales │ └── RF-TRD-008-metricas.md # Métricas y estadísticas
│ ├── RF-TRD-001-charts.md # Charts y visualización ├── especificaciones/ # Especificaciones técnicas
│ ├── RF-TRD-002-indicadores.md # Indicadores técnicos │ ├── ET-TRD-001-market-data.md # Obtención de datos de mercado
│ ├── RF-TRD-003-watchlists.md # Gestión de watchlists │ ├── ET-TRD-002-websocket.md # Conexiones WebSocket
│ ├── RF-TRD-004-paper-trading.md # Paper trading │ ├── ET-TRD-003-database.md # Modelo de datos
│ ├── RF-TRD-005-ordenes.md # Sistema de órdenes │ ├── ET-TRD-004-api.md # Endpoints REST
│ ├── RF-TRD-006-posiciones.md # Gestión de posiciones │ ├── ET-TRD-005-frontend.md # Componentes React
│ ├── RF-TRD-007-historial.md # Historial y trades │ ├── ET-TRD-006-indicadores.md # Cálculo de indicadores
│ └── RF-TRD-008-metricas.md # Métricas y estadísticas │ ├── ET-TRD-007-paper-engine.md # Motor de paper trading
├── especificaciones/ # Especificaciones técnicas │ └── ET-TRD-008-performance.md # Optimizaciones
│ ├── ET-TRD-001-market-data.md # Obtención de datos de mercado ├── historias-usuario/ # User Stories
│ ├── ET-TRD-002-websocket.md # Conexiones WebSocket │ ├── US-TRD-001-ver-chart.md
│ ├── ET-TRD-003-database.md # Modelo de datos │ ├── US-TRD-002-cambiar-timeframe.md
│ ├── ET-TRD-004-api.md # Endpoints REST │ ├── US-TRD-003-agregar-indicador.md
│ ├── ET-TRD-005-frontend.md # Componentes React │ ├── US-TRD-004-crear-watchlist.md
│ ├── ET-TRD-006-indicadores.md # Cálculo de indicadores │ ├── US-TRD-005-agregar-simbolo.md
│ ├── ET-TRD-007-paper-engine.md # Motor de paper trading │ ├── US-TRD-006-crear-orden-market.md
│ └── ET-TRD-008-performance.md # Optimizaciones │ ├── US-TRD-007-crear-orden-limit.md
├── historias-usuario/ # User Stories │ ├── US-TRD-008-cerrar-posicion.md
│ ├── US-TRD-001-ver-chart.md │ ├── US-TRD-009-ver-posiciones.md
│ ├── US-TRD-002-cambiar-timeframe.md │ ├── US-TRD-010-ver-historial.md
│ ├── US-TRD-003-agregar-indicador.md │ ├── US-TRD-011-ver-estadisticas.md
│ ├── US-TRD-004-crear-watchlist.md │ ├── US-TRD-012-configurar-tp-sl.md
│ ├── US-TRD-005-agregar-simbolo.md │ ├── US-TRD-013-alertas-precio.md
│ ├── US-TRD-006-crear-orden-market.md │ ├── US-TRD-014-reset-balance.md
│ ├── US-TRD-007-crear-orden-limit.md │ ├── US-TRD-015-exportar-trades.md
│ ├── US-TRD-008-cerrar-posicion.md │ ├── US-TRD-016-modo-oscuro-chart.md
│ ├── US-TRD-009-ver-posiciones.md │ ├── US-TRD-017-zoom-pan-chart.md
│ ├── US-TRD-010-ver-historial.md │ └── US-TRD-018-comparar-simbolos.md
│ ├── US-TRD-011-ver-estadisticas.md └── implementacion/ # Trazabilidad de implementación
│ ├── US-TRD-012-configurar-tp-sl.md └── TRACEABILITY.yml
│ ├── 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 ## Requerimientos Funcionales
│ └── US-TRD-018-comparar-simbolos.md
└── implementacion/ # Trazabilidad de implementación | ID | Nombre | Prioridad | SP | Estado |
└── TRACEABILITY.yml |----|--------|-----------|-----|--------|
``` | 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 |
## Requerimientos Funcionales | RF-TRD-005 | Sistema de Órdenes | P0 | 8 | Pendiente |
| RF-TRD-006 | Gestión de Posiciones | P0 | 5 | Pendiente |
| ID | Nombre | Prioridad | SP | Estado | | RF-TRD-007 | Historial y Trades | P1 | 5 | Pendiente |
|----|--------|-----------|-----|--------| | RF-TRD-008 | Métricas y Estadísticas | P2 | 6 | Pendiente |
| RF-TRD-001 | Charts y Visualización | P0 | 8 | Pendiente |
| RF-TRD-002 | Indicadores Técnicos | P1 | 5 | Pendiente | **Total:** 55 SP
| 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 | ## Especificaciones Técnicas
| RF-TRD-007 | Historial y Trades | P1 | 5 | Pendiente |
| RF-TRD-008 | Métricas y Estadísticas | P2 | 6 | Pendiente | | ID | Nombre | Componente | Estado |
|----|--------|------------|--------|
**Total:** 55 SP | 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 |
## Especificaciones Técnicas | ET-TRD-005 | Frontend | Frontend | Pendiente |
| ET-TRD-006 | Indicadores | Backend/ML | Pendiente |
| ID | Nombre | Componente | Estado | | ET-TRD-007 | Paper Engine | Backend | Pendiente |
|----|--------|------------|--------| | ET-TRD-008 | Performance | All | Pendiente |
| 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 | ## Historias de Usuario
| ET-TRD-005 | Frontend | Frontend | Pendiente |
| ET-TRD-006 | Indicadores | Backend/ML | Pendiente | | ID | Historia | Prioridad | SP | Estado |
| ET-TRD-007 | Paper Engine | Backend | Pendiente | |----|----------|-----------|-----|--------|
| ET-TRD-008 | Performance | All | Pendiente | | 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 |
## Historias de Usuario | US-TRD-005 | Agregar símbolo a watchlist | P1 | 2 | Pendiente |
| US-TRD-006 | Crear orden market | P0 | 5 | Pendiente |
| ID | Historia | Prioridad | SP | Estado | | US-TRD-007 | Crear orden limit | P1 | 3 | Pendiente |
|----|----------|-----------|-----|--------| | US-TRD-008 | Cerrar posición | P0 | 3 | Pendiente |
| US-TRD-001 | Ver chart de un símbolo | P0 | 5 | Pendiente | | US-TRD-009 | Ver posiciones abiertas | P0 | 3 | Pendiente |
| US-TRD-002 | Cambiar timeframe | P0 | 2 | Pendiente | | US-TRD-010 | Ver historial de trades | P1 | 3 | Pendiente |
| US-TRD-003 | Agregar indicador al chart | P1 | 3 | Pendiente | | US-TRD-011 | Ver estadísticas de rendimiento | P1 | 3 | Pendiente |
| US-TRD-004 | Crear watchlist | P1 | 2 | Pendiente | | US-TRD-012 | Configurar TP/SL | P1 | 3 | Pendiente |
| US-TRD-005 | Agregar símbolo a watchlist | P1 | 2 | Pendiente | | US-TRD-013 | Configurar alertas de precio | P2 | 3 | Pendiente |
| US-TRD-006 | Crear orden market | P0 | 5 | Pendiente | | US-TRD-014 | Resetear balance paper | P2 | 1 | Pendiente |
| US-TRD-007 | Crear orden limit | P1 | 3 | Pendiente | | US-TRD-015 | Exportar trades a CSV | P2 | 2 | Pendiente |
| US-TRD-008 | Cerrar posición | P0 | 3 | Pendiente | | US-TRD-016 | Modo oscuro en chart | P2 | 2 | Pendiente |
| US-TRD-009 | Ver posiciones abiertas | P0 | 3 | Pendiente | | US-TRD-017 | Zoom y pan en chart | P1 | 3 | Pendiente |
| US-TRD-010 | Ver historial de trades | P1 | 3 | Pendiente | | US-TRD-018 | Comparar múltiples símbolos | P2 | 5 | Pendiente |
| US-TRD-011 | Ver estadísticas de rendimiento | P1 | 3 | Pendiente |
| US-TRD-012 | Configurar TP/SL | P1 | 3 | Pendiente | **Total:** 55 SP
| 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 | ## Dependencias
| US-TRD-017 | Zoom y pan en chart | P1 | 3 | Pendiente |
| US-TRD-018 | Comparar múltiples símbolos | P2 | 5 | Pendiente | ### Depende de:
**Total:** 55 SP - **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado
--- ### Bloquea:
## Dependencias - **OQI-006:** ML Signals (integración con charts)
### Depende de: ---
- **OQI-001:** Autenticación (usuarios, JWT) - ✅ Completado ## Stack Técnico
### Bloquea: | Capa | Tecnología | Uso |
|------|------------|-----|
- **OQI-006:** ML Signals (integración con charts) | Frontend | Lightweight Charts | Renderizado de velas |
| Frontend | React + Zustand | Estado y componentes |
--- | Backend | Express.js | API REST |
| Backend | ws | WebSocket server |
## Stack Técnico | Database | PostgreSQL | Persistencia |
| External | Binance API | Datos de mercado |
| Capa | Tecnología | Uso |
|------|------------|-----| ---
| Frontend | Lightweight Charts | Renderizado de velas |
| Frontend | React + Zustand | Estado y componentes | ## Criterios de Aceptación
| Backend | Express.js | API REST |
| Backend | ws | WebSocket server | ### Funcionales
| Database | PostgreSQL | Persistencia |
| External | Binance API | Datos de mercado | - [ ] Charts renderizan correctamente con datos de Binance
- [ ] 7 timeframes disponibles y funcionales
--- - [ ] Mínimo 5 indicadores técnicos implementados
- [ ] Watchlists CRUD completo
## Criterios de Aceptación - [ ] Paper trading ejecuta órdenes market y limit
- [ ] Posiciones se actualizan en tiempo real
### Funcionales - [ ] Historial muestra todos los trades cerrados
- [ ] Métricas calculan win rate y P&L correctamente
- [ ] Charts renderizan correctamente con datos de Binance
- [ ] 7 timeframes disponibles y funcionales ### No Funcionales
- [ ] Mínimo 5 indicadores técnicos implementados
- [ ] Watchlists CRUD completo - [ ] Chart carga en < 2 segundos
- [ ] Paper trading ejecuta órdenes market y limit - [ ] Updates en tiempo real < 500ms latencia
- [ ] Posiciones se actualizan en tiempo real - [ ] Soporta 1000+ velas sin lag
- [ ] Historial muestra todos los trades cerrados - [ ] Mobile responsive
- [ ] Métricas calculan win rate y P&L correctamente
### Técnicos
### No Funcionales
- [ ] Cobertura de tests > 70%
- [ ] Chart carga en < 2 segundos - [ ] Documentación API completa
- [ ] Updates en tiempo real < 500ms latencia - [ ] Sin memory leaks en WebSocket
- [ ] Soporta 1000+ velas sin lag
- [ ] Mobile responsive ---
### Técnicos ## Hitos
- [ ] Cobertura de tests > 70% | Hito | Entregables | Target |
- [ ] Documentación API completa |------|-------------|--------|
- [ ] Sin memory leaks en WebSocket | M1 | Charts básicos + timeframes | Sprint 3 |
| M2 | Indicadores + watchlists | Sprint 3 |
--- | M3 | Paper trading completo | Sprint 4 |
| M4 | Métricas + polish | Sprint 4 |
## Hitos
---
| Hito | Entregables | Target |
|------|-------------|--------| ## Referencias
| M1 | Charts básicos + timeframes | Sprint 3 |
| M2 | Indicadores + watchlists | Sprint 3 | - [README Principal](./README.md)
| M3 | Paper trading completo | Sprint 4 | - [Vision del Producto](../../00-vision-general/VISION-PRODUCTO.md)
| M4 | Métricas + polish | Sprint 4 | - [Arquitectura](../../00-vision-general/ARQUITECTURA-GENERAL.md)
- [_MAP Fase MVP](../_MAP.md)
---
## 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)

Some files were not shown because too many files have changed in this diff Show More