Compare commits

...

2 Commits

Author SHA1 Message Date
a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00
9422e3d3a9 docs(ml): Update PLAN-IMPLEMENTACION-FASES with execution results
- Document all 7 phases completed
- Add commits and test results
- 38/38 tests passing
- Branch: feature/ml-integration-v2

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 23:08:43 -06:00
355 changed files with 127676 additions and 85524 deletions

339
AGENTS.md Normal file
View File

@ -0,0 +1,339 @@
# Guia para Agentes de IA - Trading Platform (OrbiQuant)
**Version:** 1.0
**Ultima actualizacion:** 2026-01-04
**Basado en:** Estandar-SCRUM.md (Principio SIMCO)
---
## 1. Estructura del Proyecto
### Ubicaciones Clave
| Tipo | Ubicacion |
|------|-----------|
| Documentacion general | `/docs/` |
| Planificacion | `/docs/planning/` |
| Definicion de Modulos | `/docs/02-definicion-modulos/` |
| Backlog | `/docs/04-fase-backlog/` |
| Transversal | `/docs/90-transversal/` |
| Guias de Desarrollo | `/docs/95-guias-desarrollo/` |
| Quick Reference | `/docs/96-quick-reference/` |
| ADRs | `/docs/97-adr/` |
| Tareas | `/docs/planning/tasks/` |
| Bugs | `/docs/planning/bugs/` |
| Tablero Kanban | `/docs/planning/Board.md` |
| Orquestacion | `/orchestration/` |
### Estructura de un Modulo (Epica)
```
docs/02-definicion-modulos/OQI-XXX-{nombre}/
├── _MAP.md # Indice del modulo
├── README.md # Descripcion del modulo
├── historias-usuario/ # User Stories (US-*.md)
├── requerimientos/ # Requerimientos Funcionales (RF-*.md)
├── especificaciones/ # Especificaciones Tecnicas (ET-*.md)
└── implementacion/ # Trazabilidad de implementacion
```
---
## 2. Prefijos de Nomenclatura
| Prefijo | Tipo | Ejemplo | Descripcion |
|---------|------|---------|-------------|
| OQI- | Epica/Modulo | OQI-001-fundamentos-auth | Modulos principales |
| US- | Historia de Usuario | US-AUTH-001 | User Stories |
| TASK- | Tarea | TASK-001 | Tareas ejecutables |
| BUG- | Bug | BUG-001 | Defectos/errores |
| RF- | Requerimiento Funcional | RF-AUTH-001 | Requerimientos |
| ET- | Especificacion Tecnica | ET-AUTH-001 | Especificaciones |
| ADR- | Decision Record | ADR-001 | Decisiones arquitectonicas |
### Categorias de User Stories
| Sufijo | Modulo | Epica |
|--------|--------|-------|
| AUTH | Autenticacion | OQI-001 |
| EDU | Educacion | OQI-002 |
| TRD | Trading Charts | OQI-003 |
| INV | Investment Accounts | OQI-004 |
| PAY | Payments/Stripe | OQI-005 |
| ML | ML Signals | OQI-006 |
| LLM | LLM Agent | OQI-007 |
| PFM | Portfolio Manager | OQI-008 |
---
## 3. Como Trabajar con Tareas
### Tomar una Tarea
1. **Identificar tarea** en `/docs/planning/Board.md` (columna "Por Hacer")
2. **Leer archivo** `TASK-XXX.md` correspondiente
3. **Editar YAML front-matter**:
```yaml
status: "In Progress"
assignee: "@NombreAgente"
started_date: "YYYY-MM-DD"
```
4. **Mover tarea** a columna "En Progreso" en Board.md
5. **Commit**: `git commit -m "Start TASK-XXX: [descripcion breve]"`
### Completar una Tarea
1. **Verificar** TODOS los criterios de aceptacion cumplidos
2. **Editar YAML front-matter**:
```yaml
status: "Done"
completed_date: "YYYY-MM-DD"
actual_hours: X
```
3. **Agregar seccion** "## Notas de Implementacion" con detalles
4. **Mover tarea** a columna "Hecho" en Board.md
5. **Commit**: `git commit -m "Fixes TASK-XXX: [descripcion breve]"`
### Reportar Bloqueo
1. Cambiar `status: "Blocked"`
2. Agregar seccion "## Bloqueo" con:
- Descripcion del bloqueo
- Dependencias faltantes
- Accion requerida
3. Notificar en Board.md
---
## 4. Como Trabajar con Bugs
### Reportar un Bug
1. **Crear archivo** `/docs/planning/bugs/BUG-XXX-descripcion.md`
2. **Usar plantilla YAML**:
```yaml
---
id: "BUG-XXX"
title: "Descripcion del bug"
type: "Bug"
status: "Open"
severity: "P0|P1|P2|P3"
priority: "Critica|Alta|Media|Baja"
assignee: ""
affected_module: "Backend|Frontend|Database"
steps_to_reproduce:
- "Paso 1"
- "Paso 2"
expected_behavior: "Lo que deberia pasar"
actual_behavior: "Lo que pasa realmente"
created_date: "YYYY-MM-DD"
---
```
3. **Incluir secciones**: Descripcion, Contexto, Impacto
4. **Commit**: `git commit -m "Report BUG-XXX: [descripcion]"`
### Resolver un Bug
1. Editar YAML: `status: "Done"`, agregar `resolved_date`
2. Documentar solucion en seccion "## Solucion Implementada"
3. Agregar referencia al commit: `fix_commit: "abc123"`
4. **Commit**: `git commit -m "Fix BUG-XXX: [descripcion]"`
---
## 5. Formato YAML Front-Matter
### Historia de Usuario (US)
```yaml
---
id: "US-AUTH-001"
title: "Registro con Email"
type: "User Story"
status: "Done"
priority: "Alta"
assignee: "@Backend-Agent"
epic: "OQI-001"
story_points: 5
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
```
### Tarea (TASK)
```yaml
---
id: "TASK-001"
title: "Implementar endpoint POST /auth/register"
type: "Task"
status: "Done"
priority: "P1"
assignee: "@Backend-Agent"
parent_us: "US-AUTH-001"
epic: "OQI-001"
estimated_hours: 4
actual_hours: 4.5
created_date: "2025-12-05"
completed_date: "2025-12-05"
---
```
### Requerimiento Funcional (RF)
```yaml
---
id: "RF-AUTH-001"
title: "OAuth Multi-proveedor"
type: "Requirement"
status: "Done"
priority: "Alta"
module: "auth"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
```
### Especificacion Tecnica (ET)
```yaml
---
id: "ET-AUTH-001"
title: "OAuth Providers Implementation"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-001"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
```
---
## 6. Convenciones de Commit
```
<tipo>(<scope>): <descripcion>
Tipos:
- feat: Nueva funcionalidad
- fix: Correccion de bug
- docs: Documentacion
- refactor: Refactoring
- test: Tests
- chore: Mantenimiento
Scopes comunes:
- auth, education, trading, investment, payments, ml, llm, portfolio
- database, backend, frontend (capas)
- US-XXX, TASK-XXX, BUG-XXX (referencias)
Ejemplos:
- feat(auth): Implement OAuth 2.0 with Google
- fix(BUG-001): Resolve login redirect issue
- docs(US-AUTH-001): Add acceptance criteria
- Start TASK-XXX: Begin implementation
- Fixes TASK-XXX: Complete implementation
```
---
## 7. Estados Validos
### Para Tareas y User Stories
| Estado | Descripcion |
|--------|-------------|
| Backlog | En cola, no planificado |
| To Do | Planificado para sprint actual |
| In Progress | En desarrollo activo |
| Blocked | Bloqueado por dependencia |
| In Review | En revision/testing |
| Done | Completado y validado |
### Para Bugs
| Estado | Descripcion |
|--------|-------------|
| Open | Reportado, pendiente |
| In Progress | En investigacion/correccion |
| Fixed | Corregido, pendiente validacion |
| Done | Corregido y validado |
| Won't Fix | No se corregira (documentar razon) |
---
## 8. Archivos Importantes
| Archivo | Proposito |
|---------|-----------|
| `/docs/planning/Board.md` | Tablero Kanban actual |
| `/docs/planning/config.yml` | Configuracion del proyecto |
| `/docs/04-fase-backlog/DEFINITION-OF-READY.md` | Criterios para Ready |
| `/docs/04-fase-backlog/DEFINITION-OF-DONE.md` | Criterios para Done |
| `/docs/_MAP.md` | Mapa de navegacion principal |
| `/docs/02-definicion-modulos/_MAP.md` | Indice de modulos |
---
## 9. Validaciones Antes de Commit
- [ ] YAML front-matter valido (sin errores de sintaxis)
- [ ] Campo `id` presente y unico
- [ ] Campo `status` actualizado correctamente
- [ ] Board.md actualizado si cambio estado
- [ ] Referencias cruzadas verificadas
- [ ] Criterios de aceptacion actualizados (si aplica)
- [ ] _MAP.md actualizado si se agrego/elimino archivo
---
## 10. Flujo de Trabajo Recomendado
```
1. Consultar Board.md para ver tareas disponibles
2. Seleccionar tarea de "Por Hacer"
3. Leer archivo TASK-XXX.md completo
4. Verificar dependencias resueltas
5. Cambiar status a "In Progress"
6. Ejecutar trabajo
7. Documentar notas de implementacion
8. Verificar criterios de aceptacion
9. Cambiar status a "Done"
10. Actualizar Board.md
11. Commit con mensaje apropiado
```
---
## 11. Modulos del Proyecto
| Epica | Nombre | Descripcion | Estado |
|-------|--------|-------------|--------|
| OQI-001 | Fundamentos Auth | Autenticacion multi-proveedor | Implementado |
| OQI-002 | Education | Cursos y lecciones de trading | Implementado |
| OQI-003 | Trading Charts | Charts con indicadores tecnicos | Implementado |
| OQI-004 | Investment Accounts | Cuentas de inversion | Implementado |
| OQI-005 | Payments Stripe | Pagos y suscripciones | Implementado |
| OQI-006 | ML Signals | Senales con Machine Learning | Implementado |
| OQI-007 | LLM Agent | Agente conversacional | Implementado |
| OQI-008 | Portfolio Manager | Gestion de portafolio | Implementado |
---
## 12. Contacto y Soporte
Para dudas sobre el proceso:
- Revisar `/docs/README.md` para vision general
- Consultar `/orchestration/directivas/` para directivas
- Ver ejemplos en modulos completados (OQI-001 a OQI-008)
---
**Creado:** 2026-01-04
**Mantenido por:** Architecture Team
**Version:** 1.0

View File

@ -0,0 +1,52 @@
# MCP Binance Connector Configuration
# Copy this file to .env and configure values
# ==========================================
# Server Configuration
# ==========================================
PORT=3606
NODE_ENV=development
# ==========================================
# MCP Authentication
# ==========================================
MCP_API_KEY=your_mcp_api_key_here
# ==========================================
# Binance API Configuration
# ==========================================
BINANCE_API_KEY=your_binance_api_key
BINANCE_API_SECRET=your_binance_api_secret
# ==========================================
# Network Configuration
# ==========================================
# Use testnet by default (set to false for production)
BINANCE_TESTNET=true
BINANCE_FUTURES_TESTNET=true
# ==========================================
# Risk Limits
# ==========================================
# Maximum value for a single order in USDT
MAX_ORDER_VALUE_USDT=1000
# Maximum daily trading volume in USDT
MAX_DAILY_VOLUME_USDT=10000
# Maximum allowed leverage
MAX_LEVERAGE=20
# Maximum position size as percentage of equity
MAX_POSITION_SIZE_PCT=5
# ==========================================
# Request Configuration
# ==========================================
# Timeout for requests to Binance (ms)
REQUEST_TIMEOUT=10000
# Maximum retries for failed requests
MAX_RETRIES=3
# ==========================================
# Logging
# ==========================================
LOG_LEVEL=info
LOG_FILE=logs/mcp-binance.log

31
apps/mcp-binance-connector/.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Dependencies
node_modules/
# Build output
dist/
# Environment files
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Test coverage
coverage/
# Misc
*.tsbuildinfo

View File

@ -0,0 +1,57 @@
# MCP Binance Connector Dockerfile
# OrbiQuant Trading Platform
# Version: 1.0.0
# ==========================================
# Build Stage
# ==========================================
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# ==========================================
# Production Stage
# ==========================================
FROM node:20-alpine
WORKDIR /app
# Install production dependencies only
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy built application
COPY --from=builder /app/dist ./dist
# Create non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S mcpuser -u 1001 -G nodejs
# Create logs directory
RUN mkdir -p logs && chown -R mcpuser:nodejs logs
# Switch to non-root user
USER mcpuser
# Environment configuration
ENV NODE_ENV=production
ENV PORT=3606
# Expose port
EXPOSE 3606
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3606/health || exit 1
# Start application
CMD ["node", "dist/index.js"]

View File

@ -0,0 +1,345 @@
# MCP Binance Connector
**Version:** 1.0.0
**Date:** 2026-01-04
**System:** OrbiQuant Trading Platform + NEXUS v3.4 + SIMCO
---
## Description
MCP Server that exposes Binance cryptocurrency exchange capabilities as tools for AI agents. This service enables AI agents to:
- Query market data (prices, order books, candles)
- Monitor account balances
- View and manage open orders
- Execute trades (buy/sell with market, limit, stop orders)
Uses [CCXT](https://github.com/ccxt/ccxt) library for Binance API integration.
---
## Installation
```bash
# Navigate to the project
cd /home/isem/workspace-v1/projects/trading-platform/apps/mcp-binance-connector
# Install dependencies
npm install
# Configure environment
cp .env.example .env
# Edit .env with your Binance API credentials
```
---
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | MCP Server port | 3606 |
| `MCP_API_KEY` | API key for MCP authentication | - |
| `BINANCE_API_KEY` | Binance API key | - |
| `BINANCE_API_SECRET` | Binance API secret | - |
| `BINANCE_TESTNET` | Use Binance testnet | true |
| `MAX_ORDER_VALUE_USDT` | Max order value limit | 1000 |
| `MAX_DAILY_VOLUME_USDT` | Max daily trading volume | 10000 |
| `MAX_LEVERAGE` | Max leverage allowed | 20 |
| `LOG_LEVEL` | Logging level | info |
### Example .env
```env
PORT=3606
BINANCE_API_KEY=your_api_key_here
BINANCE_API_SECRET=your_api_secret_here
BINANCE_TESTNET=true
MAX_ORDER_VALUE_USDT=1000
MAX_DAILY_VOLUME_USDT=10000
LOG_LEVEL=info
```
---
## Usage
### Start Server
```bash
# Development (with hot reload)
npm run dev
# Production
npm run build
npm start
```
### Health Check
```bash
curl http://localhost:3606/health
```
### List Available Tools
```bash
curl http://localhost:3606/tools
```
### Execute a Tool
```bash
# Get BTC price
curl -X POST http://localhost:3606/tools/binance_get_ticker \
-H "Content-Type: application/json" \
-d '{"parameters": {"symbol": "BTCUSDT"}}'
# Get order book
curl -X POST http://localhost:3606/tools/binance_get_orderbook \
-H "Content-Type: application/json" \
-d '{"parameters": {"symbol": "ETHUSDT", "limit": 10}}'
# Get candlestick data
curl -X POST http://localhost:3606/tools/binance_get_klines \
-H "Content-Type: application/json" \
-d '{"parameters": {"symbol": "BTCUSDT", "interval": "1h", "limit": 24}}'
# Get account balance (requires API keys)
curl -X POST http://localhost:3606/tools/binance_get_account \
-H "Content-Type: application/json" \
-d '{"parameters": {}}'
# Create order (requires API keys) - HIGH RISK
curl -X POST http://localhost:3606/tools/binance_create_order \
-H "Content-Type: application/json" \
-d '{"parameters": {"symbol": "BTCUSDT", "side": "buy", "type": "market", "amount": 0.001}}'
```
---
## MCP Tools Available
| Tool | Description | Risk Level |
|------|-------------|------------|
| `binance_get_ticker` | Get current price and 24h stats | LOW |
| `binance_get_orderbook` | Get order book depth | LOW |
| `binance_get_klines` | Get OHLCV candles | LOW |
| `binance_get_account` | Get account balances | MEDIUM |
| `binance_get_open_orders` | List open orders | MEDIUM |
| `binance_create_order` | Create buy/sell order | HIGH (*) |
| `binance_cancel_order` | Cancel pending order | MEDIUM |
(*) Tools marked with HIGH risk require explicit confirmation and pass through risk checks.
---
## Project Structure
```
mcp-binance-connector/
├── README.md # This file
├── package.json # Dependencies
├── tsconfig.json # TypeScript configuration
├── .env.example # Environment template
├── Dockerfile # Container configuration
└── src/
├── index.ts # Server entry point
├── config.ts # Configuration management
├── utils/
│ └── logger.ts # Winston logger
├── services/
│ └── binance-client.ts # CCXT wrapper
├── middleware/
│ └── risk-check.ts # Pre-trade risk validation
└── tools/
├── index.ts # Tool registry
├── market.ts # Market data tools
├── account.ts # Account tools
└── orders.ts # Order management tools
```
---
## Development
### Build
```bash
npm run build
```
### Type Check
```bash
npm run typecheck
```
### Lint
```bash
npm run lint
npm run lint:fix
```
### Test
```bash
npm run test
npm run test:coverage
```
---
## Docker
### Build Image
```bash
docker build -t mcp-binance-connector:1.0.0 .
```
### Run Container
```bash
docker run -d \
--name mcp-binance-connector \
-p 3606:3606 \
-e BINANCE_API_KEY=your_key \
-e BINANCE_API_SECRET=your_secret \
-e BINANCE_TESTNET=true \
mcp-binance-connector:1.0.0
```
---
## Integration with Claude
### MCP Configuration
Add to your Claude/MCP configuration:
```json
{
"mcpServers": {
"binance": {
"url": "http://localhost:3606",
"transport": "http"
}
}
}
```
### Example Agent Prompts
```
"What's the current Bitcoin price?"
-> Uses binance_get_ticker({ symbol: "BTCUSDT" })
"Show me the ETH order book"
-> Uses binance_get_orderbook({ symbol: "ETHUSDT" })
"Get the last 50 hourly candles for BTC"
-> Uses binance_get_klines({ symbol: "BTCUSDT", interval: "1h", limit: 50 })
"Check my Binance balance"
-> Uses binance_get_account()
"Buy 0.01 BTC at market price"
-> Uses binance_create_order({ symbol: "BTCUSDT", side: "buy", type: "market", amount: 0.01 })
```
---
## Risk Management
The connector includes built-in risk checks:
1. **Maximum Order Value**: Orders exceeding `MAX_ORDER_VALUE_USDT` are rejected
2. **Daily Volume Limit**: Trading stops when `MAX_DAILY_VOLUME_USDT` is reached
3. **Balance Check**: Buy orders verify sufficient balance
4. **Testnet Default**: Testnet is enabled by default for safety
5. **High-Risk Confirmation**: Orders require explicit confirmation flag
---
## Dependencies
### Runtime
- `express` - HTTP server
- `ccxt` - Cryptocurrency exchange library
- `zod` - Input validation
- `winston` - Logging
- `dotenv` - Environment configuration
- `@modelcontextprotocol/sdk` - MCP protocol
### Development
- `typescript` - Type safety
- `ts-node-dev` - Development server
- `jest` - Testing framework
- `eslint` - Code linting
---
## Prerequisites
1. **Binance Account** with API keys (optional for public data)
2. **Testnet API Keys** for testing (recommended)
3. **Node.js** >= 20.0.0
### Getting Binance API Keys
1. Log into [Binance](https://www.binance.com)
2. Go to API Management
3. Create a new API key
4. Enable Spot Trading permissions
5. (Optional) For testnet: [Binance Testnet](https://testnet.binance.vision/)
---
## Troubleshooting
### Cannot connect to Binance
```bash
# Check connectivity
curl https://api.binance.com/api/v3/ping
# If using testnet, check testnet connectivity
curl https://testnet.binance.vision/api/v3/ping
```
### Authentication errors
```bash
# Verify API keys are set
cat .env | grep BINANCE
# Check health endpoint for config status
curl http://localhost:3606/health
```
### Order rejected by risk check
The order may exceed configured limits. Check:
- `MAX_ORDER_VALUE_USDT` - single order limit
- `MAX_DAILY_VOLUME_USDT` - daily trading limit
- Available balance for buy orders
---
## References
- [MCP Protocol](https://modelcontextprotocol.io)
- [CCXT Documentation](https://docs.ccxt.com)
- [Binance API](https://binance-docs.github.io/apidocs/)
- Architecture: `/docs/01-arquitectura/MCP-BINANCE-CONNECTOR-SPEC.md`
- MT4 Connector: `/apps/mcp-mt4-connector/` (reference implementation)
---
**Maintained by:** @PERFIL_MCP_DEVELOPER
**Project:** OrbiQuant Trading Platform

View File

@ -0,0 +1,54 @@
{
"name": "mcp-binance-connector",
"version": "1.0.0",
"description": "MCP Server for Binance trading operations via CCXT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn src/index.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"typecheck": "tsc --noEmit",
"health-check": "curl -s http://localhost:${PORT:-3606}/health || echo 'Server not running'"
},
"keywords": [
"mcp",
"model-context-protocol",
"anthropic",
"claude",
"binance",
"crypto",
"trading",
"ccxt"
],
"author": "OrbiQuant Trading Platform",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"ccxt": "^4.0.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"winston": "^3.11.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=20.0.0"
}
}

View File

@ -0,0 +1,159 @@
/**
* Configuration Module
*
* Manages environment variables and creates Binance clients via CCXT.
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import ccxt from 'ccxt';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// ==========================================
// Configuration Interface
// ==========================================
export interface BinanceConfig {
apiKey: string;
apiSecret: string;
testnet: boolean;
futuresTestnet: boolean;
timeout: number;
}
export interface RiskConfig {
maxOrderValueUsdt: number;
maxDailyVolumeUsdt: number;
maxLeverage: number;
maxPositionSizePct: number;
}
export interface ServerConfig {
port: number;
nodeEnv: string;
mcpApiKey: string;
logLevel: string;
}
// ==========================================
// Configuration Loading
// ==========================================
export const binanceConfig: BinanceConfig = {
apiKey: process.env.BINANCE_API_KEY || '',
apiSecret: process.env.BINANCE_API_SECRET || '',
testnet: process.env.BINANCE_TESTNET === 'true',
futuresTestnet: process.env.BINANCE_FUTURES_TESTNET === 'true',
timeout: parseInt(process.env.REQUEST_TIMEOUT || '10000', 10),
};
export const riskConfig: RiskConfig = {
maxOrderValueUsdt: parseFloat(process.env.MAX_ORDER_VALUE_USDT || '1000'),
maxDailyVolumeUsdt: parseFloat(process.env.MAX_DAILY_VOLUME_USDT || '10000'),
maxLeverage: parseInt(process.env.MAX_LEVERAGE || '20', 10),
maxPositionSizePct: parseFloat(process.env.MAX_POSITION_SIZE_PCT || '5'),
};
export const serverConfig: ServerConfig = {
port: parseInt(process.env.PORT || '3606', 10),
nodeEnv: process.env.NODE_ENV || 'development',
mcpApiKey: process.env.MCP_API_KEY || '',
logLevel: process.env.LOG_LEVEL || 'info',
};
// ==========================================
// Binance Client Factory
// ==========================================
/**
* Create a Binance Spot client
*/
export function createBinanceSpotClient(): ccxt.binance {
const isTestnet = binanceConfig.testnet;
const client = new ccxt.binance({
apiKey: binanceConfig.apiKey,
secret: binanceConfig.apiSecret,
sandbox: isTestnet,
options: {
defaultType: 'spot',
adjustForTimeDifference: true,
},
enableRateLimit: true,
rateLimit: 100,
timeout: binanceConfig.timeout,
});
return client;
}
/**
* Create a Binance Futures client
*/
export function createBinanceFuturesClient(): ccxt.binance {
const isTestnet = binanceConfig.futuresTestnet;
const client = new ccxt.binance({
apiKey: binanceConfig.apiKey,
secret: binanceConfig.apiSecret,
sandbox: isTestnet,
options: {
defaultType: 'future',
adjustForTimeDifference: true,
},
enableRateLimit: true,
rateLimit: 100,
timeout: binanceConfig.timeout,
});
return client;
}
// ==========================================
// Configuration Validation
// ==========================================
export function validateConfig(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
// Binance API keys are optional for public endpoints
// but required for account/trading operations
if (!binanceConfig.apiKey && serverConfig.nodeEnv === 'production') {
errors.push('BINANCE_API_KEY is required in production');
}
if (!binanceConfig.apiSecret && serverConfig.nodeEnv === 'production') {
errors.push('BINANCE_API_SECRET is required in production');
}
// Validate risk limits
if (riskConfig.maxOrderValueUsdt <= 0) {
errors.push('MAX_ORDER_VALUE_USDT must be positive');
}
if (riskConfig.maxLeverage < 1 || riskConfig.maxLeverage > 125) {
errors.push('MAX_LEVERAGE must be between 1 and 125');
}
return {
valid: errors.length === 0,
errors,
};
}
// ==========================================
// Exports
// ==========================================
export default {
binance: binanceConfig,
risk: riskConfig,
server: serverConfig,
createBinanceSpotClient,
createBinanceFuturesClient,
validateConfig,
};

View File

@ -0,0 +1,332 @@
/**
* MCP Server: Binance Connector
*
* Exposes Binance trading capabilities as MCP tools for AI agents.
* Uses CCXT library to communicate with Binance API.
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import express, { Request, Response, NextFunction } from 'express';
import dotenv from 'dotenv';
import { mcpToolSchemas, toolHandlers, getAllToolDefinitions, toolRequiresConfirmation, getToolRiskLevel } from './tools';
import { getBinanceClient } from './services/binance-client';
import { serverConfig, binanceConfig, validateConfig } from './config';
import { logger } from './utils/logger';
// Load environment variables
dotenv.config();
const app = express();
const PORT = serverConfig.port;
const SERVICE_NAME = 'mcp-binance-connector';
const VERSION = '1.0.0';
// ==========================================
// Middleware
// ==========================================
app.use(express.json());
// Request logging
app.use((req: Request, _res: Response, next: NextFunction) => {
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
userAgent: req.get('user-agent'),
});
next();
});
// MCP API Key authentication (optional, for protected endpoints)
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const mcpKey = req.headers['x-mcp-api-key'];
// Skip auth if MCP_API_KEY is not configured
if (!serverConfig.mcpApiKey) {
next();
return;
}
if (mcpKey !== serverConfig.mcpApiKey) {
res.status(401).json({ error: 'Invalid MCP API key' });
return;
}
next();
};
// ==========================================
// Health & Status Endpoints
// ==========================================
/**
* Health check endpoint
*/
app.get('/health', async (_req: Request, res: Response) => {
try {
const client = getBinanceClient();
const binanceConnected = await client.isConnected();
const binanceConfigured = client.isConfigured();
res.json({
status: binanceConnected ? 'healthy' : 'degraded',
service: SERVICE_NAME,
version: VERSION,
timestamp: new Date().toISOString(),
testnet: binanceConfig.testnet,
dependencies: {
binance: binanceConnected ? 'connected' : 'disconnected',
binanceApiConfigured: binanceConfigured,
},
});
} catch (error) {
res.json({
status: 'unhealthy',
service: SERVICE_NAME,
version: VERSION,
timestamp: new Date().toISOString(),
testnet: binanceConfig.testnet,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
/**
* List available MCP tools
*/
app.get('/tools', (_req: Request, res: Response) => {
res.json({
tools: mcpToolSchemas.map((tool) => ({
name: tool.name,
description: tool.description,
riskLevel: (tool as { riskLevel?: string }).riskLevel,
requiresConfirmation: (tool as { requiresConfirmation?: boolean }).requiresConfirmation,
})),
count: mcpToolSchemas.length,
});
});
/**
* Get specific tool schema
*/
app.get('/tools/:toolName', (req: Request, res: Response) => {
const { toolName } = req.params;
const tool = mcpToolSchemas.find((t) => t.name === toolName);
if (!tool) {
res.status(404).json({
error: `Tool '${toolName}' not found`,
availableTools: mcpToolSchemas.map((t) => t.name),
});
return;
}
res.json(tool);
});
// ==========================================
// MCP Tool Execution Endpoints
// ==========================================
/**
* Execute an MCP tool
* POST /tools/:toolName
* Body: { parameters: {...} }
*/
app.post('/tools/:toolName', authMiddleware, async (req: Request, res: Response) => {
const { toolName } = req.params;
const { parameters = {} } = req.body;
// Validate tool exists
const handler = toolHandlers[toolName];
if (!handler) {
res.status(404).json({
success: false,
error: `Tool '${toolName}' not found`,
availableTools: Object.keys(toolHandlers),
});
return;
}
try {
logger.info(`Executing tool: ${toolName}`, {
parameters,
riskLevel: getToolRiskLevel(toolName),
requiresConfirmation: toolRequiresConfirmation(toolName),
});
const result = await handler(parameters);
res.json({
success: true,
tool: toolName,
result,
});
} catch (error) {
logger.error(`Tool execution failed: ${toolName}`, { error, parameters });
// Handle Zod validation errors
if (error && typeof error === 'object' && 'issues' in error) {
res.status(400).json({
success: false,
error: 'Validation error',
details: (error as { issues: unknown[] }).issues,
});
return;
}
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// ==========================================
// MCP Protocol Endpoints (Standard)
// ==========================================
/**
* MCP Initialize
* Returns server capabilities
*/
app.post('/mcp/initialize', (_req: Request, res: Response) => {
res.json({
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
},
serverInfo: {
name: SERVICE_NAME,
version: VERSION,
},
});
});
/**
* MCP List Tools
* Returns all available tools in MCP format
*/
app.post('/mcp/tools/list', (_req: Request, res: Response) => {
res.json({
tools: getAllToolDefinitions(),
});
});
/**
* MCP Call Tool
* Execute a tool with parameters
*/
app.post('/mcp/tools/call', authMiddleware, async (req: Request, res: Response) => {
const { name, arguments: args = {} } = req.body;
if (!name) {
res.status(400).json({
error: {
code: 'invalid_request',
message: 'Tool name is required',
},
});
return;
}
const handler = toolHandlers[name];
if (!handler) {
res.status(404).json({
error: {
code: 'unknown_tool',
message: `Tool '${name}' not found`,
},
});
return;
}
try {
const result = await handler(args);
res.json(result);
} catch (error) {
// Handle Zod validation errors
if (error && typeof error === 'object' && 'issues' in error) {
res.status(400).json({
error: {
code: 'invalid_params',
message: 'Invalid tool parameters',
data: (error as { issues: unknown[] }).issues,
},
});
return;
}
res.status(500).json({
error: {
code: 'internal_error',
message: error instanceof Error ? error.message : 'Unknown error',
},
});
}
});
// ==========================================
// Error Handler
// ==========================================
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
logger.error('Unhandled error', { error: err });
res.status(500).json({
error: 'Internal server error',
message: err.message,
});
});
// ==========================================
// Start Server
// ==========================================
// Validate configuration before starting
const configValidation = validateConfig();
if (!configValidation.valid) {
logger.warn('Configuration warnings', { errors: configValidation.errors });
}
app.listen(PORT, () => {
console.log('');
console.log('================================================================');
console.log(' MCP Binance Connector - OrbiQuant Trading Platform ');
console.log('================================================================');
console.log(` Service: ${SERVICE_NAME}`);
console.log(` Version: ${VERSION}`);
console.log(` Port: ${PORT}`);
console.log(` Environment: ${serverConfig.nodeEnv}`);
console.log(` Testnet Mode: ${binanceConfig.testnet ? 'ENABLED' : 'DISABLED'}`);
console.log(` API Configured: ${binanceConfig.apiKey ? 'Yes' : 'No'}`);
console.log('----------------------------------------------------------------');
console.log(' Endpoints:');
console.log(` - Health: http://localhost:${PORT}/health`);
console.log(` - Tools: http://localhost:${PORT}/tools`);
console.log('----------------------------------------------------------------');
console.log(' MCP Tools Available:');
mcpToolSchemas.forEach((tool) => {
const risk = (tool as { riskLevel?: string }).riskLevel || 'N/A';
const confirm = (tool as { requiresConfirmation?: boolean }).requiresConfirmation ? ' (!)' : '';
console.log(` - ${tool.name} [${risk}]${confirm}`);
});
console.log('================================================================');
console.log('');
});
// ==========================================
// Graceful Shutdown
// ==========================================
process.on('SIGTERM', () => {
logger.info('Received SIGTERM, shutting down gracefully...');
process.exit(0);
});
process.on('SIGINT', () => {
logger.info('Received SIGINT, shutting down gracefully...');
process.exit(0);
});
export default app;

View File

@ -0,0 +1,209 @@
/**
* Risk Check Middleware
*
* Pre-trade risk validation to ensure orders comply with risk limits.
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import { riskConfig } from '../config';
import { getBinanceClient } from '../services/binance-client';
import { logger } from '../utils/logger';
// ==========================================
// Types
// ==========================================
export interface RiskCheckParams {
symbol: string;
side: 'buy' | 'sell';
amount: number;
price?: number;
}
export interface RiskCheckResult {
allowed: boolean;
reason?: string;
warnings?: string[];
orderValue?: number;
}
// Daily volume tracking (in-memory, resets on restart)
let dailyVolume = 0;
let lastVolumeResetDate = new Date().toDateString();
// ==========================================
// Risk Check Functions
// ==========================================
/**
* Reset daily volume counter at midnight
*/
function checkAndResetDailyVolume(): void {
const today = new Date().toDateString();
if (today !== lastVolumeResetDate) {
dailyVolume = 0;
lastVolumeResetDate = today;
logger.info('Daily volume counter reset');
}
}
/**
* Get the quote asset from a symbol (e.g., USDT from BTCUSDT)
*/
function getQuoteAsset(symbol: string): string {
const stablecoins = ['USDT', 'BUSD', 'USDC', 'TUSD', 'DAI'];
for (const stable of stablecoins) {
if (symbol.endsWith(stable)) {
return stable;
}
}
return 'USDT';
}
/**
* Perform comprehensive risk check before order execution
*/
export async function performRiskCheck(params: RiskCheckParams): Promise<RiskCheckResult> {
const { symbol, side, amount, price } = params;
const warnings: string[] = [];
try {
checkAndResetDailyVolume();
const client = getBinanceClient();
// 1. Get current price if not provided
let orderPrice = price;
if (!orderPrice) {
try {
orderPrice = await client.getCurrentPrice(symbol);
} catch (error) {
logger.warn(`Could not fetch current price for ${symbol}, using amount as value estimate`);
orderPrice = 1; // Fallback
}
}
// 2. Calculate order value in quote currency (usually USDT)
const orderValue = amount * orderPrice;
// 3. Check maximum order value
if (orderValue > riskConfig.maxOrderValueUsdt) {
return {
allowed: false,
reason: `Order value ${orderValue.toFixed(2)} USDT exceeds maximum ${riskConfig.maxOrderValueUsdt} USDT`,
orderValue,
};
}
// 4. Check daily volume limit
if (dailyVolume + orderValue > riskConfig.maxDailyVolumeUsdt) {
return {
allowed: false,
reason: `Daily volume limit reached. Current: ${dailyVolume.toFixed(2)} USDT, Limit: ${riskConfig.maxDailyVolumeUsdt} USDT`,
orderValue,
};
}
// 5. Check if API keys are configured for trading
if (!client.isConfigured()) {
return {
allowed: false,
reason: 'Binance API keys are not configured. Cannot execute trades.',
orderValue,
};
}
// 6. Verify we can connect to Binance
const connected = await client.isConnected();
if (!connected) {
return {
allowed: false,
reason: 'Cannot connect to Binance. Please check your network and API configuration.',
orderValue,
};
}
// 7. Check balance for buy orders (if we have account access)
if (side === 'buy') {
try {
const account = await client.getAccount();
const quoteAsset = getQuoteAsset(symbol);
const quoteBalance = account.balances.find(b => b.asset === quoteAsset);
const available = quoteBalance?.free ?? 0;
if (available < orderValue) {
return {
allowed: false,
reason: `Insufficient ${quoteAsset} balance. Required: ${orderValue.toFixed(2)}, Available: ${available.toFixed(2)}`,
orderValue,
};
}
// Warning if using more than 50% of available balance
if (orderValue > available * 0.5) {
warnings.push(`This order uses ${((orderValue / available) * 100).toFixed(1)}% of your available ${quoteAsset}`);
}
} catch (error) {
warnings.push('Could not verify account balance');
logger.warn('Balance check failed', { error });
}
}
// 8. Check for large order warning
if (orderValue > riskConfig.maxOrderValueUsdt * 0.5) {
warnings.push(`Large order: ${orderValue.toFixed(2)} USDT (${((orderValue / riskConfig.maxOrderValueUsdt) * 100).toFixed(0)}% of max)`);
}
// All checks passed
return {
allowed: true,
orderValue,
warnings: warnings.length > 0 ? warnings : undefined,
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error('Risk check failed', { error, params });
return {
allowed: false,
reason: `Risk check error: ${message}`,
};
}
}
/**
* Record executed trade volume
*/
export function recordTradeVolume(orderValue: number): void {
checkAndResetDailyVolume();
dailyVolume += orderValue;
logger.info(`Trade recorded. Daily volume: ${dailyVolume.toFixed(2)} USDT`);
}
/**
* Get current daily volume
*/
export function getDailyVolume(): number {
checkAndResetDailyVolume();
return dailyVolume;
}
/**
* Get remaining daily volume allowance
*/
export function getRemainingDailyVolume(): number {
checkAndResetDailyVolume();
return Math.max(0, riskConfig.maxDailyVolumeUsdt - dailyVolume);
}
// ==========================================
// Exports
// ==========================================
export default {
performRiskCheck,
recordTradeVolume,
getDailyVolume,
getRemainingDailyVolume,
};

View File

@ -0,0 +1,471 @@
/**
* Binance Client Service
*
* CCXT wrapper for Binance operations.
* Provides a unified interface for both Spot and Futures trading.
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import ccxt, { Ticker, OrderBook, OHLCV, Balance, Order, Trade } from 'ccxt';
import { createBinanceSpotClient, createBinanceFuturesClient, binanceConfig } from '../config';
import { logger } from '../utils/logger';
// ==========================================
// Types
// ==========================================
export interface BinanceTicker {
symbol: string;
price: number;
bid: number;
ask: number;
high24h: number;
low24h: number;
volume24h: number;
change24h: number;
timestamp: number;
}
export interface BinanceOrderBook {
symbol: string;
bids: [number, number][];
asks: [number, number][];
spread: number;
spreadPercentage: number;
timestamp: number;
}
export interface BinanceKline {
timestamp: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
export interface BinanceAccountBalance {
asset: string;
free: number;
locked: number;
total: number;
}
export interface BinanceAccount {
accountType: string;
balances: BinanceAccountBalance[];
canTrade: boolean;
canWithdraw: boolean;
updateTime: number;
}
export interface BinanceOrder {
id: string;
symbol: string;
side: string;
type: string;
price: number | null;
amount: number;
filled: number;
remaining: number;
status: string;
createdAt: number;
}
export interface CreateOrderParams {
symbol: string;
side: 'buy' | 'sell';
type: 'market' | 'limit' | 'stop_loss' | 'take_profit';
amount: number;
price?: number;
stopPrice?: number;
}
export interface OrderResult {
success: boolean;
order?: BinanceOrder;
error?: string;
}
// ==========================================
// Binance Client Class
// ==========================================
export class BinanceClient {
private spotClient: ccxt.binance;
private futuresClient: ccxt.binance;
private marketsLoaded: boolean = false;
constructor() {
this.spotClient = createBinanceSpotClient();
this.futuresClient = createBinanceFuturesClient();
}
/**
* Check if client is properly configured
*/
isConfigured(): boolean {
return binanceConfig.apiKey !== '' && binanceConfig.apiSecret !== '';
}
/**
* Test connectivity to Binance
*/
async isConnected(): Promise<boolean> {
try {
await this.spotClient.fetchTime();
return true;
} catch {
return false;
}
}
/**
* Load markets if not already loaded
*/
private async ensureMarketsLoaded(): Promise<void> {
if (!this.marketsLoaded) {
await this.spotClient.loadMarkets();
this.marketsLoaded = true;
}
}
// ==========================================
// Market Data Methods
// ==========================================
/**
* Get ticker for a symbol
*/
async getTicker(symbol: string): Promise<BinanceTicker> {
try {
await this.ensureMarketsLoaded();
const ticker: Ticker = await this.spotClient.fetchTicker(symbol);
return {
symbol: ticker.symbol,
price: ticker.last ?? 0,
bid: ticker.bid ?? 0,
ask: ticker.ask ?? 0,
high24h: ticker.high ?? 0,
low24h: ticker.low ?? 0,
volume24h: ticker.baseVolume ?? 0,
change24h: ticker.percentage ?? 0,
timestamp: ticker.timestamp ?? Date.now(),
};
} catch (error) {
logger.error(`Failed to get ticker for ${symbol}`, { error });
throw error;
}
}
/**
* Get order book for a symbol
*/
async getOrderBook(symbol: string, limit: number = 20): Promise<BinanceOrderBook> {
try {
await this.ensureMarketsLoaded();
const orderbook: OrderBook = await this.spotClient.fetchOrderBook(symbol, limit);
const topBid = orderbook.bids[0]?.[0] ?? 0;
const topAsk = orderbook.asks[0]?.[0] ?? 0;
const spread = topAsk - topBid;
const spreadPercentage = topBid > 0 ? (spread / topBid) * 100 : 0;
return {
symbol,
bids: orderbook.bids.slice(0, limit) as [number, number][],
asks: orderbook.asks.slice(0, limit) as [number, number][],
spread,
spreadPercentage,
timestamp: orderbook.timestamp ?? Date.now(),
};
} catch (error) {
logger.error(`Failed to get order book for ${symbol}`, { error });
throw error;
}
}
/**
* Get OHLCV (klines/candles) for a symbol
*/
async getKlines(
symbol: string,
interval: string = '5m',
limit: number = 100
): Promise<BinanceKline[]> {
try {
await this.ensureMarketsLoaded();
const ohlcv: OHLCV[] = await this.spotClient.fetchOHLCV(symbol, interval, undefined, limit);
return ohlcv.map((candle) => ({
timestamp: candle[0] as number,
open: candle[1] as number,
high: candle[2] as number,
low: candle[3] as number,
close: candle[4] as number,
volume: candle[5] as number,
}));
} catch (error) {
logger.error(`Failed to get klines for ${symbol}`, { error });
throw error;
}
}
// ==========================================
// Account Methods
// ==========================================
/**
* Get account balance
*/
async getAccount(): Promise<BinanceAccount> {
try {
if (!this.isConfigured()) {
throw new Error('Binance API keys not configured');
}
const balance: Balance = await this.spotClient.fetchBalance();
// Filter non-zero balances
const balances: BinanceAccountBalance[] = Object.entries(balance.total)
.filter(([_, amount]) => (amount as number) > 0)
.map(([asset, total]) => ({
asset,
free: (balance.free[asset] as number) ?? 0,
locked: (balance.used[asset] as number) ?? 0,
total: total as number,
}));
return {
accountType: 'SPOT',
balances,
canTrade: true,
canWithdraw: true,
updateTime: Date.now(),
};
} catch (error) {
logger.error('Failed to get account info', { error });
throw error;
}
}
/**
* Get open orders
*/
async getOpenOrders(symbol?: string): Promise<BinanceOrder[]> {
try {
if (!this.isConfigured()) {
throw new Error('Binance API keys not configured');
}
const orders: Order[] = await this.spotClient.fetchOpenOrders(symbol);
return orders.map((order) => ({
id: order.id,
symbol: order.symbol,
side: order.side,
type: order.type,
price: order.price,
amount: order.amount,
filled: order.filled,
remaining: order.remaining,
status: order.status,
createdAt: order.timestamp ?? Date.now(),
}));
} catch (error) {
logger.error('Failed to get open orders', { error });
throw error;
}
}
/**
* Get trade history
*/
async getTradeHistory(symbol: string, limit: number = 50): Promise<Trade[]> {
try {
if (!this.isConfigured()) {
throw new Error('Binance API keys not configured');
}
return await this.spotClient.fetchMyTrades(symbol, undefined, limit);
} catch (error) {
logger.error(`Failed to get trade history for ${symbol}`, { error });
throw error;
}
}
// ==========================================
// Order Methods
// ==========================================
/**
* Create a new order
*/
async createOrder(params: CreateOrderParams): Promise<OrderResult> {
try {
if (!this.isConfigured()) {
throw new Error('Binance API keys not configured');
}
await this.ensureMarketsLoaded();
let order: Order;
switch (params.type) {
case 'market':
order = await this.spotClient.createMarketOrder(
params.symbol,
params.side,
params.amount
);
break;
case 'limit':
if (!params.price) {
return { success: false, error: 'Price is required for limit orders' };
}
order = await this.spotClient.createLimitOrder(
params.symbol,
params.side,
params.amount,
params.price
);
break;
case 'stop_loss':
if (!params.stopPrice) {
return { success: false, error: 'Stop price is required for stop loss orders' };
}
order = await this.spotClient.createOrder(
params.symbol,
'stop_loss',
params.side,
params.amount,
undefined,
{ stopPrice: params.stopPrice }
);
break;
case 'take_profit':
if (!params.stopPrice) {
return { success: false, error: 'Stop price is required for take profit orders' };
}
order = await this.spotClient.createOrder(
params.symbol,
'take_profit',
params.side,
params.amount,
undefined,
{ stopPrice: params.stopPrice }
);
break;
default:
return { success: false, error: `Unsupported order type: ${params.type}` };
}
return {
success: true,
order: {
id: order.id,
symbol: order.symbol,
side: order.side,
type: order.type,
price: order.price ?? order.average ?? null,
amount: order.amount,
filled: order.filled,
remaining: order.remaining,
status: order.status,
createdAt: order.timestamp ?? Date.now(),
},
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error('Failed to create order', { error, params });
return { success: false, error: message };
}
}
/**
* Cancel an order
*/
async cancelOrder(orderId: string, symbol: string): Promise<OrderResult> {
try {
if (!this.isConfigured()) {
throw new Error('Binance API keys not configured');
}
const result = await this.spotClient.cancelOrder(orderId, symbol);
return {
success: true,
order: {
id: result.id,
symbol: result.symbol,
side: result.side,
type: result.type,
price: result.price,
amount: result.amount,
filled: result.filled,
remaining: result.remaining,
status: 'CANCELLED',
createdAt: result.timestamp ?? Date.now(),
},
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error('Failed to cancel order', { error, orderId, symbol });
return { success: false, error: message };
}
}
/**
* Cancel all orders for a symbol
*/
async cancelAllOrders(symbol: string): Promise<{ success: boolean; cancelledCount: number; error?: string }> {
try {
if (!this.isConfigured()) {
throw new Error('Binance API keys not configured');
}
const result = await this.spotClient.cancelAllOrders(symbol);
return {
success: true,
cancelledCount: Array.isArray(result) ? result.length : 0,
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
logger.error('Failed to cancel all orders', { error, symbol });
return { success: false, cancelledCount: 0, error: message };
}
}
/**
* Get current price for a symbol (helper method)
*/
async getCurrentPrice(symbol: string): Promise<number> {
const ticker = await this.getTicker(symbol);
return ticker.price;
}
}
// ==========================================
// Singleton Instance
// ==========================================
let clientInstance: BinanceClient | null = null;
export function getBinanceClient(): BinanceClient {
if (!clientInstance) {
clientInstance = new BinanceClient();
}
return clientInstance;
}
export function resetBinanceClient(): void {
clientInstance = null;
}

View File

@ -0,0 +1,265 @@
/**
* Binance Account Tools
*
* - binance_get_account: Get account balance and status
* - binance_get_open_orders: Get all open orders
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import { z } from 'zod';
import { getBinanceClient, BinanceAccount, BinanceOrder } from '../services/binance-client';
// ==========================================
// binance_get_account
// ==========================================
/**
* Tool: binance_get_account
* Get account balance and status
*/
export const binanceGetAccountSchema = {
name: 'binance_get_account',
description: 'Get Binance account balance and status. Shows all assets with non-zero balance.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [] as string[],
},
};
export const BinanceGetAccountInputSchema = z.object({});
export type BinanceGetAccountInput = z.infer<typeof BinanceGetAccountInputSchema>;
export interface BinanceGetAccountResult {
success: boolean;
data?: BinanceAccount & { totalUsdtEstimate?: number };
error?: string;
}
export async function binance_get_account(
_params: BinanceGetAccountInput
): Promise<BinanceGetAccountResult> {
try {
const client = getBinanceClient();
if (!client.isConfigured()) {
return {
success: false,
error: 'Binance API keys are not configured',
};
}
const connected = await client.isConnected();
if (!connected) {
return {
success: false,
error: 'Cannot connect to Binance. Please check your network.',
};
}
const account = await client.getAccount();
// Estimate total value in USDT
let totalUsdtEstimate = 0;
for (const balance of account.balances) {
if (balance.asset === 'USDT' || balance.asset === 'BUSD' || balance.asset === 'USDC') {
totalUsdtEstimate += balance.total;
} else if (balance.total > 0) {
try {
const price = await client.getCurrentPrice(`${balance.asset}USDT`);
totalUsdtEstimate += balance.total * price;
} catch {
// Skip if no USDT pair exists
}
}
}
return {
success: true,
data: {
...account,
totalUsdtEstimate,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleBinanceGetAccount(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = BinanceGetAccountInputSchema.parse(params);
const result = await binance_get_account(validatedParams);
if (result.success && result.data) {
const d = result.data;
// Sort balances by total value
const sortedBalances = [...d.balances].sort((a, b) => {
// USDT first, then by total
if (a.asset === 'USDT') return -1;
if (b.asset === 'USDT') return 1;
return b.total - a.total;
});
let balancesStr = sortedBalances
.slice(0, 20) // Top 20 assets
.map((b) => {
const lockedStr = b.locked > 0 ? ` (Locked: ${b.locked.toFixed(8)})` : '';
return ` ${b.asset.padEnd(8)} Free: ${b.free.toFixed(8)}${lockedStr}`;
})
.join('\n');
const formattedOutput = `
Binance Account Information
${'='.repeat(35)}
Account Type: ${d.accountType}
Can Trade: ${d.canTrade ? 'Yes' : 'No'}
Can Withdraw: ${d.canWithdraw ? 'Yes' : 'No'}
Estimated Total Value
---------------------
~$${d.totalUsdtEstimate?.toFixed(2) ?? 'N/A'} USDT
Asset Balances (${d.balances.length} with balance)
${'='.repeat(35)}
${balancesStr}
${d.balances.length > 20 ? `\n ... and ${d.balances.length - 20} more assets` : ''}
Last Update: ${new Date(d.updateTime).toISOString()}
`.trim();
return {
content: [{ type: 'text', text: formattedOutput }],
};
}
return {
content: [{ type: 'text', text: `Error: ${result.error}` }],
};
}
// ==========================================
// binance_get_open_orders
// ==========================================
/**
* Tool: binance_get_open_orders
* Get all open (pending) orders
*/
export const binanceGetOpenOrdersSchema = {
name: 'binance_get_open_orders',
description: 'Get all open (pending) orders. Optionally filter by symbol.',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Optional: Filter by trading pair symbol (e.g., BTCUSDT)',
},
},
required: [] as string[],
},
};
export const BinanceGetOpenOrdersInputSchema = z.object({
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()).optional(),
});
export type BinanceGetOpenOrdersInput = z.infer<typeof BinanceGetOpenOrdersInputSchema>;
export interface BinanceGetOpenOrdersResult {
success: boolean;
data?: {
orders: BinanceOrder[];
count: number;
};
error?: string;
}
export async function binance_get_open_orders(
params: BinanceGetOpenOrdersInput
): Promise<BinanceGetOpenOrdersResult> {
try {
const client = getBinanceClient();
if (!client.isConfigured()) {
return {
success: false,
error: 'Binance API keys are not configured',
};
}
const orders = await client.getOpenOrders(params.symbol);
return {
success: true,
data: {
orders,
count: orders.length,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleBinanceGetOpenOrders(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = BinanceGetOpenOrdersInputSchema.parse(params);
const result = await binance_get_open_orders(validatedParams);
if (result.success && result.data) {
const d = result.data;
if (d.count === 0) {
return {
content: [
{
type: 'text',
text: `No open orders${validatedParams.symbol ? ` for ${validatedParams.symbol}` : ''}`,
},
],
};
}
let ordersStr = d.orders
.map((o) => {
const priceStr = o.price ? `$${o.price.toFixed(8)}` : 'MARKET';
const filledPct = o.amount > 0 ? ((o.filled / o.amount) * 100).toFixed(1) : '0';
return ` #${o.id}
Symbol: ${o.symbol} | ${o.side.toUpperCase()} | ${o.type.toUpperCase()}
Price: ${priceStr} | Amount: ${o.amount.toFixed(8)}
Filled: ${o.filled.toFixed(8)} (${filledPct}%) | Remaining: ${o.remaining.toFixed(8)}
Status: ${o.status} | Created: ${new Date(o.createdAt).toISOString()}`;
})
.join('\n\n');
const formattedOutput = `
Open Orders${validatedParams.symbol ? ` - ${validatedParams.symbol}` : ''}
${'='.repeat(35)}
Total Orders: ${d.count}
${ordersStr}
`.trim();
return {
content: [{ type: 'text', text: formattedOutput }],
};
}
return {
content: [{ type: 'text', text: `Error: ${result.error}` }],
};
}

View File

@ -0,0 +1,288 @@
/**
* MCP Tools Index
*
* Exports all Binance MCP tools and their schemas for registration
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
// Import handlers for use in toolHandlers map
import { handleBinanceGetTicker, handleBinanceGetOrderbook, handleBinanceGetKlines } from './market';
import { handleBinanceGetAccount, handleBinanceGetOpenOrders } from './account';
import { handleBinanceCreateOrder, handleBinanceCancelOrder } from './orders';
// ==========================================
// Market Tools Exports
// ==========================================
export {
binanceGetTickerSchema,
binance_get_ticker,
handleBinanceGetTicker,
BinanceGetTickerInputSchema,
type BinanceGetTickerInput,
type BinanceGetTickerResult,
binanceGetOrderbookSchema,
binance_get_orderbook,
handleBinanceGetOrderbook,
BinanceGetOrderbookInputSchema,
type BinanceGetOrderbookInput,
type BinanceGetOrderbookResult,
binanceGetKlinesSchema,
binance_get_klines,
handleBinanceGetKlines,
BinanceGetKlinesInputSchema,
type BinanceGetKlinesInput,
type BinanceGetKlinesResult,
} from './market';
// ==========================================
// Account Tools Exports
// ==========================================
export {
binanceGetAccountSchema,
binance_get_account,
handleBinanceGetAccount,
BinanceGetAccountInputSchema,
type BinanceGetAccountInput,
type BinanceGetAccountResult,
binanceGetOpenOrdersSchema,
binance_get_open_orders,
handleBinanceGetOpenOrders,
BinanceGetOpenOrdersInputSchema,
type BinanceGetOpenOrdersInput,
type BinanceGetOpenOrdersResult,
} from './account';
// ==========================================
// Order Tools Exports
// ==========================================
export {
binanceCreateOrderSchema,
binance_create_order,
handleBinanceCreateOrder,
BinanceCreateOrderInputSchema,
type BinanceCreateOrderInput,
type BinanceCreateOrderResult,
binanceCancelOrderSchema,
binance_cancel_order,
handleBinanceCancelOrder,
BinanceCancelOrderInputSchema,
type BinanceCancelOrderInput,
type BinanceCancelOrderResult,
} from './orders';
// ==========================================
// Tool Registry
// ==========================================
/**
* All available MCP tools with their schemas
* Follows MCP protocol format
*/
export const mcpToolSchemas = [
// Market Data Tools (Low Risk)
{
name: 'binance_get_ticker',
description: 'Get the current price and 24-hour statistics for a Binance trading pair',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT, BNBUSDT)',
},
},
required: ['symbol'] as string[],
},
riskLevel: 'LOW',
},
{
name: 'binance_get_orderbook',
description: 'Get the order book (bids and asks) with the specified depth for a trading pair',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT)',
},
limit: {
type: 'number',
description: 'Order book depth (5, 10, 20, 50, or 100). Default: 20',
},
},
required: ['symbol'] as string[],
},
riskLevel: 'LOW',
},
{
name: 'binance_get_klines',
description: 'Get historical candlestick (OHLCV) data for technical analysis',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT)',
},
interval: {
type: 'string',
description: 'Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 5m',
enum: ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'],
},
limit: {
type: 'number',
description: 'Number of candles to retrieve (max 500). Default: 100',
},
},
required: ['symbol'] as string[],
},
riskLevel: 'LOW',
},
// Account Tools (Medium Risk)
{
name: 'binance_get_account',
description: 'Get Binance account balance and status. Shows all assets with non-zero balance.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [] as string[],
},
riskLevel: 'MEDIUM',
},
{
name: 'binance_get_open_orders',
description: 'Get all open (pending) orders. Optionally filter by symbol.',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Optional: Filter by trading pair symbol (e.g., BTCUSDT)',
},
},
required: [] as string[],
},
riskLevel: 'MEDIUM',
},
// Order Tools (High Risk)
{
name: 'binance_create_order',
description: 'Create a new buy or sell order on Binance. HIGH RISK - Ensure you validate with the user before executing.',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT)',
},
side: {
type: 'string',
enum: ['buy', 'sell'],
description: 'Order direction: buy or sell',
},
type: {
type: 'string',
enum: ['market', 'limit', 'stop_loss', 'take_profit'],
description: 'Order type. Default: market',
},
amount: {
type: 'number',
description: 'Amount of the base asset to buy/sell',
},
price: {
type: 'number',
description: 'Price per unit (required for limit orders)',
},
stopPrice: {
type: 'number',
description: 'Stop price (required for stop_loss and take_profit orders)',
},
},
required: ['symbol', 'side', 'amount'] as string[],
},
riskLevel: 'HIGH',
requiresConfirmation: true,
},
{
name: 'binance_cancel_order',
description: 'Cancel a pending order by order ID and symbol',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT)',
},
orderId: {
type: 'string',
description: 'Order ID to cancel',
},
},
required: ['symbol', 'orderId'] as string[],
},
riskLevel: 'MEDIUM',
},
];
/**
* Tool handler routing map
* Maps tool names to their handler functions
*/
export const toolHandlers: Record<
string,
(params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>
> = {
// Market tools
binance_get_ticker: handleBinanceGetTicker,
binance_get_orderbook: handleBinanceGetOrderbook,
binance_get_klines: handleBinanceGetKlines,
// Account tools
binance_get_account: handleBinanceGetAccount,
binance_get_open_orders: handleBinanceGetOpenOrders,
// Order tools
binance_create_order: handleBinanceCreateOrder,
binance_cancel_order: handleBinanceCancelOrder,
};
/**
* Get all tool definitions for MCP protocol
*/
export function getAllToolDefinitions() {
return mcpToolSchemas.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
}
/**
* Get tool by name
*/
export function getToolByName(name: string) {
return mcpToolSchemas.find((tool) => tool.name === name);
}
/**
* Check if a tool requires confirmation
*/
export function toolRequiresConfirmation(name: string): boolean {
const tool = mcpToolSchemas.find((t) => t.name === name);
return (tool as { requiresConfirmation?: boolean })?.requiresConfirmation === true;
}
/**
* Get tool risk level
*/
export function getToolRiskLevel(name: string): string {
const tool = mcpToolSchemas.find((t) => t.name === name);
return (tool as { riskLevel?: string })?.riskLevel ?? 'UNKNOWN';
}

View File

@ -0,0 +1,392 @@
/**
* Binance Market Data Tools
*
* - binance_get_ticker: Get current price and 24h stats
* - binance_get_orderbook: Get order book depth
* - binance_get_klines: Get OHLCV candles
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import { z } from 'zod';
import { getBinanceClient, BinanceTicker, BinanceOrderBook, BinanceKline } from '../services/binance-client';
// ==========================================
// binance_get_ticker
// ==========================================
/**
* Tool: binance_get_ticker
* Get current price and 24h statistics for a trading pair
*/
export const binanceGetTickerSchema = {
name: 'binance_get_ticker',
description: 'Get the current price and 24-hour statistics for a Binance trading pair',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT, BNBUSDT)',
},
},
required: ['symbol'] as string[],
},
};
export const BinanceGetTickerInputSchema = z.object({
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
});
export type BinanceGetTickerInput = z.infer<typeof BinanceGetTickerInputSchema>;
export interface BinanceGetTickerResult {
success: boolean;
data?: BinanceTicker;
error?: string;
}
export async function binance_get_ticker(
params: BinanceGetTickerInput
): Promise<BinanceGetTickerResult> {
try {
const client = getBinanceClient();
const ticker = await client.getTicker(params.symbol);
return {
success: true,
data: ticker,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleBinanceGetTicker(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = BinanceGetTickerInputSchema.parse(params);
const result = await binance_get_ticker(validatedParams);
if (result.success && result.data) {
const d = result.data;
const changeSymbol = d.change24h >= 0 ? '+' : '';
const formattedOutput = `
Binance Ticker: ${d.symbol}
${'='.repeat(35)}
Current Price: $${d.price.toFixed(getPriceDecimals(d.symbol))}
Bid: $${d.bid.toFixed(getPriceDecimals(d.symbol))}
Ask: $${d.ask.toFixed(getPriceDecimals(d.symbol))}
24h Statistics
--------------
High: $${d.high24h.toFixed(getPriceDecimals(d.symbol))}
Low: $${d.low24h.toFixed(getPriceDecimals(d.symbol))}
Volume: ${formatVolume(d.volume24h)}
Change: ${changeSymbol}${d.change24h.toFixed(2)}%
Last Update: ${new Date(d.timestamp).toISOString()}
`.trim();
return {
content: [{ type: 'text', text: formattedOutput }],
};
}
return {
content: [{ type: 'text', text: `Error: ${result.error}` }],
};
}
// ==========================================
// binance_get_orderbook
// ==========================================
/**
* Tool: binance_get_orderbook
* Get order book (bids and asks) with specified depth
*/
export const binanceGetOrderbookSchema = {
name: 'binance_get_orderbook',
description: 'Get the order book (bids and asks) with the specified depth for a trading pair',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT)',
},
limit: {
type: 'number',
description: 'Order book depth (5, 10, 20, 50, or 100). Default: 20',
},
},
required: ['symbol'] as string[],
},
};
export const BinanceGetOrderbookInputSchema = z.object({
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
limit: z.number().int().min(5).max(100).default(20),
});
export type BinanceGetOrderbookInput = z.infer<typeof BinanceGetOrderbookInputSchema>;
export interface BinanceGetOrderbookResult {
success: boolean;
data?: BinanceOrderBook;
error?: string;
}
export async function binance_get_orderbook(
params: BinanceGetOrderbookInput
): Promise<BinanceGetOrderbookResult> {
try {
const client = getBinanceClient();
const orderbook = await client.getOrderBook(params.symbol, params.limit);
return {
success: true,
data: orderbook,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleBinanceGetOrderbook(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = BinanceGetOrderbookInputSchema.parse(params);
const result = await binance_get_orderbook(validatedParams);
if (result.success && result.data) {
const d = result.data;
const decimals = getPriceDecimals(d.symbol);
// Format top 10 levels
const topBids = d.bids.slice(0, 10);
const topAsks = d.asks.slice(0, 10);
let bidsStr = topBids
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
.join('\n');
let asksStr = topAsks
.map(([price, qty]) => ` $${price.toFixed(decimals)} | ${qty.toFixed(6)}`)
.join('\n');
const formattedOutput = `
Order Book: ${d.symbol}
${'='.repeat(35)}
Spread: $${d.spread.toFixed(decimals)} (${d.spreadPercentage.toFixed(4)}%)
Top ${topAsks.length} Asks (Sell Orders)
${'-'.repeat(25)}
${asksStr}
Top ${topBids.length} Bids (Buy Orders)
${'-'.repeat(25)}
${bidsStr}
Timestamp: ${new Date(d.timestamp).toISOString()}
`.trim();
return {
content: [{ type: 'text', text: formattedOutput }],
};
}
return {
content: [{ type: 'text', text: `Error: ${result.error}` }],
};
}
// ==========================================
// binance_get_klines
// ==========================================
/**
* Tool: binance_get_klines
* Get historical OHLCV candles for technical analysis
*/
export const binanceGetKlinesSchema = {
name: 'binance_get_klines',
description: 'Get historical candlestick (OHLCV) data for technical analysis',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT)',
},
interval: {
type: 'string',
description: 'Candle interval: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 5m',
enum: ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w'],
},
limit: {
type: 'number',
description: 'Number of candles to retrieve (max 500). Default: 100',
},
},
required: ['symbol'] as string[],
},
};
export const BinanceGetKlinesInputSchema = z.object({
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
interval: z.enum(['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w']).default('5m'),
limit: z.number().int().min(1).max(500).default(100),
});
export type BinanceGetKlinesInput = z.infer<typeof BinanceGetKlinesInputSchema>;
export interface BinanceGetKlinesResult {
success: boolean;
data?: {
symbol: string;
interval: string;
candles: BinanceKline[];
count: number;
};
error?: string;
}
export async function binance_get_klines(
params: BinanceGetKlinesInput
): Promise<BinanceGetKlinesResult> {
try {
const client = getBinanceClient();
const klines = await client.getKlines(params.symbol, params.interval, params.limit);
return {
success: true,
data: {
symbol: params.symbol,
interval: params.interval,
candles: klines,
count: klines.length,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleBinanceGetKlines(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = BinanceGetKlinesInputSchema.parse(params);
const result = await binance_get_klines(validatedParams);
if (result.success && result.data) {
const d = result.data;
const decimals = getPriceDecimals(d.symbol);
// Get last 5 candles for display
const recentCandles = d.candles.slice(-5);
let candlesStr = recentCandles
.map((c) => {
const time = new Date(c.timestamp).toISOString().slice(0, 16).replace('T', ' ');
const direction = c.close >= c.open ? 'UP' : 'DOWN';
return ` ${time} | O:${c.open.toFixed(decimals)} H:${c.high.toFixed(decimals)} L:${c.low.toFixed(decimals)} C:${c.close.toFixed(decimals)} | V:${formatVolume(c.volume)} | ${direction}`;
})
.join('\n');
// Calculate basic stats
const closes = d.candles.map((c) => c.close);
const high = Math.max(...d.candles.map((c) => c.high));
const low = Math.min(...d.candles.map((c) => c.low));
const avgVolume = d.candles.reduce((sum, c) => sum + c.volume, 0) / d.candles.length;
const formattedOutput = `
Klines: ${d.symbol} (${d.interval})
${'='.repeat(45)}
Retrieved: ${d.count} candles
Period Statistics
-----------------
Highest High: $${high.toFixed(decimals)}
Lowest Low: $${low.toFixed(decimals)}
Avg Volume: ${formatVolume(avgVolume)}
Recent Candles (last 5)
-----------------------
${candlesStr}
First Candle: ${new Date(d.candles[0].timestamp).toISOString()}
Last Candle: ${new Date(d.candles[d.candles.length - 1].timestamp).toISOString()}
`.trim();
return {
content: [{ type: 'text', text: formattedOutput }],
};
}
return {
content: [{ type: 'text', text: `Error: ${result.error}` }],
};
}
// ==========================================
// Helper Functions
// ==========================================
/**
* Get appropriate decimal places for price display
*/
function getPriceDecimals(symbol: string): number {
const upper = symbol.toUpperCase();
// Stablecoins and fiat pairs
if (upper.includes('USD') && !upper.startsWith('BTC') && !upper.startsWith('ETH')) {
return 4;
}
// BTC pairs
if (upper === 'BTCUSDT' || upper === 'BTCBUSD') {
return 2;
}
// ETH pairs
if (upper === 'ETHUSDT' || upper === 'ETHBUSD') {
return 2;
}
// Small value coins
if (upper.includes('SHIB') || upper.includes('DOGE') || upper.includes('PEPE')) {
return 8;
}
// Default
return 4;
}
/**
* Format large volume numbers
*/
function formatVolume(volume: number): string {
if (volume >= 1_000_000_000) {
return `${(volume / 1_000_000_000).toFixed(2)}B`;
}
if (volume >= 1_000_000) {
return `${(volume / 1_000_000).toFixed(2)}M`;
}
if (volume >= 1_000) {
return `${(volume / 1_000).toFixed(2)}K`;
}
return volume.toFixed(4);
}

View File

@ -0,0 +1,334 @@
/**
* Binance Order Management Tools
*
* - binance_create_order: Create a new order (HIGH RISK)
* - binance_cancel_order: Cancel a pending order
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import { z } from 'zod';
import { getBinanceClient, BinanceOrder, CreateOrderParams } from '../services/binance-client';
import { performRiskCheck, recordTradeVolume } from '../middleware/risk-check';
import { logger } from '../utils/logger';
// ==========================================
// binance_create_order
// ==========================================
/**
* Tool: binance_create_order
* Create a new buy or sell order
* HIGH RISK - Requires confirmation
*/
export const binanceCreateOrderSchema = {
name: 'binance_create_order',
description: 'Create a new buy or sell order on Binance. HIGH RISK - Ensure you validate with the user before executing.',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT, ETHUSDT)',
},
side: {
type: 'string',
enum: ['buy', 'sell'],
description: 'Order direction: buy or sell',
},
type: {
type: 'string',
enum: ['market', 'limit', 'stop_loss', 'take_profit'],
description: 'Order type. Default: market',
},
amount: {
type: 'number',
description: 'Amount of the base asset to buy/sell',
},
price: {
type: 'number',
description: 'Price per unit (required for limit orders)',
},
stopPrice: {
type: 'number',
description: 'Stop price (required for stop_loss and take_profit orders)',
},
},
required: ['symbol', 'side', 'amount'] as string[],
},
riskLevel: 'HIGH',
requiresConfirmation: true,
};
export const BinanceCreateOrderInputSchema = z.object({
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
side: z.enum(['buy', 'sell']),
type: z.enum(['market', 'limit', 'stop_loss', 'take_profit']).default('market'),
amount: z.number().positive(),
price: z.number().positive().optional(),
stopPrice: z.number().positive().optional(),
});
export type BinanceCreateOrderInput = z.infer<typeof BinanceCreateOrderInputSchema>;
export interface BinanceCreateOrderResult {
success: boolean;
data?: {
order: BinanceOrder;
riskWarnings?: string[];
};
error?: string;
riskCheckFailed?: boolean;
}
export async function binance_create_order(
params: BinanceCreateOrderInput
): Promise<BinanceCreateOrderResult> {
try {
const client = getBinanceClient();
if (!client.isConfigured()) {
return {
success: false,
error: 'Binance API keys are not configured',
};
}
// 1. Perform risk check
const riskCheck = await performRiskCheck({
symbol: params.symbol,
side: params.side,
amount: params.amount,
price: params.price,
});
if (!riskCheck.allowed) {
logger.warn('Order rejected by risk check', {
params,
reason: riskCheck.reason,
});
return {
success: false,
error: riskCheck.reason,
riskCheckFailed: true,
};
}
// 2. Validate order parameters
if (params.type === 'limit' && !params.price) {
return {
success: false,
error: 'Price is required for limit orders',
};
}
if ((params.type === 'stop_loss' || params.type === 'take_profit') && !params.stopPrice) {
return {
success: false,
error: `Stop price is required for ${params.type} orders`,
};
}
// 3. Create the order
const orderParams: CreateOrderParams = {
symbol: params.symbol,
side: params.side,
type: params.type,
amount: params.amount,
price: params.price,
stopPrice: params.stopPrice,
};
const result = await client.createOrder(orderParams);
if (result.success && result.order) {
// Record trade volume for daily limit tracking
if (riskCheck.orderValue) {
recordTradeVolume(riskCheck.orderValue);
}
logger.info('Order created successfully', {
orderId: result.order.id,
symbol: params.symbol,
side: params.side,
amount: params.amount,
});
return {
success: true,
data: {
order: result.order,
riskWarnings: riskCheck.warnings,
},
};
}
return {
success: false,
error: result.error || 'Failed to create order',
};
} catch (error) {
logger.error('Order creation failed', { error, params });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleBinanceCreateOrder(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = BinanceCreateOrderInputSchema.parse(params);
const result = await binance_create_order(validatedParams);
if (result.success && result.data) {
const o = result.data.order;
const priceStr = o.price ? `$${o.price.toFixed(8)}` : 'MARKET';
let warningsStr = '';
if (result.data.riskWarnings && result.data.riskWarnings.length > 0) {
warningsStr = `\n\nWarnings:\n${result.data.riskWarnings.map((w) => ` - ${w}`).join('\n')}`;
}
const formattedOutput = `
Order Created Successfully
${'='.repeat(35)}
Order ID: ${o.id}
Symbol: ${o.symbol}
Side: ${o.side.toUpperCase()}
Type: ${o.type.toUpperCase()}
Price: ${priceStr}
Amount: ${o.amount.toFixed(8)}
Filled: ${o.filled.toFixed(8)}
Status: ${o.status}
Created: ${new Date(o.createdAt).toISOString()}${warningsStr}
`.trim();
return {
content: [{ type: 'text', text: formattedOutput }],
};
}
const errorPrefix = result.riskCheckFailed ? 'Risk Check Failed: ' : 'Error: ';
return {
content: [{ type: 'text', text: `${errorPrefix}${result.error}` }],
};
}
// ==========================================
// binance_cancel_order
// ==========================================
/**
* Tool: binance_cancel_order
* Cancel a pending order
*/
export const binanceCancelOrderSchema = {
name: 'binance_cancel_order',
description: 'Cancel a pending order by order ID and symbol',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading pair symbol (e.g., BTCUSDT)',
},
orderId: {
type: 'string',
description: 'Order ID to cancel',
},
},
required: ['symbol', 'orderId'] as string[],
},
riskLevel: 'MEDIUM',
};
export const BinanceCancelOrderInputSchema = z.object({
symbol: z.string().min(1).max(20).transform((s) => s.toUpperCase()),
orderId: z.string().min(1),
});
export type BinanceCancelOrderInput = z.infer<typeof BinanceCancelOrderInputSchema>;
export interface BinanceCancelOrderResult {
success: boolean;
data?: {
cancelledOrder: BinanceOrder;
};
error?: string;
}
export async function binance_cancel_order(
params: BinanceCancelOrderInput
): Promise<BinanceCancelOrderResult> {
try {
const client = getBinanceClient();
if (!client.isConfigured()) {
return {
success: false,
error: 'Binance API keys are not configured',
};
}
const result = await client.cancelOrder(params.orderId, params.symbol);
if (result.success && result.order) {
logger.info('Order cancelled successfully', {
orderId: params.orderId,
symbol: params.symbol,
});
return {
success: true,
data: {
cancelledOrder: result.order,
},
};
}
return {
success: false,
error: result.error || 'Failed to cancel order',
};
} catch (error) {
logger.error('Order cancellation failed', { error, params });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleBinanceCancelOrder(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = BinanceCancelOrderInputSchema.parse(params);
const result = await binance_cancel_order(validatedParams);
if (result.success && result.data) {
const o = result.data.cancelledOrder;
const formattedOutput = `
Order Cancelled Successfully
${'='.repeat(35)}
Order ID: ${o.id}
Symbol: ${o.symbol}
Side: ${o.side.toUpperCase()}
Type: ${o.type.toUpperCase()}
Original Amount: ${o.amount.toFixed(8)}
Filled Before Cancel: ${o.filled.toFixed(8)}
Status: ${o.status}
`.trim();
return {
content: [{ type: 'text', text: formattedOutput }],
};
}
return {
content: [{ type: 'text', text: `Error: ${result.error}` }],
};
}

View File

@ -0,0 +1,67 @@
/**
* Logger Utility
*
* Winston-based logging for the MCP Binance Connector.
*
* @version 1.0.0
* @author OrbiQuant Trading Platform
*/
import winston from 'winston';
import { serverConfig } from '../config';
const { combine, timestamp, printf, colorize, errors } = winston.format;
// Custom log format
const logFormat = printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
});
// Create logger instance
export const logger = winston.createLogger({
level: serverConfig.logLevel,
format: combine(
errors({ stack: true }),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat
),
defaultMeta: { service: 'mcp-binance-connector' },
transports: [
// Console transport
new winston.transports.Console({
format: combine(
colorize(),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat
),
}),
],
});
// Add file transport in production
if (serverConfig.nodeEnv === 'production') {
logger.add(
new winston.transports.File({
filename: process.env.LOG_FILE || 'logs/mcp-binance.log',
maxsize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
})
);
logger.add(
new winston.transports.File({
filename: 'logs/mcp-binance-error.log',
level: 'error',
maxsize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
}
export default logger;

View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}

View File

@ -0,0 +1,31 @@
# MCP MT4 Connector Configuration
# Copy this file to .env and configure values
# ==========================================
# Server Configuration
# ==========================================
PORT=3605
NODE_ENV=development
# ==========================================
# MT4 Gateway Connection
# ==========================================
# Host where mt4-gateway is running
MT4_GATEWAY_HOST=localhost
# Port of the mt4-gateway service
MT4_GATEWAY_PORT=8081
# Authentication token for mt4-gateway
MT4_GATEWAY_AUTH_TOKEN=your-secret-token-here
# ==========================================
# Request Configuration
# ==========================================
# Timeout for requests to MT4 Gateway (ms)
REQUEST_TIMEOUT=10000
# Maximum retries for failed requests
MAX_RETRIES=3
# ==========================================
# Logging
# ==========================================
LOG_LEVEL=info

31
apps/mcp-mt4-connector/.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Dependencies
node_modules/
# Build output
dist/
# Environment files
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Test coverage
coverage/
# Temporary files
tmp/
temp/

View File

@ -0,0 +1,277 @@
# MCP MT4 Connector
**Version:** 0.1.0
**Date:** 2026-01-04
**System:** OrbiQuant Trading Platform + NEXUS v3.4 + SIMCO
---
## Description
MCP Server that exposes MetaTrader 4 (MT4) trading capabilities as tools for AI agents. This service enables AI agents to:
- Query account information
- Monitor open positions
- Execute trades (BUY/SELL)
- Manage positions (modify SL/TP, close)
- Get real-time price quotes
---
## Installation
```bash
# Navigate to the project
cd /home/isem/workspace-v1/projects/trading-platform/apps/mcp-mt4-connector
# Install dependencies
npm install
# Configure environment
cp .env.example .env
# Edit .env with your MT4 Gateway credentials
```
---
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | MCP Server port | 3605 |
| `MT4_GATEWAY_HOST` | MT4 Gateway hostname | localhost |
| `MT4_GATEWAY_PORT` | MT4 Gateway port | 8081 |
| `MT4_GATEWAY_AUTH_TOKEN` | Authentication token | secret |
| `REQUEST_TIMEOUT` | Request timeout (ms) | 10000 |
| `LOG_LEVEL` | Logging level | info |
### Example .env
```env
PORT=3605
MT4_GATEWAY_HOST=localhost
MT4_GATEWAY_PORT=8081
MT4_GATEWAY_AUTH_TOKEN=your-secure-token
REQUEST_TIMEOUT=10000
LOG_LEVEL=info
```
---
## Usage
### Start Server
```bash
# Development (with hot reload)
npm run dev
# Production
npm run build
npm start
```
### Health Check
```bash
curl http://localhost:3605/health
```
### List Available Tools
```bash
curl http://localhost:3605/tools
```
### Execute a Tool
```bash
# Get account info
curl -X POST http://localhost:3605/tools/mt4_get_account \
-H "Content-Type: application/json" \
-d '{"parameters": {}}'
# Get price quote
curl -X POST http://localhost:3605/tools/mt4_get_quote \
-H "Content-Type: application/json" \
-d '{"parameters": {"symbol": "XAUUSD"}}'
# Execute trade
curl -X POST http://localhost:3605/tools/mt4_execute_trade \
-H "Content-Type: application/json" \
-d '{"parameters": {"symbol": "XAUUSD", "action": "buy", "lots": 0.1}}'
```
---
## MCP Tools Available
| Tool | Description | Risk |
|------|-------------|------|
| `mt4_get_account` | Get account balance, equity, margin | Low |
| `mt4_get_positions` | List open positions | Low |
| `mt4_get_quote` | Get current bid/ask price | Low |
| `mt4_execute_trade` | Execute BUY/SELL order | HIGH |
| `mt4_close_position` | Close a position | HIGH |
| `mt4_modify_position` | Modify SL/TP | Medium |
---
## Project Structure
```
mcp-mt4-connector/
├── README.md # This file
├── package.json # Dependencies
├── tsconfig.json # TypeScript configuration
├── .env.example # Environment template
├── .gitignore # Git ignore rules
├── docs/
│ ├── ARCHITECTURE.md # Architecture documentation
│ └── MCP-TOOLS-SPEC.md # Detailed tool specifications
└── src/
├── index.ts # Server entry point
├── tools/
│ ├── index.ts # Tool exports
│ ├── account.ts # mt4_get_account
│ ├── positions.ts # mt4_get_positions, mt4_close_position
│ ├── trading.ts # mt4_execute_trade, mt4_modify_position
│ └── quotes.ts # mt4_get_quote
└── services/
└── mt4-client.ts # MT4 Gateway HTTP client
```
---
## Development
### Build
```bash
npm run build
```
### Type Check
```bash
npm run typecheck
```
### Lint
```bash
npm run lint
npm run lint:fix
```
### Test
```bash
npm run test
npm run test:coverage
```
---
## Integration with Claude
### MCP Configuration
Add to your Claude/MCP configuration:
```json
{
"mcpServers": {
"mt4": {
"url": "http://localhost:3605",
"transport": "http"
}
}
}
```
### Example Agent Prompts
```
"Check my MT4 account balance"
→ Uses mt4_get_account
"What's the current gold price?"
→ Uses mt4_get_quote({ symbol: "XAUUSD" })
"Buy 0.1 lots of XAUUSD with stop loss at 2640"
→ Uses mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1, stopLoss: 2640 })
"Close my profitable gold positions"
→ Uses mt4_get_positions({ symbol: "XAUUSD" }) + mt4_close_position for each
```
---
## Dependencies
### Runtime
- `express` - HTTP server
- `axios` - HTTP client
- `zod` - Input validation
- `dotenv` - Environment configuration
- `@modelcontextprotocol/sdk` - MCP protocol
### Development
- `typescript` - Type safety
- `ts-node-dev` - Development server
- `jest` - Testing framework
- `eslint` - Code linting
---
## Prerequisites
1. **MT4 Gateway** running on configured host:port
2. **MT4 Terminal** connected with EA Bridge active
3. **Node.js** >= 18.0.0
---
## Troubleshooting
### Cannot connect to MT4 Gateway
```bash
# Check if mt4-gateway is running
curl http://localhost:8081/status
# Verify environment variables
cat .env | grep MT4
```
### Tool execution fails
```bash
# Check health endpoint for dependency status
curl http://localhost:3605/health
# Check server logs
npm run dev # Logs will show in console
```
### Invalid parameters error
```bash
# Verify tool schema
curl http://localhost:3605/tools/mt4_execute_trade
# Check parameter names match schema
```
---
## References
- [MCP Protocol](https://modelcontextprotocol.io)
- MT4 Gateway: `apps/mt4-gateway/`
- SIMCO-MCP: `orchestration/directivas/simco/SIMCO-MCP.md`
- Architecture: `docs/ARCHITECTURE.md`
- Tool Specs: `docs/MCP-TOOLS-SPEC.md`
---
**Maintained by:** @PERFIL_MCP_DEVELOPER
**Project:** OrbiQuant Trading Platform

View File

@ -0,0 +1,272 @@
# MCP MT4 Connector - Architecture
**Version:** 0.1.0
**Date:** 2026-01-04
**System:** OrbiQuant Trading Platform
---
## Overview
The MCP MT4 Connector is a Model Context Protocol (MCP) server that exposes MetaTrader 4 trading capabilities as tools that AI agents can use. It acts as a bridge between MCP-compatible AI systems (like Claude) and the MT4 trading terminal.
---
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ AI Agent (Claude) │
│ │
│ "Execute a buy order for 0.1 lots of XAUUSD with SL at 2640" │
└─────────────────────────────────────┬───────────────────────────────────┘
│ MCP Protocol
┌─────────────────────────────────────────────────────────────────────────┐
│ MCP MT4 Connector (Port 3605) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Express Server │ │
│ │ │ │
│ │ /health - Health check endpoint │ │
│ │ /tools - List available tools │ │
│ │ /tools/:name - Execute specific tool │ │
│ │ /mcp/* - MCP protocol endpoints │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Tool Handlers │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
│ │ │ account.ts │ │positions.ts │ │ trading.ts │ │ quotes.ts │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MT4 Client Service │ │
│ │ │ │
│ │ - HTTP client wrapper for mt4-gateway │ │
│ │ - Request/response handling │ │
│ │ - Error management │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────┬───────────────────────────────────┘
│ HTTP/REST
┌─────────────────────────────────────────────────────────────────────────┐
│ MT4 Gateway (Port 8081) │
│ │
│ Python service that communicates with MT4 EA Bridge │
└─────────────────────────────────────┬───────────────────────────────────┘
│ Local Socket/HTTP
┌─────────────────────────────────────────────────────────────────────────┐
│ MT4 Terminal + EA Bridge │
│ │
│ Windows MT4 with Expert Advisor running │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Component Details
### 1. Express Server (`src/index.ts`)
The main entry point that:
- Hosts the MCP server on port 3605
- Provides REST endpoints for tool execution
- Implements MCP protocol endpoints
- Handles health checks and service discovery
**Endpoints:**
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Health check with MT4 connection status |
| `/tools` | GET | List all available MCP tools |
| `/tools/:name` | GET | Get specific tool schema |
| `/tools/:name` | POST | Execute tool with parameters |
| `/mcp/initialize` | POST | MCP initialization handshake |
| `/mcp/tools/list` | POST | MCP tool listing |
| `/mcp/tools/call` | POST | MCP tool execution |
### 2. Tool Handlers (`src/tools/`)
Individual tool implementations following the MCP tool pattern:
| File | Tools | Description |
|------|-------|-------------|
| `account.ts` | `mt4_get_account` | Account information retrieval |
| `positions.ts` | `mt4_get_positions`, `mt4_close_position` | Position management |
| `trading.ts` | `mt4_execute_trade`, `mt4_modify_position` | Trade execution |
| `quotes.ts` | `mt4_get_quote` | Price data retrieval |
Each tool handler:
- Defines Zod validation schemas
- Implements the core logic
- Formats responses for MCP protocol
- Handles errors gracefully
### 3. MT4 Client Service (`src/services/mt4-client.ts`)
HTTP client wrapper that:
- Manages connection to mt4-gateway
- Handles authentication (Bearer token)
- Provides typed interfaces for all operations
- Manages request timeouts and retries
---
## Data Flow
### Example: Execute Trade
```
1. Agent Request
POST /mcp/tools/call
{
"name": "mt4_execute_trade",
"arguments": {
"symbol": "XAUUSD",
"action": "buy",
"lots": 0.1,
"stopLoss": 2640,
"takeProfit": 2680
}
}
2. Tool Handler (trading.ts)
- Validates input with Zod schema
- Checks MT4 connection status
- Validates SL/TP logic
- Calls MT4Client.executeTrade()
3. MT4 Client Service
- Formats request payload
- Sends HTTP POST to mt4-gateway
- Receives and parses response
4. MT4 Gateway
- Forwards to EA Bridge
- EA executes trade on MT4
- Returns result
5. Response to Agent
{
"content": [{
"type": "text",
"text": "Trade Executed Successfully\n..."
}]
}
```
---
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | 3605 | MCP server port |
| `MT4_GATEWAY_HOST` | localhost | MT4 Gateway host |
| `MT4_GATEWAY_PORT` | 8081 | MT4 Gateway port |
| `MT4_GATEWAY_AUTH_TOKEN` | secret | Authentication token |
| `REQUEST_TIMEOUT` | 10000 | Request timeout (ms) |
| `LOG_LEVEL` | info | Logging level |
---
## Error Handling
### Error Types
1. **Connection Errors**
- MT4 Gateway unreachable
- MT4 Terminal disconnected
2. **Validation Errors**
- Invalid parameters (Zod)
- Invalid SL/TP configuration
3. **Trading Errors**
- Insufficient margin
- Market closed
- Invalid symbol
### Error Response Format
```json
{
"success": false,
"error": "Error message description"
}
```
---
## Security Considerations
1. **Authentication**
- Bearer token for mt4-gateway communication
- No external network exposure by default
2. **Validation**
- All inputs validated with Zod schemas
- Type-safe throughout the codebase
3. **Rate Limiting**
- Consider adding rate limiting for production
- Respect MT4 order frequency limits
---
## Dependencies
| Package | Purpose |
|---------|---------|
| `express` | HTTP server |
| `axios` | HTTP client |
| `zod` | Input validation |
| `dotenv` | Environment configuration |
| `@modelcontextprotocol/sdk` | MCP protocol types |
---
## Deployment
### Development
```bash
npm install
cp .env.example .env
# Edit .env with your configuration
npm run dev
```
### Production
```bash
npm run build
npm start
```
### Docker (Future)
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
EXPOSE 3605
CMD ["node", "dist/index.js"]
```
---
## References
- MCP Protocol: https://modelcontextprotocol.io
- MT4 Bridge Client: `apps/mt4-gateway/src/providers/mt4_bridge_client.py`
- Trading Platform: `projects/trading-platform/`
- SIMCO-MCP Directive: `orchestration/directivas/simco/SIMCO-MCP.md`

View File

@ -0,0 +1,428 @@
# MCP MT4 Connector - Tools Specification
**Version:** 0.1.0
**Date:** 2026-01-04
**Total Tools:** 6
---
## Tool Overview
| Tool Name | Description | Risk Level |
|-----------|-------------|------------|
| `mt4_get_account` | Get account information | Low |
| `mt4_get_positions` | List open positions | Low |
| `mt4_get_quote` | Get current price quote | Low |
| `mt4_execute_trade` | Execute market order | HIGH |
| `mt4_close_position` | Close a position | HIGH |
| `mt4_modify_position` | Modify SL/TP | Medium |
---
## mt4_get_account
### Description
Retrieves comprehensive account information from the connected MT4 terminal including balance, equity, margin, leverage, and broker details.
### Parameters
| Name | Type | Required | Description |
|------|------|----------|-------------|
| - | - | - | No parameters required |
### Return Value
```json
{
"success": true,
"data": {
"balance": 10000.00,
"equity": 10250.50,
"margin": 500.00,
"freeMargin": 9750.50,
"marginLevel": 2050.10,
"profit": 250.50,
"currency": "USD",
"leverage": 100,
"name": "Demo Account",
"server": "ICMarkets-Demo",
"company": "IC Markets"
}
}
```
### Example Usage
```typescript
// Get account info
const result = await mt4_get_account({});
// Response content:
// MT4 Account Information
// =======================
// Account Name: Demo Account
// Server: ICMarkets-Demo
// Broker: IC Markets
// Leverage: 1:100
//
// Financial Summary
// -----------------
// Balance: 10000.00 USD
// Equity: 10250.50 USD
// Profit/Loss: +250.50 USD
```
### Errors
| Code | Message | Solution |
|------|---------|----------|
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
---
## mt4_get_positions
### Description
Lists all currently open trading positions from the MT4 terminal. Can optionally filter by symbol.
### Parameters
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `symbol` | string | No | Filter positions by symbol (e.g., XAUUSD) |
### Return Value
```json
{
"success": true,
"data": {
"positions": [
{
"ticket": 123456,
"symbol": "XAUUSD",
"type": "buy",
"lots": 0.10,
"openPrice": 2650.50,
"currentPrice": 2655.00,
"stopLoss": 2640.00,
"takeProfit": 2680.00,
"profit": 45.00,
"swap": -1.20,
"openTime": "2026-01-04T10:30:00Z",
"magic": 12345,
"comment": "AI Signal"
}
],
"totalProfit": 45.00,
"count": 1
}
}
```
### Example Usage
```typescript
// Get all positions
const result = await mt4_get_positions({});
// Get only XAUUSD positions
const goldPositions = await mt4_get_positions({ symbol: "XAUUSD" });
```
### Errors
| Code | Message | Solution |
|------|---------|----------|
| - | MT4 terminal is not connected | Check MT4 Gateway connection |
---
## mt4_get_quote
### Description
Retrieves the current bid/ask prices for a trading symbol. Also calculates the spread in points/pips.
### Parameters
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD, EURUSD) |
### Return Value
```json
{
"success": true,
"data": {
"symbol": "XAUUSD",
"bid": 2650.50,
"ask": 2650.80,
"spread": 0.30,
"timestamp": "2026-01-04T12:00:00.000Z"
}
}
```
### Example Usage
```typescript
// Get gold price
const quote = await mt4_get_quote({ symbol: "XAUUSD" });
// Response content:
// Price Quote: XAUUSD
// =========================
// Bid: 2650.50
// Ask: 2650.80
// Spread: 0.30 (3.0 pips)
// Time: 2026-01-04T12:00:00.000Z
```
### Errors
| Code | Message | Solution |
|------|---------|----------|
| - | No quote data available for {symbol} | Verify symbol is available on broker |
---
## mt4_execute_trade
### Description
Opens a new trading position with a market order. Supports BUY and SELL orders with optional stop loss and take profit levels.
### Parameters
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `symbol` | string | Yes | Trading symbol (e.g., XAUUSD) |
| `action` | string | Yes | Trade direction: "buy" or "sell" |
| `lots` | number | Yes | Volume in lots (e.g., 0.01, 0.1, 1.0) |
| `stopLoss` | number | No | Stop loss price level |
| `takeProfit` | number | No | Take profit price level |
| `slippage` | number | No | Maximum slippage in points (default: 3) |
| `magic` | number | No | Magic number for EA identification (default: 12345) |
| `comment` | string | No | Order comment (max 31 chars) |
### Return Value
```json
{
"success": true,
"data": {
"success": true,
"ticket": 123456,
"message": "Order placed successfully",
"symbol": "XAUUSD",
"action": "buy",
"lots": 0.1
}
}
```
### Example Usage
```typescript
// Simple buy order
const result = await mt4_execute_trade({
symbol: "XAUUSD",
action: "buy",
lots: 0.1
});
// Buy with risk management
const result = await mt4_execute_trade({
symbol: "XAUUSD",
action: "buy",
lots: 0.1,
stopLoss: 2640.00,
takeProfit: 2680.00,
comment: "AI Signal - Gold Long"
});
// Sell order
const result = await mt4_execute_trade({
symbol: "EURUSD",
action: "sell",
lots: 0.5,
stopLoss: 1.1050,
takeProfit: 1.0900
});
```
### Validation Rules
- For BUY orders: stopLoss must be below takeProfit
- For SELL orders: stopLoss must be above takeProfit
- Lots must be positive and reasonable (max 100)
### Errors
| Code | Message | Solution |
|------|---------|----------|
| - | For BUY orders, stop loss must be below take profit | Fix SL/TP levels |
| - | For SELL orders, stop loss must be above take profit | Fix SL/TP levels |
| - | Trade execution failed | Check margin, market hours |
---
## mt4_close_position
### Description
Closes an open position by ticket number. Can optionally close only a partial volume.
### Parameters
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `ticket` | number | Yes | Position ticket number to close |
| `lots` | number | No | Partial volume to close (default: close all) |
| `slippage` | number | No | Maximum slippage in points (default: 3) |
### Return Value
```json
{
"success": true,
"data": {
"success": true,
"ticket": 123456,
"message": "Position closed"
}
}
```
### Example Usage
```typescript
// Close entire position
const result = await mt4_close_position({
ticket: 123456
});
// Close partial position (0.5 of 1.0 lots)
const result = await mt4_close_position({
ticket: 123456,
lots: 0.5
});
```
### Errors
| Code | Message | Solution |
|------|---------|----------|
| - | Position with ticket {x} not found | Verify ticket number |
| - | Requested lots exceeds position size | Reduce lots parameter |
---
## mt4_modify_position
### Description
Modifies the stop loss and/or take profit levels of an existing open position.
### Parameters
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `ticket` | number | Yes | Position ticket number to modify |
| `stopLoss` | number | No | New stop loss price level |
| `takeProfit` | number | No | New take profit price level |
### Return Value
```json
{
"success": true,
"data": {
"success": true,
"ticket": 123456,
"message": "Position modified successfully"
}
}
```
### Example Usage
```typescript
// Set both SL and TP
const result = await mt4_modify_position({
ticket: 123456,
stopLoss: 2640.00,
takeProfit: 2680.00
});
// Update only take profit (trailing)
const result = await mt4_modify_position({
ticket: 123456,
takeProfit: 2700.00
});
// Set only stop loss (risk management)
const result = await mt4_modify_position({
ticket: 123456,
stopLoss: 2650.00
});
```
### Validation Rules
- At least one of stopLoss or takeProfit must be provided
- For BUY positions: stopLoss must be below takeProfit
- For SELL positions: stopLoss must be above takeProfit
### Errors
| Code | Message | Solution |
|------|---------|----------|
| - | At least one of stopLoss or takeProfit must be provided | Add SL or TP |
| - | Position with ticket {x} not found | Verify ticket number |
| - | For BUY positions, stop loss must be below take profit | Fix SL/TP levels |
---
## Common Error Responses
### Connection Error
```json
{
"success": false,
"error": "MT4 terminal is not connected"
}
```
### Validation Error
```json
{
"success": false,
"error": "Validation error",
"details": [
{
"path": ["symbol"],
"message": "Required"
}
]
}
```
### Trading Error
```json
{
"success": false,
"error": "Trade execution failed: Insufficient margin"
}
```
---
## Usage Examples with AI Agent
### Scenario 1: Check Account and Open Trade
```
Agent: "Check my account balance and if equity is above 10000, buy 0.1 lots of XAUUSD"
1. Call mt4_get_account({})
2. Parse response, check equity > 10000
3. Call mt4_execute_trade({ symbol: "XAUUSD", action: "buy", lots: 0.1 })
```
### Scenario 2: Risk Management
```
Agent: "Set stop loss at 2640 and take profit at 2680 for my gold position"
1. Call mt4_get_positions({ symbol: "XAUUSD" })
2. Get ticket number from response
3. Call mt4_modify_position({ ticket: 123456, stopLoss: 2640, takeProfit: 2680 })
```
### Scenario 3: Close Profitable Trades
```
Agent: "Close all profitable gold positions"
1. Call mt4_get_positions({ symbol: "XAUUSD" })
2. Filter positions where profit > 0
3. For each: Call mt4_close_position({ ticket: ticketNumber })
```
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 0.1.0 | 2026-01-04 | Initial release with 6 core tools |

7170
apps/mcp-mt4-connector/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
{
"name": "mcp-mt4-connector",
"version": "0.1.0",
"description": "MCP Server for MT4 trading operations via EA Bridge",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn src/index.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"typecheck": "tsc --noEmit",
"health-check": "curl -s http://localhost:${PORT:-3605}/health || echo 'Server not running'"
},
"keywords": [
"mcp",
"model-context-protocol",
"anthropic",
"claude",
"mt4",
"metatrader",
"trading",
"forex"
],
"author": "OrbiQuant Trading Platform",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"axios": "^1.6.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node-dev": "^2.0.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,291 @@
/**
* MCP Server: MT4 Connector
*
* Exposes MT4 trading capabilities as MCP tools for AI agents.
* Communicates with mt4-gateway service to execute trading operations.
*
* @version 0.1.0
* @author OrbiQuant Trading Platform
*/
import express, { Request, Response, NextFunction } from 'express';
import dotenv from 'dotenv';
import { mcpToolSchemas, toolHandlers } from './tools';
import { getMT4Client } from './services/mt4-client';
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3605;
const SERVICE_NAME = 'mcp-mt4-connector';
const VERSION = '0.1.0';
// ==========================================
// Middleware
// ==========================================
app.use(express.json());
// Request logging
app.use((req: Request, _res: Response, next: NextFunction) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next();
});
// ==========================================
// Health & Status Endpoints
// ==========================================
/**
* Health check endpoint
*/
app.get('/health', async (_req: Request, res: Response) => {
try {
const client = getMT4Client();
const mt4Connected = await client.isConnected();
res.json({
status: 'ok',
service: SERVICE_NAME,
version: VERSION,
timestamp: new Date().toISOString(),
dependencies: {
mt4Gateway: mt4Connected ? 'connected' : 'disconnected',
},
});
} catch (error) {
res.json({
status: 'degraded',
service: SERVICE_NAME,
version: VERSION,
timestamp: new Date().toISOString(),
dependencies: {
mt4Gateway: 'error',
},
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
/**
* List available MCP tools
*/
app.get('/tools', (_req: Request, res: Response) => {
res.json({
tools: mcpToolSchemas,
count: mcpToolSchemas.length,
});
});
/**
* Get specific tool schema
*/
app.get('/tools/:toolName', (req: Request, res: Response) => {
const { toolName } = req.params;
const tool = mcpToolSchemas.find(t => t.name === toolName);
if (!tool) {
res.status(404).json({
error: `Tool '${toolName}' not found`,
availableTools: mcpToolSchemas.map(t => t.name),
});
return;
}
res.json(tool);
});
// ==========================================
// MCP Tool Execution Endpoints
// ==========================================
/**
* Execute an MCP tool
* POST /tools/:toolName
* Body: { parameters: {...} }
*/
app.post('/tools/:toolName', async (req: Request, res: Response) => {
const { toolName } = req.params;
const { parameters = {} } = req.body;
// Validate tool exists
const handler = toolHandlers[toolName];
if (!handler) {
res.status(404).json({
success: false,
error: `Tool '${toolName}' not found`,
availableTools: Object.keys(toolHandlers),
});
return;
}
try {
console.log(`[${new Date().toISOString()}] Executing tool: ${toolName}`);
console.log(`Parameters: ${JSON.stringify(parameters)}`);
const result = await handler(parameters);
res.json({
success: true,
tool: toolName,
result,
});
} catch (error) {
console.error(`[${new Date().toISOString()}] Tool error: ${toolName}`, error);
// Handle Zod validation errors
if (error && typeof error === 'object' && 'issues' in error) {
res.status(400).json({
success: false,
error: 'Validation error',
details: (error as { issues: unknown[] }).issues,
});
return;
}
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// ==========================================
// MCP Protocol Endpoints (Standard)
// ==========================================
/**
* MCP Initialize
* Returns server capabilities
*/
app.post('/mcp/initialize', (_req: Request, res: Response) => {
res.json({
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
},
serverInfo: {
name: SERVICE_NAME,
version: VERSION,
},
});
});
/**
* MCP List Tools
* Returns all available tools in MCP format
*/
app.post('/mcp/tools/list', (_req: Request, res: Response) => {
res.json({
tools: mcpToolSchemas.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
});
});
/**
* MCP Call Tool
* Execute a tool with parameters
*/
app.post('/mcp/tools/call', async (req: Request, res: Response) => {
const { name, arguments: args = {} } = req.body;
if (!name) {
res.status(400).json({
error: {
code: 'invalid_request',
message: 'Tool name is required',
},
});
return;
}
const handler = toolHandlers[name];
if (!handler) {
res.status(404).json({
error: {
code: 'unknown_tool',
message: `Tool '${name}' not found`,
},
});
return;
}
try {
const result = await handler(args);
res.json(result);
} catch (error) {
// Handle Zod validation errors
if (error && typeof error === 'object' && 'issues' in error) {
res.status(400).json({
error: {
code: 'invalid_params',
message: 'Invalid tool parameters',
data: (error as { issues: unknown[] }).issues,
},
});
return;
}
res.status(500).json({
error: {
code: 'internal_error',
message: error instanceof Error ? error.message : 'Unknown error',
},
});
}
});
// ==========================================
// Error Handler
// ==========================================
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(`[${new Date().toISOString()}] Unhandled error:`, err);
res.status(500).json({
error: 'Internal server error',
message: err.message,
});
});
// ==========================================
// Start Server
// ==========================================
app.listen(PORT, () => {
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ MCP MT4 Connector - Trading Platform ║');
console.log('╠══════════════════════════════════════════════════════════╣');
console.log(`║ Service: ${SERVICE_NAME.padEnd(45)}`);
console.log(`║ Version: ${VERSION.padEnd(45)}`);
console.log(`║ Port: ${String(PORT).padEnd(48)}`);
console.log('╠══════════════════════════════════════════════════════════╣');
console.log('║ Endpoints: ║');
console.log(`║ - Health: http://localhost:${PORT}/health`.padEnd(63) + '║');
console.log(`║ - Tools: http://localhost:${PORT}/tools`.padEnd(63) + '║');
console.log('╠══════════════════════════════════════════════════════════╣');
console.log('║ MCP Tools Available: ║');
mcpToolSchemas.forEach(tool => {
console.log(`║ - ${tool.name.padEnd(54)}`);
});
console.log('╚══════════════════════════════════════════════════════════╝');
console.log('');
});
// ==========================================
// Graceful Shutdown
// ==========================================
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down gracefully...');
process.exit(0);
});

View File

@ -0,0 +1,375 @@
/**
* MT4 Client Service
*
* HTTP client wrapper for communicating with mt4-gateway.
* This service mirrors the functionality of mt4_bridge_client.py
* but is written in TypeScript for the MCP Server.
*/
import axios, { AxiosInstance, AxiosError } from 'axios';
// ==========================================
// Types
// ==========================================
export interface MT4AccountInfo {
balance: number;
equity: number;
margin: number;
freeMargin: number;
marginLevel: number | null;
profit: number;
currency: string;
leverage: number;
name: string;
server: string;
company: string;
}
export interface MT4Position {
ticket: number;
symbol: string;
type: 'buy' | 'sell';
lots: number;
openPrice: number;
currentPrice: number;
stopLoss: number | null;
takeProfit: number | null;
profit: number;
swap: number;
openTime: string;
magic: number;
comment: string;
}
export interface MT4Tick {
symbol: string;
bid: number;
ask: number;
timestamp: string;
spread: number;
}
export interface TradeResult {
success: boolean;
ticket?: number;
message: string;
errorCode?: number;
}
export interface TradeRequest {
action: 'buy' | 'sell';
symbol: string;
lots: number;
stopLoss?: number;
takeProfit?: number;
price?: number;
slippage?: number;
magic?: number;
comment?: string;
}
export interface ClosePositionRequest {
ticket: number;
lots?: number;
slippage?: number;
}
export interface ModifyPositionRequest {
ticket: number;
stopLoss?: number;
takeProfit?: number;
}
export interface MT4ClientConfig {
host: string;
port: number;
authToken: string;
timeout?: number;
}
// ==========================================
// MT4 Client Class
// ==========================================
export class MT4Client {
private client: AxiosInstance;
private baseUrl: string;
constructor(config: MT4ClientConfig) {
this.baseUrl = `http://${config.host}:${config.port}`;
this.client = axios.create({
baseURL: this.baseUrl,
timeout: config.timeout || 10000,
headers: {
'Authorization': `Bearer ${config.authToken}`,
'Content-Type': 'application/json',
},
});
}
/**
* Check if the MT4 terminal is connected
*/
async isConnected(): Promise<boolean> {
try {
const response = await this.client.get('/status');
return response.data?.connected ?? false;
} catch {
return false;
}
}
/**
* Get MT4 account information
*/
async getAccountInfo(): Promise<MT4AccountInfo> {
try {
const response = await this.client.get('/account');
const data = response.data;
return {
balance: data.balance ?? 0,
equity: data.equity ?? 0,
margin: data.margin ?? 0,
freeMargin: data.freeMargin ?? 0,
marginLevel: data.marginLevel ?? null,
profit: data.profit ?? 0,
currency: data.currency ?? 'USD',
leverage: data.leverage ?? 100,
name: data.name ?? '',
server: data.server ?? '',
company: data.company ?? '',
};
} catch (error) {
throw this.handleError(error, 'Failed to get account info');
}
}
/**
* Get current tick (quote) for a symbol
*/
async getTick(symbol: string): Promise<MT4Tick> {
try {
const response = await this.client.get(`/tick/${symbol}`);
const data = response.data;
const bid = data.bid ?? 0;
const ask = data.ask ?? 0;
return {
symbol,
bid,
ask,
timestamp: data.time ?? new Date().toISOString(),
spread: Math.round((ask - bid) * 100000) / 100000,
};
} catch (error) {
throw this.handleError(error, `Failed to get tick for ${symbol}`);
}
}
/**
* Get all open positions
*/
async getPositions(): Promise<MT4Position[]> {
try {
const response = await this.client.get('/positions');
const data = response.data;
if (!Array.isArray(data)) {
return [];
}
return data.map((p: Record<string, unknown>) => ({
ticket: (p.ticket as number) ?? 0,
symbol: (p.symbol as string) ?? '',
type: (p.type as 'buy' | 'sell') ?? 'buy',
lots: (p.lots as number) ?? 0,
openPrice: (p.openPrice as number) ?? 0,
currentPrice: (p.currentPrice as number) ?? 0,
stopLoss: (p.stopLoss as number | null) ?? null,
takeProfit: (p.takeProfit as number | null) ?? null,
profit: (p.profit as number) ?? 0,
swap: (p.swap as number) ?? 0,
openTime: (p.openTime as string) ?? new Date().toISOString(),
magic: (p.magic as number) ?? 0,
comment: (p.comment as string) ?? '',
}));
} catch (error) {
throw this.handleError(error, 'Failed to get positions');
}
}
/**
* Get a specific position by ticket
*/
async getPosition(ticket: number): Promise<MT4Position | null> {
const positions = await this.getPositions();
return positions.find(p => p.ticket === ticket) ?? null;
}
/**
* Execute a trade (buy or sell)
*/
async executeTrade(request: TradeRequest): Promise<TradeResult> {
try {
const payload: Record<string, unknown> = {
action: request.action,
symbol: request.symbol,
lots: request.lots,
slippage: request.slippage ?? 3,
magic: request.magic ?? 12345,
comment: request.comment ?? 'MCP-MT4',
};
if (request.stopLoss !== undefined) {
payload.stopLoss = request.stopLoss;
}
if (request.takeProfit !== undefined) {
payload.takeProfit = request.takeProfit;
}
if (request.price !== undefined) {
payload.price = request.price;
}
const response = await this.client.post('/trade', payload);
const data = response.data;
return {
success: data.success ?? false,
ticket: data.ticket,
message: data.message ?? '',
errorCode: data.errorCode,
};
} catch (error) {
return {
success: false,
message: this.getErrorMessage(error),
};
}
}
/**
* Close a position
*/
async closePosition(request: ClosePositionRequest): Promise<TradeResult> {
try {
const payload: Record<string, unknown> = {
action: 'close',
ticket: request.ticket,
slippage: request.slippage ?? 3,
};
if (request.lots !== undefined) {
payload.lots = request.lots;
}
const response = await this.client.post('/trade', payload);
const data = response.data;
return {
success: data.success ?? false,
ticket: request.ticket,
message: data.message ?? '',
errorCode: data.errorCode,
};
} catch (error) {
return {
success: false,
ticket: request.ticket,
message: this.getErrorMessage(error),
};
}
}
/**
* Modify a position (SL/TP)
*/
async modifyPosition(request: ModifyPositionRequest): Promise<TradeResult> {
try {
const payload: Record<string, unknown> = {
action: 'modify',
ticket: request.ticket,
};
if (request.stopLoss !== undefined) {
payload.stopLoss = request.stopLoss;
}
if (request.takeProfit !== undefined) {
payload.takeProfit = request.takeProfit;
}
const response = await this.client.post('/trade', payload);
const data = response.data;
return {
success: data.success ?? false,
ticket: request.ticket,
message: data.message ?? '',
errorCode: data.errorCode,
};
} catch (error) {
return {
success: false,
ticket: request.ticket,
message: this.getErrorMessage(error),
};
}
}
/**
* Handle axios errors and convert to meaningful messages
*/
private handleError(error: unknown, context: string): Error {
const message = this.getErrorMessage(error);
return new Error(`${context}: ${message}`);
}
/**
* Extract error message from various error types
*/
private getErrorMessage(error: unknown): string {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
if (axiosError.response) {
return `HTTP ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data)}`;
}
if (axiosError.code === 'ECONNREFUSED') {
return 'Connection refused - MT4 Gateway is not running';
}
if (axiosError.code === 'ETIMEDOUT') {
return 'Connection timeout - MT4 Gateway is not responding';
}
return axiosError.message;
}
if (error instanceof Error) {
return error.message;
}
return 'Unknown error';
}
}
// ==========================================
// Singleton Instance
// ==========================================
let clientInstance: MT4Client | null = null;
export function getMT4Client(): MT4Client {
if (!clientInstance) {
const config: MT4ClientConfig = {
host: process.env.MT4_GATEWAY_HOST || 'localhost',
port: parseInt(process.env.MT4_GATEWAY_PORT || '8081', 10),
authToken: process.env.MT4_GATEWAY_AUTH_TOKEN || 'secret',
timeout: parseInt(process.env.REQUEST_TIMEOUT || '10000', 10),
};
clientInstance = new MT4Client(config);
}
return clientInstance;
}
export function resetMT4Client(): void {
clientInstance = null;
}

View File

@ -0,0 +1,143 @@
/**
* mt4_get_account - Get MT4 account information
*
* @description Retrieves comprehensive account information from the connected MT4 terminal
* including balance, equity, margin, leverage, and broker details.
*
* @returns Account information object with balance, equity, margin details
*
* @example
* const result = await mt4_get_account({});
* // Returns:
* // {
* // balance: 10000.00,
* // equity: 10250.50,
* // margin: 500.00,
* // freeMargin: 9750.50,
* // marginLevel: 2050.10,
* // profit: 250.50,
* // currency: "USD",
* // leverage: 100,
* // name: "Demo Account",
* // server: "ICMarkets-Demo",
* // company: "IC Markets"
* // }
*/
import { z } from 'zod';
import { getMT4Client, MT4AccountInfo } from '../services/mt4-client';
// ==========================================
// Schema Definition
// ==========================================
export const mt4GetAccountSchema = {
name: 'mt4_get_account',
description: 'Get MT4 account information including balance, equity, margin, and broker details',
inputSchema: {
type: 'object' as const,
properties: {},
required: [] as string[],
},
};
// Input validation schema (no params required)
export const Mt4GetAccountInputSchema = z.object({});
export type Mt4GetAccountInput = z.infer<typeof Mt4GetAccountInputSchema>;
// ==========================================
// Tool Implementation
// ==========================================
export interface Mt4GetAccountResult {
success: boolean;
data?: MT4AccountInfo;
error?: string;
}
export async function mt4_get_account(
_params: Mt4GetAccountInput
): Promise<Mt4GetAccountResult> {
try {
const client = getMT4Client();
// Check connection first
const isConnected = await client.isConnected();
if (!isConnected) {
return {
success: false,
error: 'MT4 terminal is not connected',
};
}
// Get account info
const accountInfo = await client.getAccountInfo();
return {
success: true,
data: accountInfo,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
// ==========================================
// MCP Tool Handler
// ==========================================
export async function handleMt4GetAccount(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
// Validate input
const validatedParams = Mt4GetAccountInputSchema.parse(params);
// Execute tool
const result = await mt4_get_account(validatedParams);
// Format response for MCP
if (result.success && result.data) {
const formattedOutput = `
MT4 Account Information
=======================
Account Name: ${result.data.name}
Server: ${result.data.server}
Broker: ${result.data.company}
Leverage: 1:${result.data.leverage}
Financial Summary
-----------------
Balance: ${result.data.balance.toFixed(2)} ${result.data.currency}
Equity: ${result.data.equity.toFixed(2)} ${result.data.currency}
Profit/Loss: ${result.data.profit >= 0 ? '+' : ''}${result.data.profit.toFixed(2)} ${result.data.currency}
Margin Details
--------------
Used Margin: ${result.data.margin.toFixed(2)} ${result.data.currency}
Free Margin: ${result.data.freeMargin.toFixed(2)} ${result.data.currency}
Margin Level: ${result.data.marginLevel !== null ? result.data.marginLevel.toFixed(2) + '%' : 'N/A'}
`.trim();
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Error: ${result.error}`,
},
],
};
}

View File

@ -0,0 +1,212 @@
/**
* MCP Tools Index
*
* Exports all MT4 MCP tools and their schemas for registration
*/
// Import handlers for use in toolHandlers map
import { handleMt4GetAccount } from './account';
import { handleMt4GetPositions, handleMt4ClosePosition } from './positions';
import { handleMt4ExecuteTrade, handleMt4ModifyPosition } from './trading';
import { handleMt4GetQuote } from './quotes';
// Account tools
export {
mt4GetAccountSchema,
mt4_get_account,
handleMt4GetAccount,
Mt4GetAccountInputSchema,
type Mt4GetAccountInput,
type Mt4GetAccountResult,
} from './account';
// Position tools
export {
mt4GetPositionsSchema,
mt4_get_positions,
handleMt4GetPositions,
Mt4GetPositionsInputSchema,
mt4ClosePositionSchema,
mt4_close_position,
handleMt4ClosePosition,
Mt4ClosePositionInputSchema,
type Mt4GetPositionsInput,
type Mt4GetPositionsResult,
type Mt4ClosePositionInput,
type Mt4ClosePositionResult,
} from './positions';
// Trading tools
export {
mt4ExecuteTradeSchema,
mt4_execute_trade,
handleMt4ExecuteTrade,
Mt4ExecuteTradeInputSchema,
mt4ModifyPositionSchema,
mt4_modify_position,
handleMt4ModifyPosition,
Mt4ModifyPositionInputSchema,
type Mt4ExecuteTradeInput,
type Mt4ExecuteTradeResult,
type Mt4ModifyPositionInput,
type Mt4ModifyPositionResult,
} from './trading';
// Quote tools
export {
mt4GetQuoteSchema,
mt4_get_quote,
handleMt4GetQuote,
Mt4GetQuoteInputSchema,
type Mt4GetQuoteInput,
type Mt4GetQuoteResult,
} from './quotes';
// ==========================================
// Tool Registry
// ==========================================
/**
* All available MCP tools with their schemas
*/
export const mcpToolSchemas = [
{
name: 'mt4_get_account',
description: 'Get MT4 account information including balance, equity, margin, and broker details',
inputSchema: {
type: 'object' as const,
properties: {},
required: [] as string[],
},
},
{
name: 'mt4_get_positions',
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
},
},
required: [] as string[],
},
},
{
name: 'mt4_execute_trade',
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
},
action: {
type: 'string',
enum: ['buy', 'sell'],
description: 'Trade direction: buy or sell',
},
lots: {
type: 'number',
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
},
stopLoss: {
type: 'number',
description: 'Optional: Stop loss price level',
},
takeProfit: {
type: 'number',
description: 'Optional: Take profit price level',
},
slippage: {
type: 'number',
description: 'Optional: Maximum slippage in points (default: 3)',
},
magic: {
type: 'number',
description: 'Optional: Magic number for EA identification (default: 12345)',
},
comment: {
type: 'string',
description: 'Optional: Order comment (max 31 chars)',
},
},
required: ['symbol', 'action', 'lots'] as string[],
},
},
{
name: 'mt4_close_position',
description: 'Close an open trading position by ticket number. Can close partially.',
inputSchema: {
type: 'object' as const,
properties: {
ticket: {
type: 'number',
description: 'Position ticket number to close',
},
lots: {
type: 'number',
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
},
slippage: {
type: 'number',
description: 'Optional: Maximum slippage in points (default: 3)',
},
},
required: ['ticket'] as string[],
},
},
{
name: 'mt4_modify_position',
description: 'Modify stop loss and/or take profit of an existing position',
inputSchema: {
type: 'object' as const,
properties: {
ticket: {
type: 'number',
description: 'Position ticket number to modify',
},
stopLoss: {
type: 'number',
description: 'New stop loss price level (optional)',
},
takeProfit: {
type: 'number',
description: 'New take profit price level (optional)',
},
},
required: ['ticket'] as string[],
},
},
{
name: 'mt4_get_quote',
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
},
},
required: ['symbol'] as string[],
},
},
];
/**
* Tool handler routing map
*/
export const toolHandlers: Record<
string,
(params: unknown) => Promise<{ content: Array<{ type: string; text: string }> }>
> = {
mt4_get_account: handleMt4GetAccount,
mt4_get_positions: handleMt4GetPositions,
mt4_execute_trade: handleMt4ExecuteTrade,
mt4_close_position: handleMt4ClosePosition,
mt4_modify_position: handleMt4ModifyPosition,
mt4_get_quote: handleMt4GetQuote,
};

View File

@ -0,0 +1,315 @@
/**
* MT4 Position Tools
*
* - mt4_get_positions: List all open positions
* - mt4_close_position: Close a specific position
*/
import { z } from 'zod';
import { getMT4Client, MT4Position, TradeResult } from '../services/mt4-client';
// ==========================================
// mt4_get_positions
// ==========================================
/**
* mt4_get_positions - List all open positions
*
* @description Retrieves all currently open positions from MT4 terminal.
* Can optionally filter by symbol.
*
* @param symbol - Optional symbol to filter positions (e.g., "XAUUSD")
* @returns Array of open positions with details
*
* @example
* const result = await mt4_get_positions({});
* // Returns all positions
*
* const result = await mt4_get_positions({ symbol: "XAUUSD" });
* // Returns only XAUUSD positions
*/
export const mt4GetPositionsSchema = {
name: 'mt4_get_positions',
description: 'List all open trading positions from MT4. Optionally filter by symbol.',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Optional: Filter positions by symbol (e.g., XAUUSD, EURUSD)',
},
},
required: [] as string[],
},
};
export const Mt4GetPositionsInputSchema = z.object({
symbol: z.string().optional(),
});
export type Mt4GetPositionsInput = z.infer<typeof Mt4GetPositionsInputSchema>;
export interface Mt4GetPositionsResult {
success: boolean;
data?: {
positions: MT4Position[];
totalProfit: number;
count: number;
};
error?: string;
}
export async function mt4_get_positions(
params: Mt4GetPositionsInput
): Promise<Mt4GetPositionsResult> {
try {
const client = getMT4Client();
// Check connection
const isConnected = await client.isConnected();
if (!isConnected) {
return {
success: false,
error: 'MT4 terminal is not connected',
};
}
// Get all positions
let positions = await client.getPositions();
// Filter by symbol if specified
if (params.symbol) {
positions = positions.filter(
p => p.symbol.toUpperCase() === params.symbol!.toUpperCase()
);
}
// Calculate total profit
const totalProfit = positions.reduce((sum, p) => sum + p.profit, 0);
return {
success: true,
data: {
positions,
totalProfit,
count: positions.length,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleMt4GetPositions(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = Mt4GetPositionsInputSchema.parse(params);
const result = await mt4_get_positions(validatedParams);
if (result.success && result.data) {
if (result.data.count === 0) {
return {
content: [
{
type: 'text',
text: params && (params as Mt4GetPositionsInput).symbol
? `No open positions found for ${(params as Mt4GetPositionsInput).symbol}`
: 'No open positions found',
},
],
};
}
const positionLines = result.data.positions.map(p => {
const direction = p.type.toUpperCase();
const profitSign = p.profit >= 0 ? '+' : '';
const slInfo = p.stopLoss !== null ? `SL: ${p.stopLoss}` : 'SL: None';
const tpInfo = p.takeProfit !== null ? `TP: ${p.takeProfit}` : 'TP: None';
return `
#${p.ticket} | ${p.symbol} | ${direction} ${p.lots} lots
Open: ${p.openPrice} | Current: ${p.currentPrice}
${slInfo} | ${tpInfo}
P/L: ${profitSign}${p.profit.toFixed(2)} | Swap: ${p.swap.toFixed(2)}
Opened: ${p.openTime}
Magic: ${p.magic} | Comment: ${p.comment || 'None'}`;
});
const formattedOutput = `
Open Positions (${result.data.count})
${'='.repeat(30)}
${positionLines.join('\n---\n')}
Total P/L: ${result.data.totalProfit >= 0 ? '+' : ''}${result.data.totalProfit.toFixed(2)}
`.trim();
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Error: ${result.error}`,
},
],
};
}
// ==========================================
// mt4_close_position
// ==========================================
/**
* mt4_close_position - Close a trading position
*
* @description Closes an open position by ticket number.
* Can optionally close partial volume.
*
* @param ticket - Position ticket number to close
* @param lots - Optional: Partial volume to close (default: close all)
* @param slippage - Optional: Maximum slippage in points (default: 3)
* @returns Trade result with success status
*
* @example
* // Close entire position
* const result = await mt4_close_position({ ticket: 123456 });
*
* // Close partial position
* const result = await mt4_close_position({ ticket: 123456, lots: 0.5 });
*/
export const mt4ClosePositionSchema = {
name: 'mt4_close_position',
description: 'Close an open trading position by ticket number. Can close partially.',
inputSchema: {
type: 'object' as const,
properties: {
ticket: {
type: 'number',
description: 'Position ticket number to close',
},
lots: {
type: 'number',
description: 'Optional: Partial volume to close. If not specified, closes entire position.',
},
slippage: {
type: 'number',
description: 'Optional: Maximum slippage in points (default: 3)',
},
},
required: ['ticket'] as string[],
},
};
export const Mt4ClosePositionInputSchema = z.object({
ticket: z.number().int().positive(),
lots: z.number().positive().optional(),
slippage: z.number().int().min(0).max(100).optional(),
});
export type Mt4ClosePositionInput = z.infer<typeof Mt4ClosePositionInputSchema>;
export interface Mt4ClosePositionResult {
success: boolean;
data?: TradeResult;
error?: string;
}
export async function mt4_close_position(
params: Mt4ClosePositionInput
): Promise<Mt4ClosePositionResult> {
try {
const client = getMT4Client();
// Check connection
const isConnected = await client.isConnected();
if (!isConnected) {
return {
success: false,
error: 'MT4 terminal is not connected',
};
}
// Verify position exists
const position = await client.getPosition(params.ticket);
if (!position) {
return {
success: false,
error: `Position with ticket ${params.ticket} not found`,
};
}
// Validate lots if specified
if (params.lots !== undefined && params.lots > position.lots) {
return {
success: false,
error: `Requested lots (${params.lots}) exceeds position size (${position.lots})`,
};
}
// Close position
const result = await client.closePosition({
ticket: params.ticket,
lots: params.lots,
slippage: params.slippage,
});
return {
success: result.success,
data: result,
error: result.success ? undefined : result.message,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleMt4ClosePosition(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = Mt4ClosePositionInputSchema.parse(params);
const result = await mt4_close_position(validatedParams);
if (result.success && result.data) {
const formattedOutput = `
Position Closed Successfully
============================
Ticket: ${validatedParams.ticket}
${validatedParams.lots ? `Closed Volume: ${validatedParams.lots} lots` : 'Closed: Entire position'}
Message: ${result.data.message || 'Position closed'}
`.trim();
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Error closing position: ${result.error}`,
},
],
};
}

View File

@ -0,0 +1,193 @@
/**
* mt4_get_quote - Get current price quote for a symbol
*
* @description Retrieves the current bid/ask prices for a trading symbol.
* Also calculates the spread in points.
*
* @param symbol - Trading symbol to get quote for (e.g., "XAUUSD")
* @returns Current bid, ask, spread, and timestamp
*
* @example
* const result = await mt4_get_quote({ symbol: "XAUUSD" });
* // Returns:
* // {
* // symbol: "XAUUSD",
* // bid: 2650.50,
* // ask: 2650.80,
* // spread: 0.30,
* // timestamp: "2026-01-04T12:00:00.000Z"
* // }
*/
import { z } from 'zod';
import { getMT4Client, MT4Tick } from '../services/mt4-client';
// ==========================================
// Schema Definition
// ==========================================
export const mt4GetQuoteSchema = {
name: 'mt4_get_quote',
description: 'Get current price quote (bid/ask/spread) for a trading symbol',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading symbol to get quote for (e.g., XAUUSD, EURUSD, GBPUSD)',
},
},
required: ['symbol'] as string[],
},
};
export const Mt4GetQuoteInputSchema = z.object({
symbol: z.string().min(1).max(20),
});
export type Mt4GetQuoteInput = z.infer<typeof Mt4GetQuoteInputSchema>;
// ==========================================
// Tool Implementation
// ==========================================
export interface Mt4GetQuoteResult {
success: boolean;
data?: MT4Tick;
error?: string;
}
export async function mt4_get_quote(
params: Mt4GetQuoteInput
): Promise<Mt4GetQuoteResult> {
try {
const client = getMT4Client();
// Check connection
const isConnected = await client.isConnected();
if (!isConnected) {
return {
success: false,
error: 'MT4 terminal is not connected',
};
}
// Get tick data
const tick = await client.getTick(params.symbol.toUpperCase());
// Validate we got valid data
if (tick.bid === 0 && tick.ask === 0) {
return {
success: false,
error: `No quote data available for ${params.symbol}. Symbol may not be available on this broker.`,
};
}
return {
success: true,
data: tick,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
// ==========================================
// MCP Tool Handler
// ==========================================
export async function handleMt4GetQuote(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = Mt4GetQuoteInputSchema.parse(params);
const result = await mt4_get_quote(validatedParams);
if (result.success && result.data) {
// Determine decimal places based on symbol
const decimals = getDecimalPlaces(result.data.symbol);
const spreadPips = calculateSpreadPips(result.data.spread, result.data.symbol);
const formattedOutput = `
Price Quote: ${result.data.symbol}
${'='.repeat(25)}
Bid: ${result.data.bid.toFixed(decimals)}
Ask: ${result.data.ask.toFixed(decimals)}
Spread: ${result.data.spread.toFixed(decimals)} (${spreadPips.toFixed(1)} pips)
Time: ${result.data.timestamp}
`.trim();
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Error: ${result.error}`,
},
],
};
}
// ==========================================
// Helper Functions
// ==========================================
/**
* Determine decimal places based on symbol type
*/
function getDecimalPlaces(symbol: string): number {
const upperSymbol = symbol.toUpperCase();
// JPY pairs have 3 decimals, most forex 5 decimals
if (upperSymbol.includes('JPY')) {
return 3;
}
// Gold and metals typically 2 decimals
if (upperSymbol.startsWith('XAU') || upperSymbol.startsWith('XAG')) {
return 2;
}
// Indices vary, use 2 as default
if (
upperSymbol.includes('US30') ||
upperSymbol.includes('US500') ||
upperSymbol.includes('NAS')
) {
return 2;
}
// Default forex pairs
return 5;
}
/**
* Calculate spread in pips based on symbol
*/
function calculateSpreadPips(spread: number, symbol: string): number {
const upperSymbol = symbol.toUpperCase();
// JPY pairs: 1 pip = 0.01
if (upperSymbol.includes('JPY')) {
return spread * 100;
}
// Gold: 1 pip = 0.10
if (upperSymbol.startsWith('XAU')) {
return spread * 10;
}
// Default forex: 1 pip = 0.0001
return spread * 10000;
}

View File

@ -0,0 +1,402 @@
/**
* MT4 Trading Tools
*
* - mt4_execute_trade: Execute BUY/SELL market orders
* - mt4_modify_position: Modify SL/TP of existing positions
*/
import { z } from 'zod';
import { getMT4Client, TradeResult } from '../services/mt4-client';
// ==========================================
// mt4_execute_trade
// ==========================================
/**
* mt4_execute_trade - Execute a market order (BUY or SELL)
*
* @description Opens a new trading position with optional SL/TP levels.
* Supports market orders only (pending orders not implemented).
*
* @param symbol - Trading symbol (e.g., "XAUUSD", "EURUSD")
* @param action - Trade direction: "buy" or "sell"
* @param lots - Volume in lots (e.g., 0.01, 0.1, 1.0)
* @param stopLoss - Optional stop loss price
* @param takeProfit - Optional take profit price
* @param slippage - Optional max slippage in points (default: 3)
* @param magic - Optional magic number for EA identification
* @param comment - Optional order comment
* @returns Trade result with ticket number
*
* @example
* // Simple buy order
* const result = await mt4_execute_trade({
* symbol: "XAUUSD",
* action: "buy",
* lots: 0.1
* });
*
* // Buy with SL/TP
* const result = await mt4_execute_trade({
* symbol: "XAUUSD",
* action: "buy",
* lots: 0.1,
* stopLoss: 2640.00,
* takeProfit: 2680.00,
* comment: "AI Signal"
* });
*/
export const mt4ExecuteTradeSchema = {
name: 'mt4_execute_trade',
description: 'Execute a market order (BUY or SELL) on MT4 with optional stop loss and take profit',
inputSchema: {
type: 'object' as const,
properties: {
symbol: {
type: 'string',
description: 'Trading symbol (e.g., XAUUSD, EURUSD, GBPUSD)',
},
action: {
type: 'string',
enum: ['buy', 'sell'],
description: 'Trade direction: buy or sell',
},
lots: {
type: 'number',
description: 'Volume in lots (e.g., 0.01, 0.1, 1.0)',
},
stopLoss: {
type: 'number',
description: 'Optional: Stop loss price level',
},
takeProfit: {
type: 'number',
description: 'Optional: Take profit price level',
},
slippage: {
type: 'number',
description: 'Optional: Maximum slippage in points (default: 3)',
},
magic: {
type: 'number',
description: 'Optional: Magic number for EA identification (default: 12345)',
},
comment: {
type: 'string',
description: 'Optional: Order comment (max 31 chars)',
},
},
required: ['symbol', 'action', 'lots'] as string[],
},
};
export const Mt4ExecuteTradeInputSchema = z.object({
symbol: z.string().min(1).max(20),
action: z.enum(['buy', 'sell']),
lots: z.number().positive().max(100),
stopLoss: z.number().positive().optional(),
takeProfit: z.number().positive().optional(),
slippage: z.number().int().min(0).max(100).optional(),
magic: z.number().int().optional(),
comment: z.string().max(31).optional(),
});
export type Mt4ExecuteTradeInput = z.infer<typeof Mt4ExecuteTradeInputSchema>;
export interface Mt4ExecuteTradeResult {
success: boolean;
data?: TradeResult & {
symbol: string;
action: string;
lots: number;
};
error?: string;
}
export async function mt4_execute_trade(
params: Mt4ExecuteTradeInput
): Promise<Mt4ExecuteTradeResult> {
try {
const client = getMT4Client();
// Check connection
const isConnected = await client.isConnected();
if (!isConnected) {
return {
success: false,
error: 'MT4 terminal is not connected',
};
}
// Validate SL/TP logic (basic check)
if (params.stopLoss !== undefined && params.takeProfit !== undefined) {
if (params.action === 'buy') {
// For buy: SL should be below current price, TP above
if (params.stopLoss >= params.takeProfit) {
return {
success: false,
error: 'For BUY orders, stop loss must be below take profit',
};
}
} else {
// For sell: SL should be above current price, TP below
if (params.stopLoss <= params.takeProfit) {
return {
success: false,
error: 'For SELL orders, stop loss must be above take profit',
};
}
}
}
// Execute trade
const result = await client.executeTrade({
symbol: params.symbol.toUpperCase(),
action: params.action,
lots: params.lots,
stopLoss: params.stopLoss,
takeProfit: params.takeProfit,
slippage: params.slippage,
magic: params.magic,
comment: params.comment,
});
if (result.success) {
return {
success: true,
data: {
...result,
symbol: params.symbol.toUpperCase(),
action: params.action,
lots: params.lots,
},
};
}
return {
success: false,
error: result.message || 'Trade execution failed',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleMt4ExecuteTrade(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = Mt4ExecuteTradeInputSchema.parse(params);
const result = await mt4_execute_trade(validatedParams);
if (result.success && result.data) {
const formattedOutput = `
Trade Executed Successfully
===========================
Ticket: #${result.data.ticket}
Symbol: ${result.data.symbol}
Direction: ${result.data.action.toUpperCase()}
Volume: ${result.data.lots} lots
${validatedParams.stopLoss ? `Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Not set'}
${validatedParams.takeProfit ? `Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Not set'}
Message: ${result.data.message || 'Order placed successfully'}
`.trim();
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Error executing trade: ${result.error}`,
},
],
};
}
// ==========================================
// mt4_modify_position
// ==========================================
/**
* mt4_modify_position - Modify SL/TP of an existing position
*
* @description Updates the stop loss and/or take profit levels of an open position.
*
* @param ticket - Position ticket number to modify
* @param stopLoss - New stop loss price (null to remove)
* @param takeProfit - New take profit price (null to remove)
* @returns Modification result
*
* @example
* // Set both SL and TP
* const result = await mt4_modify_position({
* ticket: 123456,
* stopLoss: 2640.00,
* takeProfit: 2680.00
* });
*
* // Update only TP
* const result = await mt4_modify_position({
* ticket: 123456,
* takeProfit: 2700.00
* });
*/
export const mt4ModifyPositionSchema = {
name: 'mt4_modify_position',
description: 'Modify stop loss and/or take profit of an existing position',
inputSchema: {
type: 'object' as const,
properties: {
ticket: {
type: 'number',
description: 'Position ticket number to modify',
},
stopLoss: {
type: 'number',
description: 'New stop loss price level (optional)',
},
takeProfit: {
type: 'number',
description: 'New take profit price level (optional)',
},
},
required: ['ticket'] as string[],
},
};
export const Mt4ModifyPositionInputSchema = z.object({
ticket: z.number().int().positive(),
stopLoss: z.number().positive().optional(),
takeProfit: z.number().positive().optional(),
});
export type Mt4ModifyPositionInput = z.infer<typeof Mt4ModifyPositionInputSchema>;
export interface Mt4ModifyPositionResult {
success: boolean;
data?: TradeResult;
error?: string;
}
export async function mt4_modify_position(
params: Mt4ModifyPositionInput
): Promise<Mt4ModifyPositionResult> {
try {
const client = getMT4Client();
// Check connection
const isConnected = await client.isConnected();
if (!isConnected) {
return {
success: false,
error: 'MT4 terminal is not connected',
};
}
// Validate at least one parameter is provided
if (params.stopLoss === undefined && params.takeProfit === undefined) {
return {
success: false,
error: 'At least one of stopLoss or takeProfit must be provided',
};
}
// Verify position exists
const position = await client.getPosition(params.ticket);
if (!position) {
return {
success: false,
error: `Position with ticket ${params.ticket} not found`,
};
}
// Validate SL/TP based on position type
const sl = params.stopLoss;
const tp = params.takeProfit;
if (sl !== undefined && tp !== undefined) {
if (position.type === 'buy') {
if (sl >= tp) {
return {
success: false,
error: 'For BUY positions, stop loss must be below take profit',
};
}
} else {
if (sl <= tp) {
return {
success: false,
error: 'For SELL positions, stop loss must be above take profit',
};
}
}
}
// Modify position
const result = await client.modifyPosition({
ticket: params.ticket,
stopLoss: params.stopLoss,
takeProfit: params.takeProfit,
});
return {
success: result.success,
data: result,
error: result.success ? undefined : result.message,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
export async function handleMt4ModifyPosition(
params: unknown
): Promise<{ content: Array<{ type: string; text: string }> }> {
const validatedParams = Mt4ModifyPositionInputSchema.parse(params);
const result = await mt4_modify_position(validatedParams);
if (result.success && result.data) {
const formattedOutput = `
Position Modified Successfully
==============================
Ticket: #${validatedParams.ticket}
${validatedParams.stopLoss !== undefined ? `New Stop Loss: ${validatedParams.stopLoss}` : 'Stop Loss: Unchanged'}
${validatedParams.takeProfit !== undefined ? `New Take Profit: ${validatedParams.takeProfit}` : 'Take Profit: Unchanged'}
Message: ${result.data.message || 'Position modified successfully'}
`.trim();
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Error modifying position: ${result.error}`,
},
],
};
}

View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}

View File

@ -13,23 +13,23 @@ services:
# =========================================================================== # ===========================================================================
postgres: postgres:
image: postgres:15-alpine image: postgres:16-alpine
container_name: orbiquant-postgres container_name: orbiquantia-postgres
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_DB: orbiquant_trading POSTGRES_DB: orbiquantia_platform
POSTGRES_USER: orbiquant_user POSTGRES_USER: orbiquantia
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_dev_2025}
POSTGRES_INITDB_ARGS: "-E UTF8" POSTGRES_INITDB_ARGS: "-E UTF8"
ports: ports:
- "${POSTGRES_PORT:-5432}:5432" - "${POSTGRES_PORT:-5433}: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 orbiquant_user -d orbiquant_trading"] test: ["CMD-SHELL", "pg_isready -U orbiquantia -d orbiquantia_platform"]
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: orbiquant_trading DB_NAME: orbiquantia_platform
DB_USER: orbiquant_user DB_USER: orbiquantia
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025} DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_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: orbiquant_trading DB_NAME: orbiquantia_platform
DB_USER: orbiquant_user DB_USER: orbiquantia
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025} DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_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: orbiquant_trading DB_NAME: orbiquantia_platform
DB_USER: orbiquant_user DB_USER: orbiquantia
DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquant_dev_2025} DB_PASSWORD: ${POSTGRES_PASSWORD:-orbiquantia_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,3 +1,12 @@
---
id: "NOTA-DISCREPANCIA-PUERTOS-2025-12-08"
title: "Discrepancia de Puertos Detectada"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# NOTA: Discrepancia de Puertos Detectada # NOTA: Discrepancia de Puertos Detectada
**Fecha:** 2025-12-08 **Fecha:** 2025-12-08

View File

@ -1,3 +1,12 @@
---
id: "ARQUITECTURA-GENERAL"
title: "Arquitectura General - OrbiQuant IA"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# Arquitectura General - OrbiQuant IA # Arquitectura General - OrbiQuant IA
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -0,0 +1,414 @@
Modulo 1
Elementos básicos
Mercado de derivados y OTC
Derivados
El mercado de derivados es donde se negocian instrumentos financieros cuyo valor depende (o deriva) del precio de otro activo, como acciones, divisas, materias primas, índices, etc.
Algunos ejemplos de derivados son:
• Futuros
• Opciones
• CFDs (Contratos por diferencia)
• Swaps
OTC
OTC significa "fuera de mercado" o "sobre el mostrador". Es decir, son operaciones que no pasan por una bolsa formal como la de Nueva York o Chicago.
En el mercado OTC:
• Las operaciones se hacen directamente entre dos partes, como un trader y un broker.
• Hay más flexibilidad en los contratos (pueden ser a medida).
• Es común en productos como CFDs, divisas (forex), swaps, etc.
Tipos de gráficos
1.- Gráfico de líneas
• Qué muestra: Solo el precio de cierre de cada periodo (por ejemplo, cada día, hora o minuto).
• Cómo se ve: Una línea continua que conecta los precios de cierre.
• Ventajas:
o Simple y limpio, ideal para ver la tendencia general.
o Útil para principiantes que no quieren sobrecargarse de información.
• Desventaja: No muestra el rango completo de precios (apertura, máximo y mínimo).
• Ejemplo 1:
2.- Gráfico de barras
• Qué muestra: También OHLC, como las velas, pero de forma diferente.
• Cómo se ve:
o Una línea vertical representa el rango (mínimo a máximo).
o Una línea corta a la izquierda marca la apertura.
o Una línea corta a la derecha marca el cierre.
• Ventajas:
o Más compacto que las velas, puede mostrar más datos en menos espacio.
• Desventaja: Menos visual y atractivo que las velas japonesas.
3.- Gráfico de velas japonesas
• Qué muestra: Apertura, máximo, mínimo y cierre (OHLC) de cada periodo.
• Cómo se ve: Cada vela tiene un cuerpo (de color verde o rojo) y "mechas" arriba y abajo.
o Cuerpo: distancia entre el precio de apertura y cierre.
o Mechas: muestran los precios más altos y bajos alcanzados.
• Ventajas:
o Muy visual, permite detectar patrones de comportamiento del mercado.
o Ideal para análisis técnico.
• Desventaja: Puede parecer complejo al principio por la cantidad de información.
Siendo este último gráfico el más relevante para nosotros, pues será el principal que observaremos al estar en la operativa.
Velas japonesas
Material de apoyo, ejemplos y su uso:
Modulo 2
2.- Factores y herramientas
Tipos de broker
ECN (Electronic Communication Network)
• Cómo funciona: Te conecta directamente con una red de participantes del mercado (otros traders, bancos, instituciones). Tú operas con precios reales del mercado.
• Ventaja: Spreads ultra bajos, alta transparencia, sin mesa de negociación.
• Desventaja: Comisiones por operación, puede requerir mayor capital mínimo.
• Ideal para: Traders profesionales, scalpers, o quienes quieren máxima precisión.
STP (Straight Through Processing)
• Cómo funciona: El broker no opera contra ti, sino que pasa tus órdenes directamente a sus proveedores de liquidez (como bancos o instituciones grandes).
• Ventaja: Menos conflicto de interés, spreads más realistas.
• Desventaja: Puede haber comisiones por operación, y en momentos de alta volatilidad la ejecución puede no ser tan instantánea.
• Ideal para: Traders intermedios o con enfoque más técnico.
Market Maker (creador de mercado)
• Cómo funciona: El broker crea el mercado interno. Es decir, toma el otro lado de tu operación. Si tú compras, ellos venden, y viceversa.
• Ventaja: Spreads bajos, ejecución rápida, acceso con poco capital.
• Desventaja: Puede haber conflicto de interés (porque ganan cuando tú pierdes, aunque muchos están regulados para evitar abusos).
• Ideal para: Traders principiantes o con capital pequeño.
Híbrido (STP + Market Maker)
• Muchos brokers usan un modelo mixto. Por ejemplo, pueden ejecutar operaciones pequeñas como Market Maker y operaciones grandes pasarlas a proveedores de liquidez (STP).
• Buscan ofrecer lo mejor de ambos mundos: costos bajos y buena ejecución.
Regulaciones perspectiva global
# Regulador País / Región Nivel de Protección Comentarios clave
1 FCA (Financial Conduct Authority) Reino Unido 🇬🇧 Muy alto 🟢🟢🟢 Supervisión estricta, protección de fondos, política clara contra conflictos.
2 ASIC (Australian Securities & Investments Commission) Australia 🇦🇺 Muy alto 🟢🟢🟢 Requiere transparencia, separación de fondos, protección al retail.
3 CySEC (Cyprus Securities and Exchange Commission) Chipre 🇨🇾 (UE) Alto 🟢🟢 Muy común en brokers europeos, cumple con normativa MiFID II.
4 BaFin (Federal Financial Supervisory Authority) Alemania 🇩🇪 Alto 🟢🟢 Muy sólida, regulaciones conservadoras, parte de la UE.
5 CNMV (Comisión Nacional del Mercado de Valores) España 🇪🇸 Alto 🟢🟢 Buen nivel de supervisión, dentro del marco europeo.
6 FINMA (Swiss Financial Market Supervisory Authority) Suiza 🇨🇭 Alto 🟢🟢 Muy confiable, aunque pocos brokers CFDs están bajo esta licencia.
7 CFTC / NFA (Commodity Futures Trading Commission / National Futures Association) EE.UU. 🇺🇸 Muy alto 🟢🟢🟢 pero... Muy estricta, pero no permite CFDs para retail en EE.UU.
8 DFSA (Dubai Financial Services Authority) Dubái 🇦🇪 Medio 🟡 Creciendo en prestigio, aún en desarrollo.
9 FSA (Financial Services Authority) Seychelles / Mauricio / Belice 🌴 Bajo 🔴 Regulación suave, común en brokers offshore. Cuidado.
10 No regulado / Offshore sin supervisión Ninguno Muy bajo 🔴🔴🔴 Riesgo muy alto. No recomendable.
Terminal operativa (Metatrader 4 y 5)
¿Qué es MetaTrader (MT4 y MT5)?
MetaTrader es una plataforma de trading muy popular que permite operar en mercados como forex, CFDs, acciones, índices, criptos, etc.
• MT4: Lanzado en 2005. Muy usado en forex y CFDs.
• MT5: Lanzado en 2010. Más moderno, con más funciones y acceso a más mercados (acciones reales, futuros, etc.).
Funciones Básicas (tanto en MT4 como en MT5)
Función Descripción
📊 Gráficos en tiempo real Puedes analizar el movimiento del precio con velas, líneas o barras.
🧰 Indicadores técnicos Vienen con indicadores como RSI, MACD, Medias Móviles, etc.
🎯 Órdenes de compra/venta Ejecuta operaciones con distintos tipos de órdenes (market, stop, limit).
🧱 Trading automático (EA) Puedes usar o crear Expert Advisors para operar de forma automática.
🗂️ Gestión de múltiples activos Puedes operar varios pares o instrumentos al mismo tiempo.
📝 Historial y análisis Ver resultados de tus operaciones pasadas y analizar tu rendimiento.
Diferencias clave entre MT4 y MT5
Característica MT4 MT5
🛠️ Año de lanzamiento 2005 2010
🎯 Mercados disponibles Principalmente Forex y CFDs Forex, CFDs, acciones, futuros, criptos
🧠 Tipo de arquitectura 32 bits 64 bits (más rápido y eficiente)
📅 Calendario económico ❌ No integrado ✅ Integrado
🧮 Tipos de órdenes 4 tipos 6 tipos (más flexibilidad)
📊 Timeframes de gráficos 9 21 (más opciones para análisis)
💻 Lenguaje de programación MQL4 (más limitado) MQL5 (más potente, tipo C++)
🔄 Compatibilidad con EAs Solo EAs de MT4 Solo EAs de MT5 (no son intercambiables)
Terminal de análisis (Tradingview)
¿Qué es TradingView?
TradingView es una plataforma en línea para análisis técnico y social. Permite ver gráficos en tiempo real de acciones, criptomonedas, forex, índices y más, desde cualquier navegador o app.
Es muy popular entre traders por su interfaz intuitiva, herramientas visuales y comunidad activa.
Funciones básicas e importantes
Función Descripción
📈 Gráficos interactivos Personalizables, con múltiples tipos: velas, líneas, barras, Heikin Ashi…
🧰 Indicadores técnicos Vienen más de 100 por defecto (RSI, MACD, EMA, etc.)
✏️ Herramientas de dibujo Líneas, retrocesos de Fibonacci, canales, patrones armónicos, etc.
⏱️ Múltiples marcos de tiempo Desde segundos hasta meses
💬 Ideas de la comunidad Miles de análisis públicos de otros traders para inspirarte o aprender
🧠 Pine Script (avanzado) Lenguaje para crear tus propios indicadores o estrategias automáticas
🔔 Alertas Te avisa cuando el precio cumple una condición que tú defines
🖥️ Multipantalla y diseño Puedes ver varios activos a la vez, en la misma pantalla
Nota: Existe una versión gratuita y otra de paga, difieren en su potencia y accesibilidad a herramientas, etc.
Para nuestro uso inicial usaremos la versión gratuita que nos permite operar perfectamente funcional.
Tipos de cuenta
ECN, Standard, Cent, Zero, FixAPI y Social Trading (PAMM y Copy)
Creación de cuentas y conexión a terminal operativa
Crea una cuenta en un broker 100% B2B, hiper regulado, con las mejores condiciones de mercado y retiros instantáneos.
CXM una excelente opción para operar con confianza y precisión.
link de apertura ⬇⬇⬇
CXM
QR de apertura ⬇⬇⬇
Modulo 3
Introducción al IPDA
Interbank Price Delivery Algorithmic, estructura y narrativa del precio
Comencemos con la pregunta inicial base ⬇️⬇️⬇️
🧠 ¿Qué es el IPDA (Interbank Price Delivery Algorithmic).pdf
¿Cómo es visualmente?
El IPDA crea fractales en formación A+B+C
Formación de fractal alcista
Formación de fractal bajista
Durante la creación de un fractal completo, cuando los puntos ABC están listos para el siguiente movimiento del precio, sucede una actualización en la estructura del IPDA para el siguiente fractal que se forme (fractal 2), donde el punto C se transforma en un Nuevo punto A (Ai).
Al ser superado el anterior punto B, iniciamos la creación de un nuevo fractal Ai + Bi + Ci
Ejemplo:
Fractal alcista 1 completado, fractal alcista 2 en formación
Fractal bajista 1 completado, fractal bajista 2 en formación
Es así como llegamos a la siguiente nomenclatura que nos ayudará a crear una narrativa inicial del precio y entender la tendencia.
Modificamos A+B+C por HH, HL, LH, LL
Tabla descriptiva:
Ejemplo visual comparativa de HH y HL (tendencia alcista)
Ejemplo visual de LL y LH (tendencia bajista)
🎯 En resumen:
• HH + HL = Mercado subiendo con fuerza (tendencia alcista).
• LH + LL = Mercado cayendo (tendencia bajista).
• Mezcla de ambos = Rango o posible cambio de tendencia.
Swing High y Swing Low
Durante la creación de la estructura del precio, tenemos nuevos máximos y nuevos mínimos respectivamente, pero para determinar si dicho punto es o no valido para la estructura debemos incluir nueva información, información disponible en el grafico de velas japonesas.
¿Qué es un Swing High y un Swing Low?
Término Definición simple
Swing High Es un pico o punto más alto, rodeado por dos velas a su izquierda y derecha con máximos más bajos.
Swing Low Es un valle o punto más bajo, rodeado por dos velas a su izquierda y derecha con mínimos más altos.
Swing High
Es una vela alcista seguida de 2 velas bajistas
Swing Low
Es una vela bajista seguida de 2 velas alcistas
Existe un aumento de relevancia Segun el time frame, mientras más alto sea el time frame en el que se observan las velas, más contundente podría ser nuestro Swing High/Low respectivamente.
Switch Market (Shift Market), BOS y BIS
🧠 GLOSARIO ICT
Concepto Significado básico
BOS (Break of Structure) El mercado confirma la continuidad de la tendencia actual.
SHIFT (Market Structure Shift) El mercado cambia su dirección: de alcista a bajista o viceversa.
BIOS (Bias) Es la dirección más probable en que se moverá el mercado (tu “norte”).
Switch Market Es cuando el mercado hace un SHIFT, cambia de tendencia (ej. de bullish a bearish), y te da señales claras para entrar en sentido contrario.
🔄 ¿Qué es un Market Structure Shift (o "Switch Market")?
Un SHIFT es cuando el mercado rompe la estructura contraria.
Ejemplo:
• El mercado iba haciendo HH y HL (alcista).
• Pero rompe un HL anterior → esto no es BOS, es un cambio: ahora hay intención bajista.
📌 El SHIFT es una señal de que algo cambió, y puede indicar una reversión.
Ejemplo visual Switch Market bajista
Ejemplo visual Switch Market Alcista
¿Qué es un BOS (Break of Structure)?
Imagina que el mercado está haciendo Higher Highs y Higher Lows (tendencia alcista).
Cuando rompe el último Higher High (HH) y hace un nuevo HH más alto → eso es un BOS alcista.
Es una confirmación de que el precio sigue su rumbo.
▶️ ¿Qué confirma un BOS?
• Que la tendencia continúa.
• Que el smart money sigue en control en esa dirección.
Ejemplo Visual BOS bajista creado por un SW Market y seguido de BIS
Modulo 4
1.- Tendencia
Se dice que la tendencia es tu amiga o aliada, se tiene una tendencia cuando un conjunto de máximos y mínimos tienen una misma orientación.
¿Qué es la tendencia en los mercados financieros?
La tendencia es la dirección general en la que se mueve el precio de un activo durante un periodo de tiempo. Hay tres tipos principales:
1. Tendencia alcista (bullish): el precio sube y forma mínimos y máximos cada vez más altos.
o Ejemplo: El precio pasa de 100 → 105 → 110 → 115.
2. Tendencia bajista (bearish): el precio baja y forma mínimos y máximos cada vez más bajos.
o Ejemplo: El precio cae de 100 → 95 → 90 → 85.
3. Tendencia lateral (rango o consolidación): el precio no tiene una dirección clara, se mueve entre un soporte y una resistencia.
o Ejemplo: El precio oscila entre 98 y 102 por varios días.
Otros detalles importantes:
• Una tendencia no significa que el precio suba o baje siempre en línea recta. Hay retrocesos (correcciones) normales dentro de una tendencia.
• La temporalidad importa: algo puede estar bajista en 5 minutos y alcista en 1 hora.
• Las tendencias fuertes suelen respetar zonas clave de soporte o resistencia.
• Nunca operes en contra de la tendencia, a menos que tengas una estrategia avanzada (no recomendada para principiantes).
2.- Key levels (KL, KLi, KLii)
Es definido como un precio especifico en el que los agentes institucionales inyectan liquidez a la estructura del precio, suelen ser atractores del precio, y también detonadores de nuevas tendencias. Son precios que suelen tener terminación en 00 y hasta 000.
¿Como se verían visualmente?
aquí hay un ejemplo de cómo se ven los KL en el índice Nasdaq, grafico semanal.
Una vez detectado un KL, es posible crear una proyección interna sobre otros precios relevante en la estructura macro, tomando como referencia los KL ya negociados buscaremos el 50% de esa área, y una vez identificado el punto medio es importante destacar un nuevo KL interno (KLi) mismo precio que tendrá un uso similar a su sucesor macro, ya que son atractores del precio e inyectores de liquidez.
¿Como se vería visualmente?
Aquí un ejemplo de cómo se ven los KLi en el índice Nasdaq, conservando los KL previos
Como punto de acceso adicional, existen precios usados como confluencia adicional, para el uso de la estrategia los definimos como Key Level interno de grado inferior (KLii), y se encuentran en el 50% del area disponible entre KL y KLi.
¿Cómo se verían visualmente?
Aquí un ejemplo de cómo se ven los KLii en el índice Nasdaq, conservando los KL y KLi previos
3.- Power of three
¿Qué es el Power of Three (POT)?
El Power of Three es un patrón de comportamiento del precio durante una sesión o ciclo de mercado, que refleja la manipulación institucional intencional del precio antes de su verdadero movimiento. Según la teoría del IPDA, los grandes participantes (instituciones, bancos, algoritmos interbancarios) manipulan el mercado para maximizar liquidez y tomar el lado opuesto del público minorista.
Las 3 Fases del Power of Three (AMD)
1. Acumulación (Accumulation):
o El precio se mueve de forma lateral o con pequeñas trampas.
o Objetivo: Crear una zona de liquidez o engañar al trader minorista.
o Se acumulan órdenes de compra/venta en zonas clave (como zonas de liquidez, equal highs/lows, etc.).
2. Manipulación
o Aquí el precio se mueve hacia la dirección institucional real, rompiendo estructuras o zonas manipuladas.
o Este movimiento busca capturar la liquidez que se generó en la fase de acumulación.
3. Distribución - Expansión (Expansión / Real Move; Distribution / Reversal or Take Profit):
o Movimiento impulsivo fuerte en una dirección (el "true move").
o El precio desacelera o se revierte.
o Objetivo: Cerrar operaciones institucionales, tomar beneficios, o preparar una nueva trampa.
o Suele terminar el ciclo diario o la sesión, y puede dar inicio a otro patrón POT.
Esquema visual de líneas POT/AMD, en un proceso de creación de un fractal con orientación alcista.
Esquema visual de líneas POT/AMD, en un proceso de creación de un fractal con orientación bajista.
Ejemplo X: AMD/POT alcista con volumen
Detalles clave desde la teoría IPDA:
• El precio es entregado a través de algoritmos interbancarios diseñados para buscar liquidez y ejecutar órdenes institucionales.
• El POT refleja la lógica detrás de este algoritmo: crear desequilibrio (imbalance), inducir a los trades a cometer un error (liquidez inducida), ejecutar el movimiento real (expansión), y luego estabilizar.
• IPDA y POT van de la mano en el análisis de cómo se mueve el precio en función del "Smart Money" y nuestro método/lenguaje de estudio ICT.
4.- Matrices IPDA
Aqui es donde comienza a ponerse interesante, pues una vez que tengamos claro como leer una Matriz IPDA, es posible que la idea de que esto es un mercado aleatorio comienza a no tener tanto sentido y por el contrario comienza un entendimiento aún más profundo de la estructura.
Una matriz está compuesta por distintos factores, los principales son:
1.- Fractal en formación
2.- Un punto máximo, un punto mínimo y un punto de equilibrio
3.- Temporalidad
4.- Zona Premium y zona Discount
¿Como se ve una matriz en el grafico?
Aquí un ejemplo visual de una matriz que ya ha completado su fractal y adicional a ello a culminado con el movimiento expansivo.
Al ser una matriz alcista una vez completado el fractal e ingresado en zona de Discount es posible determinar que el siguiente movimiento será alcista
Mismo ejemplo usando POT/AMD, durante la creación de dicho fractal alcista.
4.1.- Uniendo las primeras piezas del juego
Tomando en consideración las confluencias que conocemos hasta este momento, podríamos ingresar en un trade alcista (una compra; BUY), usando nuevamente el Ejemplo X:
1.- Ubicamos la zona de acumulación con los LL iguales creados al cierre del día anterior.
2.- Una vez ha sido superado ese nivel, trazamos los LH y LL de dicho movimiento (manipulación).
3.- El Switch market es nuestra referencia y nuestra ventaja estadística para la operativa, cuando lo observamos trazamos la matriz con una caja de GANN sin vertices o un fibonacci (indicadores disponibles en todas las terminales)
4.- Tomaremos el trade una vez haya creado un descuento o retroceso a la zona Discount IPDA, en la matriz creada por el ultimo LL y el switch market.
5.- EL Stop Loss (SL) irá por debajo del último LL, creado en la manipulación y el Take Profit (TP) ira exactamente a 2 veces el SL, es decir un trade donde arriesgamos 1 y buscamos ganar 2.
Trade 1 a 2
Módulo 5
Killer zone y momentum
1.- Killer zone:
Son los horarios que competen a la apertura y cierre de las principales bolsas de valores del mundo.
Bolsa de Valores Apertura Cierre Zona horaria
Asia 17:00 20:59 (UTC -6)
London 23:00 02:59 (UTC -6)
New York 07:30 12:00 (UTC -6)
La Killer zone es una de las confluencias más importantes para la toma de Trades de alta probabilidad, pues es el horario cuando las “manos más grandes están dentro del juego” buscando oportunidades.
Durante la creación de la estructura, el precio tiene horarios específicos de terminaos por el IPDA, y son importantes ya que suelen ser puntos de atracción del precio y puntos de reacción del precio.
Aquí los enlistamos e iremos añadiendo a un gráfico de timeframe de 1 minuto, para una mejor comprensión sobre su uso, y visualización.
1.- New York Open (07:30 UTC -6).
Una vela relevante pues a partir del OPEN de la primera vela del primer minuto en este horario, inicia también el mercado “real”, en el CME inician las cotizaciones.
2.- Inicio IPDA, Real inicio del mercado, NY (22:00 UTC -6).
Una vela relevante, en el OPEN de dicha vela representa el reinicio de los algoritmos bancarios implícitos en el IPDA.
3.- High y Low de la Sesión Asiática (17:00 - 20:59 UTC-6) y Sesión de Londres (23:00 - 02:59 UTC -6)
Representa la liquidez creada durante la sesión de trading 100% electrónico, y nos da el previo a la Kill zone de New york, mismas que tendrán distinta relevancia según el activo que estés operando, y destacamos New York, ya que es la principal sesión, con mayor liquidez (según el activo).
4.- Liquidez adicional: High y Low de Lunch NY (09:30 - 11:29 UTC -6)
Un rango donde suelen haber continuaciones, regresos y expansiones, un rango realmente importante para el desarrollo del día y la sesión.
5.- Last Cumulation LAC (13:15 - 13:44 UTC -6)
El precio se prepara para el cierre, y suele crear liquidez o tomarla.
6.- Movement complete MOC (13:50 - 13:59 UTC -6)
Durante este breve “rango final”, suele tener las últimas extensiones o momento de volatilidad antes del cierre de bolsa
Módulo 6
Order Blocks
Son zonas del gráfico donde las manos grandes (bancos, fondos de inversión, etc.) han colocado órdenes importantes, ya sea de compra o de venta, tiene distintos timeframes y distintas interpretaciones.
¿Qué caracteriza a un Order Block?
Un Order Block típicamente es:
• La última vela contraria antes de un movimiento fuerte del precio.
o Por ejemplo, una vela bajista antes de una gran subida (order block de compra).
o O una vela alcista antes de una gran caída (order block de venta).
• Zona de consolidación previa al rompimiento.
• Lugar donde probablemente se activaron muchas órdenes institucionales.
¿Por qué son útiles?
• Pueden funcionar como zonas de entrada precisas, con buen riesgo-beneficio.
• Marcan niveles de interés institucional, donde podría haber reversión o continuación.
• Ayudan a evitar entrar en zonas manipuladas.
Tipos de Order Blocks
• Order Block High Timeframe (OB)
• Order Block Low Timeframe (OB)
• Order Block en Quiebre (OBQ)
• Killer Block (KOB)
• Ghost Block (GOB)
• Propulsion Block (POB)
Todos los anteriores los tendrás disponibles en cada Timeframe, su interpretación depende de su tamaño, volumen, estructura, tiempo de creación, ETC.
Hablemos de los Order Blocks de High time frame, los encontraremos en el grafico semanal (W), Diario (D), y 4 horas (4H).
Estos bloques a pesar de ser enormes y para nada recomendables para buscar una entrada por el tamaño que tendría tu prospecto Stop Loss (en una estrategia Intraday), si te pueden ayudar a tener una primera orientación sobre la posible dirección del día, tomando en cuenta el activo que estemos analizando.
Crucial Timeframe, o mejor dicho los timeframe que usaremos para considerar tomar una posición en el mercado (1H, 15m). en este caso los OB que encontremos, sí tendrán oportunidad de darnos acceso a un trade de probabilidad y de riesgo bien gestionado usando la estructura del precio a nuestro favor, y fungen excelente en todo tipo de estrategias.
Ejemplo visual, estructura actual Nasdaq (01/06/25) podemos observar en este grafico de Timeframe Diario, que tenemos actualmente 2 Order blocks (OB) alcistas y 2 Order blocks En quiebre (OBQ) siendo 1 alcista y 1 bajista.
La estructura se va creando entorno a estos bloques, mismos que delimitan e impulsan el precio en una dirección según sea su interpretación
Ejemplo visual mismo activo Nasdaq (01/06/25) en un time frame de 4H podemos observar la estructura creándose internamente con los OB ahora disponibles.
Cada time frame tendrá acceso a distintos OB dentro de los OB del time frame superior, dándote una mayor perspectiva de cual podría ser el siguiente movimiento del precio, y también dándote acceso a nuevos objetivos y zonas de posible entrada.
Order Block en Quiebre (OBQ)
El quiebre de estructura o de OB, hace referencia a momentos del mercado donde la tendencia actual es cambiada por el inicio de un proceso de giro o cambio de tendencia tras una toma de liquidez relevante (proceso de absorción; profundizaremos en el tema más adelante en los siguientes módulos), es decir se crea un Shift Market, y para tener una confluencia adicional que confirme el giro, este debe ser acompañado por un OBQ, para tener la primera referencia visual.
Ejemplo visual, donde únicamente conservare los OBQ del anterior ejemplo con Nasdaq
Un OBQ es válido si una vela de su mismo time frame tiene un Close por debajo o por encima (respectivamente) del Open de la vela en cuestión OBQ, adicional al cierre como ya habíamos mencionado, mientras más alto sea el timeframe en el que estamos observando el OBQ en cuestión, será de mayor probabilidad para una posible entrada y mientras menor sea el timeframe habrá una mayor probabilidad de que el precio NO respete la zona.
Ejemplo visual OBQ en timeframe de 1H, tras un proceso de absorción, donde el precio supera los anteriores máximos, para posteriormente hacer el quiebre del Bloque que inició el movimiento, Nasdaq (01/06/25)
Killer Block (KOB)
Es un bloque especial, importante ya que suele ser muy protagonista en el movimiento actual y en un futuro suele ser testeado en distintas ocasiones, ya que posee liquidez prácticamente garantizada.
Su característica principal, es que suele crear un HH o un LL, y marca el final de una tendencia.
Ejemplo visual usando el anterior con Nasdaq time frame 1H, podemos observar en el máximo nuevo creado tras el proceso de absorción o toma de liquidez de máximos anteriores, se crea en la cima un KOB.
Ghost Block (GOB)
Caracterizado por ser difícil de detectar, únicamente dejando rastro de mechas y su contundencia y relevancia para toma de decisiones es de grado menor, conocerlos como una confluencia más, pienso que puede ser positivo.
Ejemplo visual usando el anterior ejemplo, donde se presentó un GOB en 1H, Nasdaq (01/06/25), y aunque no fue el principal factor del movimiento alcista, si formo parte del protagonismo del movimiento.
Propulsion Block (POB)
Ejemplo visual de POB, timeframe 4H, el precio crea su estructura y presenta distintos tipos de bloques, pero el re-testeo del POB, crea la oportunidad de entrar en un fuerte movimiento bajista. Se caracteriza por ser una vela contraria al movimiento expansivo actual, y su re-testeo suele indicar trade de alta probabilidad.
Módulo 7
Imbalances
Fair Value Gap
Se refiere a un desequilibrio o ineficiencia en el precio que ocurre cuando el mercado se mueve bruscamente en una dirección, dejando un vacío entre velas consecutivas sin que se hayan ejecutado transacciones en esa zona. Este hueco representa una zona donde no hubo oferta y demanda equilibradas.
Este mismo será la base para comprender el resto de los tipos de imbalances disponibles en el chart (grafico), ya que su interacción, una vez que han sido creados, puede tener distintas interpretaciones, lo que se vuelve un tema profundo.
Un FVG está representado por 3 velas, donde la vela 2 es la creadora del movimiento expansivo y las velas 1 y 3 con sus respectivas mechas, en confluencia con el cuerpo de la vela 2, forman el high y el low de nuestro FVG.
Ejemplo visual, FVG bajista.
Ejemplo visual, FVG alcista.
El FVG es una buena confluencia para la creación de una estrategia, ya que suelen ser importantes zonas de reacción y atractores del precio. el IPDA está programado para balancear las órdenes. y crear momentum berish o bullish (bajista o alcista). donde no hubo oportunidad de tenerlo.
El FVG está presente en todas las temporalidades, y su interpretación dependerá de:
• La estructura actual del precio.
• Tendencia macro (1h y 4h).
iFVG (inversión Fair Value Gap)
Es una transformación del FVG cotidiano, la característica principal de este imbalance es que el precio a cruzado delante del FVG inicial, no respetando la interpretación inicial y usándolo precisamente al revés.
Es decir, un FVG alcista al convertirse en iFVG podríamos considerar que su interpretación ahora será bajista.
Y lo mismo si el FVG inicial fuera bajista al transformarse en iFVG ahora su interpretación deberá ser alcista
Ejemplo visual iFVG bajista
Ejemplo visual iFVG alcista
BPR (Balance Price Range)
Al igual que el iFVG su interpretación deberá modificarse según el testeo de este imbalance, la diferencia con el iFVG únicamente se centra en que el BPR es “respetado” en esencia de imbalance, a pesar de ser cruzado por completo por enfrente por parte del precio
Ejemplo visual BPR
VFVG (Volume Fair Value Gap)
Representa un momento en el gráfico donde hubo un cierre y una apertura de 2 velas consecutivas, con una diferencia de puntos que crea este imbalance, pero con la cualidad de que las mechas de dichas velas coinciden dentro del imbalance y el cierre y apertura de las velas creadoras, son el máximo y el mínimo respectivamente para este imbalance.
Suelen ser muy esporádicos, y suelen aparecer en momentos de mucha volatilidad, representando así un atractor y zona de reacción para el precio.
Ejemplo visual de VFVG
NWOG (New Weekly Open Gap)
Su aparición tiene en esencia una importante participación durante el desarrollo de la vela semanal y de la estructura semanal. Suele ser un catalizador y también uno de los atractores del precio con mayor relevancia.
Durante la apertura de la bolsa Asiática el día Domingo, podemos observar el NWOG, es muy importante tenerlo en cuenta si se requiere tener un BIAS semanal con claridad.
Ejemplo visual de NWOG
NWOG creado de viernes a domingo.
NDOG (New Daily Open Gap)
Suele aparecer cuando la sesión americana termina con un movimiento expansivo o se tiene un proceso de alta volatilidad o se requiere tomar un punto de liquidez sin oportunidad de nuevos trades.
Al reiniciar la sesión asiática, sin haber cambio de semana, el NDOG suele ser importante, pero con menor nivel de relevancia que el NWOG.
Ejemplo visual de NDOG
NDOG creado de jueves para viernes durante el desarrollo semanal

View File

@ -1,3 +1,12 @@
---
id: "STACK-TECNOLOGICO"
title: "Stack Tecnologico - OrbiQuant IA"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# Stack Tecnologico - OrbiQuant IA # Stack Tecnologico - OrbiQuant IA
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,12 @@
---
id: "VISION-PRODUCTO"
title: "Vision del Producto - OrbiQuant IA"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# Vision del Producto - OrbiQuant IA # Vision del Producto - OrbiQuant IA
**Version:** 1.0.0 **Version:** 1.0.0
@ -115,6 +124,87 @@ Empoderar a personas de todos los niveles de experiencia para que puedan inverti
**Objetivo:** Aprender de la comunidad y validar estrategias. **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
@ -229,3 +319,6 @@ 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,3 +1,11 @@
---
id: "MAP-00-vision-general"
title: "Mapa de 00-vision-general"
type: "Index"
project: "trading-platform"
updated_date: "2026-01-04"
---
# _MAP: Vision General # _MAP: Vision General
**Ultima actualizacion:** 2025-12-05 **Ultima actualizacion:** 2025-12-05

View File

@ -0,0 +1,252 @@
---
id: "ARQUITECTURA-INTEGRACION-MT4-MCP-LLM"
title: "Arquitectura de Integracion MT4-MCP-LLM"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
created_date: "2026-01-04"
---
# Arquitectura de Integracion MT4-MCP-LLM
**Version:** 1.0.0
**Fecha:** 2026-01-04
**Autor:** Tech-Leader Agent
**Estado:** En Desarrollo
---
## Vision General
Este documento describe la arquitectura de integracion entre:
- **MetaTrader4 (MT4)** - Plataforma de trading forex
- **MCP Server** - Model Context Protocol para herramientas
- **LLM Agent** - Copiloto de trading con IA
- **ML Engine** - Modelos de prediccion de mercado
---
## Diagrama de Arquitectura
```
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ ORBIQUANT IA - TRADING PLATFORM ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ USER INTERFACES │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Web App │ │ Chat Widget │ │ Telegram Bot │ │ CLI │ │ │
│ │ │ (React UI) │ │ (LLM UI) │ │ (Future) │ │ (Future) │ │ │
│ │ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │ │
│ └──────────┼──────────────────┼──────────────────┼──────────────────┼─────────────┘ │
│ │ │ │ │ │
│ └──────────────────┼──────────────────┴──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ API GATEWAY (Backend Express) │ │
│ │ :3000 │ │
│ └────────────────────────────────────┬────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┼────────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ LLM AGENT │ │ ML ENGINE │ │ TRADING AGENTS │ │
│ │ :8003 │ │ :8001 │ │ :8004 │ │
│ │ │ │ │ │ │ │
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌────────┐ ┌────────┐ │ │
│ │ │ Ollama LLM │ │ │ │ AMD Detector │ │ │ │ Atlas │ │ Orion │ │ │
│ │ │ Llama 3 8B │ │ │ │ RangePredict │ │ │ │ (Cons) │ │ (Mod) │ │ │
│ │ │ (GPU) │ │ │ │ TPSLClass │ │ │ └────────┘ └────────┘ │ │
│ │ └──────────────┘ │ │ └──────────────┘ │ │ │ │
│ │ │ │ │ │ ┌────────┐ │ │
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │ Nova │ │ │
│ │ │Trading Tools │◄┼─────┼─│ Signal API │ │ │ │ (Aggr) │ │ │
│ │ │ - MT4 Tools │ │ │ └──────────────┘ │ │ └────────┘ │ │
│ │ │ - ML Tools │ │ │ │ │ │ │
│ │ │ - Strategy │ │ │ │ │ │ │
│ │ └──────────────┘ │ │ │ │ │ │
│ └────────┬─────────┘ └──────────────────┘ └──────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ MCP SERVER MT4 CONNECTOR │ │
│ │ :3605 │ │
│ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ MCP Tools: │ │ │
│ │ │ - mt4_get_account - mt4_execute_trade - mt4_get_quote │ │ │
│ │ │ - mt4_get_positions - mt4_close_position - mt4_modify_position │ │ │
│ │ └────────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ MT4 GATEWAY SERVICE │ │
│ │ :8005 │ │
│ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ FastAPI Endpoints: │ │ │
│ │ │ - GET /api/mt4/account - GET /api/mt4/positions │ │ │
│ │ │ - POST /api/mt4/trade - DELETE /api/mt4/positions/{id} │ │ │
│ │ │ - GET /api/mt4/tick/{symbol} - PUT /api/mt4/positions/{id} │ │ │
│ │ └─────────────────────────────────────┬──────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────────────────────────▼──────────────────────────────────┐ │ │
│ │ │ MT4 Bridge Client (aiohttp) │ │ │
│ │ │ - Comunicacion con EA Bridge │ │ │
│ │ └────────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ │ HTTP/WebSocket │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ METATRADER 4 TERMINALS │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ EA Bridge #1 │ │ EA Bridge #2 │ │ EA Bridge #3 │ │ │
│ │ │ (IC Markets) │ │ (Pepperstone) │ │ (XM) │ │ │
│ │ │ :8081 │ │ :8082 │ │ :8083 │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ PostgreSQL │ │ Redis │ │ Binance API │ │ Market Data │ │ │
│ │ │ :5432 │ │ :6379 │ │ (external) │ │ (external) │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────┘
```
---
## Flujo de Operaciones
### 1. Flujo de Chat con LLM
```
Usuario (Chat)
LLM Agent (:8003)
├── Procesa mensaje
├── Detecta intencion
¿Requiere Tool?
├─[SI]─► Ejecuta Tool
│ │
│ ├── MT4 Tool ──► MCP Server ──► MT4 Gateway ──► MT4
│ ├── ML Tool ──► ML Engine
│ └── Strategy Tool ──► Trading Agents
│ │
│ ▼
│ Procesa resultado
└─[NO]─► Genera respuesta directa
Respuesta al Usuario
```
### 2. Flujo de Ejecucion de Trade
```
LLM Agent (Tool: execute_trade)
MCP Server MT4 (:3605)
│ (Valida request, formatea)
MT4 Gateway (:8005)
│ (Prepara orden, risk check)
MT4 Bridge Client
│ (HTTP POST /trade)
EA Bridge (MT4 Terminal)
│ (Ejecuta orden)
Broker Server
│ (Confirma ejecucion)
Response ──► MT4 Gateway ──► MCP Server ──► LLM Agent ──► Usuario
```
---
## Puertos de Servicios
| Servicio | Puerto | Descripcion |
|----------|--------|-------------|
| Frontend | 5173 | React UI (Vite) |
| Backend API | 3000 | Express.js Gateway |
| ML Engine | 8001 | FastAPI - Modelos ML |
| Data Service | 8002 | FastAPI - Market Data |
| LLM Agent | 8003 | FastAPI - Copiloto AI |
| Trading Agents | 8004 | FastAPI - Atlas/Orion/Nova |
| MT4 Gateway | 8005 | FastAPI - Bridge MT4 |
| MCP Server MT4 | 3605 | MCP Tools MT4 |
| PostgreSQL | 5432 | Base de datos |
| Redis | 6379 | Cache y sesiones |
| Ollama | 11434 | LLM Server (GPU) |
| EA Bridge #1 | 8081 | MT4 Terminal 1 |
| EA Bridge #2 | 8082 | MT4 Terminal 2 |
| EA Bridge #3 | 8083 | MT4 Terminal 3 |
---
## Componentes Implementados
### Completados
| Componente | Ubicacion | Estado |
|------------|-----------|--------|
| MT4 Bridge Client | `apps/mt4-gateway/src/providers/mt4_bridge_client.py` | OK |
| LLM Tools MT4 | `apps/llm-agent/src/tools/mt4_tools.py` | OK |
| LLM Tools ML | `apps/llm-agent/src/tools/ml_tools.py` | OK |
| ML Engine | `apps/ml-engine/` | OK |
| Trading Agents | `apps/trading-agents/` | OK |
### En Desarrollo
| Componente | Ubicacion | Estado |
|------------|-----------|--------|
| MT4 Gateway API | `apps/mt4-gateway/src/api/` | En progreso |
| MCP Server MT4 | `apps/mcp-mt4-connector/` | En progreso |
| Fine-tuning Pipeline | `apps/llm-agent/fine-tuning/` | En progreso |
| Strategy Analysis Tools | `apps/llm-agent/src/tools/strategy_analysis.py` | En progreso |
---
## Seguridad
### Autenticacion
- JWT tokens para API Gateway
- API keys para servicios internos
- Auth token para EA Bridge
### Validaciones
- Risk checks antes de ejecutar trades
- Validacion de volumenes y simbolos
- Rate limiting en endpoints criticos
---
## Referencias
- `docs/01-arquitectura/INTEGRACION-METATRADER4.md`
- `docs/01-arquitectura/INTEGRACION-LLM-LOCAL.md`
- `orchestration/PROXIMA-ACCION.md`
- `core/mcp-servers/README.md`
---
*Documento generado por Tech-Leader Agent*
*OrbiQuant IA Trading Platform*

View File

@ -1,3 +1,12 @@
---
id: "ARQUITECTURA-MULTI-AGENTE-MT4"
title: "Arquitectura Multi-Agente MT4"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# Arquitectura Multi-Agente MT4 # 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,3 +1,12 @@
---
id: "INTEGRACION-API-MASSIVE"
title: "Integracion API Massive - Pipeline de Datos"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# Integracion API Massive - Pipeline de Datos # 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,3 +1,12 @@
---
id: "INTEGRACION-LLM-LOCAL"
title: "Integracion LLM Local - chatgpt-oss 16GB"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# Integracion LLM Local - chatgpt-oss 16GB # Integracion LLM Local - chatgpt-oss 16GB
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,12 @@
---
id: "INTEGRACION-METATRADER4"
title: "Integracion MetaTrader4 via MetaAPI"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# Integracion MetaTrader4 via MetaAPI # 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,3 +1,12 @@
---
id: "README"
title: "Fundamentos y Autenticación"
type: "Documentation"
project: "trading-platform"
version: "1.0.0"
updated_date: "2026-01-04"
---
# OQI-001: Fundamentos y Autenticación # OQI-001: Fundamentos y Autenticación
## Resumen Ejecutivo ## Resumen Ejecutivo

View File

@ -1,3 +1,11 @@
---
id: "MAP-OQI-001-fundamentos-auth"
title: "Mapa de OQI-001-fundamentos-auth"
type: "Index"
project: "trading-platform"
updated_date: "2026-01-04"
---
# _MAP: OQI-001 - Fundamentos y Autenticación # _MAP: OQI-001 - Fundamentos y Autenticación
**Ultima actualizacion:** 2025-12-05 **Ultima actualizacion:** 2025-12-05

View File

@ -1,3 +1,15 @@
---
id: "ET-AUTH-001"
title: "OAuth Providers Implementation"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-001"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-AUTH-001: Especificación Técnica - OAuth Providers # ET-AUTH-001: Especificación Técnica - OAuth Providers
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,15 @@
---
id: "ET-AUTH-002"
title: "JWT Tokens Implementation"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-002"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-AUTH-002: Especificación Técnica - JWT Tokens # ET-AUTH-002: Especificación Técnica - JWT Tokens
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,15 @@
---
id: "ET-AUTH-003"
title: "Database Schema for Auth"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-002"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-AUTH-003: Especificación Técnica - Esquema de Base de Datos # ET-AUTH-003: Especificación Técnica - Esquema de Base de Datos
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,15 @@
---
id: "ET-AUTH-004"
title: "API Endpoints for Auth"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-002"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-AUTH-004: Especificación Técnica - API Endpoints # ET-AUTH-004: Especificación Técnica - API Endpoints
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,15 @@
---
id: "ET-AUTH-005"
title: "Security Implementation"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-005"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# ET-AUTH-005: Especificación Técnica - Seguridad # ET-AUTH-005: Especificación Técnica - Seguridad
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,15 @@
---
id: "US-AUTH-001"
title: "Registro con Email"
type: "User Story"
status: "Done"
priority: "Alta"
epic: "OQI-001"
story_points: 5
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# US-AUTH-001: Registro con Email # US-AUTH-001: Registro con Email
**Version:** 1.0.0 **Version:** 1.0.0

View File

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

View File

@ -1,3 +1,15 @@
---
id: "US-AUTH-003"
title: "Login con Google"
type: "User Story"
status: "Done"
priority: "Alta"
epic: "OQI-001"
story_points: 5
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# US-AUTH-003: Login con Google # US-AUTH-003: Login con Google
**Version:** 1.0.0 **Version:** 1.0.0

View File

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

View File

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

View File

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

View File

@ -1,307 +1,319 @@
# US-AUTH-007: OAuth GitHub ---
id: "US-AUTH-007"
**Version:** 1.0.0 title: "OAuth GitHub"
**Fecha:** 2025-12-05 type: "User Story"
**Estado:** Pendiente status: "To Do"
**Story Points:** 3 priority: "Media"
**Prioridad:** P2 (Media) epic: "OQI-001"
**Épica:** [OQI-001](../_MAP.md) story_points: 3
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
**Quiero** poder registrarme e iniciar sesión usando mi cuenta de GitHub **Version:** 1.0.0
**Para** tener un acceso rápido usando mis credenciales de desarrollador **Fecha:** 2025-12-05
**Estado:** Pendiente
--- **Story Points:** 3
**Prioridad:** P2 (Media)
## Criterios de Aceptación **Épica:** [OQI-001](../_MAP.md)
### AC-001: Botón de GitHub visible ---
**Dado** que estoy en la página de registro o login ## Historia de Usuario
**Cuando** veo las opciones de autenticación
**Entonces** debería ver un botón "Continuar con GitHub" **Como** desarrollador o usuario técnico de OrbiQuant
**Y** debería tener el logo oficial de GitHub **Quiero** poder registrarme e iniciar sesión usando mi cuenta 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"
**Cuando** se abre la ventana de GitHub ## Criterios de Aceptación
**Entonces** debería:
1. Ver la pantalla de autorización de GitHub ### AC-001: Botón de GitHub visible
2. Poder revisar los permisos solicitados
3. Poder autorizar la aplicación **Dado** que estoy en la página de registro o login
**Cuando** veo las opciones de autenticación
### AC-003: Permisos solicitados **Entonces** debería ver un botón "Continuar con GitHub"
**Y** debería tener el logo oficial de GitHub
**Dado** que estoy en la pantalla de autorización de GitHub
**Cuando** reviso los permisos ### AC-002: Flujo de OAuth
**Entonces** la app debería solicitar únicamente:
- `user:email` (para leer email) **Dado** que hago click en "Continuar con GitHub"
- `read:user` (para leer perfil básico) **Cuando** se abre la ventana de GitHub
**Entonces** debería:
### AC-004: Primer registro exitoso 1. Ver la pantalla de autorización de GitHub
2. Poder revisar los permisos solicitados
**Dado** que es mi primera vez usando GitHub OAuth 3. Poder autorizar la aplicación
**Cuando** autorizo los permisos
**Entonces** debería: ### AC-003: Permisos solicitados
1. Crear mi cuenta automáticamente
2. Recibir un JWT token **Dado** que estoy en la pantalla de autorización de GitHub
3. Ser redirigido al dashboard **Cuando** reviso los permisos
4. Ver mi nombre y avatar de GitHub **Entonces** la app debería solicitar únicamente:
5. Usar mi email primario de GitHub - `user:email` (para leer email)
- `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
**Cuando** autorizo la aplicación **Dado** que es mi primera vez usando GitHub OAuth
**Entonces** debería usar mi email noreply de GitHub **Cuando** autorizo los permisos
**O** solicitar un email alternativo **Entonces** debería:
1. Crear mi cuenta automáticamente
### AC-006: Login existente 2. Recibir un JWT token
3. Ser redirigido al dashboard
**Dado** que ya tengo una cuenta vinculada con GitHub 4. Ver mi nombre y avatar de GitHub
**Cuando** uso "Continuar con GitHub" 5. Usar mi email primario de GitHub
**Entonces** debería:
1. Iniciar sesión automáticamente ### AC-005: Email primario privado
2. Ser redirigido al dashboard
**Dado** que tengo mi email configurado como privado en GitHub
### AC-007: Múltiples emails en GitHub **Cuando** autorizo la aplicación
**Entonces** debería usar mi email noreply de GitHub
**Dado** que tengo múltiples emails en mi cuenta de GitHub **O** solicitar un email alternativo
**Cuando** autorizo la aplicación
**Entonces** debería usar el email marcado como primario ### AC-006: Login existente
**Y** debería verificar que no esté ya registrado
**Dado** que ya tengo una cuenta vinculada con GitHub
### AC-008: Cancelación del flujo **Cuando** uso "Continuar con GitHub"
**Entonces** debería:
**Dado** que inicio el flujo de GitHub OAuth 1. Iniciar sesión automáticamente
**Cuando** cancelo en la ventana de GitHub 2. Ser redirigido al dashboard
**Entonces** debería volver a login/registro
**Y** ver mensaje "Autenticación cancelada" ### AC-007: Múltiples emails en GitHub
--- **Dado** que tengo múltiples emails en mi cuenta de GitHub
**Cuando** autorizo la aplicación
## Mockup **Entonces** debería usar el email marcado como primario
**Y** debería verificar que no esté ya registrado
```
┌─────────────────────────────────────────────────────────────┐ ### AC-008: Cancelación del flujo
│ │
│ 🌟 Bienvenido a OrbiQuant │ **Dado** que inicio el flujo de GitHub OAuth
│ │ **Cuando** cancelo en la ventana de GitHub
│ ┌─────────────────────────────────────────────────────┐ │ **Entonces** debería volver a login/registro
│ │ 📧 Email │ │ **Y** ver mensaje "Autenticación cancelada"
│ └─────────────────────────────────────────────────────┘ │
│ │ ---
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🔴 Continuar con Google │ │ ## Mockup
│ └─────────────────────────────────────────────────────┘ │
│ │ ```
│ [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 │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
│ OrbiQuant by OrbiQuant Team │ │ │ ⚫ Continuar con GitHub │ │
│ wants to access your juanperez account │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ This application will be able to: │ └─────────────────────────────────────────────────────────────┘
│ │
│ ✓ Verify your GitHub identity │ Ventana de GitHub OAuth:
│ ✓ Read your email addresses │ ┌─────────────────────────────────────────────────────────────┐
│ ✓ Read your profile information │ │ github.com ✕ │
│ │ ├─────────────────────────────────────────────────────────────┤
│ Authorizing will redirect to: │ │ │
│ https://orbiquant.com/api/v1/auth/github/callback │ │ Authorize OrbiQuant │
│ │ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ OrbiQuant by OrbiQuant Team │
│ │ Authorize OrbiQuant │ │ │ wants to access your juanperez account │
│ └─────────────────────────────────────────────────────┘ │ │ │
│ │ │ 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 │ │
│ ┌─────────────────────────────────────────────────────┐ │
### Database (DB) │ │ Authorize OrbiQuant │ │
│ └─────────────────────────────────────────────────────┘ │
- [ ] Agregar campos a tabla `users`: │ │
```sql │ Cancel │
ALTER TABLE users ADD COLUMN github_id VARCHAR(255) UNIQUE; │ │
ALTER TABLE users ADD COLUMN github_username VARCHAR(255); └─────────────────────────────────────────────────────────────┘
``` ```
- [ ] Usar tabla `oauth_connections` existente
- [ ] Índice en `github_id` ---
### Backend (BE) ## Tareas Técnicas
- [ ] Crear GitHub OAuth App ### Database (DB)
- [ ] Obtener Client ID y Client Secret
- [ ] Endpoint `GET /api/v1/auth/github` - [ ] Agregar campos a tabla `users`:
- Redirige a GitHub OAuth ```sql
- [ ] Endpoint `GET /api/v1/auth/github/callback` ALTER TABLE users ADD COLUMN github_id VARCHAR(255) UNIQUE;
- Recibe código de autorización ALTER TABLE users ADD COLUMN github_username VARCHAR(255);
- Intercambia por access token ```
- Obtiene perfil del usuario - [ ] Usar tabla `oauth_connections` existente
- Obtiene emails del usuario - [ ] Índice en `github_id`
- Selecciona email primario
- Crea o actualiza usuario ### Backend (BE)
- Genera JWT token
- [ ] Service `GitHubOAuthService` - [ ] Crear GitHub OAuth App
- `getAuthorizationUrl()` - [ ] Obtener Client ID y Client Secret
- `exchangeCodeForToken()` - [ ] Endpoint `GET /api/v1/auth/github`
- `getUserProfile()` - Redirige a GitHub OAuth
- `getUserEmails()` - [ ] Endpoint `GET /api/v1/auth/github/callback`
- `selectPrimaryEmail()` - Recibe código de autorización
- `linkAccount()` - Intercambia por access token
- [ ] Tests unitarios (8 casos) - Obtiene perfil del usuario
- [ ] Tests de integración con mock de GitHub API - Obtiene emails del usuario
- Selecciona email primario
### Frontend (FE) - Crea o actualiza usuario
- Genera JWT token
- [ ] Botón "Continuar con GitHub" - [ ] Service `GitHubOAuthService`
- [ ] Manejo de popup/redirect OAuth - `getAuthorizationUrl()`
- [ ] Recepción de callback - `exchangeCodeForToken()`
- [ ] Almacenamiento de token JWT - `getUserProfile()`
- [ ] Estado de loading - `getUserEmails()`
- [ ] Manejo de errores - `selectPrimaryEmail()`
- [ ] Tests con React Testing Library - `linkAccount()`
- [ ] Tests unitarios (8 casos)
### Testing (QA) - [ ] Tests de integración con mock de GitHub API
- [ ] E2E: Registro con GitHub ### Frontend (FE)
- [ ] E2E: Login existente
- [ ] E2E: Email privado/noreply - [ ] Botón "Continuar con GitHub"
- [ ] E2E: Múltiples emails - [ ] Manejo de popup/redirect OAuth
- [ ] E2E: Cancelación del flujo - [ ] Recepción de callback
- [ ] Test de seguridad: Validación de tokens - [ ] Almacenamiento de token JWT
- [ ] Test de seguridad: CSRF protection - [ ] Estado de loading
- [ ] Mock de GitHub API - [ ] Manejo de errores
- [ ] Tests con React Testing Library
---
### Testing (QA)
## Dependencias
- [ ] E2E: Registro con GitHub
- **Bloqueantes:** - [ ] E2E: Login existente
- GitHub OAuth App creada - [ ] E2E: Email privado/noreply
- Credenciales configuradas - [ ] E2E: Múltiples emails
- SSL/HTTPS en producción - [ ] E2E: Cancelación del flujo
- [ ] Test de seguridad: Validación de tokens
- **Deseables:** - [ ] Test de seguridad: CSRF protection
- US-AUTH-003: Consistencia con otros OAuth - [ ] Mock de GitHub API
--- ---
## Definition of Ready (DoR) ## Dependencias
- [ ] GitHub OAuth App creada - **Bloqueantes:**
- [ ] Client ID y Secret disponibles - GitHub OAuth App creada
- [ ] Mockups aprobados - Credenciales configuradas
- [ ] API contract definido - SSL/HTTPS en producción
- [ ] Callback URL configurada
- **Deseables:**
--- - US-AUTH-003: Consistencia con otros OAuth
## Definition of Done (DoD) ---
- [ ] Código implementado y revisado ## Definition of Ready (DoR)
- [ ] Tests unitarios con 80%+ cobertura
- [ ] Tests de integración pasando - [ ] GitHub OAuth App creada
- [ ] Tests E2E implementados - [ ] Client ID y Secret disponibles
- [ ] Manejo de emails privados funcional - [ ] Mockups aprobados
- [ ] Documentación actualizada - [ ] API contract definido
- [ ] Logs implementados - [ ] Callback URL configurada
- [ ] QA aprobado en staging
- [ ] Deploy a producción exitoso ---
--- ## Definition of Done (DoD)
## Notas Técnicas - [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura
### GitHub OAuth Flow - [ ] Tests de integración pasando
- [ ] Tests E2E implementados
1. Frontend redirige a `/api/v1/auth/github` - [ ] Manejo de emails privados funcional
2. Backend redirige a GitHub con: - [ ] Documentación actualizada
- `client_id` - [ ] Logs implementados
- `redirect_uri` - [ ] QA aprobado en staging
- `scope=user:email read:user` - [ ] Deploy a producción exitoso
- `state` (CSRF token)
3. Usuario autoriza en GitHub ---
4. GitHub redirige a `redirect_uri` con `code`
5. Backend intercambia `code` por `access_token` ## Notas Técnicas
6. Backend obtiene perfil: `GET /user`
7. Backend obtiene emails: `GET /user/emails` ### GitHub OAuth Flow
8. Backend selecciona email primario y verificado
9. Backend crea/actualiza usuario 1. Frontend redirige a `/api/v1/auth/github`
10. Backend genera JWT 2. Backend redirige a GitHub con:
- `client_id`
### GitHub API Endpoints - `redirect_uri`
- `scope=user:email read:user`
- Authorization: `https://github.com/login/oauth/authorize` - `state` (CSRF token)
- Token exchange: `https://github.com/login/oauth/access_token` 3. Usuario autoriza en GitHub
- User profile: `https://api.github.com/user` 4. GitHub redirige a `redirect_uri` con `code`
- User emails: `https://api.github.com/user/emails` 5. Backend intercambia `code` por `access_token`
6. Backend obtiene perfil: `GET /user`
### Email Selection Logic 7. Backend obtiene emails: `GET /user/emails`
8. Backend selecciona email primario y verificado
```typescript 9. Backend crea/actualiza usuario
// Prioridad de selección de email: 10. Backend genera JWT
1. Email primario + verificado
2. Email primario (aunque no esté verificado) ### GitHub API Endpoints
3. Primer email verificado
4. Solicitar email adicional al usuario - Authorization: `https://github.com/login/oauth/authorize`
``` - Token exchange: `https://github.com/login/oauth/access_token`
- User profile: `https://api.github.com/user`
### Environment Variables - User emails: `https://api.github.com/user/emails`
```env ### Email Selection Logic
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret ```typescript
GITHUB_CALLBACK_URL=https://orbiquant.com/api/v1/auth/github/callback // Prioridad de selección de email:
``` 1. Email primario + verificado
2. Email primario (aunque no esté verificado)
### Consideraciones Especiales de GitHub 3. Primer email verificado
4. Solicitar email adicional al usuario
- GitHub permite múltiples emails por cuenta ```
- Email puede ser privado (noreply@github.com)
- Necesita dos llamadas: una para perfil, otra para emails ### Environment Variables
- Access tokens no expiran (a menos que sean revocados)
- Scopes mínimos: `user:email` y `read:user` ```env
- Rate limit: 5000 requests/hour para usuarios autenticados GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
### Security Considerations GITHUB_CALLBACK_URL=https://orbiquant.com/api/v1/auth/github/callback
```
- Validar `state` parameter (CSRF)
- Usar HTTPS en callbacks ### Consideraciones Especiales de GitHub
- No almacenar access tokens sin encriptar
- Rate limiting en endpoints - GitHub permite múltiples emails por cuenta
- Logs de intentos de autenticación - Email puede ser privado (noreply@github.com)
- Validar que el email sea verificado en GitHub - Necesita dos llamadas: una para perfil, otra para emails
- Access tokens no expiran (a menos que sean revocados)
--- - Scopes mínimos: `user:email` y `read:user`
- Rate limit: 5000 requests/hour para usuarios autenticados
## Requerimientos Relacionados
### Security Considerations
- [RF-AUTH-003: OAuth Social](../requerimientos/RF-AUTH-003-oauth.md)
- Validar `state` parameter (CSRF)
## Especificaciones Relacionadas - Usar HTTPS en callbacks
- No almacenar access tokens sin encriptar
- [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
- 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,442 +1,454 @@
# US-AUTH-008: Autenticación con SMS (Twilio) ---
id: "US-AUTH-008"
**Version:** 1.0.0 title: "Autenticacion con SMS (Twilio)"
**Fecha:** 2025-12-05 type: "User Story"
**Estado:** Pendiente status: "To Do"
**Story Points:** 5 priority: "Alta"
**Prioridad:** P1 (Alta) epic: "OQI-001"
**Épica:** [OQI-001](../_MAP.md) story_points: 5
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
**Quiero** poder registrarme e iniciar sesión usando mi número de teléfono y un código SMS **Version:** 1.0.0
**Para** tener un acceso rápido sin necesidad de recordar contraseñas **Fecha:** 2025-12-05
**Estado:** Pendiente
--- **Story Points:** 5
**Prioridad:** P1 (Alta)
## Criterios de Aceptación **Épica:** [OQI-001](../_MAP.md)
### AC-001: Formulario de teléfono ---
**Dado** que estoy en la página de registro/login ## Historia de Usuario
**Cuando** selecciono la opción de teléfono
**Entonces** debería ver: **Como** usuario de OrbiQuant
- Selector de país con banderas (+1, +52, +34, etc.) **Quiero** poder registrarme e iniciar sesión usando mi número de teléfono y un código SMS
- Campo para número de teléfono **Para** tener un acceso rápido sin necesidad de recordar contraseñas
- Formato visual según el país seleccionado
- Botón "Enviar código" ---
### AC-002: Validación de número ## Criterios de Aceptación
**Dado** que ingreso un número de teléfono ### AC-001: Formulario de teléfono
**Cuando** el número no es válido para el país seleccionado
**Entonces** debería ver un mensaje de error **Dado** que estoy en la página de registro/login
**Y** el botón "Enviar código" debería estar deshabilitado **Cuando** selecciono la opción de teléfono
**Entonces** debería ver:
### AC-003: Envío de código SMS - Selector de país con banderas (+1, +52, +34, etc.)
- Campo para número de teléfono
**Dado** que ingresé un número válido - Formato visual según el país seleccionado
**Cuando** hago click en "Enviar código" - Botón "Enviar código"
**Entonces** debería:
1. Ver un mensaje "Enviando código..." ### AC-002: Validación de número
2. Recibir un SMS con un código de 6 dígitos
3. Ver pantalla de verificación de código **Dado** que ingreso un número de teléfono
4. El código debería expirar en 10 minutos **Cuando** el número no es válido para el país seleccionado
**Entonces** debería ver un mensaje de error
### AC-004: Formato del SMS **Y** el botón "Enviar código" debería estar deshabilitado
**Dado** que solicité un código ### AC-003: Envío de código SMS
**Cuando** recibo el SMS
**Entonces** debería tener el formato: **Dado** que ingresé un número válido
``` **Cuando** hago click en "Enviar código"
Tu código de OrbiQuant es: 123456 **Entonces** debería:
1. Ver un mensaje "Enviando código..."
Válido por 10 minutos. 2. Recibir un SMS con un código de 6 dígitos
No compartas este código. 3. Ver pantalla de verificación de código
``` 4. El código debería expirar en 10 minutos
### AC-005: Ingreso de código ### AC-004: Formato del SMS
**Dado** que recibí el código por SMS **Dado** que solicité un código
**Cuando** ingreso el código en la app **Cuando** recibo el SMS
**Entonces** debería: **Entonces** debería tener el formato:
- Autoformatear con espacios (123 456) ```
- Auto-enviar al completar 6 dígitos Tu código de OrbiQuant es: 123456
- Validar el código en tiempo real
Válido por 10 minutos.
### AC-006: Código correcto - Primer registro No compartas este código.
```
**Dado** que es mi primera vez usando este número
**Cuando** ingreso el código correcto ### AC-005: Ingreso de código
**Entonces** debería:
1. Ver formulario para completar perfil (nombre, apellido, email opcional) **Dado** que recibí el código por SMS
2. Crear mi cuenta **Cuando** ingreso el código en la app
3. Recibir un JWT token **Entonces** debería:
4. Ser redirigido al dashboard - Autoformatear con espacios (123 456)
- Auto-enviar al completar 6 dígitos
### AC-007: Código correcto - Login existente - Validar el código en tiempo real
**Dado** que ya tengo una cuenta con este número ### AC-006: Código correcto - Primer registro
**Cuando** ingreso el código correcto
**Entonces** debería: **Dado** que es mi primera vez usando este número
1. Iniciar sesión automáticamente **Cuando** ingreso el código correcto
2. Recibir un JWT token **Entonces** debería:
3. Ser redirigido al dashboard 1. Ver formulario para completar perfil (nombre, apellido, email opcional)
2. Crear mi cuenta
### AC-008: Código incorrecto 3. Recibir un JWT token
4. Ser redirigido al dashboard
**Dado** que ingreso un código incorrecto
**Cuando** envío el código ### AC-007: Código correcto - Login existente
**Entonces** debería:
- Ver mensaje "Código incorrecto" **Dado** que ya tengo una cuenta con este número
- Poder intentar nuevamente **Cuando** ingreso el código correcto
- Después de 3 intentos fallidos, invalidar el código **Entonces** debería:
- Poder solicitar un nuevo código 1. Iniciar sesión automáticamente
2. Recibir un JWT token
### AC-009: Código expirado 3. Ser redirigido al dashboard
**Dado** que pasaron más de 10 minutos desde el envío ### AC-008: Código incorrecto
**Cuando** intento usar el código
**Entonces** debería ver mensaje "Código expirado" **Dado** que ingreso un código incorrecto
**Y** debería poder solicitar un nuevo código **Cuando** envío el código
**Entonces** debería:
### AC-010: Reenvío de código - Ver mensaje "Código incorrecto"
- Poder intentar nuevamente
**Dado** que no recibí el código o expiró - Después de 3 intentos fallidos, invalidar el código
**Cuando** hago click en "Reenviar código" - Poder solicitar un nuevo código
**Entonces** debería:
- Esperar 60 segundos antes de permitir reenvío ### AC-009: Código expirado
- Ver contador regresivo "Reenviar en 59s..."
- Recibir un nuevo código (el anterior se invalida) **Dado** que pasaron más de 10 minutos desde el envío
**Cuando** intento usar el código
### AC-011: Rate limiting **Entonces** debería ver mensaje "Código expirado"
**Y** debería poder solicitar un nuevo código
**Dado** que solicité 5 códigos en 1 hora
**Cuando** intento solicitar otro ### AC-010: Reenvío de código
**Entonces** debería ver mensaje:
- "Demasiados intentos. Intenta en 1 hora" **Dado** que no recibí el código o expiró
**Cuando** hago click en "Reenviar código"
### AC-012: Número ya registrado con email **Entonces** debería:
- Esperar 60 segundos antes de permitir reenvío
**Dado** que mi número ya está vinculado a una cuenta de email - Ver contador regresivo "Reenviar en 59s..."
**Cuando** completo la verificación SMS - Recibir un nuevo código (el anterior se invalida)
**Entonces** debería iniciar sesión en esa cuenta
**Y** tener ambos métodos de autenticación disponibles ### AC-011: Rate limiting
--- **Dado** que solicité 5 códigos en 1 hora
**Cuando** intento solicitar otro
## Mockup **Entonces** debería ver mensaje:
- "Demasiados intentos. Intenta en 1 hora"
```
Paso 1: Ingreso de teléfono ### AC-012: Número ya registrado con email
┌─────────────────────────────────────────────────────────────┐
│ │ **Dado** que mi número ya está vinculado a una cuenta de email
│ 🌟 Ingresa con tu número de teléfono │ **Cuando** completo la verificación SMS
│ │ **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 ─────────────────── │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
│ [Email] [Google] [Facebook] [Apple] │ │ │ Número de teléfono │ │
│ │ │ │ ┌────────┐ ┌──────────────────────────────────────┐ │ │
└─────────────────────────────────────────────────────────────┘ │ │ │ 🇺🇸 +1 ▾│ │ (555) 123-4567 │ │ │
│ │ └────────┘ └──────────────────────────────────────┘ │ │
Paso 2: Verificación de código │ └─────────────────────────────────────────────────────┘ │
┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
│ 📱 Ingresa el código que enviamos │ │ │ Enviar código │ │
│ │ │ └─────────────────────────────────────────────────────┘ │
│ 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
│ └─────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────┐
│ │ │ │
│ ¿No recibiste el código? │ │ 📱 Ingresa el código que enviamos │
│ Reenviar código (disponible en 58s) │ │ │
│ │ │ Enviamos un código a +1 (555) 123-4567 │
│ ← Cambiar número │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │
└─────────────────────────────────────────────────────────────┘ │ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
Paso 3: Completar perfil (solo registro nuevo) │ │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ │ │
┌─────────────────────────────────────────────────────────────┐ │ │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │ │
│ │ │ │ │ │
│ 🎉 ¡Bienvenido! Completa tu perfil │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │ │ ¿No recibiste el código? │
│ │ Nombre │ │ Apellido │ │ │ Reenviar código (disponible en 58s) │
│ │ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │ │ │
│ │ │ Juan │ │ │ │ Pérez │ │ │ │ ← Cambiar número │
│ │ └──────────────────┘ │ │ └──────────────────┘ │ │ │ │
│ └────────────────────────┘ └────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
│ │
│ ┌─────────────────────────────────────────────────────┐ │ Paso 3: Completar perfil (solo registro nuevo)
│ │ Email (opcional) │ │ ┌─────────────────────────────────────────────────────────────┐
│ │ ┌─────────────────────────────────────────────────┐ │ │ │ │
│ │ │ juan@email.com │ │ │ │ 🎉 ¡Bienvenido! Completa tu perfil │
│ │ └─────────────────────────────────────────────────┘ │ │ │ │
│ └─────────────────────────────────────────────────────┘ │ │ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ │ │ 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) │ │
│ ┌─────────────────────────────────────────────────────┐ │
- [ ] Agregar campos a tabla `users`: │ │ Crear mi cuenta │ │
```sql │ └─────────────────────────────────────────────────────┘ │
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) UNIQUE; │ │
ALTER TABLE users ADD COLUMN phone_verified_at TIMESTAMP; └─────────────────────────────────────────────────────────────┘
ALTER TABLE users ADD COLUMN phone_country_code VARCHAR(5); ```
```
- [ ] Tabla `phone_verification_codes`: ---
```sql
CREATE TABLE phone_verification_codes ( ## Tareas Técnicas
id UUID PRIMARY KEY,
phone_number VARCHAR(20) NOT NULL, ### Database (DB)
code VARCHAR(6) NOT NULL,
attempts INT DEFAULT 0, - [ ] Agregar campos a tabla `users`:
expires_at TIMESTAMP NOT NULL, ```sql
used_at TIMESTAMP, ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) UNIQUE;
created_at TIMESTAMP DEFAULT NOW(), ALTER TABLE users ADD COLUMN phone_verified_at TIMESTAMP;
INDEX idx_phone_expires (phone_number, expires_at) ALTER TABLE users ADD COLUMN phone_country_code VARCHAR(5);
); ```
``` - [ ] Tabla `phone_verification_codes`:
- [ ] Tabla `phone_rate_limits`: ```sql
```sql CREATE TABLE phone_verification_codes (
CREATE TABLE phone_rate_limits ( id UUID PRIMARY KEY,
id UUID PRIMARY KEY, phone_number VARCHAR(20) NOT NULL,
phone_number VARCHAR(20) NOT NULL, code VARCHAR(6) NOT NULL,
ip_address VARCHAR(45), attempts INT DEFAULT 0,
attempts INT DEFAULT 1, expires_at TIMESTAMP NOT NULL,
window_start TIMESTAMP DEFAULT NOW(), used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_phone_window (phone_number, window_start) INDEX idx_phone_expires (phone_number, expires_at)
); );
``` ```
- [ ] Tabla `phone_rate_limits`:
### Backend (BE) ```sql
CREATE TABLE phone_rate_limits (
- [ ] Configurar cuenta de Twilio id UUID PRIMARY KEY,
- [ ] Obtener Account SID y Auth Token phone_number VARCHAR(20) NOT NULL,
- [ ] Configurar Twilio Phone Number ip_address VARCHAR(45),
- [ ] Endpoint `POST /api/v1/auth/phone/send-code` attempts INT DEFAULT 1,
- Validar número con libphonenumber window_start TIMESTAMP DEFAULT NOW(),
- Rate limiting (5 códigos / hora) created_at TIMESTAMP DEFAULT NOW(),
- Generar código aleatorio de 6 dígitos INDEX idx_phone_window (phone_number, window_start)
- Guardar en DB con expiración );
- Enviar SMS via Twilio ```
- [ ] Endpoint `POST /api/v1/auth/phone/verify-code`
- Validar código ### Backend (BE)
- Verificar no expirado
- Verificar intentos < 3 - [ ] Configurar cuenta de Twilio
- Crear o actualizar usuario - [ ] Obtener Account SID y Auth Token
- Generar JWT token - [ ] Configurar Twilio Phone Number
- [ ] Endpoint `POST /api/v1/auth/phone/resend-code` - [ ] Endpoint `POST /api/v1/auth/phone/send-code`
- Invalidar código anterior - Validar número con libphonenumber
- Generar nuevo código - Rate limiting (5 códigos / hora)
- Verificar cooldown de 60s - Generar código aleatorio de 6 dígitos
- [ ] Service `TwilioSMSService` - Guardar en DB con expiración
- `sendVerificationCode()` - Enviar SMS via Twilio
- `verifyCode()` - [ ] Endpoint `POST /api/v1/auth/phone/verify-code`
- `formatPhoneNumber()` - Validar código
- [ ] Librería: `twilio` SDK - Verificar no expirado
- [ ] Librería: `libphonenumber-js` para validación - Verificar intentos < 3
- [ ] Tests unitarios (12 casos) - Crear o actualizar usuario
- [ ] Tests de integración con mock de Twilio - Generar JWT token
- [ ] Endpoint `POST /api/v1/auth/phone/resend-code`
### Frontend (FE) - Invalidar código anterior
- Generar nuevo código
- [ ] Componente `PhoneAuth.tsx` - Verificar cooldown de 60s
- [ ] Selector de país con banderas - [ ] Service `TwilioSMSService`
- [ ] Input de teléfono con formato automático - `sendVerificationCode()`
- [ ] Componente `CodeInput.tsx` (6 dígitos) - `verifyCode()`
- [ ] Componente `CompleteProfile.tsx` - `formatPhoneNumber()`
- [ ] Validación con React Hook Form - [ ] Librería: `twilio` SDK
- [ ] Librería: `react-phone-number-input` - [ ] Librería: `libphonenumber-js` para validación
- [ ] Contador regresivo para reenvío - [ ] Tests unitarios (12 casos)
- [ ] Tests con React Testing Library - [ ] Tests de integración con mock de Twilio
### Testing (QA) ### Frontend (FE)
- [ ] E2E: Registro con teléfono completo - [ ] Componente `PhoneAuth.tsx`
- [ ] E2E: Login con teléfono existente - [ ] Selector de país con banderas
- [ ] E2E: Código incorrecto (3 intentos) - [ ] Input de teléfono con formato automático
- [ ] E2E: Código expirado - [ ] Componente `CodeInput.tsx` (6 dígitos)
- [ ] E2E: Reenvío de código - [ ] Componente `CompleteProfile.tsx`
- [ ] E2E: Rate limiting - [ ] Validación con React Hook Form
- [ ] Test de integración con Twilio Test Credentials - [ ] Librería: `react-phone-number-input`
- [ ] Test de seguridad: Brute force protection - [ ] Contador regresivo para reenvío
- [ ] Performance: Envío de SMS < 2s - [ ] Tests con React Testing Library
--- ### Testing (QA)
## Dependencias - [ ] E2E: Registro con teléfono completo
- [ ] E2E: Login con teléfono existente
- **Bloqueantes:** - [ ] E2E: Código incorrecto (3 intentos)
- Cuenta de Twilio activa - [ ] E2E: Código expirado
- Twilio Phone Number comprado - [ ] E2E: Reenvío de código
- Presupuesto para SMS (aprox $0.0075 USD por SMS) - [ ] E2E: Rate limiting
- [ ] Test de integración con Twilio Test Credentials
- **Deseables:** - [ ] Test de seguridad: Brute force protection
- US-AUTH-001: Para vinculación de cuentas - [ ] Performance: Envío de SMS < 2s
--- ---
## Definition of Ready (DoR) ## Dependencias
- [ ] Cuenta de Twilio configurada - **Bloqueantes:**
- [ ] Twilio Phone Number asignado - Cuenta de Twilio activa
- [ ] Presupuesto aprobado para SMS - Twilio Phone Number comprado
- [ ] Mockups aprobados - Presupuesto para SMS (aprox $0.0075 USD por SMS)
- [ ] API contract definido
- [ ] Estrategia de rate limiting definida - **Deseables:**
- US-AUTH-001: Para vinculación de cuentas
---
---
## Definition of Done (DoD)
## Definition of Ready (DoR)
- [ ] Código implementado y revisado
- [ ] Tests unitarios con 80%+ cobertura - [ ] Cuenta de Twilio configurada
- [ ] Tests de integración pasando - [ ] Twilio Phone Number asignado
- [ ] Tests E2E implementados - [ ] Presupuesto aprobado para SMS
- [ ] Twilio configurado en todos los ambientes - [ ] Mockups aprobados
- [ ] Rate limiting implementado - [ ] API contract definido
- [ ] Logs y monitoring de SMS - [ ] Estrategia de rate limiting definida
- [ ] Costos monitoreados
- [ ] Documentación actualizada ---
- [ ] 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
### Twilio SMS Flow - [ ] Twilio configurado en todos los ambientes
- [ ] Rate limiting implementado
1. Usuario ingresa número de teléfono - [ ] Logs y monitoring de SMS
2. Frontend valida formato - [ ] Costos monitoreados
3. Frontend llama `POST /api/v1/auth/phone/send-code` - [ ] Documentación actualizada
4. Backend valida número con libphonenumber - [ ] QA aprobado en staging
5. Backend verifica rate limits - [ ] Deploy a producción exitoso
6. Backend genera código aleatorio (6 dígitos)
7. Backend guarda código en DB (expira en 10 min) ---
8. Backend envía SMS via Twilio API
9. Usuario recibe SMS e ingresa código ## Notas Técnicas
10. Frontend llama `POST /api/v1/auth/phone/verify-code`
11. Backend valida código ### Twilio SMS Flow
12. Backend crea/actualiza usuario
13. Backend genera JWT token 1. Usuario ingresa número de teléfono
2. Frontend valida formato
### Environment Variables 3. Frontend llama `POST /api/v1/auth/phone/send-code`
4. Backend valida número con libphonenumber
```env 5. Backend verifica rate limits
TWILIO_ACCOUNT_SID=AC... 6. Backend genera código aleatorio (6 dígitos)
TWILIO_AUTH_TOKEN=your_auth_token 7. Backend guarda código en DB (expira en 10 min)
TWILIO_PHONE_NUMBER=+15551234567 8. Backend envía SMS via Twilio API
TWILIO_VERIFY_SERVICE_SID=VA... # Opcional: usar Twilio Verify 9. Usuario recibe SMS e ingresa código
``` 10. Frontend llama `POST /api/v1/auth/phone/verify-code`
11. Backend valida código
### Twilio API Usage 12. Backend crea/actualiza usuario
13. Backend genera JWT token
```typescript
import twilio from 'twilio'; ### Environment Variables
const client = twilio(accountSid, authToken); ```env
TWILIO_ACCOUNT_SID=AC...
// Enviar SMS TWILIO_AUTH_TOKEN=your_auth_token
await client.messages.create({ TWILIO_PHONE_NUMBER=+15551234567
body: `Tu código de OrbiQuant es: ${code}\n\nVálido por 10 minutos.`, TWILIO_VERIFY_SERVICE_SID=VA... # Opcional: usar Twilio Verify
from: twilioPhoneNumber, ```
to: userPhoneNumber
}); ### Twilio API Usage
```
```typescript
### Phone Number Validation import twilio from 'twilio';
```typescript const client = twilio(accountSid, authToken);
import { parsePhoneNumber } from 'libphonenumber-js';
// Enviar SMS
const phoneNumber = parsePhoneNumber(input, countryCode); await client.messages.create({
if (!phoneNumber || !phoneNumber.isValid()) { body: `Tu código de OrbiQuant es: ${code}\n\nVálido por 10 minutos.`,
throw new Error('Invalid phone number'); from: twilioPhoneNumber,
} to: userPhoneNumber
``` });
```
### Code Generation
### Phone Number Validation
```typescript
// Generar código de 6 dígitos ```typescript
const code = Math.floor(100000 + Math.random() * 900000).toString(); import { parsePhoneNumber } from 'libphonenumber-js';
```
const phoneNumber = parsePhoneNumber(input, countryCode);
### Rate Limiting Strategy if (!phoneNumber || !phoneNumber.isValid()) {
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
- Máximo 3 intentos de verificación por código ### Code Generation
### Security Considerations ```typescript
// Generar código de 6 dígitos
- Códigos de 6 dígitos (1 millón de combinaciones) const code = Math.floor(100000 + Math.random() * 900000).toString();
- Expiración de 10 minutos ```
- Invalidar después de 3 intentos fallidos
- Rate limiting estricto ### Rate Limiting Strategy
- Logs de todos los intentos
- Validación de número en backend - 5 códigos por número de teléfono por hora
- No devolver información si el número existe o no - 10 códigos por IP por hora
- Cooldown de 60 segundos entre reenvíos
### Cost Optimization - Máximo 3 intentos de verificación por código
- SMS en USA: ~$0.0075 USD ### Security Considerations
- SMS internacional: $0.0075 - $0.10 USD
- Usar Twilio Verify Service para mejor pricing - Códigos de 6 dígitos (1 millón de combinaciones)
- Implementar captcha para prevenir abuso - Expiración de 10 minutos
- Alertas si se excede presupuesto mensual - Invalidar después de 3 intentos fallidos
- Rate limiting estricto
### Alternative: Twilio Verify Service - Logs de todos los intentos
- Validación de número en backend
En lugar de manejar códigos manualmente, considerar usar Twilio Verify: - No devolver información si el número existe o no
- Manejo automático de códigos
- Rate limiting incluido ### Cost Optimization
- Mejor pricing
- Reenvíos automáticos - SMS en USA: ~$0.0075 USD
- Múltiples canales (SMS, Voice, WhatsApp) - SMS internacional: $0.0075 - $0.10 USD
- Usar Twilio Verify Service para mejor pricing
--- - Implementar captcha para prevenir abuso
- Alertas si se excede presupuesto mensual
## 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:
## Especificaciones Relacionadas - Manejo automático de códigos
- Rate limiting incluido
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md) - Mejor pricing
- [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md) - Reenvíos automáticos
- Múltiples canales (SMS, Voice, WhatsApp)
---
## Requerimientos Relacionados
- [RF-AUTH-004: Autenticación por Teléfono](../requerimientos/RF-AUTH-004-phone.md)
## Especificaciones Relacionadas
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
- [ET-AUTH-005: Phone Authentication](../especificaciones/ET-AUTH-005-phone.md)

View File

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

View File

@ -1,3 +1,15 @@
---
id: "US-AUTH-010"
title: "Configurar 2FA"
type: "User Story"
status: "Done"
priority: "Alta"
epic: "OQI-001"
story_points: 5
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# US-AUTH-010: Configurar 2FA # US-AUTH-010: Configurar 2FA
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,16 @@
---
id: "RF-AUTH-001"
title: "OAuth Multi-proveedor"
type: "Requirement"
status: "Done"
priority: "Alta"
module: "auth"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# RF-AUTH-001: OAuth Multi-proveedor # RF-AUTH-001: OAuth Multi-proveedor
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,16 @@
---
id: "RF-AUTH-002"
title: "Autenticacion por Email"
type: "Requirement"
status: "Done"
priority: "Alta"
module: "auth"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# RF-AUTH-002: Autenticación por Email # RF-AUTH-002: Autenticación por Email
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,16 @@
---
id: "RF-AUTH-003"
title: "Autenticacion por Telefono"
type: "Requirement"
status: "Done"
priority: "Alta"
module: "auth"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# RF-AUTH-003: Autenticación por Teléfono # RF-AUTH-003: Autenticación por Teléfono
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,16 @@
---
id: "RF-AUTH-004"
title: "Two-Factor Authentication (2FA)"
type: "Requirement"
status: "Done"
priority: "Alta"
module: "auth"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# RF-AUTH-004: Two-Factor Authentication (2FA) # RF-AUTH-004: Two-Factor Authentication (2FA)
**Version:** 1.0.0 **Version:** 1.0.0

View File

@ -1,3 +1,16 @@
---
id: "RF-AUTH-005"
title: "Gestion de Sesiones"
type: "Requirement"
status: "Done"
priority: "Alta"
module: "auth"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# RF-AUTH-005: Gestión de Sesiones # RF-AUTH-005: Gestión de Sesiones
**Version:** 1.0.0 **Version:** 1.0.0

View File

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

View File

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

View File

@ -1,229 +1,238 @@
# Especificaciones Técnicas - OQI-002 Módulo Educativo ---
id: "README"
Este directorio contiene las especificaciones técnicas detalladas para el módulo educativo de OrbiQuant IA. title: "Especificaciones Técnicas - OQI-002 Módulo Educativo"
type: "Documentation"
## Índice de Especificaciones project: "trading-platform"
version: "1.0.0"
### [ET-EDU-001: Modelo de Datos - Schema Education](./ET-EDU-001-database.md) updated_date: "2026-01-04"
**Componente:** Database ---
**Tamaño:** ~30KB
# Especificaciones Técnicas - OQI-002 Módulo Educativo
Define el schema completo de PostgreSQL para el módulo educativo:
- 11 tablas principales (categories, courses, modules, lessons, enrollments, progress, quizzes, quiz_questions, quiz_attempts, certificates, user_achievements) Este directorio contiene las especificaciones técnicas detalladas para el módulo educativo de OrbiQuant IA.
- 6 ENUMs personalizados
- Triggers y funciones automáticas ## Índice de Especificaciones
- Vistas optimizadas
- Índices de performance ### [ET-EDU-001: Modelo de Datos - Schema Education](./ET-EDU-001-database.md)
- Row Level Security (RLS) **Componente:** Database
**Tamaño:** ~30KB
**Contenido clave:**
- Schema education completo con relaciones Define el schema completo de PostgreSQL para el módulo educativo:
- Triggers para auto-actualización de progreso - 11 tablas principales (categories, courses, modules, lessons, enrollments, progress, quizzes, quiz_questions, quiz_attempts, certificates, user_achievements)
- Vistas para queries complejas - 6 ENUMs personalizados
- Estrategias de indexación - Triggers y funciones automáticas
- Vistas optimizadas
--- - Índices de performance
- Row Level Security (RLS)
### [ET-EDU-002: API REST - Endpoints del Módulo](./ET-EDU-002-api.md)
**Componente:** Backend **Contenido clave:**
**Tamaño:** ~42KB - Schema education completo con relaciones
- Triggers para auto-actualización de progreso
Especificación completa de la API REST con Express.js + TypeScript: - Vistas para queries complejas
- 10 grupos de endpoints (Categories, Courses, Modules, Lessons, Enrollments, Progress, Quizzes, Certificates, Achievements, Gamification) - Estrategias de indexación
- Request/Response con TypeScript interfaces
- Autenticación JWT y autorización por roles ---
- Rate limiting y paginación
- Validación con Zod ### [ET-EDU-002: API REST - Endpoints del Módulo](./ET-EDU-002-api.md)
- Manejo de errores estandarizado **Componente:** Backend
**Tamaño:** ~42KB
**Contenido clave:**
- ~60 endpoints documentados Especificación completa de la API REST con Express.js + TypeScript:
- Middleware stack completo - 10 grupos de endpoints (Categories, Courses, Modules, Lessons, Enrollments, Progress, Quizzes, Certificates, Achievements, Gamification)
- Códigos de error estandarizados - Request/Response con TypeScript interfaces
- Ejemplos de tests con Supertest - Autenticación JWT y autorización por roles
- 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)
**Componente:** Frontend **Contenido clave:**
**Tamaño:** ~46KB - ~60 endpoints documentados
- Middleware stack completo
Arquitectura frontend con React 18, TypeScript, Zustand y TailwindCSS: - Códigos de error estandarizados
- 7 páginas principales (CoursesPage, CourseDetailPage, LessonPage, ProgressPage, QuizPage, CertificatesPage, AchievementsPage) - Ejemplos de tests con Supertest
- 20+ componentes reutilizables
- Custom hooks para data fetching ---
- Stores Zustand para state management
- Integración con React Query ### [ET-EDU-003: Componentes Frontend - React + TypeScript](./ET-EDU-003-frontend.md)
**Componente:** Frontend
**Contenido clave:** **Tamaño:** ~46KB
- Código completo de componentes principales
- Hooks personalizados (useCourses, useEnrollment, useVideoProgress, etc.) Arquitectura frontend con React 18, TypeScript, Zustand y TailwindCSS:
- Stores con Zustand (courseStore, progressStore, gamificationStore) - 7 páginas principales (CoursesPage, CourseDetailPage, LessonPage, ProgressPage, QuizPage, CertificatesPage, AchievementsPage)
- Configuración de TailwindCSS - 20+ componentes reutilizables
- 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)
**Componente:** Backend/Infraestructura **Contenido clave:**
**Tamaño:** ~30KB - Código completo de componentes principales
- Hooks personalizados (useCourses, useEnrollment, useVideoProgress, etc.)
Integración de video streaming con Vimeo y AWS S3+CloudFront: - Stores con Zustand (courseStore, progressStore, gamificationStore)
- Configuración de Vimeo Pro/Business - Configuración de TailwindCSS
- Upload y gestión de videos
- AWS S3 + CloudFront con signed URLs ---
- Transcoding HLS con FFmpeg
- Player configuration (Vimeo Player / Video.js) ### [ET-EDU-004: Sistema de Streaming de Video](./ET-EDU-004-video.md)
- Tracking de progreso de video **Componente:** Backend/Infraestructura
- Subtítulos WebVTT **Tamaño:** ~30KB
**Contenido clave:** Integración de video streaming con Vimeo y AWS S3+CloudFront:
- Servicios de upload a Vimeo - Configuración de Vimeo Pro/Business
- Generación de signed URLs en CloudFront - Upload y gestión de videos
- Pipeline de transcoding HLS multi-bitrate - AWS S3 + CloudFront con signed URLs
- Componentes de video player (VimeoPlayer, HLSPlayer) - Transcoding HLS con FFmpeg
- Configuración de CloudFront Distribution - Player configuration (Vimeo Player / Video.js)
- Tracking de progreso de video
--- - Subtítulos WebVTT
### [ET-EDU-005: Motor de Evaluaciones y Quizzes](./ET-EDU-005-quizzes.md) **Contenido clave:**
**Componente:** Backend/Frontend - Servicios de upload a Vimeo
**Tamaño:** ~33KB - Generación de signed URLs en CloudFront
- Pipeline de transcoding HLS multi-bitrate
Sistema completo de evaluaciones con múltiples tipos de preguntas: - Componentes de video player (VimeoPlayer, HLSPlayer)
- 5 tipos de preguntas (Multiple Choice, True/False, Multiple Select, Fill in the Blank, Code Challenge) - Configuración de CloudFront Distribution
- Algoritmo de scoring con crédito parcial
- Gestión de intentos con límites ---
- Timer con auto-submit
- Validación de respuestas (incluyendo fuzzy matching) ### [ET-EDU-005: Motor de Evaluaciones y Quizzes](./ET-EDU-005-quizzes.md)
- Analytics de quizzes **Componente:** Backend/Frontend
**Tamaño:** ~33KB
**Contenido clave:**
- QuizScoringService con algoritmos completos Sistema completo de evaluaciones con múltiples tipos de preguntas:
- QuizAttemptService para flujo de quiz - 5 tipos de preguntas (Multiple Choice, True/False, Multiple Select, Fill in the Blank, Code Challenge)
- Componentes de UI (QuizQuestion, QuizTimer, QuizResults) - Algoritmo de scoring con crédito parcial
- Cálculo de dificultad de preguntas - Gestión de intentos con límites
- Distribución de puntajes - Timer con auto-submit
- Validación de respuestas (incluyendo fuzzy matching)
--- - Analytics de quizzes
### [ET-EDU-006: Sistema de Gamificación](./ET-EDU-006-gamification.md) **Contenido clave:**
**Componente:** Backend/Frontend - QuizScoringService con algoritmos completos
**Tamaño:** ~35KB - QuizAttemptService para flujo de quiz
- Componentes de UI (QuizQuestion, QuizTimer, QuizResults)
Sistema de gamificación para aumentar engagement: - Cálculo de dificultad de preguntas
- Sistema de XP con múltiples fuentes - Distribución de puntajes
- Fórmula de niveles: `Level = floor(sqrt(totalXP / 100))`
- 15+ achievements predefinidos (common, uncommon, rare, epic, legendary) ---
- Sistema de rachas diarias con rewards
- Leaderboard global y por períodos ### [ET-EDU-006: Sistema de Gamificación](./ET-EDU-006-gamification.md)
- Notificaciones de logros **Componente:** Backend/Frontend
**Tamaño:** ~35KB
**Contenido clave:**
- XPManagerService con cálculo de niveles Sistema de gamificación para aumentar engagement:
- AchievementManagerService con verificación automática - Sistema de XP con múltiples fuentes
- StreakManagerService para rachas diarias - Fórmula de niveles: `Level = floor(sqrt(totalXP / 100))`
- LeaderboardManagerService con caching - 15+ achievements predefinidos (common, uncommon, rare, epic, legendary)
- Configuración de achievements y recompensas - Sistema de rachas diarias con rewards
- Componentes de UI (XPBar, LevelBadge, AchievementCard) - Leaderboard global y por períodos
- Notificaciones de logros
---
**Contenido clave:**
## Stack Tecnológico - XPManagerService con cálculo de niveles
- AchievementManagerService con verificación automática
### Backend - StreakManagerService para rachas diarias
- **Runtime:** Node.js 18+ - LeaderboardManagerService con caching
- **Framework:** Express.js - Configuración de achievements y recompensas
- **Language:** TypeScript 5.3+ - Componentes de UI (XPBar, LevelBadge, AchievementCard)
- **Database:** PostgreSQL 15+
- **ORM:** Prisma / TypeORM (opcional) ---
- **Validation:** Zod
- **Auth:** JWT (jsonwebtoken) ## Stack Tecnológico
- **Caching:** Redis 4+
- **Video Processing:** FFmpeg ### Backend
- **Runtime:** Node.js 18+
### Frontend - **Framework:** Express.js
- **Framework:** React 18 - **Language:** TypeScript 5.3+
- **Language:** TypeScript 5.3+ - **Database:** PostgreSQL 15+
- **State Management:** Zustand 4+ - **ORM:** Prisma / TypeORM (opcional)
- **Data Fetching:** TanStack React Query 5+ - **Validation:** Zod
- **Styling:** TailwindCSS 3+ - **Auth:** JWT (jsonwebtoken)
- **Routing:** React Router 6+ - **Caching:** Redis 4+
- **Video Player:** Vimeo Player / Video.js - **Video Processing:** FFmpeg
- **Forms:** React Hook Form + Zod
### Frontend
### Infraestructura - **Framework:** React 18
- **Video CDN:** Vimeo Pro/Business o AWS S3 + CloudFront - **Language:** TypeScript 5.3+
- **Object Storage:** AWS S3 - **State Management:** Zustand 4+
- **Cache:** Redis - **Data Fetching:** TanStack React Query 5+
- **Monitoring:** (TBD) - **Styling:** TailwindCSS 3+
- **Routing:** React Router 6+
--- - **Video Player:** Vimeo Player / Video.js
- **Forms:** React Hook Form + Zod
## Convenciones de Nomenclatura
### Infraestructura
### Archivos de Especificación - **Video CDN:** Vimeo Pro/Business o AWS S3 + CloudFront
``` - **Object Storage:** AWS S3
ET-EDU-XXX-{nombre}.md - **Cache:** Redis
``` - **Monitoring:** (TBD)
- **ET:** Especificación Técnica
- **EDU:** Módulo Education ---
- **XXX:** Número secuencial (001-999)
- **{nombre}:** Identificador descriptivo ## Convenciones de Nomenclatura
### Versiones ### Archivos de Especificación
Todas las especificaciones están en versión **1.0.0** (2025-12-05) ```
ET-EDU-XXX-{nombre}.md
--- ```
- **ET:** Especificación Técnica
## Cómo Usar Este Documento - **EDU:** Módulo Education
- **XXX:** Número secuencial (001-999)
1. **Para Desarrolladores Backend:** - **{nombre}:** Identificador descriptivo
- Leer ET-EDU-001 (Database) y ET-EDU-002 (API)
- Referencias: ET-EDU-004 (Video), ET-EDU-005 (Quizzes), ET-EDU-006 (Gamification) ### Versiones
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:**
- Leer ET-EDU-001 (Database setup) 1. **Para Desarrolladores Backend:**
- Leer ET-EDU-004 (Video Infrastructure) - Leer ET-EDU-001 (Database) y ET-EDU-002 (API)
- Variables de entorno en cada especificación - Referencias: ET-EDU-004 (Video), ET-EDU-005 (Quizzes), ET-EDU-006 (Gamification)
4. **Para Product Managers:** 2. **Para Desarrolladores Frontend:**
- Todas las especificaciones contienen descripción y arquitectura - Leer ET-EDU-002 (API) y ET-EDU-003 (Frontend)
- Ver sección de "Interfaces/Tipos" para data models - Referencias: ET-EDU-004 (Video Player), ET-EDU-005 (Quiz UI), ET-EDU-006 (Gamification UI)
--- 3. **Para DevOps:**
- Leer ET-EDU-001 (Database setup)
## Estado de Implementación - Leer ET-EDU-004 (Video Infrastructure)
- Variables de entorno en cada especificación
| Especificación | Estado | Prioridad | Notas |
|---------------|--------|-----------|-------| 4. **Para Product Managers:**
| ET-EDU-001 | Pendiente | Alta | Base de datos requerida primero | - Todas las especificaciones contienen descripción y arquitectura
| ET-EDU-002 | Pendiente | Alta | Depende de ET-EDU-001 | - Ver sección de "Interfaces/Tipos" para data models
| ET-EDU-003 | Pendiente | Alta | Depende de ET-EDU-002 |
| ET-EDU-004 | Pendiente | Media | Puede iniciar en paralelo | ---
| ET-EDU-005 | Pendiente | Media | Depende de ET-EDU-001, ET-EDU-002 |
| ET-EDU-006 | Pendiente | Baja | Feature post-MVP | ## Estado de Implementación
--- | Especificación | Estado | Prioridad | Notas |
|---------------|--------|-----------|-------|
## Próximos Pasos | ET-EDU-001 | Pendiente | Alta | Base de datos requerida primero |
| ET-EDU-002 | Pendiente | Alta | Depende de ET-EDU-001 |
1. **Revisión Técnica:** Validar especificaciones con el equipo de desarrollo | ET-EDU-003 | Pendiente | Alta | Depende de ET-EDU-002 |
2. **Priorización:** Definir orden de implementación | ET-EDU-004 | Pendiente | Media | Puede iniciar en paralelo |
3. **Estimación:** Calcular esfuerzo de desarrollo por especificación | ET-EDU-005 | Pendiente | Media | Depende de ET-EDU-001, ET-EDU-002 |
4. **Asignación:** Distribuir tareas entre el equipo | ET-EDU-006 | Pendiente | Baja | Feature post-MVP |
5. **Implementación:** Comenzar desarrollo siguiendo las especificaciones
---
---
## Próximos Pasos
## Contacto
1. **Revisión Técnica:** Validar especificaciones con el equipo de desarrollo
Para preguntas o aclaraciones sobre estas especificaciones, contactar al **Requirements Analyst** del proyecto. 2. **Priorización:** Definir orden de implementación
3. **Estimación:** Calcular esfuerzo de desarrollo por especificación
--- 4. **Asignación:** Distribuir tareas entre el equipo
5. **Implementación:** Comenzar desarrollo siguiendo las especificaciones
**Ú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,314 +1,326 @@
# US-EDU-001: Ver Catálogo de Cursos ---
id: "US-EDU-001"
## Metadata title: "Ver Catalogo de Cursos"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Alta"
| **ID** | US-EDU-001 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 3
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P0 | updated_date: "2026-01-04"
| **Story Points** | 3 | ---
| **Sprint** | Sprint 3 |
| **Estado** | Pendiente | # US-EDU-001: Ver Catálogo de Cursos
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-001 |
**Como** usuario interesado en aprender trading, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** ver un catálogo completo de cursos disponibles con filtros y búsqueda, | **Módulo** | education |
**para** descubrir contenido educativo relevante a mi nivel y áreas de interés. | **Prioridad** | P0 |
| **Story Points** | 3 |
## Descripción Detallada | **Sprint** | Sprint 3 |
| **Estado** | Pendiente |
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. | **Asignado a** | Por asignar |
## Mockups/Wireframes ---
``` ## Historia de Usuario
┌─────────────────────────────────────────────────────────────────┐
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │ **Como** usuario interesado en aprender trading,
├─────────────────────────────────────────────────────────────────┤ **quiero** ver un catálogo completo de cursos disponibles con filtros y búsqueda,
│ │ **para** descubrir contenido educativo relevante a mi nivel y áreas de interés.
│ CATÁLOGO DE CURSOS [🔍 Buscar cursos...] │
│ │ ## Descripción Detallada
│ ┌────────────────┐ ┌──────────────────────────────────────┐ │
│ │ FILTROS │ │ Mostrando 24 de 47 cursos │ │ 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.
│ │ │ │ Ordenar: [Más recientes ▼] │ │
│ │ Categorías │ │ │ │ ## Mockups/Wireframes
│ │ □ Fundamentos │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │ ✓ Análisis T. │ │ │[IMG] │ │[IMG] │ │[IMG] ││ ```
│ │ □ Gestión Riesgo│ │ │Fibonacci│ │Candlestk│ │Day Trad.││ ┌─────────────────────────────────────────────────────────────────┐
│ │ │ │ │básico │ │Avanzado │ │Pro ││ │ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
│ │ Nivel │ │ │⭐ 4.8 │ │⭐ 4.9 │ │⭐ 4.7 ││ ├─────────────────────────────────────────────────────────────────┤
│ │ ✓ Principiante │ │ │2h 30m │ │4h 15m │ │6h 45m ││ │ │
│ │ ✓ Intermedio │ │ │1,234 👥 │ │892 👥 │ │567 👥 ││ │ CATÁLOGO DE CURSOS [🔍 Buscar cursos...] │
│ │ □ Avanzado │ │ │[Ver curso]│ │[60% ✓] │ │[Iniciar]││ │ │
│ │ │ │ └─────────┘ └─────────┘ └─────────┘│ │ ┌────────────────┐ ┌──────────────────────────────────────┐ │
│ │ Duración │ │ │ │ │ │ FILTROS │ │ Mostrando 24 de 47 cursos │ │
│ │ □ < 2 horas │ │ │ │ Ordenar: [Más recientes ▼] │ │
│ │ ✓ 2-5 horas │ │ │ ...más cursos... ││ │ │ Categorías │ │ │ │
│ │ □ > 5 horas │ │ └─────────┘ └─────────┘ └─────────┘│ │ │ □ Fundamentos │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │ │ │ │ │ │ │ ✓ Análisis T. │ │ │[IMG] │ │[IMG] │ │[IMG] ││
│ │ [Limpiar] │ │ [1] 2 3 4 ... 8 │ │ │ │ □ Gestión Riesgo│ │ │Fibonacci│ │Candlestk│ │Day Trad.││
│ └────────────────┘ └──────────────────────────────────────┘ │ │ │ │ │ │básico │ │Avanzado │ │Pro ││
│ │ │ │ Nivel │ │ │⭐ 4.8 │ │⭐ 4.9 │ │⭐ 4.7 ││
│ ┌──────────────────────────────────────────────────────────┐ │ │ │ ✓ Principiante │ │ │2h 30m │ │4h 15m │ │6h 45m ││
│ │ 📚 Recomendado para ti │ │ │ │ ✓ Intermedio │ │ │1,234 👥 │ │892 👥 │ │567 👥 ││
│ │ [Curso A] [Curso B] [Curso C] │ │ │ │ □ Avanzado │ │ │[Ver curso]│ │[60% ✓] │ │[Iniciar]││
│ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘│
│ │ │ │ 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** │ ┌──────────────────────────────────────────────────────────┐ │
```gherkin │ │ 📚 Recomendado para ti │ │
DADO que el usuario está autenticado │ │ [Curso A] [Curso B] [Curso C] │ │
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 2: Filtrar por categoría** **Escenario 1: Ver catálogo completo**
```gherkin ```gherkin
DADO que el usuario está en el catálogo DADO que el usuario está autenticado
CUANDO selecciona filtro "Análisis Técnico" CUANDO navega a /education/courses
ENTONCES solo se muestran cursos de esa categoría ENTONCES se muestra el catálogo de cursos
Y el filtro se marca como activo (checkbox marcado) Y se muestran 12 cursos por página
Y la URL se actualiza a ?category=technical-analysis Y cada curso muestra: imagen, título, instructor, duración, rating, estudiantes
Y el contador se actualiza "Mostrando X de Y cursos" Y se muestran filtros en sidebar izquierdo
``` Y se muestra barra de búsqueda
Y se muestra contador "Mostrando X de Y cursos"
**Escenario 3: Filtrar por nivel** ```
```gherkin
DADO que el usuario está en el catálogo **Escenario 2: Filtrar por categoría**
CUANDO selecciona "Principiante" e "Intermedio" ```gherkin
ENTONCES solo se muestran cursos de esos niveles DADO que el usuario está en el catálogo
Y se pueden combinar con otros filtros activos CUANDO selecciona filtro "Análisis Técnico"
Y se muestra badge de nivel en cada curso ENTONCES solo se muestran cursos de esa categoría
``` Y el filtro se marca como activo (checkbox marcado)
Y la URL se actualiza a ?category=technical-analysis
**Escenario 4: Buscar curso** Y el contador se actualiza "Mostrando X de Y cursos"
```gherkin ```
DADO que el usuario está en el catálogo
CUANDO escribe "fibonacci" en el buscador **Escenario 3: Filtrar por nivel**
ENTONCES se filtran cursos en tiempo real ```gherkin
Y se muestran solo cursos que contengan "fibonacci" en título o descripción DADO que el usuario está en el catálogo
Y se resalta el término buscado en resultados CUANDO selecciona "Principiante" e "Intermedio"
Y se muestra "X resultados para 'fibonacci'" ENTONCES solo se muestran cursos de esos niveles
``` Y se pueden combinar con otros filtros activos
Y se muestra badge de nivel en cada curso
**Escenario 5: Sin resultados** ```
```gherkin
DADO que el usuario aplicó filtros **Escenario 4: Buscar curso**
Y no hay cursos que cumplan los criterios ```gherkin
ENTONCES se muestra mensaje "No se encontraron cursos" DADO que el usuario está en el catálogo
Y se sugiere "Intenta ajustar los filtros" CUANDO escribe "fibonacci" en el buscador
Y se muestra botón "Limpiar filtros" ENTONCES se filtran cursos en tiempo real
``` Y se muestran solo cursos que contengan "fibonacci" en título o descripción
Y se resalta el término buscado en resultados
**Escenario 6: Ver progreso en curso inscrito** Y se muestra "X resultados para 'fibonacci'"
```gherkin ```
DADO que el usuario ya está inscrito en un curso
CUANDO ve la tarjeta del curso en el catálogo **Escenario 5: Sin resultados**
ENTONCES se muestra barra de progreso (ej: "60% completado") ```gherkin
Y el botón dice "Continuar" en lugar de "Ver curso" DADO que el usuario aplicó filtros
Y al hacer click, navega a la última lección vista Y no hay cursos que cumplan los criterios
``` ENTONCES se muestra mensaje "No se encontraron cursos"
Y se sugiere "Intenta ajustar los filtros"
**Escenario 7: Ver recomendaciones** Y se muestra botón "Limpiar filtros"
```gherkin ```
DADO que el usuario tiene cursos en progreso
CUANDO accede al catálogo **Escenario 6: Ver progreso en curso inscrito**
ENTONCES se muestra sección "Recomendado para ti" ```gherkin
Y aparecen máximo 6 cursos relacionados DADO que el usuario ya está inscrito en un curso
Y se basa en: cursos en progreso, nivel del usuario, categorías de interés CUANDO ve la tarjeta del curso en el catálogo
``` ENTONCES se muestra barra de progreso (ej: "60% completado")
Y el botón dice "Continuar" en lugar de "Ver curso"
**Escenario 8: Ordenar resultados** Y al hacer click, navega a la última lección vista
```gherkin ```
DADO que el usuario está viendo el catálogo
CUANDO selecciona ordenar por "Mejor valorados" **Escenario 7: Ver recomendaciones**
ENTONCES los cursos se reordenan de mayor a menor rating ```gherkin
Y la paginación se mantiene DADO que el usuario tiene cursos en progreso
Y los filtros activos se mantienen CUANDO accede al catálogo
``` ENTONCES se muestra sección "Recomendado para ti"
Y aparecen máximo 6 cursos relacionados
## Criterios Adicionales Y se basa en: cursos en progreso, nivel del usuario, categorías de interés
```
- [ ] Responsive design para móvil y tablet
- [ ] Loading skeleton mientras cargan cursos **Escenario 8: Ordenar resultados**
- [ ] Infinite scroll opcional (además de paginación) ```gherkin
- [ ] Animaciones suaves al filtrar DADO que el usuario está viendo el catálogo
- [ ] Badge "Nuevo" para cursos publicados hace < 30 días CUANDO selecciona ordenar por "Mejor valorados"
- [ ] Badge "Popular" para cursos con > 1000 estudiantes ENTONCES los cursos se reordenan de mayor a menor rating
- [ ] Guardar filtros en localStorage para próxima visita Y la paginación se mantiene
Y los filtros activos se mantienen
--- ```
## Tareas Técnicas ## Criterios Adicionales
**Database:** - [ ] Responsive design para móvil y tablet
- [ ] DB-EDU-001: Verificar schema education.courses - [ ] Loading skeleton mientras cargan cursos
- [ ] DB-EDU-002: Verificar índices en category_id, level, published_at - [ ] Infinite scroll opcional (además de paginación)
- [ ] DB-EDU-003: Vista courses_catalog con joins optimizados - [ ] Animaciones suaves al filtrar
- [ ] Badge "Nuevo" para cursos publicados hace < 30 días
**Backend:** - [ ] Badge "Popular" para cursos con > 1000 estudiantes
- [ ] BE-EDU-001: Endpoint GET /education/courses (con paginación) - [ ] Guardar filtros en localStorage para próxima visita
- [ ] BE-EDU-002: Implementar filtros: category, level, duration, search
- [ ] BE-EDU-003: Implementar ordenamiento: recent, popular, rating ---
- [ ] BE-EDU-004: Endpoint GET /education/categories
- [ ] BE-EDU-005: Implementar CourseService.getCatalog() ## Tareas Técnicas
- [ ] BE-EDU-006: Implementar lógica de recomendaciones
- [ ] BE-EDU-007: Caché de catálogo en Redis (5 min) **Database:**
- [ ] DB-EDU-001: Verificar schema education.courses
**Frontend:** - [ ] DB-EDU-002: Verificar índices en category_id, level, published_at
- [ ] FE-EDU-001: Crear página CoursesPage.tsx - [ ] DB-EDU-003: Vista courses_catalog con joins optimizados
- [ ] FE-EDU-002: Crear componente CourseCard.tsx
- [ ] FE-EDU-003: Crear componente CourseFilters.tsx **Backend:**
- [ ] FE-EDU-004: Crear componente SearchBar.tsx - [ ] BE-EDU-001: Endpoint GET /education/courses (con paginación)
- [ ] FE-EDU-005: Crear componente Pagination.tsx - [ ] BE-EDU-002: Implementar filtros: category, level, duration, search
- [ ] FE-EDU-006: Implementar coursesStore (Zustand) - [ ] BE-EDU-003: Implementar ordenamiento: recent, popular, rating
- [ ] FE-EDU-007: Integrar con React Query para caché - [ ] BE-EDU-004: Endpoint GET /education/categories
- [ ] FE-EDU-008: Skeleton loader para carga - [ ] BE-EDU-005: Implementar CourseService.getCatalog()
- [ ] BE-EDU-006: Implementar lógica de recomendaciones
**Tests:** - [ ] BE-EDU-007: Caché de catálogo en Redis (5 min)
- [ ] TEST-EDU-001: Test unitario CourseService.getCatalog()
- [ ] TEST-EDU-002: Test integración GET /courses con filtros **Frontend:**
- [ ] TEST-EDU-003: Test E2E búsqueda y filtrado - [ ] FE-EDU-001: Crear página CoursesPage.tsx
- [ ] FE-EDU-002: Crear componente CourseCard.tsx
--- - [ ] FE-EDU-003: Crear componente CourseFilters.tsx
- [ ] FE-EDU-004: Crear componente SearchBar.tsx
## Dependencias - [ ] FE-EDU-005: Crear componente Pagination.tsx
- [ ] FE-EDU-006: Implementar coursesStore (Zustand)
**Depende de:** - [ ] FE-EDU-007: Integrar con React Query para caché
- [ ] US-AUTH-001: Sistema de autenticación - Estado: ✅ Completado - [ ] FE-EDU-008: Skeleton loader para carga
**Bloquea:** **Tests:**
- [ ] US-EDU-002: Ver detalle de curso - [ ] TEST-EDU-001: Test unitario CourseService.getCatalog()
- [ ] US-EDU-003: Iniciar lección - [ ] TEST-EDU-002: Test integración GET /courses con filtros
- [ ] TEST-EDU-003: Test E2E búsqueda y filtrado
---
---
## Notas Técnicas
## Dependencias
**Endpoints involucrados:**
| Método | Endpoint | Descripción | **Depende de:**
|--------|----------|-------------| - [ ] US-AUTH-001: Sistema de autenticación - Estado: ✅ Completado
| GET | /education/courses | Catálogo con filtros y paginación |
| GET | /education/categories | Listado de categorías | **Bloquea:**
- [ ] US-EDU-002: Ver detalle de curso
**Query params para GET /courses:** - [ ] US-EDU-003: Iniciar lección
```
?page=1 ---
&limit=12
&category=technical-analysis,fundamentals ## Notas Técnicas
&level=beginner,intermediate
&search=fibonacci **Endpoints involucrados:**
&sortBy=recent | Método | Endpoint | Descripción |
&sortOrder=desc |--------|----------|-------------|
``` | GET | /education/courses | Catálogo con filtros y paginación |
| GET | /education/categories | Listado de categorías |
**Response GET /courses:**
```typescript **Query params para GET /courses:**
{ ```
courses: [ ?page=1
{ &limit=12
id: "uuid-1", &category=technical-analysis,fundamentals
title: "Fibonacci Retracement Básico", &level=beginner,intermediate
slug: "fibonacci-retracement-basico", &search=fibonacci
shortDescription: "Aprende a usar Fibonacci...", &sortBy=recent
thumbnail: "https://cdn.orbiquant.com/courses/fib.jpg", &sortOrder=desc
category: { ```
id: "cat-1",
name: "Análisis Técnico", **Response GET /courses:**
slug: "technical-analysis", ```typescript
icon: "📊" {
}, courses: [
level: "beginner", {
duration: 150, // minutos id: "uuid-1",
moduleCount: 5, title: "Fibonacci Retracement Básico",
lessonCount: 23, slug: "fibonacci-retracement-basico",
studentCount: 1234, shortDescription: "Aprende a usar Fibonacci...",
rating: 4.8, thumbnail: "https://cdn.orbiquant.com/courses/fib.jpg",
reviewCount: 89, category: {
instructor: { id: "cat-1",
id: "inst-1", name: "Análisis Técnico",
name: "Carlos Mendoza", slug: "technical-analysis",
avatar: "https://...", icon: "📊"
title: "Senior Trader" },
}, level: "beginner",
isPremium: false, duration: 150, // minutos
publishedAt: "2025-11-15T10:00:00Z", moduleCount: 5,
userProgress: { lessonCount: 23,
enrolledAt: "2025-12-01T14:30:00Z", studentCount: 1234,
progressPercent: 60, rating: 4.8,
lastAccessedAt: "2025-12-04T18:20:00Z" reviewCount: 89,
} instructor: {
} id: "inst-1",
// ... más cursos name: "Carlos Mendoza",
], avatar: "https://...",
pagination: { title: "Senior Trader"
page: 1, },
limit: 12, isPremium: false,
total: 47, publishedAt: "2025-11-15T10:00:00Z",
totalPages: 4 userProgress: {
}, enrolledAt: "2025-12-01T14:30:00Z",
filters: { progressPercent: 60,
categories: [...], lastAccessedAt: "2025-12-04T18:20:00Z"
levels: ["beginner", "intermediate", "advanced", "expert"] }
} }
} // ... más cursos
``` ],
pagination: {
**Entidades/Tablas:** page: 1,
- `education.courses`: Catálogo de cursos limit: 12,
- `education.categories`: Categorías total: 47,
- `education.course_enrollments`: Inscripciones y progreso totalPages: 4
- `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
- [x] Story points estimados **Entidades/Tablas:**
- [x] Dependencias identificadas - `education.courses`: Catálogo de cursos
- [x] Sin bloqueadores - `education.categories`: Categorías
- [x] Diseño/mockup disponible - `education.course_enrollments`: Inscripciones y progreso
- [x] API spec disponible - `education.instructors`: Información de instructores
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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,364 +1,376 @@
# US-EDU-002: Ver Detalle de Curso ---
id: "US-EDU-002"
## Metadata title: "Ver Detalle de Curso"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Alta"
| **ID** | US-EDU-002 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 3
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P0 | updated_date: "2026-01-04"
| **Story Points** | 3 | ---
| **Sprint** | Sprint 3 |
| **Estado** | Pendiente | # US-EDU-002: Ver Detalle de Curso
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-002 |
**Como** usuario interesado en un curso, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** ver información detallada del curso antes de inscribirme, | **Módulo** | education |
**para** evaluar si el contenido se ajusta a mis objetivos de aprendizaje. | **Prioridad** | P0 |
| **Story Points** | 3 |
## Descripción Detallada | **Sprint** | Sprint 3 |
| **Estado** | Pendiente |
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. | **Asignado a** | Por asignar |
## Mockups/Wireframes ---
``` ## Historia de Usuario
┌─────────────────────────────────────────────────────────────────┐
│ ← Volver al catálogo 👤 │ **Como** usuario interesado en un curso,
├─────────────────────────────────────────────────────────────────┤ **quiero** ver información detallada del curso antes de inscribirme,
│ │ **para** evaluar si el contenido se ajusta a mis objetivos de aprendizaje.
│ ┌────────────────────┐ FIBONACCI RETRACEMENT BÁSICO │
│ │ │ ⭐ 4.8 (89 reseñas) | 1,234 estudiantes│ ## Descripción Detallada
│ │ [VIDEO INTRO] │ Por: Carlos Mendoza | 2h 30m | 5 módul│
│ │ ▶ Preview │ │ 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.
│ │ │ [📥 Inscribirse gratis] │
│ └────────────────────┘ │ ## Mockups/Wireframes
│ │
│ ┌────────────────────────────────────────────────────────────┐ │ ```
│ │ PESTAÑAS: [Descripción] [Temario] [Instructor] [Reseñas] │ │ ┌─────────────────────────────────────────────────────────────────┐
│ └────────────────────────────────────────────────────────────┘ │ │ ← Volver al catálogo 👤 │
│ │ ├─────────────────────────────────────────────────────────────────┤
│ QUÉ APRENDERÁS │ │ │
│ ✓ Identificar niveles de Fibonacci en gráficos │ │ ┌────────────────────┐ FIBONACCI RETRACEMENT BÁSICO │
│ ✓ Aplicar retrocesos en tendencias alcistas y bajistas │ │ │ │ ⭐ 4.8 (89 reseñas) | 1,234 estudiantes│
│ ✓ Combinar Fibonacci con otros indicadores │ │ │ [VIDEO INTRO] │ Por: Carlos Mendoza | 2h 30m | 5 módul│
│ ✓ Realizar entradas y salidas precisas │ │ │ ▶ Preview │ │
│ │ │ │ │ [📥 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...│ │ │
│ [Texto completo de descripción del curso] │ │ QUÉ APRENDERÁS │
│ │ │ ✓ Identificar niveles de Fibonacci en gráficos │
│ PARA QUIÉN ES ESTE CURSO │ │ ✓ Aplicar retrocesos en tendencias alcistas y bajistas │
│ • Traders principiantes que quieren dominar Fibonacci │ │ ✓ Combinar Fibonacci con otros indicadores │
│ • Analistas técnicos que buscan mejorar su precisión │ │ ✓ Realizar entradas y salidas precisas │
│ │ │ │
└─────────────────────────────────────────────────────────────────┘ │ REQUISITOS │
│ • Conocimientos básicos de trading │
[PESTAÑA TEMARIO] │ • Familiaridad con gráficos de velas │
┌─────────────────────────────────────────────────────────────────┐ │ │
│ 📚 TEMARIO DEL CURSO 5 módulos | 23 lecciones │ │ DESCRIPCIÓN │
│ │ │ Fibonacci es una herramienta fundamental del análisis técnico...│
│ ▼ Módulo 1: Introducción a Fibonacci (30 min) │ │ [Texto completo de descripción del curso] │
│ 1. ¿Qué es Fibonacci? (5:30) [🎬 Video] [Gratis] │ │ │
│ 2. Historia y fundamentos (8:45) [🎬 Video] [Gratis] │ │ PARA QUIÉN ES ESTE CURSO │
│ 3. La secuencia de Fibonacci (10:20) [📄 Artículo] │ │ • Traders principiantes que quieren dominar Fibonacci │
│ 4. Quiz: Fundamentos (15 preguntas) [📝 Quiz] │ │ • Analistas técnicos que buscan mejorar su precisión │
│ │ │ │
│ ▼ 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) │ [PESTAÑA TEMARIO]
│ 3. Práctica: Identificar retrocesos (20:00) │ ┌─────────────────────────────────────────────────────────────────┐
│ 4. Quiz: Niveles de retroceso │ │ 📚 TEMARIO DEL CURSO 5 módulos | 23 lecciones │
│ │ │ │
│ ▶ Módulo 3: Fibonacci en Tendencias (35 min) [🔒 Bloqueado] │ │ ▼ Módulo 1: Introducción a Fibonacci (30 min) │
│ ▶ Módulo 4: Estrategias Avanzadas (25 min) [🔒 Bloqueado] │ │ 1. ¿Qué es Fibonacci? (5:30) [🎬 Video] [Gratis] │
│ ▶ Módulo 5: Fibonacci + Otros Indicadores (15 min) [🔒] │ │ 2. Historia y fundamentos (8:45) [🎬 Video] [Gratis] │
│ │ │ 3. La secuencia de Fibonacci (10:20) [📄 Artículo] │
└─────────────────────────────────────────────────────────────────┘ │ 4. Quiz: Fundamentos (15 preguntas) [📝 Quiz] │
``` │ │
│ ▼ Módulo 2: Niveles de Retroceso (45 min) │
--- │ 1. Niveles principales: 38.2%, 50%, 61.8% (12:15) │
│ 2. Cómo dibujar Fibonacci en gráfico (18:30) │
## Criterios de Aceptación │ 3. Práctica: Identificar retrocesos (20:00) │
│ 4. Quiz: Niveles de retroceso │
**Escenario 1: Ver detalle de curso no inscrito** │ │
```gherkin │ ▶ Módulo 3: Fibonacci en Tendencias (35 min) [🔒 Bloqueado] │
DADO que el usuario está autenticado │ ▶ Módulo 4: Estrategias Avanzadas (25 min) [🔒 Bloqueado] │
Y NO está inscrito en el curso │ ▶ Módulo 5: Fibonacci + Otros Indicadores (15 min) [🔒] │
CUANDO accede a /education/courses/fibonacci-retracement-basico │ │
ENTONCES se muestra la página de detalle del curso └─────────────────────────────────────────────────────────────────┘
Y se muestra: título, rating, estudiantes, instructor, duración ```
Y se muestra botón "Inscribirse" o "Inscribirse gratis"
Y se muestra video preview del curso (si existe) ---
Y se muestra descripción completa
Y se muestra temario desglosado por módulos ## Criterios de Aceptación
```
**Escenario 1: Ver detalle de curso no inscrito**
**Escenario 2: Ver detalle de curso inscrito** ```gherkin
```gherkin DADO que el usuario está autenticado
DADO que el usuario está inscrito en el curso Y NO está inscrito en el curso
Y tiene 60% de progreso CUANDO accede a /education/courses/fibonacci-retracement-basico
CUANDO accede al detalle del curso ENTONCES se muestra la página de detalle del curso
ENTONCES se muestra barra de progreso "60% completado" Y se muestra: título, rating, estudiantes, instructor, duración
Y el botón principal dice "Continuar curso" Y se muestra botón "Inscribirse" o "Inscribirse gratis"
Y se destaca la última lección accedida Y se muestra video preview del curso (si existe)
Y se muestra resumen de progreso: "14 de 23 lecciones completadas" Y se muestra descripción completa
``` Y se muestra temario desglosado por módulos
```
**Escenario 3: Ver temario completo**
```gherkin **Escenario 2: Ver detalle de curso inscrito**
DADO que el usuario está en la pestaña "Temario" ```gherkin
ENTONCES se muestran todos los módulos del curso DADO que el usuario está inscrito en el curso
Y cada módulo muestra: título, duración total, número de lecciones Y tiene 60% de progreso
Y cada lección muestra: título, duración, tipo (video/artículo/quiz) CUANDO accede al detalle del curso
Y se puede expandir/colapsar cada módulo ENTONCES se muestra barra de progreso "60% completado"
Y lecciones gratuitas están marcadas como "Gratis" Y el botón principal dice "Continuar curso"
Y lecciones bloqueadas muestran candado 🔒 Y se destaca la última lección accedida
``` Y se muestra resumen de progreso: "14 de 23 lecciones completadas"
```
**Escenario 4: Preview de lección gratuita**
```gherkin **Escenario 3: Ver temario completo**
DADO que el usuario NO está inscrito ```gherkin
Y el curso tiene lecciones marcadas como "Gratis" DADO que el usuario está en la pestaña "Temario"
CUANDO hace click en una lección gratuita ENTONCES se muestran todos los módulos del curso
ENTONCES puede ver el contenido sin inscribirse Y cada módulo muestra: título, duración total, número de lecciones
Y se muestra CTA "Inscríbete para acceder a todo el contenido" Y cada lección muestra: título, duración, tipo (video/artículo/quiz)
``` Y se puede expandir/colapsar cada módulo
Y lecciones gratuitas están marcadas como "Gratis"
**Escenario 5: Ver información del instructor** Y lecciones bloqueadas muestran candado 🔒
```gherkin ```
DADO que el usuario está en la pestaña "Instructor"
ENTONCES se muestra: foto, nombre, título, biografía **Escenario 4: Preview de lección gratuita**
Y se muestra lista de otros cursos del instructor ```gherkin
Y se muestran estadísticas: total estudiantes, cursos, rating promedio DADO que el usuario NO está inscrito
Y se muestra botón "Ver perfil completo" (opcional) Y el curso tiene lecciones marcadas como "Gratis"
``` CUANDO hace click en una lección gratuita
ENTONCES puede ver el contenido sin inscribirse
**Escenario 6: Ver reseñas de estudiantes** Y se muestra CTA "Inscríbete para acceder a todo el contenido"
```gherkin ```
DADO que el usuario está en la pestaña "Reseñas"
ENTONCES se muestra rating promedio (ej: 4.8/5) **Escenario 5: Ver información del instructor**
Y se muestra distribución de ratings (5★: 60%, 4★: 30%, etc.) ```gherkin
Y se muestran las últimas 10 reseñas DADO que el usuario está en la pestaña "Instructor"
Y cada reseña muestra: nombre del usuario, fecha, rating, comentario ENTONCES se muestra: foto, nombre, título, biografía
Y se puede filtrar por: Todas, 5★, 4★, 3★, 2★, 1★ Y se muestra lista de otros cursos del instructor
``` Y se muestran estadísticas: total estudiantes, cursos, rating promedio
Y se muestra botón "Ver perfil completo" (opcional)
**Escenario 7: Inscribirse en curso** ```
```gherkin
DADO que el usuario NO está inscrito **Escenario 6: Ver reseñas de estudiantes**
Y el curso es gratuito ```gherkin
CUANDO hace click en "Inscribirse gratis" DADO que el usuario está en la pestaña "Reseñas"
ENTONCES se registra la inscripción en backend ENTONCES se muestra rating promedio (ej: 4.8/5)
Y se muestra toast "¡Te has inscrito en el curso!" Y se muestra distribución de ratings (5★: 60%, 4★: 30%, etc.)
Y el botón cambia a "Comenzar curso" Y se muestran las últimas 10 reseñas
Y se navega a la primera lección al hacer click Y cada reseña muestra: nombre del usuario, fecha, rating, comentario
``` Y se puede filtrar por: Todas, 5★, 4★, 3★, 2★, 1★
```
**Escenario 8: Curso premium sin suscripción**
```gherkin **Escenario 7: Inscribirse en curso**
DADO que el curso es premium ```gherkin
Y el usuario NO tiene suscripción activa DADO que el usuario NO está inscrito
CUANDO intenta inscribirse Y el curso es gratuito
ENTONCES se muestra modal "Este curso requiere suscripción Premium" CUANDO hace click en "Inscribirse gratis"
Y se muestra botón "Ver planes" que lleva a /pricing ENTONCES se registra la inscripción en backend
Y NO se permite inscripción Y se muestra toast "¡Te has inscrito en el curso!"
``` Y el botón cambia a "Comenzar curso"
Y se navega a la primera lección al hacer click
## Criterios Adicionales ```
- [ ] Video preview auto-play al cargar página (muted) **Escenario 8: Curso premium sin suscripción**
- [ ] Compartir curso en redes sociales ```gherkin
- [ ] Agregar curso a "Guardados" (wishlist) DADO que el curso es premium
- [ ] Mostrar badge "Bestseller" si es top 10 más vendido Y el usuario NO tiene suscripción activa
- [ ] Mostrar "Última actualización: DD/MM/YYYY" CUANDO intenta inscribirse
- [ ] SEO optimizado con meta tags dinámicos ENTONCES se muestra modal "Este curso requiere suscripción Premium"
- [ ] Structured data (schema.org) para Google Y se muestra botón "Ver planes" que lleva a /pricing
Y NO se permite inscripción
--- ```
## Tareas Técnicas ## Criterios Adicionales
**Database:** - [ ] Video preview auto-play al cargar página (muted)
- [ ] DB-EDU-004: Verificar relación courses -> modules -> lessons - [ ] Compartir curso en redes sociales
- [ ] DB-EDU-005: Tabla course_reviews para reseñas - [ ] Agregar curso a "Guardados" (wishlist)
- [ ] DB-EDU-006: Tabla course_enrollments para inscripciones - [ ] Mostrar badge "Bestseller" si es top 10 más vendido
- [ ] Mostrar "Última actualización: DD/MM/YYYY"
**Backend:** - [ ] SEO optimizado con meta tags dinámicos
- [ ] BE-EDU-008: Endpoint GET /education/courses/:slug - [ ] Structured data (schema.org) para Google
- [ ] BE-EDU-009: Endpoint POST /education/courses/:id/enroll
- [ ] BE-EDU-010: Endpoint GET /education/courses/:id/reviews ---
- [ ] BE-EDU-011: Implementar CourseService.getBySlug()
- [ ] BE-EDU-012: Implementar EnrollmentService.enroll() ## Tareas Técnicas
- [ ] BE-EDU-013: Verificar acceso Premium para cursos pagos
**Database:**
**Frontend:** - [ ] DB-EDU-004: Verificar relación courses -> modules -> lessons
- [ ] FE-EDU-009: Crear página CourseDetailPage.tsx - [ ] DB-EDU-005: Tabla course_reviews para reseñas
- [ ] FE-EDU-010: Crear componente CourseHeader.tsx - [ ] DB-EDU-006: Tabla course_enrollments para inscripciones
- [ ] FE-EDU-011: Crear componente CourseSyllabus.tsx (temario)
- [ ] FE-EDU-012: Crear componente InstructorCard.tsx **Backend:**
- [ ] FE-EDU-013: Crear componente CourseReviews.tsx - [ ] BE-EDU-008: Endpoint GET /education/courses/:slug
- [ ] FE-EDU-014: Crear componente EnrollButton.tsx - [ ] BE-EDU-009: Endpoint POST /education/courses/:id/enroll
- [ ] FE-EDU-015: Implementar tabs de navegación - [ ] BE-EDU-010: Endpoint GET /education/courses/:id/reviews
- [ ] FE-EDU-016: Modal de confirmación de inscripción - [ ] BE-EDU-011: Implementar CourseService.getBySlug()
- [ ] BE-EDU-012: Implementar EnrollmentService.enroll()
**Tests:** - [ ] BE-EDU-013: Verificar acceso Premium para cursos pagos
- [ ] TEST-EDU-004: Test inscripción en curso gratuito
- [ ] TEST-EDU-005: Test bloqueo de curso premium **Frontend:**
- [ ] TEST-EDU-006: Test E2E ver detalle e inscribirse - [ ] FE-EDU-009: Crear página CourseDetailPage.tsx
- [ ] FE-EDU-010: Crear componente CourseHeader.tsx
--- - [ ] FE-EDU-011: Crear componente CourseSyllabus.tsx (temario)
- [ ] FE-EDU-012: Crear componente InstructorCard.tsx
## Dependencias - [ ] FE-EDU-013: Crear componente CourseReviews.tsx
- [ ] FE-EDU-014: Crear componente EnrollButton.tsx
**Depende de:** - [ ] FE-EDU-015: Implementar tabs de navegación
- [ ] US-EDU-001: Ver catálogo - Estado: Pendiente - [ ] FE-EDU-016: Modal de confirmación de inscripción
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
**Tests:**
**Bloquea:** - [ ] TEST-EDU-004: Test inscripción en curso gratuito
- [ ] US-EDU-003: Iniciar lección - [ ] TEST-EDU-005: Test bloqueo de curso premium
- [ ] US-EDU-007: Ver progreso - [ ] TEST-EDU-006: Test E2E ver detalle e inscribirse
--- ---
## Notas Técnicas ## Dependencias
**Endpoints involucrados:** **Depende de:**
| Método | Endpoint | Descripción | - [ ] US-EDU-001: Ver catálogo - Estado: Pendiente
|--------|----------|-------------| - [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
| GET | /education/courses/:slug | Detalle completo del curso |
| POST | /education/courses/:id/enroll | Inscribirse en curso | **Bloquea:**
| GET | /education/courses/:id/reviews | Reseñas del curso | - [ ] US-EDU-003: Iniciar lección
- [ ] US-EDU-007: Ver progreso
**Response GET /courses/:slug:**
```typescript ---
{
course: { ## Notas Técnicas
id: "uuid-1",
title: "Fibonacci Retracement Básico", **Endpoints involucrados:**
slug: "fibonacci-retracement-basico", | Método | Endpoint | Descripción |
description: "Descripción completa del curso...", |--------|----------|-------------|
learningObjectives: [ | GET | /education/courses/:slug | Detalle completo del curso |
"Identificar niveles de Fibonacci", | POST | /education/courses/:id/enroll | Inscribirse en curso |
"Aplicar retrocesos en tendencias" | GET | /education/courses/:id/reviews | Reseñas del curso |
],
requirements: [ **Response GET /courses/:slug:**
"Conocimientos básicos de trading", ```typescript
"Familiaridad con gráficos" {
], course: {
targetAudience: [ id: "uuid-1",
"Traders principiantes", title: "Fibonacci Retracement Básico",
"Analistas técnicos" slug: "fibonacci-retracement-basico",
], description: "Descripción completa del curso...",
category: {...}, learningObjectives: [
level: "beginner", "Identificar niveles de Fibonacci",
duration: 150, "Aplicar retrocesos en tendencias"
moduleCount: 5, ],
lessonCount: 23, requirements: [
studentCount: 1234, "Conocimientos básicos de trading",
rating: 4.8, "Familiaridad con gráficos"
reviewCount: 89, ],
instructor: { targetAudience: [
id: "inst-1", "Traders principiantes",
name: "Carlos Mendoza", "Analistas técnicos"
avatar: "...", ],
title: "Senior Trader", category: {...},
bio: "15 años de experiencia...", level: "beginner",
coursesCount: 8, duration: 150,
studentsCount: 12500, moduleCount: 5,
rating: 4.9 lessonCount: 23,
}, studentCount: 1234,
isPremium: false, rating: 4.8,
previewVideoUrl: "https://vimeo.com/...", reviewCount: 89,
updatedAt: "2025-11-20T10:00:00Z", instructor: {
publishedAt: "2025-11-15T10:00:00Z", id: "inst-1",
name: "Carlos Mendoza",
modules: [ avatar: "...",
{ title: "Senior Trader",
id: "mod-1", bio: "15 años de experiencia...",
title: "Introducción a Fibonacci", coursesCount: 8,
order: 1, studentsCount: 12500,
duration: 30, rating: 4.9
lessonCount: 4, },
lessons: [ isPremium: false,
{ previewVideoUrl: "https://vimeo.com/...",
id: "les-1", updatedAt: "2025-11-20T10:00:00Z",
title: "¿Qué es Fibonacci?", publishedAt: "2025-11-15T10:00:00Z",
type: "video",
duration: 5.5, modules: [
isFree: true, {
isCompleted: false, id: "mod-1",
order: 1 title: "Introducción a Fibonacci",
} order: 1,
// ... más lecciones duration: 30,
] lessonCount: 4,
} lessons: [
// ... más módulos {
], id: "les-1",
title: "¿Qué es Fibonacci?",
userEnrollment: { type: "video",
enrolledAt: "2025-12-01T14:30:00Z", duration: 5.5,
progressPercent: 60, isFree: true,
lessonsCompleted: 14, isCompleted: false,
lastAccessedLesson: { order: 1
id: "les-14", }
title: "Fibonacci en tendencias bajistas" // ... más lecciones
}, ]
lastAccessedAt: "2025-12-04T18:20:00Z" }
} // ... más módulos
} ],
}
``` userEnrollment: {
enrolledAt: "2025-12-01T14:30:00Z",
**Entidades/Tablas:** progressPercent: 60,
- `education.courses` lessonsCompleted: 14,
- `education.modules` lastAccessedLesson: {
- `education.lessons` id: "les-14",
- `education.course_enrollments` title: "Fibonacci en tendencias bajistas"
- `education.course_reviews` },
- `education.instructors` lastAccessedAt: "2025-12-04T18:20:00Z"
}
--- }
}
## Definition of Ready (DoR) ```
- [x] Historia claramente escrita **Entidades/Tablas:**
- [x] Criterios de aceptación definidos - `education.courses`
- [x] Story points estimados - `education.modules`
- [x] Dependencias identificadas - `education.lessons`
- [x] Sin bloqueadores - `education.course_enrollments`
- [x] Diseño/mockup disponible - `education.course_reviews`
- [x] API spec disponible - `education.instructors`
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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,360 +1,372 @@
# US-EDU-003: Iniciar Lección ---
id: "US-EDU-003"
## Metadata title: "Iniciar Leccion"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Alta"
| **ID** | US-EDU-003 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 3
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P0 | updated_date: "2026-01-04"
| **Story Points** | 3 | ---
| **Sprint** | Sprint 3 |
| **Estado** | Pendiente | # US-EDU-003: Iniciar Lección
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-003 |
**Como** usuario inscrito en un curso, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** acceder y comenzar una lección específica, | **Módulo** | education |
**para** consumir el contenido educativo y avanzar en mi aprendizaje. | **Prioridad** | P0 |
| **Story Points** | 3 |
## Descripción Detallada | **Sprint** | Sprint 3 |
| **Estado** | Pendiente |
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. | **Asignado a** | Por asignar |
## Mockups/Wireframes ---
``` ## Historia de Usuario
┌─────────────────────────────────────────────────────────────────┐
│ ← Volver al curso [≡ Temario] 👤 │ **Como** usuario inscrito en un curso,
├─────────────────────────────────────────────────────────────────┤ **quiero** acceder y comenzar una lección específica,
│ │ **para** consumir el contenido educativo y avanzar en mi aprendizaje.
│ Módulo 2: Niveles de Retroceso │
│ Lección 1/4: Niveles principales (38.2%, 50%, 61.8%) │ ## Descripción Detallada
│ 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%) │
│ ┌─ PESTAÑAS ────────────────────────────────────────────────┐ │ │ Progreso del curso: ████████░░░░░░░░ 45% │
│ │ [Descripción] [Recursos] [Mis Notas] [Q&A] │ │ │ │
│ └───────────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │ │ │
│ 📝 DESCRIPCIÓN │ │ │ [REPRODUCTOR DE VIDEO] │ │
│ En esta lección aprenderás los tres niveles principales de │ │ │ │ │
│ Fibonacci: 38.2%, 50% y 61.8%. Veremos cómo identificarlos... │ │ │ ▶ PLAY │ │
│ │ │ │ │ │
│ 📥 RECURSOS (2) │ │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12:15 │ │
│ • Plantilla de Fibonacci.xlsx (45 KB) [Descargar] │ │ │ ⏮ ⏸ ⏭ 🔊──── ⚙ 1x CC ⛶ │ │
│ • Cheatsheet de niveles.pdf (120 KB) [Descargar] │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌─ PESTAÑAS ────────────────────────────────────────────────┐ │
│ │ [← Anterior: Historia y fundamentos] │ │ │ │ [Descripción] [Recursos] [Mis Notas] [Q&A] │ │
│ │ [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... │
│ │
[SIDEBAR TEMARIO - Desplegable] │ 📥 RECURSOS (2) │
┌────────────────────────┐ │ • Plantilla de Fibonacci.xlsx (45 KB) [Descargar] │
│ TEMARIO │ │ • Cheatsheet de niveles.pdf (120 KB) [Descargar] │
│ ✓ Módulo 1 (4/4) ✓ │ │ │
│ ▼ Módulo 2 (1/4) │ │ ┌────────────────────────────────────────────────────────────┐ │
│ ✓ 1. Niveles princ. │ ← Lección actual │ │ [← Anterior: Historia y fundamentos] │ │
│ ○ 2. Dibujar Fib. │ │ │ [Siguiente: Cómo dibujar Fibonacci →] │ │
│ 🔒 3. Práctica │ │ │ [✓ Marcar como completada] │ │
│ 🔒 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) │
## Criterios de Aceptación │ ✓ 1. Niveles princ. │ ← Lección actual
│ ○ 2. Dibujar Fib. │
**Escenario 1: Iniciar lección desbloqueada** │ 🔒 3. Práctica │
```gherkin │ 🔒 4. Quiz │
DADO que el usuario está inscrito en el curso │ 🔒 Módulo 3 │
Y la lección está desbloqueada │ 🔒 Módulo 4 │
CUANDO hace click en la lección desde el temario │ 🔒 Módulo 5 │
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 2: Intentar acceder a lección bloqueada** **Escenario 1: Iniciar lección desbloqueada**
```gherkin ```gherkin
DADO que el curso requiere orden secuencial DADO que el usuario está inscrito en el curso
Y el usuario NO ha completado la lección anterior Y la lección está desbloqueada
CUANDO intenta acceder a una lección bloqueada CUANDO hace click en la lección desde el temario
ENTONCES se muestra modal "Debes completar lecciones anteriores" ENTONCES se navega a /education/courses/:slug/lessons/:lessonSlug
Y se sugiere la última lección disponible Y se carga el contenido de la lección
Y NO se carga el contenido Y se registra en backend que el usuario inició la lección
``` Y se actualiza "última lección accedida"
Y se muestra sidebar con temario completo
**Escenario 3: Lección de video carga posición guardada** ```
```gherkin
DADO que el usuario vio 5:30 de un video de 12:15 **Escenario 2: Intentar acceder a lección bloqueada**
Y cerró la lección sin terminar ```gherkin
CUANDO vuelve a abrir la lección DADO que el curso requiere orden secuencial
ENTONCES el video se posiciona en 5:30 Y el usuario NO ha completado la lección anterior
Y se muestra toast "Continuando desde 5:30" CUANDO intenta acceder a una lección bloqueada
Y se puede resetear posición si el usuario lo desea ENTONCES se muestra modal "Debes completar lecciones anteriores"
``` Y se sugiere la última lección disponible
Y NO se carga el contenido
**Escenario 4: Usuario no inscrito intenta acceder** ```
```gherkin
DADO que el usuario NO está inscrito en el curso **Escenario 3: Lección de video carga posición guardada**
CUANDO intenta acceder directamente a una lección ```gherkin
ENTONCES se redirige a la página del curso DADO que el usuario vio 5:30 de un video de 12:15
Y se muestra mensaje "Inscríbete para acceder a este curso" Y cerró la lección sin terminar
Y se muestra botón "Inscribirse" CUANDO vuelve a abrir la lección
``` ENTONCES el video se posiciona en 5:30
Y se muestra toast "Continuando desde 5:30"
**Escenario 5: Ver recursos de la lección** Y se puede resetear posición si el usuario lo desea
```gherkin ```
DADO que la lección tiene recursos descargables
CUANDO el usuario accede a la pestaña "Recursos" **Escenario 4: Usuario no inscrito intenta acceder**
ENTONCES se muestran todos los archivos disponibles ```gherkin
Y se muestra: nombre, tipo, tamaño DADO que el usuario NO está inscrito en el curso
Y puede descargar cada archivo con un click CUANDO intenta acceder directamente a una lección
Y se registra la descarga en analytics ENTONCES se redirige a la página del curso
``` Y se muestra mensaje "Inscríbete para acceder a este curso"
Y se muestra botón "Inscribirse"
**Escenario 6: Tomar notas durante lección** ```
```gherkin
DADO que el usuario está en una lección **Escenario 5: Ver recursos de la lección**
CUANDO accede a la pestaña "Mis Notas" ```gherkin
Y escribe texto en el editor DADO que la lección tiene recursos descargables
ENTONCES las notas se guardan automáticamente cada 2s CUANDO el usuario accede a la pestaña "Recursos"
Y se muestra indicador "Guardado" cuando se persiste ENTONCES se muestran todos los archivos disponibles
Y para videos se guarda el timestamp actual Y se muestra: nombre, tipo, tamaño
``` Y puede descargar cada archivo con un click
Y se registra la descarga en analytics
**Escenario 7: Navegar entre lecciones** ```
```gherkin
DADO que el usuario está en una lección **Escenario 6: Tomar notas durante lección**
Y existe una lección siguiente ```gherkin
CUANDO hace click en "Siguiente" DADO que el usuario está en una lección
ENTONCES navega a la siguiente lección del módulo CUANDO accede a la pestaña "Mis Notas"
Y se carga el nuevo contenido Y escribe texto en el editor
Y se actualiza el sidebar ENTONCES las notas se guardan automáticamente cada 2s
``` Y se muestra indicador "Guardado" cuando se persiste
Y para videos se guarda el timestamp actual
**Escenario 8: Marcar lección como completada** ```
```gherkin
DADO que el usuario vio la lección completa **Escenario 7: Navegar entre lecciones**
CUANDO hace click en "Marcar como completada" ```gherkin
ENTONCES se actualiza el progreso en backend DADO que el usuario está en una lección
Y aparece checkmark ✓ en el sidebar Y existe una lección siguiente
Y se actualiza la barra de progreso del curso CUANDO hace click en "Siguiente"
Y si es secuencial, se desbloquea siguiente lección ENTONCES navega a la siguiente lección del módulo
``` Y se carga el nuevo contenido
Y se actualiza el sidebar
## Criterios Adicionales ```
- [ ] Auto-save de progreso de video cada 10s **Escenario 8: Marcar lección como completada**
- [ ] Atajos de teclado: Espacio=play/pause, →=+10s, ←=-10s ```gherkin
- [ ] Picture-in-Picture para videos DADO que el usuario vio la lección completa
- [ ] Modo cine (ocultar sidebar) CUANDO hace click en "Marcar como completada"
- [ ] Velocidad de reproducción: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x ENTONCES se actualiza el progreso en backend
- [ ] Subtítulos si están disponibles Y aparece checkmark ✓ en el sidebar
- [ ] Analytics: tiempo en lección, pausas, rewinds Y se actualiza la barra de progreso del curso
Y si es secuencial, se desbloquea siguiente lección
--- ```
## Tareas Técnicas ## Criterios Adicionales
**Database:** - [ ] Auto-save de progreso de video cada 10s
- [ ] DB-EDU-007: Tabla user_lesson_progress (started_at, completed_at, last_position) - [ ] Atajos de teclado: Espacio=play/pause, →=+10s, ←=-10s
- [ ] DB-EDU-008: Tabla user_notes (lesson_id, content, timestamp) - [ ] Picture-in-Picture para videos
- [ ] DB-EDU-009: Índice en user_id + lesson_id para queries rápidas - [ ] Modo cine (ocultar sidebar)
- [ ] Velocidad de reproducción: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x
**Backend:** - [ ] Subtítulos si están disponibles
- [ ] BE-EDU-014: Endpoint GET /education/courses/:id/lessons/:lessonId - [ ] Analytics: tiempo en lección, pausas, rewinds
- [ ] BE-EDU-015: Validar inscripción y acceso a lección
- [ ] BE-EDU-016: Endpoint POST /education/lessons/:id/progress (guardar posición) ---
- [ ] BE-EDU-017: Endpoint POST /education/lessons/:id/complete
- [ ] BE-EDU-018: Endpoint GET/POST /education/lessons/:id/notes ## Tareas Técnicas
- [ ] BE-EDU-019: Endpoint GET /education/lessons/:id/resources/:resourceId
- [ ] BE-EDU-020: Implementar signed URLs para videos privados **Database:**
- [ ] BE-EDU-021: Verificar orden secuencial si aplica - [ ] DB-EDU-007: Tabla user_lesson_progress (started_at, completed_at, last_position)
- [ ] DB-EDU-008: Tabla user_notes (lesson_id, content, timestamp)
**Frontend:** - [ ] DB-EDU-009: Índice en user_id + lesson_id para queries rápidas
- [ ] FE-EDU-017: Crear LessonPlayerPage.tsx
- [ ] FE-EDU-018: Crear componente VideoPlayer.tsx **Backend:**
- [ ] FE-EDU-019: Crear componente LessonSidebar.tsx - [ ] BE-EDU-014: Endpoint GET /education/courses/:id/lessons/:lessonId
- [ ] FE-EDU-020: Crear componente NotesEditor.tsx - [ ] BE-EDU-015: Validar inscripción y acceso a lección
- [ ] FE-EDU-021: Crear componente ResourcesList.tsx - [ ] BE-EDU-016: Endpoint POST /education/lessons/:id/progress (guardar posición)
- [ ] FE-EDU-022: Implementar auto-save de posición (cada 10s) - [ ] BE-EDU-017: Endpoint POST /education/lessons/:id/complete
- [ ] FE-EDU-023: Implementar lessonStore (Zustand) - [ ] BE-EDU-018: Endpoint GET/POST /education/lessons/:id/notes
- [ ] FE-EDU-024: Navegación con teclas de flecha - [ ] BE-EDU-019: Endpoint GET /education/lessons/:id/resources/:resourceId
- [ ] FE-EDU-025: Toast notifications para acciones - [ ] BE-EDU-020: Implementar signed URLs para videos privados
- [ ] BE-EDU-021: Verificar orden secuencial si aplica
**Tests:**
- [ ] TEST-EDU-007: Test validación de acceso a lección **Frontend:**
- [ ] TEST-EDU-008: Test bloqueo de lección secuencial - [ ] FE-EDU-017: Crear LessonPlayerPage.tsx
- [ ] TEST-EDU-009: Test guardar y restaurar posición de video - [ ] FE-EDU-018: Crear componente VideoPlayer.tsx
- [ ] TEST-EDU-010: Test E2E iniciar lección y marcar completada - [ ] FE-EDU-019: Crear componente LessonSidebar.tsx
- [ ] FE-EDU-020: Crear componente NotesEditor.tsx
--- - [ ] FE-EDU-021: Crear componente ResourcesList.tsx
- [ ] FE-EDU-022: Implementar auto-save de posición (cada 10s)
## Dependencias - [ ] FE-EDU-023: Implementar lessonStore (Zustand)
- [ ] FE-EDU-024: Navegación con teclas de flecha
**Depende de:** - [ ] FE-EDU-025: Toast notifications para acciones
- [ ] US-EDU-002: Ver detalle de curso - Estado: Pendiente
- [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado **Tests:**
- [ ] TEST-EDU-007: Test validación de acceso a lección
**Bloquea:** - [ ] TEST-EDU-008: Test bloqueo de lección secuencial
- [ ] US-EDU-004: Ver video completo - [ ] TEST-EDU-009: Test guardar y restaurar posición de video
- [ ] US-EDU-005: Completar lección - [ ] TEST-EDU-010: Test E2E iniciar lección y marcar completada
- [ ] US-EDU-007: Ver progreso
---
---
## Dependencias
## Notas Técnicas
**Depende de:**
**Endpoints involucrados:** - [ ] US-EDU-002: Ver detalle de curso - Estado: Pendiente
| Método | Endpoint | Descripción | - [ ] US-AUTH-001: Autenticación - Estado: ✅ Completado
|--------|----------|-------------|
| GET | /education/courses/:courseId/lessons/:lessonId | Obtener lección | **Bloquea:**
| POST | /education/lessons/:id/progress | Guardar posición | - [ ] US-EDU-004: Ver video completo
| POST | /education/lessons/:id/complete | Marcar completada | - [ ] US-EDU-005: Completar lección
| GET | /education/lessons/:id/notes | Obtener notas | - [ ] US-EDU-007: Ver progreso
| POST | /education/lessons/:id/notes | Crear/actualizar notas |
---
**Response GET /lessons/:id:**
```typescript ## Notas Técnicas
{
lesson: { **Endpoints involucrados:**
id: "les-5", | Método | Endpoint | Descripción |
courseId: "course-1", |--------|----------|-------------|
moduleId: "mod-2", | GET | /education/courses/:courseId/lessons/:lessonId | Obtener lección |
title: "Niveles principales: 38.2%, 50%, 61.8%", | POST | /education/lessons/:id/progress | Guardar posición |
slug: "niveles-principales", | POST | /education/lessons/:id/complete | Marcar completada |
description: "En esta lección aprenderás...", | GET | /education/lessons/:id/notes | Obtener notas |
type: "video", | POST | /education/lessons/:id/notes | Crear/actualizar notas |
duration: 12.25, // minutos
order: 1, **Response GET /lessons/:id:**
isFree: false, ```typescript
{
// Video específico lesson: {
videoUrl: "https://vimeo.com/signed-url-12345", id: "les-5",
videoProvider: "vimeo", courseId: "course-1",
subtitles: [ moduleId: "mod-2",
{ language: "es", url: "..." }, title: "Niveles principales: 38.2%, 50%, 61.8%",
{ language: "en", url: "..." } slug: "niveles-principales",
], description: "En esta lección aprenderás...",
type: "video",
resources: [ duration: 12.25, // minutos
{ order: 1,
id: "res-1", isFree: false,
name: "Plantilla de Fibonacci.xlsx",
type: "application/vnd.ms-excel", // Video específico
size: 45120, videoUrl: "https://vimeo.com/signed-url-12345",
url: "https://s3.../signed-url" videoProvider: "vimeo",
} subtitles: [
], { language: "es", url: "..." },
{ language: "en", url: "..." }
userProgress: { ],
startedAt: "2025-12-04T10:30:00Z",
completedAt: null, resources: [
lastPosition: 5.5, // minutos {
timeSpent: 320, // segundos id: "res-1",
isCompleted: false name: "Plantilla de Fibonacci.xlsx",
}, type: "application/vnd.ms-excel",
size: 45120,
navigation: { url: "https://s3.../signed-url"
previous: { }
id: "les-4", ],
title: "Historia y fundamentos",
slug: "historia-fundamentos" userProgress: {
}, startedAt: "2025-12-04T10:30:00Z",
next: { completedAt: null,
id: "les-6", lastPosition: 5.5, // minutos
title: "Cómo dibujar Fibonacci", timeSpent: 320, // segundos
slug: "como-dibujar-fibonacci", isCompleted: false
isLocked: false },
}
}, navigation: {
previous: {
module: { id: "les-4",
id: "mod-2", title: "Historia y fundamentos",
title: "Niveles de Retroceso", slug: "historia-fundamentos"
lessonsCompleted: 1, },
totalLessons: 4 next: {
}, id: "les-6",
title: "Cómo dibujar Fibonacci",
course: { slug: "como-dibujar-fibonacci",
id: "course-1", isLocked: false
title: "Fibonacci Retracement Básico", }
slug: "fibonacci-retracement-basico", },
progressPercent: 45,
isSequential: true module: {
} id: "mod-2",
} title: "Niveles de Retroceso",
} lessonsCompleted: 1,
``` totalLessons: 4
},
**Validaciones backend:**
1. Usuario autenticado course: {
2. Usuario inscrito en el curso id: "course-1",
3. Si curso es secuencial, validar lecciones anteriores completadas title: "Fibonacci Retracement Básico",
4. Si curso es premium, validar suscripción activa slug: "fibonacci-retracement-basico",
progressPercent: 45,
**Entidades/Tablas:** isSequential: true
- `education.lessons` }
- `education.user_lesson_progress` }
- `education.user_notes` }
- `education.lesson_resources` ```
--- **Validaciones backend:**
1. Usuario autenticado
## Definition of Ready (DoR) 2. Usuario inscrito en el curso
3. Si curso es secuencial, validar lecciones anteriores completadas
- [x] Historia claramente escrita 4. Si curso es premium, validar suscripción activa
- [x] Criterios de aceptación definidos
- [x] Story points estimados **Entidades/Tablas:**
- [x] Dependencias identificadas - `education.lessons`
- [x] Sin bloqueadores - `education.user_lesson_progress`
- [x] Diseño/mockup disponible - `education.user_notes`
- [x] API spec disponible - `education.lesson_resources`
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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,357 +1,369 @@
# US-EDU-004: Ver Video de Lección ---
id: "US-EDU-004"
## Metadata title: "Ver Video de Leccion"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Alta"
| **ID** | US-EDU-004 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 3
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P0 | updated_date: "2026-01-04"
| **Story Points** | 3 | ---
| **Sprint** | Sprint 3 |
| **Estado** | Pendiente | # US-EDU-004: Ver Video de Lección
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-004 |
**Como** usuario inscrito viendo una lección de video, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** reproducir el video con controles completos y funcionalidades avanzadas, | **Módulo** | education |
**para** consumir el contenido educativo de manera cómoda y eficiente. | **Prioridad** | P0 |
| **Story Points** | 3 |
## Descripción Detallada | **Sprint** | Sprint 3 |
| **Estado** | Pendiente |
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. | **Asignado a** | Por asignar |
## Mockups/Wireframes ---
``` ## Historia de Usuario
┌─────────────────────────────────────────────────────────────────┐
│ Lección 2.1: Niveles principales de Fibonacci │ **Como** usuario inscrito viendo una lección de video,
├─────────────────────────────────────────────────────────────────┤ **quiero** reproducir el video con controles completos y funcionalidades avanzadas,
│ │ **para** consumir el contenido educativo de manera cómoda y eficiente.
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │ ## Descripción Detallada
│ │ │ │
│ │ [VIDEO REPRODUCIÉNDOSE] │ │ 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.
│ │ │ │
│ │ Carlos explicando │ │ ## Mockups/Wireframes
│ │ niveles de Fibonacci │ │
│ │ │ │ ```
│ │ │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ ━━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━ 05:30 / 12:15 │ │ │ Lección 2.1: Niveles principales de Fibonacci │
│ │ ⏮ ⏸ ⏭ 🔊──────● ⚙ 1.25x [CC] [PIP] ⛶ │ │ ├─────────────────────────────────────────────────────────────────┤
│ │ │ │ │ │
│ │ Capítulos: │ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ • 0:00 - Introducción │ │ │ │ │ │
│ │ • 2:15 - Nivel 38.2% │ │ │ │ │ │
│ │ • 5:30 - Nivel 50% ← Actual │ │ │ │ [VIDEO REPRODUCIÉNDOSE] │ │
│ │ • 8:45 - Nivel 61.8% │ │ │ │ │ │
│ │ • 11:00 - Conclusión │ │ │ │ Carlos explicando │ │
│ └────────────────────────────────────────────────────────────┘ │ │ │ niveles de Fibonacci │ │
│ │ │ │ │ │
│ CONTROLES: │ │ │ │ │
│ • Espacio: Play/Pause │ │ │ ━━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━ 05:30 / 12:15 │ │
│ • →: Adelantar 10s │ │ │ ⏮ ⏸ ⏭ 🔊──────● ⚙ 1.25x [CC] [PIP] ⛶ │ │
│ • ←: Retroceder 10s │ │ │ │ │
│ • F: Pantalla completa │ │ │ Capítulos: │ │
│ • M: Silenciar │ │ │ • 0:00 - Introducción │ │
│ • 0-9: Saltar a ese % del video │ │ │ • 2:15 - Nivel 38.2% │ │
│ │ │ │ • 5:30 - Nivel 50% ← Actual │ │
└─────────────────────────────────────────────────────────────────┘ │ │ • 8:45 - Nivel 61.8% │ │
│ │ • 11:00 - Conclusión │ │
[MENÚ DE VELOCIDAD] │ └────────────────────────────────────────────────────────────┘ │
┌──────────────┐ │ │
│ Velocidad │ │ CONTROLES: │
│ ○ 0.5x │ │ • Espacio: Play/Pause │
│ ○ 0.75x │ │ • →: Adelantar 10s │
│ ● 1x │ ← Seleccionado │ • ←: Retroceder 10s │
│ ○ 1.25x │ │ • F: Pantalla completa │
│ ○ 1.5x │ │ • M: Silenciar │
│ ○ 2x │ │ • 0-9: Saltar a ese % del video │
└──────────────┘ │ │
└─────────────────────────────────────────────────────────────────┘
[MENÚ DE CALIDAD]
┌──────────────┐ [MENÚ DE VELOCIDAD]
│ Calidad │ ┌──────────────┐
│ ● Auto │ ← Adaptativa │ Velocidad │
│ ○ 1080p │ │ ○ 0.5x │
│ ○ 720p │ │ ○ 0.75x │
│ ○ 480p │ │ ● 1x │ ← Seleccionado
│ ○ 360p │ │ ○ 1.25x │
└──────────────┘ │ ○ 1.5x │
│ ○ 2x │
[SUBTÍTULOS] └──────────────┘
┌──────────────┐
│ Subtítulos │ [MENÚ DE CALIDAD]
│ ● Desactivado│ ┌──────────────┐
│ ○ Español │ │ Calidad │
│ ○ English │ │ ● Auto │ ← Adaptativa
└──────────────┘ │ ○ 1080p │
``` │ ○ 720p │
│ ○ 480p │
--- │ ○ 360p │
└──────────────┘
## Criterios de Aceptación
[SUBTÍTULOS]
**Escenario 1: Reproducir video** ┌──────────────┐
```gherkin │ Subtítulos │
DADO que el usuario accedió a una lección de video │ ● Desactivado│
CUANDO el reproductor carga │ ○ Español │
ENTONCES se muestra el video con controles │ ○ English │
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**
```gherkin **Escenario 1: Reproducir video**
DADO que el video está cargado ```gherkin
CUANDO el usuario hace click en Play DADO que el usuario accedió a una lección de video
ENTONCES el video se reproduce CUANDO el reproductor carga
Y el botón cambia a Pause ⏸ ENTONCES se muestra el video con controles
Y la barra de progreso avanza Y el video está pausado inicialmente
Y el tiempo actual se actualiza cada segundo Y se muestra duración total
``` Y se carga en la última posición guardada (si existe)
Y se muestra toast "Continuando desde X:XX"
**Escenario 3: Cambiar velocidad de reproducción** ```
```gherkin
DADO que el video se está reproduciendo a 1x **Escenario 2: Controles básicos funcionan**
CUANDO el usuario selecciona velocidad 1.5x ```gherkin
ENTONCES el video se reproduce 50% más rápido DADO que el video está cargado
Y el audio se ajusta automáticamente (sin distorsión) CUANDO el usuario hace click en Play
Y se muestra indicador "1.5x" en el reproductor ENTONCES el video se reproduce
Y la configuración se guarda para próximos videos Y el botón cambia a Pause ⏸
``` Y la barra de progreso avanza
Y el tiempo actual se actualiza cada segundo
**Escenario 4: Activar subtítulos** ```
```gherkin
DADO que el video tiene subtítulos en español **Escenario 3: Cambiar velocidad de reproducción**
CUANDO el usuario activa subtítulos ```gherkin
ENTONCES aparecen subtítulos sincronizados con el audio DADO que el video se está reproduciendo a 1x
Y se pueden personalizar tamaño y posición CUANDO el usuario selecciona velocidad 1.5x
Y la preferencia se guarda para próximos videos ENTONCES el video se reproduce 50% más rápido
``` Y el audio se ajusta automáticamente (sin distorsión)
Y se muestra indicador "1.5x" en el reproductor
**Escenario 5: Saltar a posición específica** Y la configuración se guarda para próximos videos
```gherkin ```
DADO que el video se está reproduciendo
CUANDO el usuario hace click en la barra de progreso **Escenario 4: Activar subtítulos**
ENTONCES el video salta a esa posición ```gherkin
Y se muestra preview al hacer hover sobre la barra DADO que el video tiene subtítulos en español
Y la nueva posición se guarda automáticamente CUANDO el usuario activa subtítulos
``` ENTONCES aparecen subtítulos sincronizados con el audio
Y se pueden personalizar tamaño y posición
**Escenario 6: Auto-guardado de progreso** Y la preferencia se guarda para próximos videos
```gherkin ```
DADO que el usuario está viendo un video
Y el video alcanza la posición 7:30 **Escenario 5: Saltar a posición específica**
CUANDO pasan 10 segundos ```gherkin
ENTONCES se guarda la posición en backend DADO que el video se está reproduciendo
Y si el usuario cierra la página y vuelve CUANDO el usuario hace click en la barra de progreso
ENTONCES el video se carga en 7:30 ENTONCES el video salta a esa posición
``` Y se muestra preview al hacer hover sobre la barra
Y la nueva posición se guarda automáticamente
**Escenario 7: Atajos de teclado** ```
```gherkin
DADO que el usuario está viendo un video **Escenario 6: Auto-guardado de progreso**
CUANDO presiona la tecla → ```gherkin
ENTONCES el video avanza 10 segundos DADO que el usuario está viendo un video
Y se muestra indicador "+10s" Y el video alcanza la posición 7:30
CUANDO pasan 10 segundos
CUANDO presiona la tecla ← ENTONCES se guarda la posición en backend
ENTONCES el video retrocede 10 segundos Y si el usuario cierra la página y vuelve
Y se muestra indicador "-10s" ENTONCES el video se carga en 7:30
```
CUANDO presiona Espacio
ENTONCES el video pausa/reanuda **Escenario 7: Atajos de teclado**
```gherkin
CUANDO presiona F DADO que el usuario está viendo un video
ENTONCES entra/sale de pantalla completa CUANDO presiona la tecla →
``` ENTONCES el video avanza 10 segundos
Y se muestra indicador "+10s"
**Escenario 8: Picture-in-Picture**
```gherkin CUANDO presiona la tecla ←
DADO que el video se está reproduciendo ENTONCES el video retrocede 10 segundos
CUANDO el usuario hace click en botón PIP Y se muestra indicador "-10s"
ENTONCES el video se minimiza en una ventana flotante
Y puede navegar a otras páginas mientras ve el video CUANDO presiona Espacio
Y los controles básicos están disponibles en PIP ENTONCES el video pausa/reanuda
Y al cerrar PIP, vuelve al reproductor normal
``` CUANDO presiona F
ENTONCES entra/sale de pantalla completa
**Escenario 9: Capítulos del video** ```
```gherkin
DADO que el video tiene capítulos definidos **Escenario 8: Picture-in-Picture**
CUANDO el usuario hace click en un capítulo ```gherkin
ENTONCES el video salta a ese timestamp DADO que el video se está reproduciendo
Y se muestra marcador de capítulo en la barra de progreso CUANDO el usuario hace click en botón PIP
Y al hacer hover en la barra, muestra nombre del capítulo ENTONCES el video se minimiza en una ventana flotante
``` Y puede navegar a otras páginas mientras ve el video
Y los controles básicos están disponibles en PIP
**Escenario 10: Completar video automáticamente** Y al cerrar PIP, vuelve al reproductor normal
```gherkin ```
DADO que el usuario está viendo un video
CUANDO el video alcanza el 90% de reproducción **Escenario 9: Capítulos del video**
ENTONCES la lección se marca automáticamente como completada ```gherkin
Y se muestra toast "Lección completada +10 XP" DADO que el video tiene capítulos definidos
Y se actualiza el progreso del curso CUANDO el usuario hace click en un capítulo
Y se desbloquea siguiente lección (si es secuencial) ENTONCES el video salta a ese timestamp
``` Y se muestra marcador de capítulo en la barra de progreso
Y al hacer hover en la barra, muestra nombre del capítulo
## Criterios Adicionales ```
- [ ] Calidad adaptativa según ancho de banda **Escenario 10: Completar video automáticamente**
- [ ] Buffer progresivo para evitar cortes ```gherkin
- [ ] Indicador de buffering cuando carga DADO que el usuario está viendo un video
- [ ] Manejo de errores (video no disponible, error de red) CUANDO el video alcanza el 90% de reproducción
- [ ] Analytics: pausas, rewinds, abandonos ENTONCES la lección se marca automáticamente como completada
- [ ] Thumbnail preview al hover sobre barra de progreso Y se muestra toast "Lección completada +10 XP"
- [ ] Continuar reproduciendo al cambiar de pestaña (background play) Y se actualiza el progreso del curso
Y se desbloquea siguiente lección (si es secuencial)
--- ```
## Tareas Técnicas ## Criterios Adicionales
**Database:** - [ ] Calidad adaptativa según ancho de banda
- [ ] DB-EDU-010: Tabla video_chapters (video_id, time, title) - [ ] Buffer progresivo para evitar cortes
- [ ] DB-EDU-011: Actualizar user_lesson_progress.last_position cada 10s - [ ] Indicador de buffering cuando carga
- [ ] Manejo de errores (video no disponible, error de red)
**Backend:** - [ ] Analytics: pausas, rewinds, abandonos
- [ ] BE-EDU-022: Endpoint POST /education/lessons/:id/progress - [ ] Thumbnail preview al hover sobre barra de progreso
- [ ] BE-EDU-023: Endpoint GET /education/videos/:id/chapters - [ ] Continuar reproduciendo al cambiar de pestaña (background play)
- [ ] BE-EDU-024: Generar signed URLs para Vimeo/S3
- [ ] BE-EDU-025: Implementar validación de acceso a video ---
- [ ] BE-EDU-026: Webhook de Vimeo para confirmar encoding completado
- [ ] BE-EDU-027: Rate limiting en auto-save (máx cada 5s) ## Tareas Técnicas
**Frontend:** **Database:**
- [ ] FE-EDU-026: Implementar VideoPlayer.tsx con React Player - [ ] DB-EDU-010: Tabla video_chapters (video_id, time, title)
- [ ] FE-EDU-027: Custom controls overlay - [ ] DB-EDU-011: Actualizar user_lesson_progress.last_position cada 10s
- [ ] FE-EDU-028: Speed control menu
- [ ] FE-EDU-029: Subtitles toggle y customización **Backend:**
- [ ] FE-EDU-030: Keyboard shortcuts (arrow keys, space, f, m) - [ ] BE-EDU-022: Endpoint POST /education/lessons/:id/progress
- [ ] FE-EDU-031: Picture-in-Picture implementation - [ ] BE-EDU-023: Endpoint GET /education/videos/:id/chapters
- [ ] FE-EDU-032: Progress bar con preview thumbnail - [ ] BE-EDU-024: Generar signed URLs para Vimeo/S3
- [ ] FE-EDU-033: Chapters navigation - [ ] BE-EDU-025: Implementar validación de acceso a video
- [ ] FE-EDU-034: Auto-save de posición cada 10s - [ ] BE-EDU-026: Webhook de Vimeo para confirmar encoding completado
- [ ] FE-EDU-035: Restore position on load - [ ] BE-EDU-027: Rate limiting en auto-save (máx cada 5s)
- [ ] FE-EDU-036: Analytics tracking (play, pause, seek, complete)
- [ ] FE-EDU-037: Loading spinner y error states **Frontend:**
- [ ] FE-EDU-026: Implementar VideoPlayer.tsx con React Player
**Tests:** - [ ] FE-EDU-027: Custom controls overlay
- [ ] TEST-EDU-011: Test auto-save de progreso - [ ] FE-EDU-028: Speed control menu
- [ ] TEST-EDU-012: Test restaurar posición guardada - [ ] FE-EDU-029: Subtitles toggle y customización
- [ ] TEST-EDU-013: Test marcar completado al 90% - [ ] FE-EDU-030: Keyboard shortcuts (arrow keys, space, f, m)
- [ ] TEST-EDU-014: Test E2E reproducir video completo - [ ] FE-EDU-031: Picture-in-Picture implementation
- [ ] FE-EDU-032: Progress bar con preview thumbnail
--- - [ ] FE-EDU-033: Chapters navigation
- [ ] FE-EDU-034: Auto-save de posición cada 10s
## Dependencias - [ ] FE-EDU-035: Restore position on load
- [ ] FE-EDU-036: Analytics tracking (play, pause, seek, complete)
**Depende de:** - [ ] FE-EDU-037: Loading spinner y error states
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
- [ ] Video CDN: Vimeo Pro o AWS S3 + CloudFront **Tests:**
- [ ] TEST-EDU-011: Test auto-save de progreso
**Bloquea:** - [ ] TEST-EDU-012: Test restaurar posición guardada
- [ ] US-EDU-005: Completar lección - [ ] TEST-EDU-013: Test marcar completado al 90%
- [ ] US-EDU-007: Ver progreso - [ ] TEST-EDU-014: Test E2E reproducir video completo
--- ---
## Notas Técnicas ## Dependencias
**CDN de Videos:** **Depende de:**
- Opción 1: Vimeo Pro (recomendado para MVP) - [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
- API robusta - [ ] Video CDN: Vimeo Pro o AWS S3 + CloudFront
- Encoding automático
- Streaming adaptativo HLS **Bloquea:**
- Subtítulos integrados - [ ] US-EDU-005: Completar lección
- Analytics incluido - [ ] US-EDU-007: Ver progreso
- Opción 2: AWS S3 + CloudFront + MediaConvert
- Más control ---
- Más setup inicial
- Costos variables ## Notas Técnicas
**Librerías recomendadas:** **CDN de Videos:**
- React Player: Wrapper para múltiples providers - Opción 1: Vimeo Pro (recomendado para MVP)
- Video.js: Player HTML5 completo y customizable - API robusta
- Plyr: Alternativa moderna y ligera - Encoding automático
- Streaming adaptativo HLS
**Auto-save strategy:** - Subtítulos integrados
```javascript - Analytics incluido
// Guardar posición cada 10s mientras reproduce - Opción 2: AWS S3 + CloudFront + MediaConvert
useEffect(() => { - Más control
const interval = setInterval(() => { - Más setup inicial
if (isPlaying) { - Costos variables
saveProgress(currentTime);
} **Librerías recomendadas:**
}, 10000); - React Player: Wrapper para múltiples providers
return () => clearInterval(interval); - Video.js: Player HTML5 completo y customizable
}, [isPlaying, currentTime]); - Plyr: Alternativa moderna y ligera
// Guardar al pausar **Auto-save strategy:**
const handlePause = () => { ```javascript
saveProgress(currentTime); // Guardar posición cada 10s mientras reproduce
}; useEffect(() => {
const interval = setInterval(() => {
// Guardar antes de salir if (isPlaying) {
useEffect(() => { saveProgress(currentTime);
return () => { }
saveProgress(currentTime); }, 10000);
}; return () => clearInterval(interval);
}, []); }, [isPlaying, currentTime]);
```
// Guardar al pausar
**Analytics events:** const handlePause = () => {
- `video_started`: Primera reproducción saveProgress(currentTime);
- `video_played`: Cada vez que presiona play };
- `video_paused`: Cada pausa
- `video_seeked`: Salto manual // Guardar antes de salir
- `video_completed`: Alcanza 90% useEffect(() => {
- `video_speed_changed`: Cambia velocidad return () => {
- `video_quality_changed`: Cambia calidad saveProgress(currentTime);
};
**Entidades/Tablas:** }, []);
- `education.lessons` (campo videoUrl) ```
- `education.video_chapters`
- `education.user_lesson_progress` (campo last_position) **Analytics events:**
- `video_started`: Primera reproducción
--- - `video_played`: Cada vez que presiona play
- `video_paused`: Cada pausa
## Definition of Ready (DoR) - `video_seeked`: Salto manual
- `video_completed`: Alcanza 90%
- [x] Historia claramente escrita - `video_speed_changed`: Cambia velocidad
- [x] Criterios de aceptación definidos - `video_quality_changed`: Cambia calidad
- [x] Story points estimados
- [x] Dependencias identificadas **Entidades/Tablas:**
- [x] Sin bloqueadores - `education.lessons` (campo videoUrl)
- [x] Diseño/mockup disponible - `education.video_chapters`
- [x] API spec disponible - `education.user_lesson_progress` (campo last_position)
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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,388 @@
# US-EDU-005: Completar Lección ---
id: "US-EDU-005"
## Metadata title: "Completar Leccion"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Alta"
| **ID** | US-EDU-005 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 3
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P0 | updated_date: "2026-01-04"
| **Story Points** | 3 | ---
| **Sprint** | Sprint 3 |
| **Estado** | Pendiente | # US-EDU-005: Completar Lección
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-005 |
**Como** usuario viendo una lección, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** marcar la lección como completada y ganar recompensas, | **Módulo** | education |
**para** registrar mi progreso, desbloquear siguiente contenido y acumular XP. | **Prioridad** | P0 |
| **Story Points** | 3 |
## Descripción Detallada | **Sprint** | Sprint 3 |
| **Estado** | Pendiente |
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. | **Asignado a** | Por asignar |
## Mockups/Wireframes ---
``` ## Historia de Usuario
[VIDEO AL 90%]
┌─────────────────────────────────────────────────────────────────┐ **Como** usuario viendo una lección,
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● 11:00 / 12:15 │ **quiero** marcar la lección como completada y ganar recompensas,
└─────────────────────────────────────────────────────────────────┘ **para** registrar mi progreso, desbloquear siguiente contenido y acumular XP.
[ANIMACIÓN DE CELEBRACIÓN] ## Descripción Detallada
┌─────────────────────────────────────────────────────────────────┐
│ ✨ ✨ ✨ │ El usuario debe poder completar lecciones de forma automática (al ver 90% del video o llegar al final de un artículo) o manual (checkbox "marcar como completada"). Al completar, el sistema debe registrar el progreso, otorgar XP, actualizar estadísticas del usuario, desbloquear la siguiente lección si el curso es secuencial, mostrar celebración visual, y sugerir el siguiente paso.
│ │
│ ✓ ¡LECCIÓN COMPLETADA! │ ## Mockups/Wireframes
│ │
│ 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! │
│ │ │ │
│ [Volver al curso] │ │ Niveles principales de Fibonacci │
│ │ │ │
└─────────────────────────────────────────────────────────────────┘ │ +10 XP ganados │
│ │
[ARTÍCULO - CHECKBOX AL FINAL] │ Progreso del curso: 50% │
┌─────────────────────────────────────────────────────────────────┐ │ ████████████░░░░░░░░ │
│ ... contenido del artículo ... │ │ │
│ │ │ ┌────────────────────────────────────┐ │
│ Conclusión │ │ │ [Siguiente lección →] │ │
│ En esta lección aprendiste los fundamentos de... │ │ │ Cómo dibujar Fibonacci │ │
│ │ │ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ │
│ │ ☐ Marcar como completada │ │ │ [Volver al curso] │
│ └────────────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘
│ [← Anterior] [Siguiente lección →] │
│ │ [ARTÍCULO - CHECKBOX AL FINAL]
└─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐
│ ... contenido del artículo ... │
[SIDEBAR ACTUALIZADO] │ │
┌────────────────────────┐ │ Conclusión │
│ TEMARIO │ │ En esta lección aprendiste los fundamentos de... │
│ ✓ Módulo 1 (4/4) ✓ │ │ │
│ ▼ Módulo 2 (2/4) │ │ ┌────────────────────────────────────────────────────────────┐ │
│ ✓ 1. Niveles princ. │ ← Completada │ │ ☐ Marcar como completada │ │
│ ● 2. Dibujar Fib. │ ← Desbloqueada │ └────────────────────────────────────────────────────────────┘ │
│ 🔒 3. Práctica │ │ │
│ 🔒 4. Quiz │ │ [← Anterior] [Siguiente lección →] │
│ 🔒 Módulo 3 │ │ │
└────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
```
[SIDEBAR ACTUALIZADO]
--- ┌────────────────────────┐
│ TEMARIO │
## Criterios de Aceptación │ ✓ Módulo 1 (4/4) ✓ │
│ ▼ Módulo 2 (2/4) │
**Escenario 1: Completar video automáticamente** │ ✓ 1. Niveles princ. │ ← Completada
```gherkin │ ● 2. Dibujar Fib. │ ← Desbloqueada
DADO que el usuario está viendo un video │ 🔒 3. Práctica │
CUANDO el video alcanza el 90% de reproducción (10:48 de 12:00) │ 🔒 4. Quiz │
ENTONCES la lección se marca automáticamente como completada │ 🔒 Módulo 3 │
Y se registra completed_at en backend └────────────────────────┘
Y se muestra modal de celebración con confeti ```
Y se muestra "+10 XP ganados"
Y se actualiza barra de progreso del curso ---
Y se actualiza sidebar con checkmark ✓
Y si curso es secuencial, se desbloquea siguiente lección ## Criterios de Aceptación
```
**Escenario 1: Completar video automáticamente**
**Escenario 2: Completar artículo manualmente** ```gherkin
```gherkin DADO que el usuario está viendo un video
DADO que el usuario está leyendo un artículo CUANDO el video alcanza el 90% de reproducción (10:48 de 12:00)
Y scrolleó hasta el final ENTONCES la lección se marca automáticamente como completada
CUANDO hace click en checkbox "Marcar como completada" Y se registra completed_at en backend
ENTONCES se marca la lección como completada Y se muestra modal de celebración con confeti
Y se registra en backend Y se muestra "+10 XP ganados"
Y se muestra toast "+15 XP ganados" Y se actualiza barra de progreso del curso
Y se habilita botón "Siguiente lección" Y se actualiza sidebar con checkmark ✓
Y checkbox cambia a checked ✓ Y si curso es secuencial, se desbloquea siguiente lección
``` ```
**Escenario 3: Ganar XP por completar** **Escenario 2: Completar artículo manualmente**
```gherkin ```gherkin
DADO que el usuario completa una lección de video DADO que el usuario está leyendo un artículo
ENTONCES se otorgan 10 XP Y scrolleó hasta el final
Y se actualiza totalXP del usuario CUANDO hace click en checkbox "Marcar como completada"
Y se verifica si sube de nivel ENTONCES se marca la lección como completada
Y si sube de nivel, se muestra animación adicional Y se registra en backend
Y se actualiza badge de nivel en UI Y se muestra toast "+15 XP ganados"
``` Y se habilita botón "Siguiente lección"
Y checkbox cambia a checked ✓
**Escenario 4: Desbloquear siguiente lección** ```
```gherkin
DADO que el curso es secuencial **Escenario 3: Ganar XP por completar**
Y el usuario completa lección 3 del módulo 2 ```gherkin
ENTONCES la lección 4 del módulo 2 se desbloquea DADO que el usuario completa una lección de video
Y el candado 🔒 se remueve del sidebar ENTONCES se otorgan 10 XP
Y el usuario puede acceder a esa lección Y se actualiza totalXP del usuario
Y si intenta saltarse a lección 5, sigue bloqueada Y se verifica si sube de nivel
``` Y si sube de nivel, se muestra animación adicional
Y se actualiza badge de nivel en UI
**Escenario 5: Completar módulo completo** ```
```gherkin
DADO que el usuario completa la última lección de un módulo **Escenario 4: Desbloquear siguiente lección**
CUANDO se marca la lección como completada ```gherkin
ENTONCES también se completa el módulo DADO que el curso es secuencial
Y se otorgan +50 XP adicionales por módulo Y el usuario completa lección 3 del módulo 2
Y se muestra "¡Módulo completado!" ENTONCES la lección 4 del módulo 2 se desbloquea
Y se desbloquea el siguiente módulo (si es secuencial) Y el candado 🔒 se remueve del sidebar
Y se actualiza contador "Módulo 2 (4/4) ✓" Y el usuario puede acceder a esa lección
``` Y si intenta saltarse a lección 5, sigue bloqueada
```
**Escenario 6: Completar curso completo**
```gherkin **Escenario 5: Completar módulo completo**
DADO que el usuario completa la última lección del último módulo ```gherkin
CUANDO se marca como completada DADO que el usuario completa la última lección de un módulo
ENTONCES se completa el curso CUANDO se marca la lección como completada
Y se otorgan +200 XP adicionales por curso ENTONCES también se completa el módulo
Y se muestra modal "¡CURSO COMPLETADO!" Y se otorgan +50 XP adicionales por módulo
Y se genera certificado automáticamente Y se muestra "¡Módulo completado!"
Y se muestra botón "Descargar certificado" Y se desbloquea el siguiente módulo (si es secuencial)
Y se envía email de felicitación Y se actualiza contador "Módulo 2 (4/4) ✓"
``` ```
**Escenario 7: Sugerir siguiente paso** **Escenario 6: Completar curso completo**
```gherkin ```gherkin
DADO que el usuario completó una lección DADO que el usuario completa la última lección del último módulo
CUANDO se muestra el modal de celebración CUANDO se marca como completada
ENTONCES se sugiere la siguiente lección ENTONCES se completa el curso
Y se muestra título y duración de la siguiente Y se otorgan +200 XP adicionales por curso
Y botón "Siguiente lección" navega directamente Y se muestra modal "¡CURSO COMPLETADO!"
Y también hay opción "Volver al curso" Y se genera certificado automáticamente
Y si no hay siguiente lección, sugiere otro curso Y se muestra botón "Descargar certificado"
``` Y se envía email de felicitación
```
**Escenario 8: Re-marcar como incompleta**
```gherkin **Escenario 7: Sugerir siguiente paso**
DADO que el usuario marcó una lección como completada ```gherkin
Y quiere revisarla de nuevo DADO que el usuario completó una lección
CUANDO desmarca el checkbox CUANDO se muestra el modal de celebración
ENTONCES la lección vuelve a estado incompleto ENTONCES se sugiere la siguiente lección
Y el progreso del curso se actualiza (disminuye) Y se muestra título y duración de la siguiente
Y NO se quita el XP ya ganado Y botón "Siguiente lección" navega directamente
Y la lección puede volver a marcarse como completada Y también hay opción "Volver al curso"
``` Y si no hay siguiente lección, sugiere otro curso
```
**Escenario 9: Actualizar racha diaria**
```gherkin **Escenario 8: Re-marcar como incompleta**
DADO que es el primer día del usuario en la plataforma ```gherkin
CUANDO completa su primera lección DADO que el usuario marcó una lección como completada
ENTONCES se inicia racha de 1 día Y quiere revisarla de nuevo
Y se muestra toast "¡Racha iniciada! 🔥 1 día" CUANDO desmarca el checkbox
ENTONCES la lección vuelve a estado incompleto
DADO que el usuario tiene racha de 5 días Y el progreso del curso se actualiza (disminuye)
Y completa su primera lección del día Y NO se quita el XP ya ganado
ENTONCES la racha aumenta a 6 días Y la lección puede volver a marcarse como completada
Y se muestra "¡Racha de 6 días! 🔥" ```
```
**Escenario 9: Actualizar racha diaria**
**Escenario 10: Primera lección del día bonus** ```gherkin
```gherkin DADO que es el primer día del usuario en la plataforma
DADO que el usuario NO completó lecciones hoy CUANDO completa su primera lección
CUANDO completa su primera lección del día ENTONCES se inicia racha de 1 día
ENTONCES recibe +5 XP adicionales de bonus Y se muestra toast "¡Racha iniciada! 🔥 1 día"
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
## Criterios Adicionales ENTONCES la racha aumenta a 6 días
Y se muestra "¡Racha de 6 días! 🔥"
- [ ] Animación de confeti al completar ```
- [ ] Sonido de celebración (opcional, con toggle)
- [ ] Compartir logro en redes sociales **Escenario 10: Primera lección del día bonus**
- [ ] Badges especiales por milestones (10, 50, 100 lecciones) ```gherkin
- [ ] Actualizar calendario de actividad DADO que el usuario NO completó lecciones hoy
- [ ] Notificación push si está habilitado CUANDO completa su primera lección del día
ENTONCES recibe +5 XP adicionales de bonus
--- Y se muestra "+10 XP + 5 XP (bonus diario)"
```
## Tareas Técnicas
## Criterios Adicionales
**Database:**
- [ ] DB-EDU-012: Campo completed_at en user_lesson_progress - [ ] Animación de confeti al completar
- [ ] DB-EDU-013: Tabla user_xp_transactions (log de XP ganado) - [ ] Sonido de celebración (opcional, con toggle)
- [ ] DB-EDU-014: Campo current_streak en users - [ ] Compartir logro en redes sociales
- [ ] Badges especiales por milestones (10, 50, 100 lecciones)
**Backend:** - [ ] Actualizar calendario de actividad
- [ ] BE-EDU-028: Endpoint POST /education/lessons/:id/complete - [ ] Notificación push si está habilitado
- [ ] BE-EDU-029: Implementar LessonService.markAsCompleted()
- [ ] BE-EDU-030: Verificar si es primera del día (bonus XP) ---
- [ ] BE-EDU-031: Otorgar XP según tipo de lección
- [ ] BE-EDU-032: Verificar si completa módulo ## Tareas Técnicas
- [ ] BE-EDU-033: Verificar si completa curso
- [ ] BE-EDU-034: Desbloquear siguiente lección/módulo **Database:**
- [ ] BE-EDU-035: Actualizar racha diaria - [ ] DB-EDU-012: Campo completed_at en user_lesson_progress
- [ ] BE-EDU-036: Verificar si sube de nivel - [ ] DB-EDU-013: Tabla user_xp_transactions (log de XP ganado)
- [ ] BE-EDU-037: Event handler para generar certificado - [ ] DB-EDU-014: Campo current_streak en users
- [ ] BE-EDU-038: Enviar email de curso completado
**Backend:**
**Frontend:** - [ ] BE-EDU-028: Endpoint POST /education/lessons/:id/complete
- [ ] FE-EDU-038: Componente LessonCompleteModal.tsx - [ ] BE-EDU-029: Implementar LessonService.markAsCompleted()
- [ ] FE-EDU-039: Animación de confeti con react-confetti - [ ] BE-EDU-030: Verificar si es primera del día (bonus XP)
- [ ] FE-EDU-040: Actualizar sidebar en tiempo real - [ ] BE-EDU-031: Otorgar XP según tipo de lección
- [ ] FE-EDU-041: Actualizar barra de progreso - [ ] BE-EDU-032: Verificar si completa módulo
- [ ] FE-EDU-042: Toast notification de XP ganado - [ ] BE-EDU-033: Verificar si completa curso
- [ ] FE-EDU-043: Modal de curso completado - [ ] BE-EDU-034: Desbloquear siguiente lección/módulo
- [ ] FE-EDU-044: Checkbox para artículos - [ ] BE-EDU-035: Actualizar racha diaria
- [ ] FE-EDU-045: Auto-complete al 90% en videos - [ ] BE-EDU-036: Verificar si sube de nivel
- [ ] FE-EDU-046: Botón "Siguiente lección" - [ ] BE-EDU-037: Event handler para generar certificado
- [ ] FE-EDU-047: Integrar con progressStore - [ ] BE-EDU-038: Enviar email de curso completado
**Tests:** **Frontend:**
- [ ] TEST-EDU-015: Test completar lección otorga XP - [ ] FE-EDU-038: Componente LessonCompleteModal.tsx
- [ ] TEST-EDU-016: Test desbloquear siguiente lección - [ ] FE-EDU-039: Animación de confeti con react-confetti
- [ ] TEST-EDU-017: Test completar módulo otorga bonus - [ ] FE-EDU-040: Actualizar sidebar en tiempo real
- [ ] TEST-EDU-018: Test completar curso genera certificado - [ ] FE-EDU-041: Actualizar barra de progreso
- [ ] TEST-EDU-019: Test actualizar racha diaria - [ ] FE-EDU-042: Toast notification de XP ganado
- [ ] TEST-EDU-020: Test E2E completar lección end-to-end - [ ] FE-EDU-043: Modal de curso completado
- [ ] FE-EDU-044: Checkbox para artículos
--- - [ ] FE-EDU-045: Auto-complete al 90% en videos
- [ ] FE-EDU-046: Botón "Siguiente lección"
## Dependencias - [ ] FE-EDU-047: Integrar con progressStore
**Depende de:** **Tests:**
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente - [ ] TEST-EDU-015: Test completar lección otorga XP
- [ ] US-EDU-004: Ver video - Estado: Pendiente - [ ] TEST-EDU-016: Test desbloquear siguiente lección
- [ ] RF-EDU-003: Sistema de progreso - [ ] TEST-EDU-017: Test completar módulo otorga bonus
- [ ] TEST-EDU-018: Test completar curso genera certificado
**Bloquea:** - [ ] TEST-EDU-019: Test actualizar racha diaria
- [ ] US-EDU-007: Ver progreso - [ ] TEST-EDU-020: Test E2E completar lección end-to-end
- [ ] US-EDU-008: Obtener certificado
---
---
## Dependencias
## Notas Técnicas
**Depende de:**
**Endpoint POST /lessons/:id/complete:** - [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
```typescript - [ ] US-EDU-004: Ver video - Estado: Pendiente
// Request - [ ] RF-EDU-003: Sistema de progreso
POST /education/lessons/lesson-uuid-123/complete
{ **Bloquea:**
completedAt: "2025-12-05T15:30:00Z", - [ ] US-EDU-007: Ver progreso
timeSpent: 720, // segundos que pasó en la lección - [ ] US-EDU-008: Obtener certificado
finalPosition: 12.15 // para videos
} ---
// Response ## Notas Técnicas
{
success: true, **Endpoint POST /lessons/:id/complete:**
lesson: { ```typescript
id: "lesson-uuid-123", // Request
isCompleted: true, POST /education/lessons/lesson-uuid-123/complete
completedAt: "2025-12-05T15:30:00Z" {
}, completedAt: "2025-12-05T15:30:00Z",
rewards: { timeSpent: 720, // segundos que pasó en la lección
xpEarned: 10, finalPosition: 12.15 // para videos
bonusXP: 5, // Si es primera del día }
totalXP: 15,
newLevel: null, // Si subió de nivel, info del nuevo nivel // Response
badges: [] // Nuevos badges ganados {
}, success: true,
progress: { lesson: {
course: { id: "lesson-uuid-123",
progressPercent: 50, isCompleted: true,
lessonsCompleted: 12, completedAt: "2025-12-05T15:30:00Z"
totalLessons: 23 },
}, rewards: {
module: { xpEarned: 10,
isCompleted: false, bonusXP: 5, // Si es primera del día
lessonsCompleted: 2, totalXP: 15,
totalLessons: 4 newLevel: null, // Si subió de nivel, info del nuevo nivel
} badges: [] // Nuevos badges ganados
}, },
nextLesson: { progress: {
id: "lesson-uuid-124", course: {
title: "Cómo dibujar Fibonacci", progressPercent: 50,
slug: "como-dibujar-fibonacci", lessonsCompleted: 12,
isUnlocked: true totalLessons: 23
}, },
streak: { module: {
current: 6, isCompleted: false,
isNewDay: true lessonsCompleted: 2,
}, totalLessons: 4
courseCompleted: false }
} },
``` nextLesson: {
id: "lesson-uuid-124",
**Reglas de XP:** title: "Cómo dibujar Fibonacci",
- Lección de video: 10 XP slug: "como-dibujar-fibonacci",
- Lección de artículo: 15 XP isUnlocked: true
- Completar módulo: +50 XP },
- Completar curso: +200 XP streak: {
- Primera lección del día: +5 XP current: 6,
- Aprobar quiz primera vez: +30 XP (ver US-EDU-006) isNewDay: true
},
**Lógica de completitud:** courseCompleted: false
```javascript }
// Video: 90% de duración ```
isVideoComplete = currentTime >= (duration * 0.9);
**Reglas de XP:**
// Artículo: Manual con checkbox - Lección de video: 10 XP
// Quiz: Aprobar con score >= passingScore - Lección de artículo: 15 XP
``` - Completar módulo: +50 XP
- Completar curso: +200 XP
**Entidades/Tablas:** - Primera lección del día: +5 XP
- `education.user_lesson_progress` (completed_at) - Aprobar quiz primera vez: +30 XP (ver US-EDU-006)
- `gamification.user_xp_transactions`
- `gamification.user_stats` (current_streak, total_xp, level) **Lógica de completitud:**
```javascript
--- // Video: 90% de duración
isVideoComplete = currentTime >= (duration * 0.9);
## Definition of Ready (DoR)
// Artículo: Manual con checkbox
- [x] Historia claramente escrita // Quiz: Aprobar con score >= passingScore
- [x] Criterios de aceptación definidos ```
- [x] Story points estimados
- [x] Dependencias identificadas **Entidades/Tablas:**
- [x] Sin bloqueadores - `education.user_lesson_progress` (completed_at)
- [x] Diseño/mockup disponible - `gamification.user_xp_transactions`
- [x] API spec disponible - `gamification.user_stats` (current_streak, total_xp, level)
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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,473 +1,485 @@
# US-EDU-006: Realizar Quiz ---
id: "US-EDU-006"
## Metadata title: "Realizar Quiz"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Alta"
| **ID** | US-EDU-006 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 5
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P1 | updated_date: "2026-01-04"
| **Story Points** | 5 | ---
| **Sprint** | Sprint 4 |
| **Estado** | Pendiente | # US-EDU-006: Realizar Quiz
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-006 |
**Como** usuario inscrito en un curso, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** realizar quizzes interactivos al final de cada módulo, | **Módulo** | education |
**para** validar mi conocimiento, recibir feedback y aprobar las evaluaciones requeridas. | **Prioridad** | P1 |
| **Story Points** | 5 |
## Descripción Detallada | **Sprint** | Sprint 4 |
| **Estado** | Pendiente |
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). | **Asignado a** | Por asignar |
## Mockups/Wireframes ---
``` ## Historia de Usuario
[PANTALLA DE INTRODUCCIÓN AL QUIZ]
┌─────────────────────────────────────────────────────────────────┐ **Como** usuario inscrito en un curso,
│ ← Volver al curso │ **quiero** realizar quizzes interactivos al final de cada módulo,
├─────────────────────────────────────────────────────────────────┤ **para** validar mi conocimiento, recibir feedback y aprobar las evaluaciones requeridas.
│ │
│ 📝 QUIZ: FUNDAMENTOS DE FIBONACCI │ ## Descripción Detallada
│ │
│ Módulo 1: Introducción a 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).
│ │
│ ┌────────────────────────────────────────────────────────────┐ │ ## Mockups/Wireframes
│ │ 📊 Información del Quiz │ │
│ │ │ │ ```
│ │ Preguntas: 10 │ │ [PANTALLA DE INTRODUCCIÓN AL QUIZ]
│ │ Tiempo límite: 15 minutos │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ Puntuación para aprobar: 70% │ │ │ ← Volver al curso │
│ │ Intentos disponibles: 2 de 3 │ │ ├─────────────────────────────────────────────────────────────────┤
│ │ Tipo: Evaluación │ │ │ │
│ │ │ │ │ 📝 QUIZ: FUNDAMENTOS DE FIBONACCI │
│ │ ✓ Las preguntas están en orden aleatorio │ │ │ │
│ │ ✓ Verás tus respuestas al finalizar │ │ │ Módulo 1: Introducción a Fibonacci │
│ │ ✓ Puedes navegar libremente entre preguntas │ │ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │ 📊 Información del Quiz │ │
│ [🚀 Comenzar Quiz] │ │ │ │ │
│ │ │ │ Preguntas: 10 │ │
│ Intentos anteriores: │ │ │ Tiempo límite: 15 minutos │ │
│ • Intento 1: 65% - Reprobado (hace 2 días) │ │ │ Puntuación para aprobar: 70% │ │
│ │ │ │ Intentos disponibles: 2 de 3 │ │
└─────────────────────────────────────────────────────────────────┘ │ │ Tipo: Evaluación │ │
│ │ │ │
[PANTALLA DE PREGUNTA] │ │ ✓ Las preguntas están en orden aleatorio │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ ✓ Verás tus respuestas al finalizar │ │
│ Quiz: Fundamentos de Fibonacci ⏱ 12:45 restantes │ │ │ ✓ Puedes navegar libremente entre preguntas │ │
│ Pregunta 3 de 10 ████████░░░░░░ 30% │ │ └────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤ │ │
│ │ │ [🚀 Comenzar Quiz] │
│ ¿Cuál es el nivel de retroceso de Fibonacci más utilizado? │ │ │
│ │ │ Intentos anteriores: │
│ ○ 23.6% │ │ • Intento 1: 65% - Reprobado (hace 2 días) │
│ ○ 38.2% │ │ │
│ ● 61.8% ← Seleccionado │ └─────────────────────────────────────────────────────────────────┘
│ ○ 78.6% │
│ │ [PANTALLA DE PREGUNTA]
│ ┌────────────────────────────────────────────────────────────┐ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ [← Anterior] [🚩 Marcar] [Siguiente →] │ │ │ Quiz: Fundamentos de Fibonacci ⏱ 12:45 restantes │
│ └────────────────────────────────────────────────────────────┘ │ │ Pregunta 3 de 10 ████████░░░░░░ 30% │
│ │ ├─────────────────────────────────────────────────────────────────┤
│ NAVEGADOR: │ │ │
│ [●1] [●2] [●3] [○4] [○5] [○6] [○7] [○8] [🚩9] [○10] │ │ ¿Cuál es el nivel de retroceso de Fibonacci más utilizado? │
│ ↑ ↑ ↑ Sin Sin Sin Sin Sin Marca Sin │ │ │
│ Resp Resp Actual resp resp resp resp resp da resp │ │ ○ 23.6% │
│ │ │ ○ 38.2% │
│ [Finalizar Quiz] │ │ ● 61.8% ← Seleccionado │
│ │ │ ○ 78.6% │
└─────────────────────────────────────────────────────────────────┘ │ │
│ ┌────────────────────────────────────────────────────────────┐ │
[CONFIRMACIÓN DE ENVÍO] │ │ [← Anterior] [🚩 Marcar] [Siguiente →] │ │
┌─────────────────────────────────────────────────────────────────┐ │ └────────────────────────────────────────────────────────────┘ │
│ ⚠ ¿Estás seguro? │ │ │
│ │ │ NAVEGADOR: │
│ Estás a punto de enviar tu quiz. │ │ [●1] [●2] [●3] [○4] [○5] [○6] [○7] [○8] [🚩9] [○10] │
│ │ │ ↑ ↑ ↑ Sin Sin Sin Sin Sin Marca Sin │
│ Resumen: │ │ Resp Resp Actual resp resp resp resp resp da resp │
│ • Preguntas respondidas: 9 de 10 │ │ │
│ • Preguntas sin responder: 1 │ │ [Finalizar Quiz] │
│ • 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 │
│ ✅ ¡QUIZ APROBADO! │ │ • Preguntas marcadas para revisar: 1 │
│ │ │ │
│ Puntuación: 85% │ │ ⚠ Una vez enviado, no podrás cambiar tus respuestas. │
│ Preguntas correctas: 8.5/10 │ │ │
│ │ │ [Cancelar] [Enviar Quiz] │
│ +30 XP ganados │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘
│ 🎯 Desglose: │
│ • Preguntas correctas: 8 │ [RESULTADO - APROBADO]
│ • Preguntas incorrectas: 2 │ ┌─────────────────────────────────────────────────────────────────┐
│ • Puntuación requerida: 70% │ │ ✨ ✨ ✨ │
│ │ │ │
│ Tiempo invertido: 10:32 de 15:00 │ │ ✅ ¡QUIZ APROBADO! │
│ Intento: 2 de 3 │ │ │
│ │ │ Puntuación: 85% │
│ [Ver respuestas y explicaciones] │ │ Preguntas correctas: 8.5/10 │
│ [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 │
**Escenario 1: Ver introducción del quiz** │ Intento: 2 de 3 │
```gherkin │ │
DADO que el usuario está inscrito en el curso │ [Ver respuestas y explicaciones] │
CUANDO accede a una lección tipo quiz │ [Continuar al siguiente contenido →] │
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**
```gherkin **Escenario 1: Ver introducción del quiz**
DADO que el usuario está en la introducción del quiz ```gherkin
CUANDO hace click en "Comenzar Quiz" DADO que el usuario está inscrito en el curso
ENTONCES se registra el inicio del intento en backend CUANDO accede a una lección tipo quiz
Y se navega a la primera pregunta ENTONCES se muestra pantalla de introducción
Y se inicia el timer countdown Y se muestra: número de preguntas, tiempo límite, puntuación para aprobar
Y se carga el navegador de preguntas Y se muestran intentos disponibles
Y se registra timestamp de inicio Y se muestra historial de intentos anteriores (si existen)
``` Y se muestra botón "Comenzar Quiz"
```
**Escenario 3: Responder pregunta de opción múltiple**
```gherkin **Escenario 2: Comenzar quiz**
DADO que el usuario está en una pregunta ```gherkin
CUANDO selecciona una opción DADO que el usuario está en la introducción del quiz
ENTONCES la opción se marca visualmente CUANDO hace click en "Comenzar Quiz"
Y la pregunta se marca como "respondida" en el navegador ENTONCES se registra el inicio del intento en backend
Y puede cambiar su respuesta antes de enviar Y se navega a la primera pregunta
Y la respuesta se guarda temporalmente en el estado Y se inicia el timer countdown
``` Y se carga el navegador de preguntas
Y se registra timestamp de inicio
**Escenario 4: Navegar entre preguntas** ```
```gherkin
DADO que el usuario respondió la pregunta 3 **Escenario 3: Responder pregunta de opción múltiple**
CUANDO hace click en "Siguiente" ```gherkin
ENTONCES navega a la pregunta 4 DADO que el usuario está en una pregunta
Y su respuesta anterior se mantiene guardada CUANDO selecciona una opción
Y puede volver a pregunta 3 con "Anterior" ENTONCES la opción se marca visualmente
Y puede saltar a cualquier pregunta desde el navegador Y la pregunta se marca como "respondida" en el navegador
``` Y puede cambiar su respuesta antes de enviar
Y la respuesta se guarda temporalmente en el estado
**Escenario 5: Marcar pregunta para revisión** ```
```gherkin
DADO que el usuario está en una pregunta **Escenario 4: Navegar entre preguntas**
Y tiene dudas sobre su respuesta ```gherkin
CUANDO hace click en "🚩 Marcar" DADO que el usuario respondió la pregunta 3
ENTONCES la pregunta se marca con bandera en el navegador CUANDO hace click en "Siguiente"
Y puede volver fácilmente a revisarla antes de enviar ENTONCES navega a la pregunta 4
Y puede desmarcarla con otro click Y su respuesta anterior se mantiene guardada
``` Y puede volver a pregunta 3 con "Anterior"
Y puede saltar a cualquier pregunta desde el navegador
**Escenario 6: Quiz con tiempo límite expira** ```
```gherkin
DADO que el quiz tiene límite de 15 minutos **Escenario 5: Marcar pregunta para revisión**
Y el usuario está respondiendo ```gherkin
CUANDO el timer llega a 0:00 DADO que el usuario está en una pregunta
ENTONCES el quiz se envía automáticamente Y tiene dudas sobre su respuesta
Y se muestra "Tiempo agotado" CUANDO hace click en "🚩 Marcar"
Y se califica con las respuestas hasta el momento ENTONCES la pregunta se marca con bandera en el navegador
Y preguntas sin responder cuentan como incorrectas Y puede volver fácilmente a revisarla antes de enviar
``` Y puede desmarcarla con otro click
```
**Escenario 7: Enviar quiz manualmente**
```gherkin **Escenario 6: Quiz con tiempo límite expira**
DADO que el usuario respondió todas las preguntas ```gherkin
CUANDO hace click en "Finalizar Quiz" DADO que el quiz tiene límite de 15 minutos
ENTONCES se muestra modal de confirmación Y el usuario está respondiendo
Y muestra resumen: respondidas, sin responder, marcadas CUANDO el timer llega a 0:00
Y al confirmar, se envía al backend ENTONCES el quiz se envía automáticamente
Y se calcula la puntuación Y se muestra "Tiempo agotado"
Y se muestra pantalla de resultados Y se califica con las respuestas hasta el momento
``` Y preguntas sin responder cuentan como incorrectas
```
**Escenario 8: Aprobar quiz**
```gherkin **Escenario 7: Enviar quiz manualmente**
DADO que el usuario envió el quiz ```gherkin
Y obtuvo 85% de puntuación DADO que el usuario respondió todas las preguntas
Y la puntuación mínima es 70% CUANDO hace click en "Finalizar Quiz"
ENTONCES se muestra "¡QUIZ APROBADO!" ENTONCES se muestra modal de confirmación
Y se otorgan +30 XP Y muestra resumen: respondidas, sin responder, marcadas
Y se marca la lección quiz como completada Y al confirmar, se envía al backend
Y se desbloquea siguiente contenido Y se calcula la puntuación
Y se actualiza progreso del curso Y se muestra pantalla de resultados
``` ```
**Escenario 9: Reprobar quiz con intentos disponibles** **Escenario 8: Aprobar quiz**
```gherkin ```gherkin
DADO que el usuario obtuvo 65% (no aprobó) DADO que el usuario envió el quiz
Y quedan 2 intentos disponibles Y obtuvo 85% de puntuación
ENTONCES se muestra "Quiz Reprobado" Y la puntuación mínima es 70%
Y se muestra puntuación obtenida ENTONCES se muestra "¡QUIZ APROBADO!"
Y se muestra "Intentos restantes: 2" Y se otorgan +30 XP
Y se muestra botón "Reintentar" Y se marca la lección quiz como completada
Y NO se desbloquea siguiente contenido Y se desbloquea siguiente contenido
Y NO se otorga XP Y se actualiza progreso del curso
``` ```
**Escenario 10: Reprobar quiz sin intentos** **Escenario 9: Reprobar quiz con intentos disponibles**
```gherkin ```gherkin
DADO que el usuario agotó todos los intentos DADO que el usuario obtuvo 65% (no aprobó)
Y no aprobó el quiz Y quedan 2 intentos disponibles
ENTONCES se muestra mensaje "Sin intentos disponibles" ENTONCES se muestra "Quiz Reprobado"
Y se sugiere "Repasa las lecciones y contacta a soporte" Y se muestra puntuación obtenida
Y el siguiente contenido permanece bloqueado Y se muestra "Intentos restantes: 2"
Y se registra el bloqueo para seguimiento Y se muestra botón "Reintentar"
``` Y NO se desbloquea siguiente contenido
Y NO se otorga XP
**Escenario 11: Ver explicaciones de respuestas** ```
```gherkin
DADO que el usuario completó el quiz **Escenario 10: Reprobar quiz sin intentos**
CUANDO hace click en "Ver respuestas y explicaciones" ```gherkin
ENTONCES se muestran todas las preguntas DADO que el usuario agotó todos los intentos
Y se destacan respuestas correctas en verde Y no aprobó el quiz
Y se destacan respuestas incorrectas en rojo ENTONCES se muestra mensaje "Sin intentos disponibles"
Y se muestra la respuesta correcta Y se sugiere "Repasa las lecciones y contacta a soporte"
Y se muestra explicación detallada de cada respuesta Y el siguiente contenido permanece bloqueado
Y se sugieren lecciones relacionadas para repasar Y se registra el bloqueo para seguimiento
``` ```
**Escenario 12: Reintentar quiz** **Escenario 11: Ver explicaciones de respuestas**
```gherkin ```gherkin
DADO que el usuario reprobó el quiz DADO que el usuario completó el quiz
Y tiene intentos disponibles CUANDO hace click en "Ver respuestas y explicaciones"
CUANDO hace click en "Reintentar" ENTONCES se muestran todas las preguntas
ENTONCES se inicia un nuevo intento Y se destacan respuestas correctas en verde
Y las preguntas pueden estar en diferente orden Y se destacan respuestas incorrectas en rojo
Y las opciones pueden estar en diferente orden Y se muestra la respuesta correcta
Y sus respuestas anteriores NO están pre-seleccionadas Y se muestra explicación detallada de cada respuesta
Y el contador de intentos se decrementa Y se sugieren lecciones relacionadas para repasar
``` ```
## Criterios Adicionales **Escenario 12: Reintentar quiz**
```gherkin
- [ ] Auto-save de respuestas cada 30s (protección contra pérdida) DADO que el usuario reprobó el quiz
- [ ] Advertencia antes de salir de la página sin enviar Y tiene intentos disponibles
- [ ] Modo revisión: ver quiz aprobado sin poder cambiar respuestas CUANDO hace click en "Reintentar"
- [ ] Estadísticas del quiz: % de aprobación, tiempo promedio ENTONCES se inicia un nuevo intento
- [ ] Preguntas con imágenes embebidas Y las preguntas pueden estar en diferente orden
- [ ] Tipo de pregunta: multiple select (varias correctas) Y las opciones pueden estar en diferente orden
- [ ] Puntuación parcial en multiple select Y sus respuestas anteriores NO están pre-seleccionadas
Y el contador de intentos se decrementa
--- ```
## Tareas Técnicas ## Criterios Adicionales
**Database:** - [ ] Auto-save de respuestas cada 30s (protección contra pérdida)
- [ ] DB-EDU-015: Tabla education.quizzes - [ ] Advertencia antes de salir de la página sin enviar
- [ ] DB-EDU-016: Tabla education.questions (FK a quiz) - [ ] Modo revisión: ver quiz aprobado sin poder cambiar respuestas
- [ ] DB-EDU-017: Tabla education.question_options - [ ] Estadísticas del quiz: % de aprobación, tiempo promedio
- [ ] DB-EDU-018: Tabla education.quiz_attempts - [ ] Preguntas con imágenes embebidas
- [ ] DB-EDU-019: Tabla education.quiz_answers (respuestas del usuario) - [ ] Tipo de pregunta: multiple select (varias correctas)
- [ ] Puntuación parcial en multiple select
**Backend:**
- [ ] BE-EDU-039: Endpoint GET /education/quizzes/:id ---
- [ ] BE-EDU-040: Endpoint POST /education/quizzes/:id/start
- [ ] BE-EDU-041: Endpoint POST /education/quizzes/:id/submit ## Tareas Técnicas
- [ ] BE-EDU-042: Endpoint GET /education/quizzes/:id/attempts
- [ ] BE-EDU-043: Endpoint GET /education/quizzes/:id/results/:attemptId **Database:**
- [ ] BE-EDU-044: Implementar QuizService.gradeAttempt() - [ ] DB-EDU-015: Tabla education.quizzes
- [ ] BE-EDU-045: Implementar shuffle de preguntas y opciones - [ ] DB-EDU-016: Tabla education.questions (FK a quiz)
- [ ] BE-EDU-046: Validar intentos disponibles - [ ] DB-EDU-017: Tabla education.question_options
- [ ] BE-EDU-047: Implementar timer y auto-submit - [ ] DB-EDU-018: Tabla education.quiz_attempts
- [ ] BE-EDU-048: NO devolver respuestas correctas en GET (seguridad) - [ ] DB-EDU-019: Tabla education.quiz_answers (respuestas del usuario)
**Frontend:** **Backend:**
- [ ] FE-EDU-048: Crear QuizIntroPage.tsx - [ ] BE-EDU-039: Endpoint GET /education/quizzes/:id
- [ ] FE-EDU-049: Crear QuizPlayerPage.tsx - [ ] BE-EDU-040: Endpoint POST /education/quizzes/:id/start
- [ ] FE-EDU-050: Crear componente QuestionRenderer.tsx - [ ] BE-EDU-041: Endpoint POST /education/quizzes/:id/submit
- [ ] FE-EDU-051: Crear componente QuizNavigator.tsx (minimap) - [ ] BE-EDU-042: Endpoint GET /education/quizzes/:id/attempts
- [ ] FE-EDU-052: Crear componente QuizTimer.tsx - [ ] BE-EDU-043: Endpoint GET /education/quizzes/:id/results/:attemptId
- [ ] FE-EDU-053: Crear QuizResultsPage.tsx - [ ] BE-EDU-044: Implementar QuizService.gradeAttempt()
- [ ] FE-EDU-054: Crear componente AnswerExplanation.tsx - [ ] BE-EDU-045: Implementar shuffle de preguntas y opciones
- [ ] FE-EDU-055: Modal de confirmación de envío - [ ] BE-EDU-046: Validar intentos disponibles
- [ ] FE-EDU-056: Auto-save de respuestas cada 30s - [ ] BE-EDU-047: Implementar timer y auto-submit
- [ ] FE-EDU-057: Advertencia antes de salir (window.onbeforeunload) - [ ] BE-EDU-048: NO devolver respuestas correctas en GET (seguridad)
- [ ] FE-EDU-058: Implementar quizStore (Zustand)
- [ ] FE-EDU-059: Animación de celebración al aprobar **Frontend:**
- [ ] FE-EDU-048: Crear QuizIntroPage.tsx
**Tests:** - [ ] FE-EDU-049: Crear QuizPlayerPage.tsx
- [ ] TEST-EDU-021: Test calificación de quiz - [ ] FE-EDU-050: Crear componente QuestionRenderer.tsx
- [ ] TEST-EDU-022: Test aprobar quiz otorga XP - [ ] FE-EDU-051: Crear componente QuizNavigator.tsx (minimap)
- [ ] TEST-EDU-023: Test auto-submit cuando expira tiempo - [ ] FE-EDU-052: Crear componente QuizTimer.tsx
- [ ] TEST-EDU-024: Test límite de intentos - [ ] FE-EDU-053: Crear QuizResultsPage.tsx
- [ ] TEST-EDU-025: Test E2E realizar quiz completo - [ ] FE-EDU-054: Crear componente AnswerExplanation.tsx
- [ ] FE-EDU-055: Modal de confirmación de envío
--- - [ ] FE-EDU-056: Auto-save de respuestas cada 30s
- [ ] FE-EDU-057: Advertencia antes de salir (window.onbeforeunload)
## Dependencias - [ ] FE-EDU-058: Implementar quizStore (Zustand)
- [ ] FE-EDU-059: Animación de celebración al aprobar
**Depende de:**
- [ ] US-EDU-003: Iniciar lección - Estado: Pendiente **Tests:**
- [ ] RF-EDU-004: Sistema de quizzes - [ ] TEST-EDU-021: Test calificación de quiz
- [ ] TEST-EDU-022: Test aprobar quiz otorga XP
**Bloquea:** - [ ] TEST-EDU-023: Test auto-submit cuando expira tiempo
- [ ] US-EDU-005: Completar lección - [ ] TEST-EDU-024: Test límite de intentos
- [ ] US-EDU-007: Ver progreso - [ ] TEST-EDU-025: Test E2E realizar quiz completo
--- ---
## Notas Técnicas ## Dependencias
**Endpoint GET /quizzes/:id (SIN respuestas correctas):** **Depende de:**
```typescript - [ ] US-EDU-003: Iniciar lección - Estado: Pendiente
{ - [ ] RF-EDU-004: Sistema de quizzes
quiz: {
id: "quiz-1", **Bloquea:**
title: "Fundamentos de Fibonacci", - [ ] US-EDU-005: Completar lección
description: "Evalúa tu conocimiento...", - [ ] US-EDU-007: Ver progreso
timeLimit: 15, // minutos
passingScore: 70, // 0-100 ---
maxAttempts: 3,
questionCount: 10, ## Notas Técnicas
totalPoints: 10,
shuffleQuestions: true, **Endpoint GET /quizzes/:id (SIN respuestas correctas):**
shuffleOptions: true, ```typescript
mode: "assessment", {
quiz: {
questions: [ id: "quiz-1",
{ title: "Fundamentos de Fibonacci",
id: "q-1", description: "Evalúa tu conocimiento...",
question: "¿Cuál es el nivel más utilizado?", timeLimit: 15, // minutos
type: "multiple_choice", passingScore: 70, // 0-100
points: 1, maxAttempts: 3,
options: [ questionCount: 10,
{ id: "opt-1", text: "23.6%" }, totalPoints: 10,
{ id: "opt-2", text: "38.2%" }, shuffleQuestions: true,
{ id: "opt-3", text: "61.8%" }, shuffleOptions: true,
{ id: "opt-4", text: "78.6%" } mode: "assessment",
]
// NO incluir isCorrect en GET questions: [
} {
], id: "q-1",
question: "¿Cuál es el nivel más utilizado?",
userAttempts: [ type: "multiple_choice",
{ points: 1,
attemptNumber: 1, options: [
score: 65, { id: "opt-1", text: "23.6%" },
passed: false, { id: "opt-2", text: "38.2%" },
submittedAt: "2025-12-03T10:30:00Z" { id: "opt-3", text: "61.8%" },
} { id: "opt-4", text: "78.6%" }
], ]
attemptsRemaining: 2 // NO incluir isCorrect en GET
} }
} ],
```
userAttempts: [
**Endpoint POST /quizzes/:id/submit:** {
```typescript attemptNumber: 1,
// Request score: 65,
{ passed: false,
answers: { submittedAt: "2025-12-03T10:30:00Z"
"q-1": "opt-3", }
"q-2": "opt-1", ],
"q-3": ["opt-2", "opt-4"], // Multiple select attemptsRemaining: 2
// ... }
}, }
timeSpent: 632 // segundos ```
}
**Endpoint POST /quizzes/:id/submit:**
// Response ```typescript
{ // Request
attempt: { {
id: "attempt-uuid", answers: {
quizId: "quiz-1", "q-1": "opt-3",
attemptNumber: 2, "q-2": "opt-1",
score: 85, "q-3": ["opt-2", "opt-4"], // Multiple select
passed: true, // ...
pointsEarned: 8.5, },
totalPoints: 10, timeSpent: 632 // segundos
submittedAt: "2025-12-05T16:45:00Z" }
},
rewards: { // Response
xpEarned: 30, {
bonusXP: 20, // Si es 100% attempt: {
totalXP: 50 id: "attempt-uuid",
}, quizId: "quiz-1",
answers: [ attemptNumber: 2,
{ score: 85,
questionId: "q-1", passed: true,
userAnswer: "opt-3", pointsEarned: 8.5,
correctAnswer: "opt-3", totalPoints: 10,
isCorrect: true, submittedAt: "2025-12-05T16:45:00Z"
pointsEarned: 1, },
explanation: "61.8% es el nivel dorado..." rewards: {
} xpEarned: 30,
// ... bonusXP: 20, // Si es 100%
], totalXP: 50
attemptsRemaining: 1, },
canRetake: true answers: [
} {
``` questionId: "q-1",
userAnswer: "opt-3",
**Reglas de calificación:** correctAnswer: "opt-3",
- Multiple choice: 1 punto si es correcta, 0 si no isCorrect: true,
- Multiple select: puntos parciales (0.5 si acierta 2 de 4) pointsEarned: 1,
- True/False: 1 punto si es correcta explanation: "61.8% es el nivel dorado..."
- Sin responder: 0 puntos }
// ...
**Entidades/Tablas:** ],
- `education.quizzes` attemptsRemaining: 1,
- `education.questions` canRetake: true
- `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)
## Definition of Ready (DoR) - True/False: 1 punto si es correcta
- Sin responder: 0 puntos
- [x] Historia claramente escrita
- [x] Criterios de aceptación definidos **Entidades/Tablas:**
- [x] Story points estimados - `education.quizzes`
- [x] Dependencias identificadas - `education.questions`
- [x] Sin bloqueadores - `education.question_options`
- [x] Diseño/mockup disponible - `education.quiz_attempts`
- [x] API spec disponible - `education.quiz_answers`
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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,422 +1,434 @@
# US-EDU-007: Ver Progreso Educativo ---
id: "US-EDU-007"
## Metadata title: "Ver Progreso Educativo"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Alta"
| **ID** | US-EDU-007 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 3
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P0 | updated_date: "2026-01-04"
| **Story Points** | 3 | ---
| **Sprint** | Sprint 4 |
| **Estado** | Pendiente | # US-EDU-007: Ver Progreso Educativo
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-007 |
**Como** usuario activo en la plataforma educativa, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** ver un dashboard con mi progreso de aprendizaje completo, | **Módulo** | education |
**para** monitorear mi avance, mantenerme motivado y planificar mi siguiente paso. | **Prioridad** | P0 |
| **Story Points** | 3 |
## Descripción Detallada | **Sprint** | Sprint 4 |
| **Estado** | Pendiente |
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. | **Asignado a** | Por asignar |
## Mockups/Wireframes ---
``` ## Historia de Usuario
┌─────────────────────────────────────────────────────────────────┐
│ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │ **Como** usuario activo en la plataforma educativa,
├─────────────────────────────────────────────────────────────────┤ **quiero** ver un dashboard con mi progreso de aprendizaje completo,
│ │ **para** monitorear mi avance, mantenerme motivado y planificar mi siguiente paso.
│ MI APRENDIZAJE │
│ │ ## Descripción Detallada
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ RESUMEN GENERAL │ │ 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
│ │ │ 🔄 3 │ │ ✅ 12 │ │ 📚 156 │ │ ⏱ 42h │ │ │
│ │ │ En prog│ │ Comple │ │ Lecc. │ │ Estudio│ │ │ ```
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ │ │ │ [LOGO] OrbiQuant IA Educación Trading Cuentas 👤 │
│ │ ┌───────────────────┐ ┌─────────────────┐ │ │ ├─────────────────────────────────────────────────────────────────┤
│ │ │ 🔥 RACHA: 12 DÍAS │ │ ⭐ NIVEL 15 │ │ │ │ │
│ │ │ ¡Sigue así! │ │ 2,450 / 3,000 XP│ │ │ │ MI APRENDIZAJE │
│ │ │ 📅📅📅📅📅📅📅 │ │ ████████░░ 82% │ │ │ │ │
│ │ └───────────────────┘ └─────────────────┘ │ │ │ ┌──────────────────────────────────────────────────────────┐ │
│ └──────────────────────────────────────────────────────────┘ │ │ │ RESUMEN GENERAL │ │
│ │ │ │ │ │
│ ┌──────────────────────────────────────────────────────────┐ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ CURSOS EN PROGRESO (3) │ │ │ │ │ 🔄 3 │ │ ✅ 12 │ │ 📚 156 │ │ ⏱ 42h │ │ │
│ │ │ │ │ │ │ En prog│ │ Comple │ │ Lecc. │ │ Estudio│ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ │ │ [IMG] Fibonacci Retracement Básico │ │ │ │ │ │ │
│ │ │ ████████████████░░░░ 75% completado │ │ │ │ │ ┌───────────────────┐ ┌─────────────────┐ │ │
│ │ │ 18 de 23 lecciones | Módulo 4 de 5 │ │ │ │ │ │ 🔥 RACHA: 12 DÍAS │ │ ⭐ NIVEL 15 │ │ │
│ │ │ Última vez: Hace 2 horas │ │ │ │ │ │ ¡Sigue así! │ │ 2,450 / 3,000 XP│ │ │
│ │ │ [Continuar →] Lección: Fibonacci en tendencias │ │ │ │ │ │ 📅📅📅📅📅📅📅 │ │ ████████░░ 82% │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ └───────────────────┘ └─────────────────┘ │ │
│ │ │ │ │ └──────────────────────────────────────────────────────────┘ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │
│ │ │ [IMG] Day Trading para Principiantes │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │
│ │ │ ██████░░░░░░░░░░░░░░ 30% completado │ │ │ │ │ CURSOS EN PROGRESO (3) │ │
│ │ │ 9 de 30 lecciones | Módulo 2 de 6 │ │ │ │ │ │ │
│ │ │ Última vez: Hace 1 día │ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ [Continuar →] Lección: Análisis de volumen │ │ │ │ │ │ [IMG] Fibonacci Retracement Básico │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ ████████████████░░░░ 75% completado │ │ │
│ │ │ │ │ │ │ 18 de 23 lecciones | Módulo 4 de 5 │ │ │
│ │ [Ver todos los cursos en progreso] │ │ │ │ │ Última vez: Hace 2 horas │ │ │
│ └──────────────────────────────────────────────────────────┘ │ │ │ │ [Continuar →] Lección: Fibonacci en tendencias │ │ │
│ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │ │ │ │
│ │ ACTIVIDAD RECIENTE │ │ CALENDARIO DE APRENDIZAJE │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │ │ │ │ [IMG] Day Trading para Principiantes │ │ │
│ │ Hoy, 15:30 │ │ L M X J V S D │ │ │ │ │ ██████░░░░░░░░░░░░░░ 30% completado │ │ │
│ │ ✓ Completaste │ │ ██ ██ ░░ ██ ██ ██ ░░ │ │ │ │ │ 9 de 30 lecciones | Módulo 2 de 6 │ │ │
│ │ "Niveles de Fib."│ │ ██ ██ ██ ██ ██ ░░ ░░ │ │ │ │ │ Última vez: Hace 1 día │ │ │
│ │ +10 XP │ │ ██ ██ ██ ░░ ░░ ░░ ░░ │ │ │ │ │ [Continuar →] Lección: Análisis de volumen │ │ │
│ │ │ │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ │ Hoy, 14:20 │ │ Días activos este mes: 18 │ │ │ │ │ │
│ │ ✅ Aprobaste │ │ │ │ │ │ [Ver todos los cursos en progreso] │ │
│ │ Quiz Módulo 3 │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │
│ │ +30 XP │ │ │ │ │ │
│ │ │ │ │ │ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │ Ayer, 18:45 │ │ │ │ │ │ ACTIVIDAD RECIENTE │ │ CALENDARIO DE APRENDIZAJE │ │
│ │ 🎓 Obtuviste │ │ │ │ │ │ │ │ │ │
│ │ badge "Week │ │ │ │ │ │ Hoy, 15:30 │ │ L M X J V S D │ │
│ │ Warrior" │ │ │ │ │ │ ✓ Completaste │ │ ██ ██ ░░ ██ ██ ██ ░░ │ │
│ │ │ │ │ │ │ │ "Niveles de Fib."│ │ ██ ██ ██ ██ ██ ░░ ░░ │ │
│ │ [Ver más] │ │ │ │ │ │ +10 XP │ │ ██ ██ ██ ░░ ░░ ░░ ░░ │ │
│ └─────────────────────┘ └─────────────────────────────────┘ │ │ │ │ │ │ │
│ │ │ │ Hoy, 14:20 │ │ Días activos este mes: 18 │ │
│ ┌──────────────────────────────────────────────────────────┐ │ │ │ ✅ Aprobaste │ │ │ │
│ │ 📊 ESTADÍSTICAS │ │ │ │ Quiz Módulo 3 │ │ │ │
│ │ │ │ │ │ +30 XP │ │ │ │
│ │ Tiempo promedio por lección: 15 min │ │ │ │ │ │ │ │
│ │ Cursos completados este mes: 3 │ │ │ │ Ayer, 18:45 │ │ │ │
│ │ Tasa de completitud: 80% (12 de 15 cursos iniciados) │ │ │ │ 🎓 Obtuviste │ │ │ │
│ │ Categoría favorita: Análisis Técnico (6 cursos) │ │ │ │ badge "Week │ │ │ │
│ │ Mejor día de la semana: Miércoles (25 lecciones) │ │ │ │ Warrior" │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ [📈 Ver estadísticas detalladas] │ │ │ │ [Ver más] │ │ │ │
│ └──────────────────────────────────────────────────────────┘ │ │ └─────────────────────┘ └─────────────────────────────────┘ │
│ │ │ │
└─────────────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────┐ │
``` │ │ 📊 ESTADÍSTICAS │ │
│ │ │ │
--- │ │ Tiempo promedio por lección: 15 min │ │
│ │ Cursos completados este mes: 3 │ │
## Criterios de Aceptación │ │ Tasa de completitud: 80% (12 de 15 cursos iniciados) │ │
│ │ Categoría favorita: Análisis Técnico (6 cursos) │ │
**Escenario 1: Ver dashboard de progreso** │ │ Mejor día de la semana: Miércoles (25 lecciones) │ │
```gherkin │ │ │ │
DADO que el usuario está autenticado │ │ [📈 Ver estadísticas detalladas] │ │
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 2: Ver cursos en progreso** **Escenario 1: Ver dashboard de progreso**
```gherkin ```gherkin
DADO que el usuario tiene 3 cursos en progreso DADO que el usuario está autenticado
CUANDO ve la sección "Cursos en progreso" CUANDO navega a /education/progress o /education/dashboard
ENTONCES se muestran los 3 cursos ordenados por última actividad ENTONCES se muestra dashboard completo de aprendizaje
Y cada curso muestra: título, imagen, porcentaje, lecciones completadas Y se muestran métricas: cursos en progreso, completados, lecciones, horas
Y se muestra "Última vez: hace X tiempo" Y se muestra racha actual con visualización de días
Y se muestra botón "Continuar" con próxima lección Y se muestra nivel actual y progreso a siguiente nivel
Y al hacer click en "Continuar", navega a esa lección Y se muestran cursos en progreso con porcentajes
``` Y se muestra actividad reciente
```
**Escenario 3: Ver racha activa**
```gherkin **Escenario 2: Ver cursos en progreso**
DADO que el usuario tiene racha de 12 días ```gherkin
CUANDO ve el widget de racha DADO que el usuario tiene 3 cursos en progreso
ENTONCES se muestra "🔥 RACHA: 12 DÍAS" CUANDO ve la sección "Cursos en progreso"
Y se muestra visualización de últimos 7 días ENTONCES se muestran los 3 cursos ordenados por última actividad
Y se muestra mensaje motivacional "¡Sigue así!" Y cada curso muestra: título, imagen, porcentaje, lecciones completadas
Y si no completó lección hoy, muestra "Completa 1 lección hoy para mantener racha" Y se muestra "Última vez: hace X tiempo"
``` Y se muestra botón "Continuar" con próxima lección
Y al hacer click en "Continuar", navega a esa lección
**Escenario 4: Ver nivel y XP** ```
```gherkin
DADO que el usuario es nivel 15 con 2450 XP **Escenario 3: Ver racha activa**
Y necesita 3000 XP para nivel 16 ```gherkin
CUANDO ve el widget de nivel DADO que el usuario tiene racha de 12 días
ENTONCES se muestra "⭐ NIVEL 15" CUANDO ve el widget de racha
Y se muestra "2,450 / 3,000 XP" ENTONCES se muestra "🔥 RACHA: 12 DÍAS"
Y se muestra barra de progreso al 82% Y se muestra visualización de últimos 7 días
Y se muestra cuántos XP faltan: "Faltan 550 XP para nivel 16" Y se muestra mensaje motivacional "¡Sigue así!"
``` Y si no completó lección hoy, muestra "Completa 1 lección hoy para mantener racha"
```
**Escenario 5: Ver calendario de actividad**
```gherkin **Escenario 4: Ver nivel y XP**
DADO que el usuario completó lecciones en 18 días este mes ```gherkin
CUANDO ve el calendario DADO que el usuario es nivel 15 con 2450 XP
ENTONCES se muestra grid estilo GitHub contributions Y necesita 3000 XP para nivel 16
Y días con actividad están resaltados (cuadros llenos) CUANDO ve el widget de nivel
Y días sin actividad están vacíos ENTONCES se muestra "⭐ NIVEL 15"
Y al hacer hover sobre un día, muestra detalle: "3 lecciones, 45 min" Y se muestra "2,450 / 3,000 XP"
Y se muestra contador "Días activos este mes: 18" Y se muestra barra de progreso al 82%
``` Y se muestra cuántos XP faltan: "Faltan 550 XP para nivel 16"
```
**Escenario 6: Ver actividad reciente**
```gherkin **Escenario 5: Ver calendario de actividad**
DADO que el usuario tiene actividad reciente ```gherkin
CUANDO ve la sección "Actividad reciente" DADO que el usuario completó lecciones en 18 días este mes
ENTONCES se muestran últimos 10 eventos CUANDO ve el calendario
Y cada evento muestra: timestamp, tipo, descripción, XP ganado ENTONCES se muestra grid estilo GitHub contributions
Y eventos incluyen: lección completada, quiz aprobado, badge obtenido Y días con actividad están resaltados (cuadros llenos)
Y se ordenan de más reciente a más antiguo Y días sin actividad están vacíos
Y hay botón "Ver más" para ver historial completo Y al hacer hover sobre un día, muestra detalle: "3 lecciones, 45 min"
``` Y se muestra contador "Días activos este mes: 18"
```
**Escenario 7: Ver estadísticas**
```gherkin **Escenario 6: Ver actividad reciente**
DADO que el usuario tiene suficiente actividad ```gherkin
CUANDO ve la sección "Estadísticas" DADO que el usuario tiene actividad reciente
ENTONCES se muestra tiempo promedio por lección CUANDO ve la sección "Actividad reciente"
Y se muestra cursos completados este mes ENTONCES se muestran últimos 10 eventos
Y se muestra tasa de completitud (cursos finalizados / iniciados) Y cada evento muestra: timestamp, tipo, descripción, XP ganado
Y se muestra categoría favorita Y eventos incluyen: lección completada, quiz aprobado, badge obtenido
Y se muestra mejor día de la semana Y se ordenan de más reciente a más antiguo
Y hay botón para ver estadísticas detalladas Y hay botón "Ver más" para ver historial completo
``` ```
**Escenario 8: Continuar curso desde dashboard** **Escenario 7: Ver estadísticas**
```gherkin ```gherkin
DADO que el usuario tiene curso en progreso DADO que el usuario tiene suficiente actividad
Y la última lección accedida fue "Lección 18" CUANDO ve la sección "Estadísticas"
CUANDO hace click en "Continuar" del curso ENTONCES se muestra tiempo promedio por lección
ENTONCES navega directamente a la Lección 19 (siguiente) Y se muestra cursos completados este mes
Y el reproductor se carga listo para comenzar Y se muestra tasa de completitud (cursos finalizados / iniciados)
``` Y se muestra categoría favorita
Y se muestra mejor día de la semana
**Escenario 9: Usuario sin actividad reciente** Y hay botón para ver estadísticas detalladas
```gherkin ```
DADO que el usuario no tiene actividad en últimos 7 días
CUANDO accede al dashboard **Escenario 8: Continuar curso desde dashboard**
ENTONCES se muestra mensaje de bienvenida ```gherkin
Y se sugieren cursos populares para comenzar DADO que el usuario tiene curso en progreso
Y se muestra motivación: "¡Comienza hoy tu racha de aprendizaje!" Y la última lección accedida fue "Lección 18"
Y las métricas muestran valores cero elegantemente CUANDO hace click en "Continuar" del curso
``` ENTONCES navega directamente a la Lección 19 (siguiente)
Y el reproductor se carga listo para comenzar
**Escenario 10: Racha en riesgo** ```
```gherkin
DADO que el usuario tiene racha de 15 días **Escenario 9: Usuario sin actividad reciente**
Y NO ha completado lecciones hoy ```gherkin
Y es después de las 6pm hora local DADO que el usuario no tiene actividad en últimos 7 días
CUANDO accede al dashboard CUANDO accede al dashboard
ENTONCES se muestra alerta "⚠ ¡Racha en riesgo!" ENTONCES se muestra mensaje de bienvenida
Y se muestra "Completa 1 lección antes de medianoche" Y se sugieren cursos populares para comenzar
Y se sugiere lección corta: "Lección rápida (5 min): [título]" Y se muestra motivación: "¡Comienza hoy tu racha de aprendizaje!"
``` Y las métricas muestran valores cero elegantemente
```
## Criterios Adicionales
**Escenario 10: Racha en riesgo**
- [ ] Gráfico de XP ganado por semana/mes ```gherkin
- [ ] Comparación con usuarios similares (opcional) DADO que el usuario tiene racha de 15 días
- [ ] Metas personales de aprendizaje Y NO ha completado lecciones hoy
- [ ] Exportar reporte de progreso en PDF Y es después de las 6pm hora local
- [ ] Compartir logros en redes sociales CUANDO accede al dashboard
- [ ] Widget de "Próximas recompensas" (badge a desbloquear) ENTONCES se muestra alerta "⚠ ¡Racha en riesgo!"
Y se muestra "Completa 1 lección antes de medianoche"
--- Y se sugiere lección corta: "Lección rápida (5 min): [título]"
```
## Tareas Técnicas
## Criterios Adicionales
**Database:**
- [ ] DB-EDU-020: Vista materialized para stats de usuario - [ ] Gráfico de XP ganado por semana/mes
- [ ] DB-EDU-021: Índices en user_activity_log por user_id + timestamp - [ ] Comparación con usuarios similares (opcional)
- [ ] Metas personales de aprendizaje
**Backend:** - [ ] Exportar reporte de progreso en PDF
- [ ] BE-EDU-049: Endpoint GET /education/progress/overview - [ ] Compartir logros en redes sociales
- [ ] BE-EDU-050: Endpoint GET /education/progress/courses - [ ] Widget de "Próximas recompensas" (badge a desbloquear)
- [ ] BE-EDU-051: Endpoint GET /education/progress/stats
- [ ] BE-EDU-052: Endpoint GET /education/progress/activity ---
- [ ] BE-EDU-053: Endpoint GET /education/progress/calendar
- [ ] BE-EDU-054: Implementar ProgressService.getOverview() ## Tareas Técnicas
- [ ] BE-EDU-055: Job diario para calcular stats agregadas
- [ ] BE-EDU-056: Caché de stats en Redis (15 min) **Database:**
- [ ] DB-EDU-020: Vista materialized para stats de usuario
**Frontend:** - [ ] DB-EDU-021: Índices en user_activity_log por user_id + timestamp
- [ ] FE-EDU-060: Crear EducationDashboardPage.tsx
- [ ] FE-EDU-061: Crear componente ProgressOverview.tsx **Backend:**
- [ ] FE-EDU-062: Crear componente CourseProgressCard.tsx - [ ] BE-EDU-049: Endpoint GET /education/progress/overview
- [ ] FE-EDU-063: Crear componente StreakWidget.tsx - [ ] BE-EDU-050: Endpoint GET /education/progress/courses
- [ ] FE-EDU-064: Crear componente LevelWidget.tsx - [ ] BE-EDU-051: Endpoint GET /education/progress/stats
- [ ] FE-EDU-065: Crear componente ActivityCalendar.tsx (GitHub style) - [ ] BE-EDU-052: Endpoint GET /education/progress/activity
- [ ] FE-EDU-066: Crear componente RecentActivity.tsx - [ ] BE-EDU-053: Endpoint GET /education/progress/calendar
- [ ] FE-EDU-067: Crear componente StatsPanel.tsx - [ ] BE-EDU-054: Implementar ProgressService.getOverview()
- [ ] FE-EDU-068: Implementar progressStore (Zustand) - [ ] BE-EDU-055: Job diario para calcular stats agregadas
- [ ] FE-EDU-069: Skeleton loaders para cada sección - [ ] BE-EDU-056: Caché de stats en Redis (15 min)
**Tests:** **Frontend:**
- [ ] TEST-EDU-026: Test cálculo de tasa de completitud - [ ] FE-EDU-060: Crear EducationDashboardPage.tsx
- [ ] TEST-EDU-027: Test cálculo de racha - [ ] FE-EDU-061: Crear componente ProgressOverview.tsx
- [ ] TEST-EDU-028: Test stats agregadas - [ ] FE-EDU-062: Crear componente CourseProgressCard.tsx
- [ ] TEST-EDU-029: Test E2E visualizar dashboard completo - [ ] FE-EDU-063: Crear componente StreakWidget.tsx
- [ ] FE-EDU-064: Crear componente LevelWidget.tsx
--- - [ ] FE-EDU-065: Crear componente ActivityCalendar.tsx (GitHub style)
- [ ] FE-EDU-066: Crear componente RecentActivity.tsx
## Dependencias - [ ] FE-EDU-067: Crear componente StatsPanel.tsx
- [ ] FE-EDU-068: Implementar progressStore (Zustand)
**Depende de:** - [ ] FE-EDU-069: Skeleton loaders para cada sección
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
- [ ] RF-EDU-003: Sistema de progreso **Tests:**
- [ ] RF-EDU-006: Gamificación (para nivel y XP) - [ ] TEST-EDU-026: Test cálculo de tasa de completitud
- [ ] TEST-EDU-027: Test cálculo de racha
**Bloquea:** - [ ] TEST-EDU-028: Test stats agregadas
- Ninguna (es página de visualización) - [ ] TEST-EDU-029: Test E2E visualizar dashboard completo
--- ---
## Notas Técnicas ## Dependencias
**Endpoint GET /education/progress/overview:** **Depende de:**
```typescript - [ ] US-EDU-005: Completar lección - Estado: Pendiente
{ - [ ] RF-EDU-003: Sistema de progreso
overview: { - [ ] RF-EDU-006: Gamificación (para nivel y XP)
coursesInProgress: 3,
coursesCompleted: 12, **Bloquea:**
coursesSaved: 5, - Ninguna (es página de visualización)
lessonsCompleted: 156,
totalLearningTime: 2520, // minutos (42h) ---
currentStreak: 12,
longestStreak: 18, ## Notas Técnicas
totalXP: 2450,
currentLevel: 15, **Endpoint GET /education/progress/overview:**
xpToNextLevel: 550 ```typescript
}, {
overview: {
coursesInProgress: [ coursesInProgress: 3,
{ coursesCompleted: 12,
courseId: "course-1", coursesSaved: 5,
title: "Fibonacci Retracement Básico", lessonsCompleted: 156,
slug: "fibonacci-retracement-basico", totalLearningTime: 2520, // minutos (42h)
thumbnail: "...", currentStreak: 12,
progressPercent: 75, longestStreak: 18,
lessonsCompleted: 18, totalXP: 2450,
totalLessons: 23, currentLevel: 15,
modulesCompleted: 3, xpToNextLevel: 550
totalModules: 5, },
lastAccessedAt: "2025-12-05T13:30:00Z",
nextLesson: { coursesInProgress: [
id: "les-19", {
title: "Fibonacci en tendencias bajistas", courseId: "course-1",
slug: "fibonacci-tendencias-bajistas", title: "Fibonacci Retracement Básico",
duration: 12 slug: "fibonacci-retracement-basico",
} thumbnail: "...",
} progressPercent: 75,
// ... más cursos lessonsCompleted: 18,
], totalLessons: 23,
modulesCompleted: 3,
recentActivity: [ totalModules: 5,
{ lastAccessedAt: "2025-12-05T13:30:00Z",
type: "lesson_completed", nextLesson: {
title: "Completaste 'Niveles de Fibonacci'", id: "les-19",
description: "Lección 2.1 del curso Fibonacci Básico", title: "Fibonacci en tendencias bajistas",
timestamp: "2025-12-05T15:30:00Z", slug: "fibonacci-tendencias-bajistas",
xpEarned: 10, duration: 12
icon: "✓" }
}, }
{ // ... más cursos
type: "quiz_passed", ],
title: "Aprobaste Quiz Módulo 3",
description: "Puntuación: 85%", recentActivity: [
timestamp: "2025-12-05T14:20:00Z", {
xpEarned: 30, type: "lesson_completed",
icon: "✅" title: "Completaste 'Niveles de Fibonacci'",
} description: "Lección 2.1 del curso Fibonacci Básico",
// ... más actividad timestamp: "2025-12-05T15:30:00Z",
], xpEarned: 10,
icon: "✓"
calendar: [ },
{ date: "2025-12-01", lessonsCompleted: 3, minutesLearned: 45 }, {
{ date: "2025-12-02", lessonsCompleted: 2, minutesLearned: 30 }, type: "quiz_passed",
{ date: "2025-12-03", lessonsCompleted: 0, minutesLearned: 0 }, title: "Aprobaste Quiz Módulo 3",
// ... description: "Puntuación: 85%",
], timestamp: "2025-12-05T14:20:00Z",
xpEarned: 30,
stats: { icon: "✅"
avgTimePerLesson: 15, }
coursesThisMonth: 3, // ... más actividad
completionRate: 80, // 12 completados de 15 iniciados ],
activeDays: 18,
favoriteCategory: "Análisis Técnico", calendar: [
bestDayOfWeek: "Wednesday", { date: "2025-12-01", lessonsCompleted: 3, minutesLearned: 45 },
preferredTimeOfDay: "Evening" { date: "2025-12-02", lessonsCompleted: 2, minutesLearned: 30 },
} { date: "2025-12-03", lessonsCompleted: 0, minutesLearned: 0 },
} // ...
``` ],
**Cálculos importantes:** stats: {
```javascript avgTimePerLesson: 15,
// Racha actual coursesThisMonth: 3,
currentStreak = countConsecutiveDaysWithActivity(today, lookback=365); completionRate: 80, // 12 completados de 15 iniciados
activeDays: 18,
// Tasa de completitud favoriteCategory: "Análisis Técnico",
completionRate = (coursesCompleted / coursesStarted) * 100; bestDayOfWeek: "Wednesday",
preferredTimeOfDay: "Evening"
// Tiempo promedio por lección }
avgTimePerLesson = totalLearningTime / lessonsCompleted; }
```
// Categoría favorita
favoriteCategory = categoryWithMostCompletedCourses(); **Cálculos importantes:**
``` ```javascript
// Racha actual
**Optimizaciones:** currentStreak = countConsecutiveDaysWithActivity(today, lookback=365);
- Usar materialized views para stats agregadas
- Calcular stats en background job nocturno // Tasa de completitud
- Cachear overview en Redis (15 min) completionRate = (coursesCompleted / coursesStarted) * 100;
- Lazy load de secciones con IntersectionObserver
- Implementar skeleton loading para mejor UX // Tiempo promedio por lección
avgTimePerLesson = totalLearningTime / lessonsCompleted;
**Entidades/Tablas:**
- `education.user_course_progress` // Categoría favorita
- `education.user_lesson_progress` favoriteCategory = categoryWithMostCompletedCourses();
- `education.user_activity_log` ```
- `gamification.user_stats`
**Optimizaciones:**
--- - Usar materialized views para stats agregadas
- Calcular stats en background job nocturno
## Definition of Ready (DoR) - Cachear overview en Redis (15 min)
- Lazy load de secciones con IntersectionObserver
- [x] Historia claramente escrita - Implementar skeleton loading para mejor UX
- [x] Criterios de aceptación definidos
- [x] Story points estimados **Entidades/Tablas:**
- [x] Dependencias identificadas - `education.user_course_progress`
- [x] Sin bloqueadores - `education.user_lesson_progress`
- [x] Diseño/mockup disponible - `education.user_activity_log`
- [x] API spec disponible - `gamification.user_stats`
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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,437 +1,449 @@
# US-EDU-008: Obtener Certificado ---
id: "US-EDU-008"
## Metadata title: "Obtener Certificado"
type: "User Story"
| Campo | Valor | status: "Done"
|-------|-------| priority: "Media"
| **ID** | US-EDU-008 | epic: "OQI-002"
| **Épica** | OQI-002 - Módulo Educativo | story_points: 3
| **Módulo** | education | created_date: "2025-12-05"
| **Prioridad** | P2 | updated_date: "2026-01-04"
| **Story Points** | 3 | ---
| **Sprint** | Sprint 5 |
| **Estado** | Pendiente | # US-EDU-008: Obtener Certificado
| **Asignado a** | Por asignar |
## Metadata
---
| Campo | Valor |
## Historia de Usuario |-------|-------|
| **ID** | US-EDU-008 |
**Como** usuario que completó un curso, | **Épica** | OQI-002 - Módulo Educativo |
**quiero** obtener un certificado digital verificable, | **Módulo** | education |
**para** validar mi logro, agregarlo a mi perfil profesional y compartirlo en redes sociales. | **Prioridad** | P2 |
| **Story Points** | 3 |
## Descripción Detallada | **Sprint** | Sprint 5 |
| **Estado** | Pendiente |
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. | **Asignado a** | Por asignar |
## 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
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │ ```
│ │ [PREVIEW DEL CERTIFICADO] │ │ [MODAL DE CURSO COMPLETADO]
│ │ │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ ┌──────────────────────────────────────┐ │ │ │ ✨ 🎓 ✨ │
│ │ │ [LOGO ORBIQUANT] │ │ │ │ │
│ │ │ CERTIFICADO DE FINALIZACIÓN │ │ │ │ ¡FELICIDADES! CURSO COMPLETADO │
│ │ │ │ │ │ │ │
│ │ │ Se certifica que │ │ │ │ Fibonacci Retracement Básico │
│ │ │ JUAN PÉREZ │ │ │ │ │
│ │ │ Ha completado exitosamente │ │ │ │ +200 XP ganados │
│ │ │ "Fibonacci Retracement Básico" │ │ │ │ │
│ │ │ │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ 05/12/2025 │ │ │ │ │ │ │
│ │ │ OQI-EDU-A3F8D291 │ │ │ │ │ [PREVIEW DEL CERTIFICADO] │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ [Firma Instructor] [Firma OQI] │ │ │ │ │ ┌──────────────────────────────────────┐ │ │
│ │ │ [QR CODE] │ │ │ │ │ │ [LOGO ORBIQUANT] │ │ │
│ │ └──────────────────────────────────────┘ │ │ │ │ │ CERTIFICADO DE FINALIZACIÓN │ │ │
│ │ │ │ │ │ │ │ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ │ │ Se certifica que │ │ │
│ │ │ │ │ JUAN PÉREZ │ │ │
│ [📥 Descargar PDF] [💼 Compartir en LinkedIn] [✕ Cerrar] │ │ │ │ Ha completado exitosamente │ │ │
│ │ │ │ │ "Fibonacci Retracement Básico" │ │ │
└─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │
│ │ │ 05/12/2025 │ │ │
[PÁGINA DE CERTIFICADOS] │ │ │ OQI-EDU-A3F8D291 │ │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │
│ MIS CERTIFICADOS [🔍] │ │ │ │ [Firma Instructor] [Firma OQI] │ │ │
│ │ │ │ │ [QR CODE] │ │ │
│ Has obtenido 12 certificados │ │ │ └──────────────────────────────────────┘ │ │
│ │ │ │ │ │
│ Filtros: [Todos ▼] [Más recientes ▼] │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ [📥 Descargar PDF] [💼 Compartir en LinkedIn] [✕ Cerrar] │
│ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ │ │
│ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────┘
│ │ Fibonacci │ │ Day Trading │ │ Candlestick │ │
│ │ Básico │ │ Pro │ │ Patterns │ │ [PÁGINA DE CERTIFICADOS]
│ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ 05/12/2025 │ │ 28/11/2025 │ │ 15/11/2025 │ │ │ MIS CERTIFICADOS [🔍] │
│ │ OQI-...-291 │ │ OQI-...-8F2 │ │ OQI-...-4A1 │ │ │ │
│ │ │ │ │ │ │ │ │ Has obtenido 12 certificados │
│ │ [Ver] [📥] │ │ [Ver] [📥] │ │ [Ver] [📥] │ │ │ │
│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ │ Filtros: [Todos ▼] [Más recientes ▼] │
│ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ [1] 2 3 4 │ │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │ [THUMBNAIL] │ │
│ │ │ │ │ │ │ │ │ │
└─────────────────────────────────────────────────────────────────┘ │ │ Fibonacci │ │ Day Trading │ │ Candlestick │ │
│ │ Básico │ │ Pro │ │ Patterns │ │
[PÁGINA DE VERIFICACIÓN PÚBLICA] │ │ │ │ │ │ │ │
┌─────────────────────────────────────────────────────────────────┐ │ │ 05/12/2025 │ │ 28/11/2025 │ │ 15/11/2025 │ │
│ [LOGO] OrbiQuant IA │ │ │ OQI-...-291 │ │ OQI-...-8F2 │ │ OQI-...-4A1 │ │
│ │ │ │ │ │ │ │ │ │
│ VERIFICACIÓN DE CERTIFICADO │ │ │ [Ver] [📥] │ │ [Ver] [📥] │ │ [Ver] [📥] │ │
│ │ │ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │ [💼 LinkedIn]│ │
│ Certificado: OQI-EDU-A3F8D291 │ │ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ [1] 2 3 4 │
│ │ ✓ CERTIFICADO VÁLIDO │ │ │ │
│ │ │ │ └─────────────────────────────────────────────────────────────────┘
│ │ Otorgado a: Juan Pérez │ │
│ │ Curso: Fibonacci Retracement Básico │ │ [PÁGINA DE VERIFICACIÓN PÚBLICA]
│ │ Categoría: Análisis Técnico │ │ ┌─────────────────────────────────────────────────────────────────┐
│ │ Fecha de finalización: 05/12/2025 │ │ │ [LOGO] OrbiQuant IA │
│ │ Duración del curso: 2.5 horas │ │ │ │
│ │ Módulos: 5 | Lecciones: 23 │ │ │ VERIFICACIÓN DE CERTIFICADO │
│ │ │ │ │ │
│ │ Instructor: Carlos Mendoza │ │ │ Certificado: OQI-EDU-A3F8D291 │
│ │ Institución: OrbiQuant IA │ │ │ │
│ │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ Estado: ✅ Activo │ │ │ │ ✓ CERTIFICADO VÁLIDO │ │
│ │ Emitido: 05/12/2025 15:45:00 UTC │ │ │ │ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ │ Otorgado a: Juan Pérez │ │
│ │ │ │ Curso: Fibonacci Retracement Básico │ │
│ Este certificado puede ser verificado en cualquier momento en: │ │ │ Categoría: Análisis Técnico │ │
│ orbiquant.com/verify/OQI-EDU-A3F8D291 │ │ │ Fecha de finalización: 05/12/2025 │ │
│ │ │ │ Duración del curso: 2.5 horas │ │
└─────────────────────────────────────────────────────────────────┘ │ │ Módulos: 5 | Lecciones: 23 │ │
``` │ │ │ │
│ │ Instructor: Carlos Mendoza │ │
--- │ │ Institución: OrbiQuant IA │ │
│ │ │ │
## Criterios de Aceptación │ │ Estado: ✅ Activo │ │
│ │ Emitido: 05/12/2025 15:45:00 UTC │ │
**Escenario 1: Completar curso genera certificado** │ └────────────────────────────────────────────────────────────┘ │
```gherkin │ │
DADO que el usuario completó todas las lecciones de un curso │ Este certificado puede ser verificado en cualquier momento en: │
Y aprobó todos los quizzes obligatorios │ orbiquant.com/verify/OQI-EDU-A3F8D291 │
CUANDO se marca la última lección como completada │ │
ENTONCES se genera automáticamente un certificado └─────────────────────────────────────────────────────────────────┘
Y se registra en backend con ID único (OQI-EDU-XXXXXXXX) ```
Y se genera PDF con diseño profesional
Y se almacena PDF en S3 o CDN ---
Y se muestra modal de felicitación
Y se envía email con certificado adjunto ## Criterios de Aceptación
```
**Escenario 1: Completar curso genera certificado**
**Escenario 2: Ver certificado en modal** ```gherkin
```gherkin DADO que el usuario completó todas las lecciones de un curso
DADO que se generó el certificado Y aprobó todos los quizzes obligatorios
CUANDO se muestra el modal de curso completado CUANDO se marca la última lección como completada
ENTONCES se muestra preview del certificado ENTONCES se genera automáticamente un certificado
Y se muestra botón "Descargar PDF" Y se registra en backend con ID único (OQI-EDU-XXXXXXXX)
Y se muestra botón "Compartir en LinkedIn" Y se genera PDF con diseño profesional
Y se muestra "Ver todos mis certificados" Y se almacena PDF en S3 o CDN
``` Y se muestra modal de felicitación
Y se envía email con certificado adjunto
**Escenario 3: Descargar certificado en PDF** ```
```gherkin
DADO que el usuario tiene un certificado **Escenario 2: Ver certificado en modal**
CUANDO hace click en "Descargar PDF" ```gherkin
ENTONCES se descarga archivo PDF DADO que se generó el certificado
Y el PDF contiene: CUANDO se muestra el modal de curso completado
- Logo de OrbiQuant IA ENTONCES se muestra preview del certificado
- Título "Certificado de Finalización" Y se muestra botón "Descargar PDF"
- Nombre completo del usuario Y se muestra botón "Compartir en LinkedIn"
- Título del curso Y se muestra "Ver todos mis certificados"
- Fecha de finalización ```
- ID único del certificado
- Firmas digitales (instructor + plataforma) **Escenario 3: Descargar certificado en PDF**
- QR code para verificación ```gherkin
- Footer con URL de verificación DADO que el usuario tiene un certificado
``` CUANDO hace click en "Descargar PDF"
ENTONCES se descarga archivo PDF
**Escenario 4: Compartir en LinkedIn** Y el PDF contiene:
```gherkin - Logo de OrbiQuant IA
DADO que el usuario quiere compartir su certificado - Título "Certificado de Finalización"
CUANDO hace click en "Compartir en LinkedIn" - Nombre completo del usuario
ENTONCES se abre nueva pestaña de LinkedIn - Título del curso
Y el formulario de certificación está pre-llenado con: - Fecha de finalización
- Nombre: "Fibonacci Retracement Básico" - ID único del certificado
- Organización: "OrbiQuant IA" - Firmas digitales (instructor + plataforma)
- Fecha de emisión: "Diciembre 2025" - QR code para verificación
- ID de certificado: "OQI-EDU-A3F8D291" - Footer con URL de verificación
- URL de verificación: "orbiquant.com/verify/..." ```
```
**Escenario 4: Compartir en LinkedIn**
**Escenario 5: Ver galería de certificados** ```gherkin
```gherkin DADO que el usuario quiere compartir su certificado
DADO que el usuario tiene 12 certificados CUANDO hace click en "Compartir en LinkedIn"
CUANDO accede a /education/certificates ENTONCES se abre nueva pestaña de LinkedIn
ENTONCES se muestra galería de todos los certificados Y el formulario de certificación está pre-llenado con:
Y cada certificado muestra: thumbnail, título del curso, fecha - Nombre: "Fibonacci Retracement Básico"
Y se muestra contador "Has obtenido 12 certificados" - Organización: "OrbiQuant IA"
Y se pueden filtrar por: categoría, fecha - Fecha de emisión: "Diciembre 2025"
Y se pueden ordenar por: más reciente, alfabético - ID de certificado: "OQI-EDU-A3F8D291"
``` - URL de verificación: "orbiquant.com/verify/..."
```
**Escenario 6: Verificar certificado públicamente**
```gherkin **Escenario 5: Ver galería de certificados**
DADO que alguien tiene el ID de un certificado ```gherkin
CUANDO accede a orbiquant.com/verify/OQI-EDU-A3F8D291 DADO que el usuario tiene 12 certificados
ENTONCES se muestra página de verificación pública CUANDO accede a /education/certificates
Y NO requiere login ENTONCES se muestra galería de todos los certificados
Y se muestra: Y cada certificado muestra: thumbnail, título del curso, fecha
- Estado: ✅ Válido Y se muestra contador "Has obtenido 12 certificados"
- Nombre del usuario Y se pueden filtrar por: categoría, fecha
- Título del curso Y se pueden ordenar por: más reciente, alfabético
- Fecha de finalización ```
- Detalles del curso
Y se confirma autenticidad del certificado **Escenario 6: Verificar certificado públicamente**
``` ```gherkin
DADO que alguien tiene el ID de un certificado
**Escenario 7: Verificar certificado inválido** CUANDO accede a orbiquant.com/verify/OQI-EDU-A3F8D291
```gherkin ENTONCES se muestra página de verificación pública
DADO que alguien accede con ID inválido Y NO requiere login
CUANDO accede a /verify/INVALID-ID-123 Y se muestra:
ENTONCES se muestra "Certificado no encontrado" - Estado: ✅ Válido
Y se muestra sugerencia "Verifica el ID ingresado" - Nombre del usuario
Y se muestra link "¿Cómo verificar un certificado?" - Título del curso
``` - Fecha de finalización
- Detalles del curso
**Escenario 8: Email de certificado** Y se confirma autenticidad del certificado
```gherkin ```
DADO que se generó un certificado
CUANDO se envía el email **Escenario 7: Verificar certificado inválido**
ENTONCES el email contiene: ```gherkin
- Asunto: "¡Felicidades! Certificado de [Curso]" DADO que alguien accede con ID inválido
- Mensaje de felicitación personalizado CUANDO accede a /verify/INVALID-ID-123
- Estadísticas: duración, lecciones completadas ENTONCES se muestra "Certificado no encontrado"
- PDF adjunto del certificado Y se muestra sugerencia "Verifica el ID ingresado"
- Botones: Ver certificado, Compartir en LinkedIn Y se muestra link "¿Cómo verificar un certificado?"
- Sugerencias de próximos cursos relacionados ```
```
**Escenario 8: Email de certificado**
**Escenario 9: Curso sin certificado disponible** ```gherkin
```gherkin DADO que se generó un certificado
DADO que un curso está marcado como "no certifiable" CUANDO se envía el email
CUANDO el usuario completa el curso ENTONCES el email contiene:
ENTONCES NO se genera certificado - Asunto: "¡Felicidades! Certificado de [Curso]"
Y se muestra "Curso completado" sin opción de certificado - Mensaje de felicitación personalizado
Y se explica "Este curso no otorga certificado" - Estadísticas: duración, lecciones completadas
``` - PDF adjunto del certificado
- Botones: Ver certificado, Compartir en LinkedIn
**Escenario 10: Certificado con requisitos adicionales** - Sugerencias de próximos cursos relacionados
```gherkin ```
DADO que un curso requiere quiz final aprobado
Y el usuario completó todas las lecciones **Escenario 9: Curso sin certificado disponible**
PERO no aprobó el quiz final ```gherkin
CUANDO intenta obtener certificado DADO que un curso está marcado como "no certifiable"
ENTONCES se muestra "Debes aprobar el quiz final" CUANDO el usuario completa el curso
Y se muestra score actual del quiz ENTONCES NO se genera certificado
Y se muestra "Intentos restantes: X" Y se muestra "Curso completado" sin opción de certificado
Y el certificado NO se genera hasta aprobar Y se explica "Este curso no otorga certificado"
``` ```
## Criterios Adicionales **Escenario 10: Certificado con requisitos adicionales**
```gherkin
- [ ] Watermark en PDF para evitar falsificación DADO que un curso requiere quiz final aprobado
- [ ] Blockchain verification (opcional, fase 2) Y el usuario completó todas las lecciones
- [ ] Traducción del certificado a inglés PERO no aprobó el quiz final
- [ ] Certificado físico por correo (premium) CUANDO intenta obtener certificado
- [ ] Badge de LinkedIn auto-agregado via API ENTONCES se muestra "Debes aprobar el quiz final"
- [ ] Opción de hacer certificado público/privado Y se muestra score actual del quiz
- [ ] Perfil público con todos los certificados del usuario Y se muestra "Intentos restantes: X"
Y el certificado NO se genera hasta aprobar
--- ```
## Tareas Técnicas ## Criterios Adicionales
**Database:** - [ ] Watermark en PDF para evitar falsificación
- [ ] DB-EDU-022: Tabla education.certificates - [ ] Blockchain verification (opcional, fase 2)
- [ ] DB-EDU-023: Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status - [ ] Traducción del certificado a inglés
- [ ] DB-EDU-024: Tabla certificate_verifications (log de verificaciones) - [ ] Certificado físico por correo (premium)
- [ ] DB-EDU-025: Índice único en certificate_number - [ ] Badge de LinkedIn auto-agregado via API
- [ ] Opción de hacer certificado público/privado
**Backend:** - [ ] Perfil público con todos los certificados del usuario
- [ ] BE-EDU-057: Endpoint POST /education/certificates/generate
- [ ] BE-EDU-058: Endpoint GET /education/certificates (del usuario) ---
- [ ] BE-EDU-059: Endpoint GET /education/certificates/:id
- [ ] BE-EDU-060: Endpoint GET /api/public/certificates/verify/:number ## Tareas Técnicas
- [ ] BE-EDU-061: Implementar CertificateService.generate()
- [ ] BE-EDU-062: Generar PDF con Puppeteer o PDFKit **Database:**
- [ ] BE-EDU-063: Generar QR code con qrcode library - [ ] DB-EDU-022: Tabla education.certificates
- [ ] BE-EDU-064: Upload de PDF a S3 con signed URL - [ ] DB-EDU-023: Campos: id, certificate_number, user_id, course_id, issued_at, pdf_url, status
- [ ] BE-EDU-065: Event handler en course completion - [ ] DB-EDU-024: Tabla certificate_verifications (log de verificaciones)
- [ ] BE-EDU-066: Email service para enviar certificado - [ ] DB-EDU-025: Índice único en certificate_number
- [ ] BE-EDU-067: Rate limiting en verificación (100/hora por IP)
**Backend:**
**Frontend:** - [ ] BE-EDU-057: Endpoint POST /education/certificates/generate
- [ ] FE-EDU-070: Modal CourseCompletedModal.tsx con preview - [ ] BE-EDU-058: Endpoint GET /education/certificates (del usuario)
- [ ] FE-EDU-071: Crear CertificatesPage.tsx - [ ] BE-EDU-059: Endpoint GET /education/certificates/:id
- [ ] FE-EDU-072: Crear componente CertificateCard.tsx - [ ] BE-EDU-060: Endpoint GET /api/public/certificates/verify/:number
- [ ] FE-EDU-073: Crear VerifyCertificatePage.tsx (pública) - [ ] BE-EDU-061: Implementar CertificateService.generate()
- [ ] FE-EDU-074: Botón "Compartir en LinkedIn" con pre-fill - [ ] BE-EDU-062: Generar PDF con Puppeteer o PDFKit
- [ ] FE-EDU-075: Preview de PDF en modal - [ ] BE-EDU-063: Generar QR code con qrcode library
- [ ] FE-EDU-076: Galería con filtros y búsqueda - [ ] BE-EDU-064: Upload de PDF a S3 con signed URL
- [ ] FE-EDU-077: Implementar certificatesStore - [ ] BE-EDU-065: Event handler en course completion
- [ ] BE-EDU-066: Email service para enviar certificado
**Tests:** - [ ] BE-EDU-067: Rate limiting en verificación (100/hora por IP)
- [ ] TEST-EDU-030: Test generación de certificado
- [ ] TEST-EDU-031: Test validación de certificado válido **Frontend:**
- [ ] TEST-EDU-032: Test verificación de certificado inválido - [ ] FE-EDU-070: Modal CourseCompletedModal.tsx con preview
- [ ] TEST-EDU-033: Test E2E completar curso y obtener certificado - [ ] FE-EDU-071: Crear CertificatesPage.tsx
- [ ] FE-EDU-072: Crear componente CertificateCard.tsx
--- - [ ] FE-EDU-073: Crear VerifyCertificatePage.tsx (pública)
- [ ] FE-EDU-074: Botón "Compartir en LinkedIn" con pre-fill
## Dependencias - [ ] FE-EDU-075: Preview de PDF en modal
- [ ] FE-EDU-076: Galería con filtros y búsqueda
**Depende de:** - [ ] FE-EDU-077: Implementar certificatesStore
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
- [ ] RF-EDU-005: Sistema de certificados **Tests:**
- [ ] PDF generation library (Puppeteer/PDFKit) - [ ] TEST-EDU-030: Test generación de certificado
- [ ] S3 bucket para almacenar PDFs - [ ] TEST-EDU-031: Test validación de certificado válido
- [ ] Email service - [ ] TEST-EDU-032: Test verificación de certificado inválido
- [ ] TEST-EDU-033: Test E2E completar curso y obtener certificado
**Bloquea:**
- Ninguna (es funcionalidad final) ---
--- ## Dependencias
## Notas Técnicas **Depende de:**
- [ ] US-EDU-005: Completar lección - Estado: Pendiente
**Generación del certificado:** - [ ] RF-EDU-005: Sistema de certificados
```javascript - [ ] PDF generation library (Puppeteer/PDFKit)
// Triggered on course completion - [ ] S3 bucket para almacenar PDFs
async function onCourseCompleted(userId, courseId) { - [ ] Email service
// 1. Validar requisitos
const isEligible = await validateCertificateEligibility(userId, courseId); **Bloquea:**
if (!isEligible) return; - Ninguna (es funcionalidad final)
// 2. Generar ID único ---
const certificateNumber = generateCertificateId(); // OQI-EDU-A3F8D291
## Notas Técnicas
// 3. Generar PDF
const pdfBuffer = await generateCertificatePDF({ **Generación del certificado:**
userName, ```javascript
courseName, // Triggered on course completion
completedDate, async function onCourseCompleted(userId, courseId) {
certificateNumber // 1. Validar requisitos
}); const isEligible = await validateCertificateEligibility(userId, courseId);
if (!isEligible) return;
// 4. Upload a S3
const pdfUrl = await uploadToS3(pdfBuffer, certificateNumber); // 2. Generar ID único
const certificateNumber = generateCertificateId(); // OQI-EDU-A3F8D291
// 5. Guardar en DB
await saveCertificate({ // 3. Generar PDF
userId, const pdfBuffer = await generateCertificatePDF({
courseId, userName,
certificateNumber, courseName,
pdfUrl completedDate,
}); certificateNumber
});
// 6. Enviar email
await sendCertificateEmail(userId, pdfUrl); // 4. Upload a S3
const pdfUrl = await uploadToS3(pdfBuffer, certificateNumber);
// 7. Otorgar XP bonus
await awardXP(userId, 100, 'certificate_earned'); // 5. Guardar en DB
} await saveCertificate({
``` userId,
courseId,
**Endpoint GET /api/public/certificates/verify/:number:** certificateNumber,
```typescript pdfUrl
// Response para certificado válido });
{
valid: true, // 6. Enviar email
certificate: { await sendCertificateEmail(userId, pdfUrl);
certificateNumber: "OQI-EDU-A3F8D291",
recipientName: "Juan Pérez", // 7. Otorgar XP bonus
courseTitle: "Fibonacci Retracement Básico", await awardXP(userId, 100, 'certificate_earned');
courseCategory: "Análisis Técnico", }
completedAt: "2025-12-05T15:45:00Z", ```
issuedAt: "2025-12-05T15:45:00Z",
courseDuration: 150, // minutos **Endpoint GET /api/public/certificates/verify/:number:**
moduleCount: 5, ```typescript
lessonCount: 23, // Response para certificado válido
instructor: "Carlos Mendoza", {
status: "active" valid: true,
} certificate: {
} certificateNumber: "OQI-EDU-A3F8D291",
recipientName: "Juan Pérez",
// Response para certificado inválido courseTitle: "Fibonacci Retracement Básico",
{ courseCategory: "Análisis Técnico",
valid: false, completedAt: "2025-12-05T15:45:00Z",
error: "Certificate not found" issuedAt: "2025-12-05T15:45:00Z",
} courseDuration: 150, // minutos
``` moduleCount: 5,
lessonCount: 23,
**Template del PDF:** instructor: "Carlos Mendoza",
- Usar HTML + CSS para diseño status: "active"
- Puppeteer para generar PDF desde HTML }
- Incluir logo en base64 para evitar carga externa }
- QR code generado con library qrcode.js
- Firmas como imágenes PNG embebidas // Response para certificado inválido
{
**LinkedIn pre-fill URL:** valid: false,
```javascript error: "Certificate not found"
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:** **Template del PDF:**
- Rate limiting en endpoint de verificación - Usar HTML + CSS para diseño
- Signed URLs de S3 con expiración de 1 hora para descargas - Puppeteer para generar PDF desde HTML
- No exponer lista de todos los certificados (solo del usuario logueado) - Incluir logo en base64 para evitar carga externa
- Validar que usuario solo puede descargar sus propios certificados - QR code generado con library qrcode.js
- Firmas como imágenes PNG embebidas
**Entidades/Tablas:**
- `education.certificates` **LinkedIn pre-fill URL:**
- `education.certificate_verifications` (log) ```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}`;
--- ```
## Definition of Ready (DoR) **Seguridad:**
- Rate limiting en endpoint de verificación
- [x] Historia claramente escrita - Signed URLs de S3 con expiración de 1 hora para descargas
- [x] Criterios de aceptación definidos - No exponer lista de todos los certificados (solo del usuario logueado)
- [x] Story points estimados - Validar que usuario solo puede descargar sus propios certificados
- [x] Dependencias identificadas
- [x] Sin bloqueadores **Entidades/Tablas:**
- [x] Diseño/mockup disponible - `education.certificates`
- [x] API spec disponible - `education.certificate_verifications` (log)
## Definition of Done (DoD) ---
- [ ] Código implementado según criterios ## Definition of Ready (DoR)
- [ ] Tests unitarios escritos y pasando
- [ ] Tests de integración pasando - [x] Historia claramente escrita
- [ ] Code review aprobado - [x] Criterios de aceptación definidos
- [ ] Documentación actualizada - [x] Story points estimados
- [ ] QA aprobado - [x] Dependencias identificadas
- [ ] Desplegado en ambiente de pruebas - [x] Sin bloqueadores
- [x] Diseño/mockup disponible
--- - [x] API spec disponible
## Historial de Cambios ## Definition of Done (DoD)
| Fecha | Cambio | Autor | - [ ] Código implementado según criterios
|-------|--------|-------| - [ ] Tests unitarios escritos y pasando
| 2025-12-05 | Creación | Requirements-Analyst | - [ ] Tests de integración pasando
- [ ] Code review aprobado
--- - [ ] Documentación actualizada
- [ ] QA aprobado
**Creada por:** Requirements-Analyst - [ ] Desplegado en ambiente de pruebas
**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.0.0" version: "1.1.0"
epic: OQI-002 epic: OQI-002
name: "Módulo Educativo - Cursos de Trading" name: "Módulo Educativo - Cursos de Trading"
updated: "2025-12-05" updated: "2026-01-04"
status: pending status: in_progress
# Resumen de trazabilidad # Resumen de trazabilidad
summary: summary:
@ -71,7 +71,14 @@ requirements:
RF-EDU-002: RF-EDU-002:
name: "Sistema de Lecciones" name: "Sistema de Lecciones"
status: pending status: implemented
implementation_notes:
- date: "2026-01-04"
changes:
- "Creado Lesson.tsx para visualización de lecciones con video player"
- "Soporte para contenido: video, article, text, quiz, exercise"
- "Navegación entre lecciones con sidebar de progreso"
- "Integrado en App.tsx con rutas /education/courses/:courseSlug/lesson/:lessonId"
specs: specs:
- ET-EDU-001 - ET-EDU-001
- ET-EDU-004 - ET-EDU-004
@ -164,7 +171,15 @@ requirements:
RF-EDU-004: RF-EDU-004:
name: "Sistema de Quizzes" name: "Sistema de Quizzes"
status: pending status: implemented
implementation_notes:
- date: "2026-01-04"
changes:
- "Creado Quiz.tsx con estados: intro, in_progress, submitted"
- "Timer de cuenta regresiva para quizzes con límite de tiempo"
- "Soporte para tipos: multiple_choice, true_false, multi_select"
- "Sistema de puntos y XP integrado"
- "Ruta: /education/courses/:courseSlug/lesson/:lessonId/quiz"
specs: specs:
- ET-EDU-005 - ET-EDU-005
user_stories: user_stories:
@ -462,3 +477,67 @@ 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,285 +1,298 @@
# RF-EDU-001: Catálogo de Cursos ---
id: "RF-EDU-001"
**Versión:** 1.0.0 title: "Catalogo de Cursos"
**Fecha:** 2025-12-05 type: "Requirement"
**Épica:** OQI-002 - Módulo Educativo status: "Done"
**Prioridad:** P0 priority: "Alta"
**Story Points:** 8 module: "education"
epic: "OQI-002"
--- version: "1.0"
created_date: "2025-12-05"
## Descripción updated_date: "2026-01-04"
---
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
## Requisitos Funcionales **Fecha:** 2025-12-05
**Épica:** OQI-002 - Módulo Educativo
### RF-EDU-001.1: Listado de Cursos **Prioridad:** P0
**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
- Mostrar badge de "Nuevo" para cursos publicados en últimos 30 días ## Descripción
- Mostrar badge de "En curso" para cursos iniciados por el usuario
- Mostrar badge de "Completado" con porcentaje para cursos en progreso 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.
- 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:
| Fundamentos | fundamentals | Conceptos básicos de trading | - Mostrar todos los cursos activos en formato de tarjetas (cards)
| Análisis Técnico | technical-analysis | Indicadores y patrones | - Incluir para cada curso: título, descripción breve, imagen, nivel, duración, módulos, progreso
| Análisis Fundamental | fundamental-analysis | Valoración de activos | - Mostrar badge de "Nuevo" para cursos publicados en últimos 30 días
| Gestión de Riesgo | risk-management | Money management | - Mostrar badge de "En curso" para cursos iniciados por el usuario
| Psicología del Trading | trading-psychology | Control emocional | - Mostrar badge de "Completado" con porcentaje para cursos en progreso
| Estrategias Avanzadas | advanced-strategies | Sistemas complejos | - Implementar paginación (12 cursos por página)
| Criptomonedas | crypto | Trading de cripto |
| IA y Trading | ai-trading | Machine Learning aplicado | ### RF-EDU-001.2: Categorías
### RF-EDU-001.3: Niveles de Dificultad El sistema debe soportar las siguientes categorías:
El sistema debe clasificar cursos en: | Categoría | Slug | Descripción |
- **Principiante:** Sin conocimientos previos requeridos |-----------|------|-------------|
- **Intermedio:** Requiere conocimientos básicos | Fundamentos | fundamentals | Conceptos básicos de trading |
- **Avanzado:** Para traders experimentados | Análisis Técnico | technical-analysis | Indicadores y patrones |
- **Experto:** Contenido especializado | Análisis Fundamental | fundamental-analysis | Valoración de activos |
| Gestión de Riesgo | risk-management | Money management |
### RF-EDU-001.4: Filtros | Psicología del Trading | trading-psychology | Control emocional |
| Estrategias Avanzadas | advanced-strategies | Sistemas complejos |
El sistema debe permitir filtrar por: | Criptomonedas | crypto | Trading de cripto |
- Categoría (múltiple selección) | IA y Trading | ai-trading | Machine Learning aplicado |
- Nivel de dificultad (múltiple selección)
- Duración (rangos: <2h, 2-5h, 5-10h, >10h) ### RF-EDU-001.3: Niveles de Dificultad
- Estado: Nuevos, En curso, Completados, No iniciados
- Instructor El sistema debe clasificar cursos en:
- Gratuitos vs Premium - **Principiante:** Sin conocimientos previos requeridos
- **Intermedio:** Requiere conocimientos básicos
### RF-EDU-001.5: Búsqueda - **Avanzado:** Para traders experimentados
- **Experto:** Contenido especializado
El sistema debe:
- Implementar barra de búsqueda en tiempo real ### RF-EDU-001.4: Filtros
- Buscar en: título, descripción, tags, nombre de instructor
- Mostrar resultados mientras el usuario escribe (debounce 300ms) El sistema debe permitir filtrar por:
- Resaltar términos coincidentes en resultados - Categoría (múltiple selección)
- Mostrar sugerencias de búsqueda basadas en términos populares - Nivel de dificultad (múltiple selección)
- Guardar historial de búsquedas del usuario - Duración (rangos: <2h, 2-5h, 5-10h, >10h)
- Estado: Nuevos, En curso, Completados, No iniciados
### RF-EDU-001.6: Ordenamiento - Instructor
- Gratuitos vs Premium
El sistema debe permitir ordenar por:
- Más recientes ### RF-EDU-001.5: Búsqueda
- Más populares (por número de estudiantes)
- Mejor valorados (rating) El sistema debe:
- Duración (ascendente/descendente) - Implementar barra de búsqueda en tiempo real
- Alfabético (A-Z, Z-A) - Buscar en: título, descripción, tags, nombre de instructor
- Progreso del usuario (para cursos iniciados) - Mostrar resultados mientras el usuario escribe (debounce 300ms)
- Resaltar términos coincidentes en resultados
### RF-EDU-001.7: Recomendaciones - Mostrar sugerencias de búsqueda basadas en términos populares
- Guardar historial de búsquedas del usuario
El sistema debe:
- Mostrar sección "Recomendado para ti" basado en: ### RF-EDU-001.6: Ordenamiento
- Cursos en progreso del usuario
- Nivel de experiencia del perfil El sistema debe permitir ordenar por:
- Cursos completados previamente - Más recientes
- Categorías de interés - Más populares (por número de estudiantes)
- Mostrar sección "Continuar aprendiendo" con cursos incompletos - Mejor valorados (rating)
- Mostrar "Cursos relacionados" al ver detalle de curso - Duración (ascendente/descendente)
- Alfabético (A-Z, Z-A)
--- - Progreso del usuario (para cursos iniciados)
## Datos de Entrada ### RF-EDU-001.7: Recomendaciones
| Campo | Tipo | Descripción | El sistema debe:
|-------|------|-------------| - Mostrar sección "Recomendado para ti" basado en:
| page | number | Número de página (default: 1) | - Cursos en progreso del usuario
| limit | number | Elementos por página (default: 12, max: 50) | - Nivel de experiencia del perfil
| category | string[] | IDs de categorías a filtrar | - Cursos completados previamente
| level | string[] | Niveles de dificultad | - Categorías de interés
| search | string | Término de búsqueda | - Mostrar sección "Continuar aprendiendo" con cursos incompletos
| sortBy | string | Campo de ordenamiento | - Mostrar "Cursos relacionados" al ver detalle de curso
| sortOrder | asc/desc | Dirección del ordenamiento |
---
---
## Datos de Entrada
## Datos de Salida
| Campo | Tipo | Descripción |
```typescript |-------|------|-------------|
interface Course { | page | number | Número de página (default: 1) |
id: string; | limit | number | Elementos por página (default: 12, max: 50) |
title: string; | category | string[] | IDs de categorías a filtrar |
slug: string; | level | string[] | Niveles de dificultad |
description: string; | search | string | Término de búsqueda |
shortDescription: string; | sortBy | string | Campo de ordenamiento |
thumbnail: string; | sortOrder | asc/desc | Dirección del ordenamiento |
category: {
id: string; ---
name: string;
slug: string; ## Datos de Salida
icon: string;
}; ```typescript
level: 'beginner' | 'intermediate' | 'advanced' | 'expert'; interface Course {
duration: number; // minutos id: string;
moduleCount: number; title: string;
lessonCount: number; slug: string;
studentCount: number; description: string;
rating: number; // 0-5 shortDescription: string;
reviewCount: number; thumbnail: string;
instructor: { category: {
id: string; id: string;
name: string; name: string;
avatar: string; slug: string;
title: string; icon: string;
}; };
tags: string[]; level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
isPremium: boolean; duration: number; // minutos
publishedAt: string; moduleCount: number;
userProgress?: { lessonCount: number;
enrolledAt: string; studentCount: number;
progressPercent: number; rating: number; // 0-5
lastAccessedAt: string; reviewCount: number;
isCompleted: boolean; instructor: {
}; id: string;
} name: string;
avatar: string;
interface CatalogResponse { title: string;
courses: Course[]; };
pagination: { tags: string[];
page: number; isPremium: boolean;
limit: number; publishedAt: string;
total: number; userProgress?: {
totalPages: number; enrolledAt: string;
}; progressPercent: number;
filters: { lastAccessedAt: string;
categories: Category[]; isCompleted: boolean;
levels: string[]; };
}; }
}
``` interface CatalogResponse {
courses: Course[];
--- pagination: {
page: number;
## Reglas de Negocio limit: number;
total: number;
1. **Cursos activos:** Solo mostrar cursos con status 'published' totalPages: number;
2. **Acceso Premium:** Cursos premium requieren suscripción activa };
3. **Visibilidad:** Cursos draft solo visibles para instructores y admins filters: {
4. **Límite de paginación:** Máximo 50 cursos por página categories: Category[];
5. **Caché:** Catálogo se cachea por 5 minutos levels: string[];
6. **Búsqueda mínima:** Al menos 2 caracteres para búsqueda };
7. **Recomendaciones:** Máximo 6 cursos en sección recomendados }
8. **Orden por defecto:** Más recientes primero para usuarios nuevos ```
--- ---
## Criterios de Aceptación ## Reglas de Negocio
```gherkin 1. **Cursos activos:** Solo mostrar cursos con status 'published'
Escenario: Usuario visualiza catálogo de cursos 2. **Acceso Premium:** Cursos premium requieren suscripción activa
DADO que el usuario está autenticado 3. **Visibilidad:** Cursos draft solo visibles para instructores y admins
Y está en la página de educación 4. **Límite de paginación:** Máximo 50 cursos por página
CUANDO accede a /education/courses 5. **Caché:** Catálogo se cachea por 5 minutos
ENTONCES se muestra un listado de cursos 6. **Búsqueda mínima:** Al menos 2 caracteres para búsqueda
Y se muestran 12 cursos por página 7. **Recomendaciones:** Máximo 6 cursos en sección recomendados
Y cada curso muestra: título, imagen, nivel, duración, rating 8. **Orden por defecto:** Más recientes primero para usuarios nuevos
Y se muestran filtros en sidebar izquierdo
Y se muestra barra de búsqueda en header ---
Escenario: Usuario filtra por categoría ## Criterios de Aceptación
DADO que el usuario está en el catálogo
CUANDO selecciona la categoría "Análisis Técnico" ```gherkin
ENTONCES se muestran solo cursos de esa categoría Escenario: Usuario visualiza catálogo de cursos
Y el filtro se marca como activo DADO que el usuario está autenticado
Y la URL se actualiza con ?category=technical-analysis Y está en la página de educación
Y se mantienen otros filtros activos CUANDO accede a /education/courses
ENTONCES se muestra un listado de cursos
Escenario: Usuario busca curso Y se muestran 12 cursos por página
DADO que el usuario está en el catálogo Y cada curso muestra: título, imagen, nivel, duración, rating
CUANDO escribe "fibonacci" en la búsqueda Y se muestran filtros en sidebar izquierdo
ENTONCES se muestran resultados en tiempo real Y se muestra barra de búsqueda en header
Y se resalta el término "fibonacci" en resultados
Y se muestra contador "X resultados para 'fibonacci'" Escenario: Usuario filtra por categoría
DADO que el usuario está en el catálogo
Escenario: Usuario sin resultados CUANDO selecciona la categoría "Análisis Técnico"
DADO que el usuario busca "xyz123" ENTONCES se muestran solo cursos de esa categoría
Y no hay cursos que coincidan Y el filtro se marca como activo
ENTONCES se muestra mensaje "No se encontraron cursos" Y la URL se actualiza con ?category=technical-analysis
Y se sugieren búsquedas alternativas Y se mantienen otros filtros activos
Y se muestran cursos populares como alternativa
Escenario: Usuario busca curso
Escenario: Ver cursos recomendados DADO que el usuario está en el catálogo
DADO que el usuario tiene cursos en progreso CUANDO escribe "fibonacci" en la búsqueda
CUANDO accede al catálogo ENTONCES se muestran resultados en tiempo real
ENTONCES se muestra sección "Recomendado para ti" Y se resalta el término "fibonacci" en resultados
Y aparecen máximo 6 cursos relacionados Y se muestra contador "X resultados para 'fibonacci'"
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"
## Dependencias Y se sugieren búsquedas alternativas
Y se muestran cursos populares como alternativa
- Education API para datos de cursos
- PostgreSQL schema education Escenario: Ver cursos recomendados
- Redis para caché de catálogo DADO que el usuario tiene cursos en progreso
- Elasticsearch para búsqueda (opcional, mejora performance) CUANDO accede al catálogo
ENTONCES se muestra sección "Recomendado para ti"
--- Y aparecen máximo 6 cursos relacionados
Y se muestra sección "Continuar aprendiendo"
## Notas Técnicas ```
- Implementar virtual scrolling para listas largas ---
- Usar React Query para caché de frontend
- Implementar skeleton loading durante carga ## Dependencias
- Optimizar imágenes con lazy loading
- Considerar SSR para mejor SEO - Education API para datos de cursos
- Implementar analytics de búsquedas para mejorar recomendaciones - PostgreSQL schema education
- Redis para caché de catálogo
--- - Elasticsearch para búsqueda (opcional, mejora performance)
## Referencias ---
- Schema database: `/backend/src/database/schemas/education.sql` ## Notas Técnicas
- API endpoints: `/backend/src/modules/courses/courses.routes.ts`
- Frontend: `/frontend/src/pages/Courses.tsx` - Implementar virtual scrolling para listas largas
- Usar React Query para caché de frontend
--- - Implementar skeleton loading durante carga
- Optimizar imágenes con lazy loading
## Tareas Técnicas - Considerar SSR para mejor SEO
- 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
- [ ] Implementar full-text search en PostgreSQL ## Referencias
**Backend:** - Schema database: `/backend/src/database/schemas/education.sql`
- [ ] Endpoint GET /education/courses con paginación y filtros - API endpoints: `/backend/src/modules/courses/courses.routes.ts`
- [ ] Endpoint GET /education/categories - Frontend: `/frontend/src/pages/Courses.tsx`
- [ ] Implementar CourseService.getCatalog()
- [ ] Implementar sistema de recomendaciones básico ---
- [ ] Agregar rate limiting a búsqueda
- [ ] Implementar caché Redis para catálogo ## Tareas Técnicas
**Frontend:** **Database:**
- [ ] Crear página CoursesPage.tsx - [ ] Verificar índices en education.courses (title, category_id, level, published_at)
- [ ] Crear componente CourseCard.tsx - [ ] Crear vista courses_catalog con joins pre-calculados
- [ ] Crear componente CourseFilters.tsx - [ ] Implementar full-text search en PostgreSQL
- [ ] Crear componente SearchBar.tsx
- [ ] Implementar coursesStore (Zustand) **Backend:**
- [ ] Implementar infinite scroll opcional - [ ] Endpoint GET /education/courses con paginación y filtros
- [ ] Agregar analytics de búsqueda - [ ] Endpoint GET /education/categories
- [ ] Implementar CourseService.getCatalog()
**Tests:** - [ ] Implementar sistema de recomendaciones básico
- [ ] Test unitario CourseService - [ ] Agregar rate limiting a búsqueda
- [ ] Test integración GET /courses con filtros - [ ] Implementar caché Redis para catálogo
- [ ] Test E2E navegación y búsqueda
**Frontend:**
--- - [ ] Crear página CoursesPage.tsx
- [ ] Crear componente CourseCard.tsx
**Creado por:** Requirements-Analyst - [ ] Crear componente CourseFilters.tsx
**Fecha:** 2025-12-05 - [ ] Crear componente SearchBar.tsx
**Última actualización:** 2025-12-05 - [ ] Implementar coursesStore (Zustand)
- [ ] Implementar infinite scroll opcional
- [ ] Agregar analytics de búsqueda
**Tests:**
- [ ] Test unitario CourseService
- [ ] Test integración GET /courses con filtros
- [ ] Test E2E navegación y búsqueda
---
**Creado por:** Requirements-Analyst
**Fecha:** 2025-12-05
**Última actualización:** 2025-12-05

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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