--- id: "ET-INV-002" title: "API REST Investment Accounts" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-004" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-INV-002: API REST Investment Accounts **Epic:** OQI-004 Cuentas de Inversión **Versión:** 1.0 **Fecha:** 2025-12-05 **Responsable:** Requirements-Analyst --- ## 1. Descripción Define los endpoints REST para la gestión completa de cuentas de inversión: - CRUD de productos de inversión - Gestión de cuentas de inversión - Depósitos y retiros - Consulta de portfolio y performance - Administración de solicitudes de retiro --- ## 2. Arquitectura de API ``` ┌─────────────────────────────────────────────────────────────────┐ │ Investment API Layer │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Routes │────►│ Controllers │────►│ Services │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌──────────────┐ │ │ │ │ │ DB │ │ │ │ │ │ (Postgres) │ │ │ │ │ └──────────────┘ │ │ │ │ │ │ │ ▼ │ │ │ ┌──────────────┐ │ │ │ │ Middlewares │ │ │ │ │ - Auth │ │ │ │ │ - Validate │ │ │ │ │ - RateLimit │ │ │ │ └──────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ Integration │ │ │ │ - Stripe │ │ │ │ - ML Engine │ │ │ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Endpoints Especificados ### 3.1 Productos de Inversión #### GET `/api/v1/investment/products` Lista todos los productos de inversión disponibles. **Request:** ```http GET /api/v1/investment/products Authorization: Bearer {token} Query Parameters: - status: string (optional) - 'active', 'paused', 'closed' - agent_type: string (optional) - 'swing', 'day', 'scalping', 'arbitrage' - risk_level: string (optional) - 'low', 'medium', 'high', 'very_high' - limit: number (optional, default: 20) - offset: number (optional, default: 0) ``` **Response 200:** ```json { "success": true, "data": { "products": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Swing Trader Pro", "description": "Agente de swing trading con estrategias de mediano plazo", "agent_type": "swing", "min_investment": 500.00, "max_investment": null, "performance_fee_percentage": 20.00, "target_annual_return": 35.50, "risk_level": "medium", "status": "active", "is_accepting_new_investors": true, "total_aum": 125000.00, "total_investors": 45, "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-20T14:30:00Z" } ], "pagination": { "total": 5, "limit": 20, "offset": 0, "has_more": false } } } ``` **Errors:** - `401 Unauthorized`: Token inválido o expirado - `500 Internal Server Error`: Error en el servidor --- #### GET `/api/v1/investment/products/:id` Obtiene detalles de un producto específico. **Request:** ```http GET /api/v1/investment/products/550e8400-e29b-41d4-a716-446655440000 Authorization: Bearer {token} ``` **Response 200:** ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Swing Trader Pro", "description": "Agente de swing trading con estrategias de mediano plazo", "agent_type": "swing", "min_investment": 500.00, "max_investment": null, "performance_fee_percentage": 20.00, "target_annual_return": 35.50, "risk_level": "medium", "ml_agent_id": "ml-agent-swing-001", "ml_config": { "trading_pairs": ["BTC/USD", "ETH/USD"], "max_position_size": 0.1, "stop_loss_percentage": 2.5 }, "status": "active", "is_accepting_new_investors": true, "total_aum": 125000.00, "total_investors": 45, "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-20T14:30:00Z" } } ``` **Errors:** - `404 Not Found`: Producto no encontrado --- #### POST `/api/v1/investment/products` (Admin Only) Crea un nuevo producto de inversión. **Request:** ```http POST /api/v1/investment/products Authorization: Bearer {admin_token} Content-Type: application/json { "name": "Scalping Expert", "description": "Agente de scalping de alta frecuencia", "agent_type": "scalping", "min_investment": 2000.00, "max_investment": 50000.00, "performance_fee_percentage": 30.00, "target_annual_return": 60.00, "risk_level": "very_high", "ml_agent_id": "ml-agent-scalping-001", "ml_config": { "timeframe": "1m", "max_trades_per_day": 100 } } ``` **Response 201:** ```json { "success": true, "data": { "id": "650e8400-e29b-41d4-a716-446655440001", "name": "Scalping Expert", "status": "active", "created_at": "2025-01-21T09:00:00Z" } } ``` **Errors:** - `400 Bad Request`: Validación fallida - `403 Forbidden`: No es administrador - `409 Conflict`: `ml_agent_id` ya existe --- ### 3.2 Cuentas de Inversión #### GET `/api/v1/investment/accounts` Lista las cuentas de inversión del usuario autenticado. **Request:** ```http GET /api/v1/investment/accounts Authorization: Bearer {token} Query Parameters: - status: string (optional) - 'active', 'paused', 'closed' - product_id: string (optional) ``` **Response 200:** ```json { "success": true, "data": { "accounts": [ { "id": "750e8400-e29b-41d4-a716-446655440000", "product_id": "550e8400-e29b-41d4-a716-446655440000", "product_name": "Swing Trader Pro", "current_balance": 5250.75, "initial_investment": 5000.00, "total_deposited": 5000.00, "total_withdrawn": 0.00, "total_profit_distributed": 250.75, "total_return_percentage": 5.015, "annualized_return_percentage": 35.10, "status": "active", "opened_at": "2025-01-10T08:00:00Z" } ], "summary": { "total_invested": 5000.00, "total_current_value": 5250.75, "total_profit": 250.75, "total_return_percentage": 5.015 } } } ``` --- #### GET `/api/v1/investment/accounts/:id` Obtiene detalles de una cuenta específica. **Request:** ```http GET /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000 Authorization: Bearer {token} ``` **Response 200:** ```json { "success": true, "data": { "account": { "id": "750e8400-e29b-41d4-a716-446655440000", "product_id": "550e8400-e29b-41d4-a716-446655440000", "current_balance": 5250.75, "initial_investment": 5000.00, "total_deposited": 5000.00, "total_withdrawn": 0.00, "total_profit_distributed": 250.75, "total_return_percentage": 5.015, "annualized_return_percentage": 35.10, "status": "active", "opened_at": "2025-01-10T08:00:00Z" }, "product": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Swing Trader Pro", "agent_type": "swing", "risk_level": "medium", "performance_fee_percentage": 20.00 }, "recent_transactions": [ { "id": "850e8400-e29b-41d4-a716-446655440000", "type": "deposit", "amount": 5000.00, "status": "completed", "created_at": "2025-01-10T08:00:00Z" } ] } } ``` **Errors:** - `403 Forbidden`: Cuenta no pertenece al usuario - `404 Not Found`: Cuenta no encontrada --- #### POST `/api/v1/investment/accounts` Crea una nueva cuenta de inversión (depósito inicial). **Request:** ```http POST /api/v1/investment/accounts Authorization: Bearer {token} Content-Type: application/json { "product_id": "550e8400-e29b-41d4-a716-446655440000", "initial_investment": 5000.00, "payment_method_id": "pm_1234567890abcdef" } ``` **Response 201:** ```json { "success": true, "data": { "account_id": "750e8400-e29b-41d4-a716-446655440000", "payment_intent": { "id": "pi_1234567890abcdef", "client_secret": "pi_1234567890abcdef_secret_xyz", "status": "requires_confirmation" } } } ``` **Errors:** - `400 Bad Request`: Validación fallida (ej: monto menor a min_investment) - `409 Conflict`: Usuario ya tiene cuenta en este producto --- ### 3.3 Depósitos #### POST `/api/v1/investment/accounts/:id/deposit` Realiza un depósito adicional a una cuenta existente. **Request:** ```http POST /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000/deposit Authorization: Bearer {token} Content-Type: application/json { "amount": 1000.00, "payment_method_id": "pm_1234567890abcdef" } ``` **Response 200:** ```json { "success": true, "data": { "transaction_id": "850e8400-e29b-41d4-a716-446655440001", "payment_intent": { "id": "pi_9876543210abcdef", "client_secret": "pi_9876543210abcdef_secret_abc", "status": "requires_confirmation" } } } ``` **Errors:** - `400 Bad Request`: Monto inválido - `403 Forbidden`: Cuenta no pertenece al usuario - `404 Not Found`: Cuenta no encontrada - `409 Conflict`: Cuenta no está activa --- ### 3.4 Retiros #### POST `/api/v1/investment/accounts/:id/withdraw` Crea una solicitud de retiro. **Request:** ```http POST /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000/withdraw Authorization: Bearer {token} Content-Type: application/json { "amount": 500.00, "withdrawal_method": "bank_transfer", "destination_details": { "bank_name": "Bank of America", "account_number": "****1234", "routing_number": "026009593", "account_holder_name": "John Doe" } } ``` **Response 201:** ```json { "success": true, "data": { "withdrawal_request_id": "950e8400-e29b-41d4-a716-446655440000", "status": "pending", "amount": 500.00, "estimated_processing_time": "2-5 business days", "requested_at": "2025-01-21T10:00:00Z" } } ``` **Errors:** - `400 Bad Request`: Monto excede balance disponible - `403 Forbidden`: Cuenta no pertenece al usuario - `409 Conflict`: Ya existe solicitud de retiro pendiente --- #### GET `/api/v1/investment/withdrawal-requests` Lista las solicitudes de retiro del usuario. **Request:** ```http GET /api/v1/investment/withdrawal-requests Authorization: Bearer {token} Query Parameters: - status: string (optional) - 'pending', 'approved', 'rejected', 'completed' - limit: number (optional, default: 20) - offset: number (optional, default: 0) ``` **Response 200:** ```json { "success": true, "data": { "requests": [ { "id": "950e8400-e29b-41d4-a716-446655440000", "account_id": "750e8400-e29b-41d4-a716-446655440000", "amount": 500.00, "withdrawal_method": "bank_transfer", "status": "pending", "requested_at": "2025-01-21T10:00:00Z" } ], "pagination": { "total": 1, "limit": 20, "offset": 0 } } } ``` --- ### 3.5 Portfolio y Performance #### GET `/api/v1/investment/portfolio` Obtiene el resumen completo del portfolio del usuario. **Request:** ```http GET /api/v1/investment/portfolio Authorization: Bearer {token} ``` **Response 200:** ```json { "success": true, "data": { "summary": { "total_invested": 10000.00, "total_current_value": 10850.50, "total_profit": 850.50, "total_return_percentage": 8.505, "annualized_return_percentage": 42.50 }, "accounts": [ { "account_id": "750e8400-e29b-41d4-a716-446655440000", "product_name": "Swing Trader Pro", "agent_type": "swing", "current_balance": 5250.75, "invested": 5000.00, "profit": 250.75, "return_percentage": 5.015, "allocation_percentage": 48.40 }, { "account_id": "750e8400-e29b-41d4-a716-446655440001", "product_name": "Day Trader Elite", "agent_type": "day", "current_balance": 5599.75, "invested": 5000.00, "profit": 599.75, "return_percentage": 11.995, "allocation_percentage": 51.60 } ], "allocation_by_risk": { "low": 0.00, "medium": 48.40, "high": 51.60, "very_high": 0.00 } } } ``` --- #### GET `/api/v1/investment/accounts/:id/performance` Obtiene el historial de performance de una cuenta. **Request:** ```http GET /api/v1/investment/accounts/750e8400-e29b-41d4-a716-446655440000/performance Authorization: Bearer {token} Query Parameters: - period: string - 'week', 'month', 'quarter', 'year', 'all' - start_date: string (optional) - ISO 8601 date - end_date: string (optional) - ISO 8601 date ``` **Response 200:** ```json { "success": true, "data": { "account_id": "750e8400-e29b-41d4-a716-446655440000", "period": "month", "performance": [ { "date": "2025-01-10", "opening_balance": 5000.00, "closing_balance": 5025.50, "daily_return": 25.50, "daily_return_percentage": 0.51, "cumulative_return": 25.50, "cumulative_return_percentage": 0.51 }, { "date": "2025-01-11", "opening_balance": 5025.50, "closing_balance": 5075.25, "daily_return": 49.75, "daily_return_percentage": 0.99, "cumulative_return": 75.25, "cumulative_return_percentage": 1.505 } ], "statistics": { "total_days": 30, "winning_days": 22, "losing_days": 8, "best_day_return": 125.50, "worst_day_return": -45.20, "average_daily_return": 8.35, "volatility": 2.15 } } } ``` --- ### 3.6 Transacciones #### GET `/api/v1/investment/transactions` Lista todas las transacciones del usuario. **Request:** ```http GET /api/v1/investment/transactions Authorization: Bearer {token} Query Parameters: - account_id: string (optional) - type: string (optional) - 'deposit', 'withdrawal', 'profit_distribution', 'fee' - status: string (optional) - 'pending', 'completed', 'failed', 'cancelled' - start_date: string (optional) - end_date: string (optional) - limit: number (optional, default: 50) - offset: number (optional, default: 0) ``` **Response 200:** ```json { "success": true, "data": { "transactions": [ { "id": "850e8400-e29b-41d4-a716-446655440000", "account_id": "750e8400-e29b-41d4-a716-446655440000", "type": "deposit", "status": "completed", "amount": 5000.00, "balance_before": 0.00, "balance_after": 5000.00, "created_at": "2025-01-10T08:00:00Z", "processed_at": "2025-01-10T08:05:00Z" } ], "pagination": { "total": 15, "limit": 50, "offset": 0 } } } ``` --- ## 4. Implementación Backend ### 4.1 Estructura de Archivos ``` src/ ├── modules/ │ └── investment/ │ ├── investment.routes.ts │ ├── investment.controller.ts │ ├── investment.service.ts │ ├── investment.repository.ts │ ├── investment.validators.ts │ └── investment.types.ts ├── middlewares/ │ ├── auth.middleware.ts │ ├── validate.middleware.ts │ └── rate-limit.middleware.ts └── utils/ ├── errors.ts └── response.ts ``` ### 4.2 Routes ```typescript // src/modules/investment/investment.routes.ts import { Router } from 'express'; import { InvestmentController } from './investment.controller'; import { authenticate, requireAdmin } from '../../middlewares/auth.middleware'; import { validate } from '../../middlewares/validate.middleware'; import { rateLimit } from '../../middlewares/rate-limit.middleware'; import { createProductSchema, createAccountSchema, depositSchema, withdrawalSchema, } from './investment.validators'; const router = Router(); const controller = new InvestmentController(); // Products router.get('/products', authenticate, controller.getProducts); router.get('/products/:id', authenticate, controller.getProductById); router.post('/products', authenticate, requireAdmin, validate(createProductSchema), controller.createProduct); router.patch('/products/:id', authenticate, requireAdmin, controller.updateProduct); // Accounts router.get('/accounts', authenticate, controller.getAccounts); router.get('/accounts/:id', authenticate, controller.getAccountById); router.post('/accounts', authenticate, validate(createAccountSchema), rateLimit(5, 3600), controller.createAccount); // Deposits router.post('/accounts/:id/deposit', authenticate, validate(depositSchema), rateLimit(10, 3600), controller.deposit); // Withdrawals router.post('/accounts/:id/withdraw', authenticate, validate(withdrawalSchema), rateLimit(3, 3600), controller.withdraw); router.get('/withdrawal-requests', authenticate, controller.getWithdrawalRequests); router.patch('/withdrawal-requests/:id', authenticate, requireAdmin, controller.updateWithdrawalRequest); // Portfolio router.get('/portfolio', authenticate, controller.getPortfolio); router.get('/accounts/:id/performance', authenticate, controller.getPerformance); // Transactions router.get('/transactions', authenticate, controller.getTransactions); export default router; ``` ### 4.3 Controller ```typescript // src/modules/investment/investment.controller.ts import { Request, Response, NextFunction } from 'express'; import { InvestmentService } from './investment.service'; import { successResponse, errorResponse } from '../../utils/response'; export class InvestmentController { private service: InvestmentService; constructor() { this.service = new InvestmentService(); } getProducts = async (req: Request, res: Response, next: NextFunction) => { try { const { status, agent_type, risk_level, limit = 20, offset = 0 } = req.query; const result = await this.service.getProducts({ status: status as string, agent_type: agent_type as string, risk_level: risk_level as string, limit: Number(limit), offset: Number(offset), }); return successResponse(res, result, 200); } catch (error) { next(error); } }; getProductById = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; const product = await this.service.getProductById(id); if (!product) { return errorResponse(res, 'Product not found', 404); } return successResponse(res, product, 200); } catch (error) { next(error); } }; createAccount = async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.user!.id; const { product_id, initial_investment, payment_method_id } = req.body; const result = await this.service.createAccount({ user_id: userId, product_id, initial_investment, payment_method_id, }); return successResponse(res, result, 201); } catch (error) { next(error); } }; deposit = async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.user!.id; const { id: accountId } = req.params; const { amount, payment_method_id } = req.body; const result = await this.service.deposit({ user_id: userId, account_id: accountId, amount, payment_method_id, }); return successResponse(res, result, 200); } catch (error) { next(error); } }; withdraw = async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.user!.id; const { id: accountId } = req.params; const { amount, withdrawal_method, destination_details } = req.body; const result = await this.service.createWithdrawalRequest({ user_id: userId, account_id: accountId, amount, withdrawal_method, destination_details, }); return successResponse(res, result, 201); } catch (error) { next(error); } }; getPortfolio = async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.user!.id; const portfolio = await this.service.getPortfolio(userId); return successResponse(res, portfolio, 200); } catch (error) { next(error); } }; getPerformance = async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.user!.id; const { id: accountId } = req.params; const { period, start_date, end_date } = req.query; const performance = await this.service.getPerformance({ user_id: userId, account_id: accountId, period: period as string, start_date: start_date as string, end_date: end_date as string, }); return successResponse(res, performance, 200); } catch (error) { next(error); } }; // ... otros métodos } ``` ### 4.4 Service ```typescript // src/modules/investment/investment.service.ts import { InvestmentRepository } from './investment.repository'; import { StripeService } from '../payments/stripe.service'; import { AppError } from '../../utils/errors'; import { CreateAccountDto, DepositDto, WithdrawalDto } from './investment.types'; export class InvestmentService { private repository: InvestmentRepository; private stripeService: StripeService; constructor() { this.repository = new InvestmentRepository(); this.stripeService = new StripeService(); } async getProducts(filters: any) { const { products, total } = await this.repository.getProducts(filters); return { products, pagination: { total, limit: filters.limit, offset: filters.offset, has_more: total > filters.offset + filters.limit, }, }; } async createAccount(data: CreateAccountDto) { // Validar producto const product = await this.repository.getProductById(data.product_id); if (!product) { throw new AppError('Product not found', 404); } if (!product.is_accepting_new_investors) { throw new AppError('Product is not accepting new investors', 400); } // Validar monto mínimo if (data.initial_investment < product.min_investment) { throw new AppError( `Minimum investment is ${product.min_investment}`, 400 ); } // Validar monto máximo if ( product.max_investment && data.initial_investment > product.max_investment ) { throw new AppError( `Maximum investment is ${product.max_investment}`, 400 ); } // Verificar si ya existe cuenta const existingAccount = await this.repository.getAccountByUserAndProduct( data.user_id, data.product_id ); if (existingAccount) { throw new AppError('Account already exists for this product', 409); } // Crear Payment Intent en Stripe const paymentIntent = await this.stripeService.createPaymentIntent({ amount: data.initial_investment, currency: 'usd', payment_method: data.payment_method_id, metadata: { type: 'investment_deposit', product_id: data.product_id, user_id: data.user_id, }, }); // Crear cuenta (pendiente de confirmación de pago) const account = await this.repository.createAccount({ user_id: data.user_id, product_id: data.product_id, initial_investment: data.initial_investment, }); // Crear transacción pendiente await this.repository.createTransaction({ account_id: account.id, user_id: data.user_id, type: 'deposit', amount: data.initial_investment, balance_before: 0, stripe_payment_intent_id: paymentIntent.id, status: 'pending', }); return { account_id: account.id, payment_intent: { id: paymentIntent.id, client_secret: paymentIntent.client_secret, status: paymentIntent.status, }, }; } async deposit(data: DepositDto) { // Validar cuenta const account = await this.repository.getAccountById(data.account_id); if (!account) { throw new AppError('Account not found', 404); } if (account.user_id !== data.user_id) { throw new AppError('Forbidden', 403); } if (account.status !== 'active') { throw new AppError('Account is not active', 409); } // Crear Payment Intent const paymentIntent = await this.stripeService.createPaymentIntent({ amount: data.amount, currency: 'usd', payment_method: data.payment_method_id, metadata: { type: 'investment_deposit', account_id: data.account_id, user_id: data.user_id, }, }); // Crear transacción pendiente const transaction = await this.repository.createTransaction({ account_id: data.account_id, user_id: data.user_id, type: 'deposit', amount: data.amount, balance_before: account.current_balance, stripe_payment_intent_id: paymentIntent.id, status: 'pending', }); return { transaction_id: transaction.id, payment_intent: { id: paymentIntent.id, client_secret: paymentIntent.client_secret, status: paymentIntent.status, }, }; } async getPortfolio(userId: string) { const accounts = await this.repository.getAccountsByUser(userId); const totalInvested = accounts.reduce((sum, acc) => sum + acc.total_deposited, 0); const totalCurrentValue = accounts.reduce((sum, acc) => sum + acc.current_balance, 0); const totalProfit = totalCurrentValue - totalInvested; const totalReturnPercentage = totalInvested > 0 ? (totalProfit / totalInvested) * 100 : 0; // Calcular allocación por riesgo const allocationByRisk = accounts.reduce((acc, account) => { const percentage = (account.current_balance / totalCurrentValue) * 100; acc[account.product.risk_level] = (acc[account.product.risk_level] || 0) + percentage; return acc; }, {} as Record); return { summary: { total_invested: totalInvested, total_current_value: totalCurrentValue, total_profit: totalProfit, total_return_percentage: totalReturnPercentage, }, accounts: accounts.map((acc) => ({ account_id: acc.id, product_name: acc.product.name, agent_type: acc.product.agent_type, current_balance: acc.current_balance, invested: acc.total_deposited, profit: acc.current_balance - acc.total_deposited, return_percentage: acc.total_return_percentage, allocation_percentage: (acc.current_balance / totalCurrentValue) * 100, })), allocation_by_risk: allocationByRisk, }; } // ... otros métodos } ``` --- ## 5. Validaciones ### 5.1 Schemas Zod ```typescript // src/modules/investment/investment.validators.ts import { z } from 'zod'; export const createProductSchema = z.object({ name: z.string().min(3).max(100), description: z.string().optional(), agent_type: z.enum(['swing', 'day', 'scalping', 'arbitrage']), min_investment: z.number().positive(), max_investment: z.number().positive().optional(), performance_fee_percentage: z.number().min(0).max(100), target_annual_return: z.number().optional(), risk_level: z.enum(['low', 'medium', 'high', 'very_high']), ml_agent_id: z.string().min(1), ml_config: z.record(z.any()).optional(), }); export const createAccountSchema = z.object({ product_id: z.string().uuid(), initial_investment: z.number().positive(), payment_method_id: z.string().min(1), }); export const depositSchema = z.object({ amount: z.number().positive(), payment_method_id: z.string().min(1), }); export const withdrawalSchema = z.object({ amount: z.number().positive(), withdrawal_method: z.enum(['bank_transfer', 'stripe_payout']), destination_details: z.record(z.any()), }); ``` --- ## 6. Seguridad ### 6.1 Rate Limiting ```typescript // Límites por endpoint const RATE_LIMITS = { createAccount: { max: 5, window: 3600 }, // 5 cuentas/hora deposit: { max: 10, window: 3600 }, // 10 depósitos/hora withdraw: { max: 3, window: 3600 }, // 3 retiros/hora }; ``` ### 6.2 Autenticación - Todos los endpoints requieren JWT válido - Endpoints de admin requieren rol `admin` - Verificación de ownership para acceso a cuentas --- ## 7. Configuración ### 7.1 Variables de Entorno ```bash # API PORT=3000 API_PREFIX=/api/v1 # Investment INVESTMENT_MIN_DEPOSIT=50.00 INVESTMENT_MIN_WITHDRAWAL=50.00 INVESTMENT_MAX_WITHDRAWAL_PENDING=5 # Rate Limits RATE_LIMIT_WINDOW_MS=3600000 RATE_LIMIT_MAX_REQUESTS=100 ``` --- ## 8. Testing ### 8.1 Test de Endpoints ```typescript // tests/investment/accounts.test.ts import request from 'supertest'; import app from '../../src/app'; describe('Investment Accounts API', () => { let authToken: string; let productId: string; beforeAll(async () => { // Setup: autenticar y crear producto authToken = await getAuthToken(); productId = await createTestProduct(); }); describe('POST /api/v1/investment/accounts', () => { it('should create new account with valid data', async () => { const response = await request(app) .post('/api/v1/investment/accounts') .set('Authorization', `Bearer ${authToken}`) .send({ product_id: productId, initial_investment: 5000, payment_method_id: 'pm_test_123', }); expect(response.status).toBe(201); expect(response.body.success).toBe(true); expect(response.body.data).toHaveProperty('account_id'); expect(response.body.data.payment_intent).toHaveProperty('client_secret'); }); it('should reject investment below minimum', async () => { const response = await request(app) .post('/api/v1/investment/accounts') .set('Authorization', `Bearer ${authToken}`) .send({ product_id: productId, initial_investment: 50, // Menor al mínimo payment_method_id: 'pm_test_123', }); expect(response.status).toBe(400); }); it('should reject duplicate account', async () => { // Intentar crear segunda cuenta en mismo producto const response = await request(app) .post('/api/v1/investment/accounts') .set('Authorization', `Bearer ${authToken}`) .send({ product_id: productId, initial_investment: 5000, payment_method_id: 'pm_test_123', }); expect(response.status).toBe(409); }); }); describe('GET /api/v1/investment/portfolio', () => { it('should return user portfolio', async () => { const response = await request(app) .get('/api/v1/investment/portfolio') .set('Authorization', `Bearer ${authToken}`); expect(response.status).toBe(200); expect(response.body.data).toHaveProperty('summary'); expect(response.body.data).toHaveProperty('accounts'); expect(response.body.data.summary).toHaveProperty('total_invested'); }); }); }); ``` --- ## 9. Documentación OpenAPI ```yaml openapi: 3.0.0 info: title: Trading Platform - Investment API version: 1.0.0 description: API para gestión de cuentas de inversión paths: /api/v1/investment/products: get: summary: Lista productos de inversión tags: [Products] security: - bearerAuth: [] responses: '200': description: Lista de productos content: application/json: schema: $ref: '#/components/schemas/ProductsResponse' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT schemas: ProductsResponse: type: object properties: success: type: boolean data: type: object properties: products: type: array items: $ref: '#/components/schemas/Product' ``` --- ## 10. Referencias - Stripe Payment Intents API - Express.js Best Practices - Zod Validation Library - PostgreSQL Transaction Management