ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
952 lines
26 KiB
Markdown
952 lines
26 KiB
Markdown
---
|
|
id: "ET-INV-003"
|
|
title: "Integración Stripe para Depósitos"
|
|
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-003: Integración Stripe para Depósitos
|
|
|
|
**Epic:** OQI-004 Cuentas de Inversión
|
|
**Versión:** 1.0
|
|
**Fecha:** 2025-12-05
|
|
**Responsable:** Requirements-Analyst
|
|
|
|
---
|
|
|
|
## 1. Descripción
|
|
|
|
Define la integración con Stripe Payment Intents para procesar depósitos en cuentas de inversión:
|
|
- Creación de Payment Intents para depósitos
|
|
- Confirmación de pagos
|
|
- Manejo de webhooks de Stripe
|
|
- Actualización de balances tras pagos exitosos
|
|
- Manejo de errores y pagos fallidos
|
|
|
|
---
|
|
|
|
## 2. Arquitectura de Integración
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Stripe Integration Flow │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Frontend Backend Stripe │
|
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
│ │ User │ │ │ │ │ │
|
|
│ │ Action │──────────►│ Create │──────►│ Payment │ │
|
|
│ └──────────┘ │ Intent │ │ Intent │ │
|
|
│ └──────────┘ └──────────┘ │
|
|
│ │ │ │
|
|
│ ▼ │ │
|
|
│ ┌──────────┐ │ │
|
|
│ │ Return │◄─────────────┘ │
|
|
│ │ Secret │ │
|
|
│ └──────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────┐ ┌──────────┐ │
|
|
│ │ Stripe │◄──────────│ Confirm │ │
|
|
│ │ Elements │ │ Payment │ │
|
|
│ └──────────┘ └──────────┘ │
|
|
│ │ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────┐ ┌──────────┐ │
|
|
│ │ Payment │───────────────────────►│ Webhook │ │
|
|
│ │ Success │ │ Handler │ │
|
|
│ └──────────┘ └──────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────┐ │
|
|
│ │ Update │ │
|
|
│ │ Balance │ │
|
|
│ └──────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Flujo de Depósitos con Stripe
|
|
|
|
### 3.1 Creación de Cuenta Nueva (Depósito Inicial)
|
|
|
|
```
|
|
1. Usuario selecciona producto y monto inicial
|
|
2. Backend crea Payment Intent en Stripe
|
|
3. Backend crea cuenta en estado "pending"
|
|
4. Backend crea transacción en estado "pending"
|
|
5. Frontend confirma pago con Stripe Elements
|
|
6. Stripe envía webhook payment_intent.succeeded
|
|
7. Backend actualiza transacción a "completed"
|
|
8. Backend actualiza balance de cuenta
|
|
9. Backend notifica ML Engine para iniciar trading
|
|
```
|
|
|
|
### 3.2 Depósito Adicional
|
|
|
|
```
|
|
1. Usuario solicita depósito adicional
|
|
2. Backend valida cuenta activa
|
|
3. Backend crea Payment Intent
|
|
4. Backend crea transacción "pending"
|
|
5. Frontend confirma pago
|
|
6. Webhook actualiza balance
|
|
7. Backend notifica ML Engine del nuevo capital
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Implementación Stripe Service
|
|
|
|
### 4.1 Stripe Service Class
|
|
|
|
```typescript
|
|
// src/services/stripe/stripe-investment.service.ts
|
|
|
|
import Stripe from 'stripe';
|
|
import { AppError } from '../../utils/errors';
|
|
|
|
export interface CreateDepositPaymentIntentDto {
|
|
user_id: string;
|
|
account_id?: string; // Opcional para nuevas cuentas
|
|
product_id: string;
|
|
amount: number;
|
|
payment_method_id: string;
|
|
customer_id?: string;
|
|
}
|
|
|
|
export class StripeInvestmentService {
|
|
private stripe: Stripe;
|
|
|
|
constructor() {
|
|
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
|
apiVersion: '2024-11-20.acacia',
|
|
typescript: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Crea un Payment Intent para depósito de inversión
|
|
*/
|
|
async createDepositPaymentIntent(
|
|
data: CreateDepositPaymentIntentDto
|
|
): Promise<Stripe.PaymentIntent> {
|
|
try {
|
|
// Crear o recuperar Stripe Customer
|
|
const customerId = await this.ensureCustomer(data.user_id, data.customer_id);
|
|
|
|
// Crear Payment Intent
|
|
const paymentIntent = await this.stripe.paymentIntents.create({
|
|
amount: Math.round(data.amount * 100), // Convertir a centavos
|
|
currency: 'usd',
|
|
customer: customerId,
|
|
payment_method: data.payment_method_id,
|
|
confirmation_method: 'manual',
|
|
confirm: false,
|
|
metadata: {
|
|
type: 'investment_deposit',
|
|
user_id: data.user_id,
|
|
product_id: data.product_id,
|
|
account_id: data.account_id || 'new_account',
|
|
},
|
|
description: `Investment deposit - ${data.product_id}`,
|
|
statement_descriptor: 'ORBIQUANT INV',
|
|
});
|
|
|
|
return paymentIntent;
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeError) {
|
|
throw new AppError(`Stripe error: ${error.message}`, 400);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirma un Payment Intent
|
|
*/
|
|
async confirmPaymentIntent(
|
|
paymentIntentId: string,
|
|
paymentMethodId?: string
|
|
): Promise<Stripe.PaymentIntent> {
|
|
try {
|
|
const params: Stripe.PaymentIntentConfirmParams = {};
|
|
|
|
if (paymentMethodId) {
|
|
params.payment_method = paymentMethodId;
|
|
}
|
|
|
|
const paymentIntent = await this.stripe.paymentIntents.confirm(
|
|
paymentIntentId,
|
|
params
|
|
);
|
|
|
|
return paymentIntent;
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeError) {
|
|
throw new AppError(`Payment confirmation failed: ${error.message}`, 400);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recupera un Payment Intent
|
|
*/
|
|
async getPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent> {
|
|
try {
|
|
return await this.stripe.paymentIntents.retrieve(paymentIntentId);
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeError) {
|
|
throw new AppError(`Payment Intent not found: ${error.message}`, 404);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancela un Payment Intent
|
|
*/
|
|
async cancelPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent> {
|
|
try {
|
|
return await this.stripe.paymentIntents.cancel(paymentIntentId);
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeError) {
|
|
throw new AppError(`Cannot cancel payment: ${error.message}`, 400);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asegura que el usuario tiene un Stripe Customer
|
|
*/
|
|
private async ensureCustomer(
|
|
userId: string,
|
|
existingCustomerId?: string
|
|
): Promise<string> {
|
|
if (existingCustomerId) {
|
|
// Verificar que el customer existe
|
|
try {
|
|
await this.stripe.customers.retrieve(existingCustomerId);
|
|
return existingCustomerId;
|
|
} catch {
|
|
// Si no existe, crear uno nuevo
|
|
}
|
|
}
|
|
|
|
// Crear nuevo customer
|
|
// Nota: En producción, buscar en DB si ya existe customer_id para este usuario
|
|
const customer = await this.stripe.customers.create({
|
|
metadata: {
|
|
user_id: userId,
|
|
},
|
|
});
|
|
|
|
return customer.id;
|
|
}
|
|
|
|
/**
|
|
* Adjunta un Payment Method a un Customer
|
|
*/
|
|
async attachPaymentMethod(
|
|
paymentMethodId: string,
|
|
customerId: string
|
|
): Promise<Stripe.PaymentMethod> {
|
|
try {
|
|
return await this.stripe.paymentMethods.attach(paymentMethodId, {
|
|
customer: customerId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeError) {
|
|
throw new AppError(`Cannot attach payment method: ${error.message}`, 400);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lista Payment Methods de un Customer
|
|
*/
|
|
async listPaymentMethods(customerId: string): Promise<Stripe.PaymentMethod[]> {
|
|
try {
|
|
const paymentMethods = await this.stripe.paymentMethods.list({
|
|
customer: customerId,
|
|
type: 'card',
|
|
});
|
|
|
|
return paymentMethods.data;
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeError) {
|
|
throw new AppError(`Cannot list payment methods: ${error.message}`, 400);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.2 Webhook Handler
|
|
|
|
```typescript
|
|
// src/services/stripe/stripe-webhook.service.ts
|
|
|
|
import Stripe from 'stripe';
|
|
import { Request } from 'express';
|
|
import { InvestmentRepository } from '../../modules/investment/investment.repository';
|
|
import { MLEngineService } from '../ml-engine/ml-engine.service';
|
|
import { logger } from '../../utils/logger';
|
|
|
|
export class StripeWebhookService {
|
|
private stripe: Stripe;
|
|
private investmentRepo: InvestmentRepository;
|
|
private mlEngineService: MLEngineService;
|
|
private webhookSecret: string;
|
|
|
|
constructor() {
|
|
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
|
apiVersion: '2024-11-20.acacia',
|
|
});
|
|
this.investmentRepo = new InvestmentRepository();
|
|
this.mlEngineService = new MLEngineService();
|
|
this.webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
|
}
|
|
|
|
/**
|
|
* Procesa webhook de Stripe
|
|
*/
|
|
async handleWebhook(req: Request): Promise<void> {
|
|
const signature = req.headers['stripe-signature'] as string;
|
|
|
|
let event: Stripe.Event;
|
|
|
|
try {
|
|
// Verificar firma del webhook
|
|
event = this.stripe.webhooks.constructEvent(
|
|
req.body,
|
|
signature,
|
|
this.webhookSecret
|
|
);
|
|
} catch (err: any) {
|
|
logger.error('Webhook signature verification failed', { error: err.message });
|
|
throw new Error(`Webhook Error: ${err.message}`);
|
|
}
|
|
|
|
// Procesar evento según tipo
|
|
switch (event.type) {
|
|
case 'payment_intent.succeeded':
|
|
await this.handlePaymentIntentSucceeded(event.data.object as Stripe.PaymentIntent);
|
|
break;
|
|
|
|
case 'payment_intent.payment_failed':
|
|
await this.handlePaymentIntentFailed(event.data.object as Stripe.PaymentIntent);
|
|
break;
|
|
|
|
case 'payment_intent.canceled':
|
|
await this.handlePaymentIntentCanceled(event.data.object as Stripe.PaymentIntent);
|
|
break;
|
|
|
|
default:
|
|
logger.info('Unhandled webhook event type', { type: event.type });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maneja pago exitoso
|
|
*/
|
|
private async handlePaymentIntentSucceeded(
|
|
paymentIntent: Stripe.PaymentIntent
|
|
): Promise<void> {
|
|
const { metadata } = paymentIntent;
|
|
|
|
if (metadata.type !== 'investment_deposit') {
|
|
return; // No es un depósito de inversión
|
|
}
|
|
|
|
logger.info('Processing successful investment deposit', {
|
|
payment_intent_id: paymentIntent.id,
|
|
amount: paymentIntent.amount / 100,
|
|
account_id: metadata.account_id,
|
|
});
|
|
|
|
try {
|
|
// Buscar transacción pendiente
|
|
const transaction = await this.investmentRepo.getTransactionByStripePaymentIntent(
|
|
paymentIntent.id
|
|
);
|
|
|
|
if (!transaction) {
|
|
logger.error('Transaction not found for payment intent', {
|
|
payment_intent_id: paymentIntent.id,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (transaction.status === 'completed') {
|
|
logger.warn('Transaction already completed', { transaction_id: transaction.id });
|
|
return; // Evitar procesamiento duplicado
|
|
}
|
|
|
|
// Actualizar cuenta
|
|
const account = await this.investmentRepo.getAccountById(transaction.account_id);
|
|
if (!account) {
|
|
throw new Error(`Account not found: ${transaction.account_id}`);
|
|
}
|
|
|
|
const newBalance = account.current_balance + transaction.amount;
|
|
|
|
// Actualizar en transacción
|
|
await this.investmentRepo.updateAccount(account.id, {
|
|
current_balance: newBalance,
|
|
total_deposited: account.total_deposited + transaction.amount,
|
|
status: 'active',
|
|
});
|
|
|
|
// Actualizar transacción
|
|
await this.investmentRepo.updateTransaction(transaction.id, {
|
|
status: 'completed',
|
|
balance_after: newBalance,
|
|
processed_at: new Date(),
|
|
});
|
|
|
|
// Notificar ML Engine
|
|
await this.mlEngineService.notifyDeposit({
|
|
account_id: account.id,
|
|
product_id: account.product_id,
|
|
amount: transaction.amount,
|
|
new_balance: newBalance,
|
|
});
|
|
|
|
logger.info('Investment deposit processed successfully', {
|
|
account_id: account.id,
|
|
amount: transaction.amount,
|
|
new_balance: newBalance,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('Error processing successful payment', {
|
|
error: error.message,
|
|
payment_intent_id: paymentIntent.id,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maneja pago fallido
|
|
*/
|
|
private async handlePaymentIntentFailed(
|
|
paymentIntent: Stripe.PaymentIntent
|
|
): Promise<void> {
|
|
const { metadata } = paymentIntent;
|
|
|
|
if (metadata.type !== 'investment_deposit') {
|
|
return;
|
|
}
|
|
|
|
logger.warn('Investment deposit payment failed', {
|
|
payment_intent_id: paymentIntent.id,
|
|
reason: paymentIntent.last_payment_error?.message,
|
|
});
|
|
|
|
try {
|
|
const transaction = await this.investmentRepo.getTransactionByStripePaymentIntent(
|
|
paymentIntent.id
|
|
);
|
|
|
|
if (transaction) {
|
|
await this.investmentRepo.updateTransaction(transaction.id, {
|
|
status: 'failed',
|
|
notes: paymentIntent.last_payment_error?.message || 'Payment failed',
|
|
});
|
|
|
|
// Si es depósito inicial, marcar cuenta como failed
|
|
if (metadata.account_id === 'new_account') {
|
|
// La cuenta fue creada pero el pago falló
|
|
// Opción: eliminar cuenta o marcarla como "pending_payment"
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
logger.error('Error handling failed payment', {
|
|
error: error.message,
|
|
payment_intent_id: paymentIntent.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maneja pago cancelado
|
|
*/
|
|
private async handlePaymentIntentCanceled(
|
|
paymentIntent: Stripe.PaymentIntent
|
|
): Promise<void> {
|
|
const { metadata } = paymentIntent;
|
|
|
|
if (metadata.type !== 'investment_deposit') {
|
|
return;
|
|
}
|
|
|
|
logger.info('Investment deposit payment canceled', {
|
|
payment_intent_id: paymentIntent.id,
|
|
});
|
|
|
|
try {
|
|
const transaction = await this.investmentRepo.getTransactionByStripePaymentIntent(
|
|
paymentIntent.id
|
|
);
|
|
|
|
if (transaction) {
|
|
await this.investmentRepo.updateTransaction(transaction.id, {
|
|
status: 'cancelled',
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
logger.error('Error handling canceled payment', {
|
|
error: error.message,
|
|
payment_intent_id: paymentIntent.id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.3 Webhook Route
|
|
|
|
```typescript
|
|
// src/routes/webhooks.routes.ts
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import { StripeWebhookService } from '../services/stripe/stripe-webhook.service';
|
|
import { logger } from '../utils/logger';
|
|
|
|
const router = Router();
|
|
const webhookService = new StripeWebhookService();
|
|
|
|
/**
|
|
* Endpoint para webhooks de Stripe
|
|
* IMPORTANTE: No usar bodyParser JSON aquí, necesitamos raw body
|
|
*/
|
|
router.post(
|
|
'/stripe',
|
|
async (req: Request, res: Response) => {
|
|
try {
|
|
await webhookService.handleWebhook(req);
|
|
res.status(200).json({ received: true });
|
|
} catch (error: any) {
|
|
logger.error('Webhook processing error', { error: error.message });
|
|
res.status(400).send(`Webhook Error: ${error.message}`);
|
|
}
|
|
}
|
|
);
|
|
|
|
export default router;
|
|
```
|
|
|
|
### 4.4 App Configuration para Webhooks
|
|
|
|
```typescript
|
|
// src/app.ts
|
|
|
|
import express from 'express';
|
|
import webhookRoutes from './routes/webhooks.routes';
|
|
|
|
const app = express();
|
|
|
|
// IMPORTANTE: Webhook route ANTES de bodyParser JSON
|
|
app.use(
|
|
'/webhooks',
|
|
express.raw({ type: 'application/json' }), // Raw body para webhooks
|
|
webhookRoutes
|
|
);
|
|
|
|
// Resto de middlewares
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
// ... otras rutas
|
|
|
|
export default app;
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Frontend Integration
|
|
|
|
### 5.1 Stripe Elements Component
|
|
|
|
```typescript
|
|
// src/components/investment/DepositForm.tsx
|
|
|
|
import React, { useState } from 'react';
|
|
import { loadStripe } from '@stripe/stripe-js';
|
|
import {
|
|
Elements,
|
|
CardElement,
|
|
useStripe,
|
|
useElements,
|
|
} from '@stripe/react-stripe-js';
|
|
import { investmentApi } from '../../api/investment.api';
|
|
|
|
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY!);
|
|
|
|
interface DepositFormProps {
|
|
accountId?: string;
|
|
productId: string;
|
|
minAmount: number;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
const DepositFormContent: React.FC<DepositFormProps> = ({
|
|
accountId,
|
|
productId,
|
|
minAmount,
|
|
onSuccess,
|
|
}) => {
|
|
const stripe = useStripe();
|
|
const elements = useElements();
|
|
const [amount, setAmount] = useState<number>(minAmount);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!stripe || !elements) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Crear Payment Method
|
|
const cardElement = elements.getElement(CardElement);
|
|
if (!cardElement) {
|
|
throw new Error('Card element not found');
|
|
}
|
|
|
|
const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({
|
|
type: 'card',
|
|
card: cardElement,
|
|
});
|
|
|
|
if (pmError || !paymentMethod) {
|
|
throw new Error(pmError?.message || 'Failed to create payment method');
|
|
}
|
|
|
|
// Crear depósito o cuenta
|
|
const response = accountId
|
|
? await investmentApi.deposit(accountId, {
|
|
amount,
|
|
payment_method_id: paymentMethod.id,
|
|
})
|
|
: await investmentApi.createAccount({
|
|
product_id: productId,
|
|
initial_investment: amount,
|
|
payment_method_id: paymentMethod.id,
|
|
});
|
|
|
|
const { payment_intent } = response.data;
|
|
|
|
// Confirmar Payment Intent
|
|
const { error: confirmError, paymentIntent } = await stripe.confirmCardPayment(
|
|
payment_intent.client_secret
|
|
);
|
|
|
|
if (confirmError) {
|
|
throw new Error(confirmError.message);
|
|
}
|
|
|
|
if (paymentIntent?.status === 'succeeded') {
|
|
onSuccess();
|
|
} else {
|
|
throw new Error('Payment not completed');
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.message || 'An error occurred');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="form-group">
|
|
<label>Amount (USD)</label>
|
|
<input
|
|
type="number"
|
|
min={minAmount}
|
|
step="0.01"
|
|
value={amount}
|
|
onChange={(e) => setAmount(Number(e.target.value))}
|
|
disabled={loading}
|
|
required
|
|
/>
|
|
<small>Minimum: ${minAmount}</small>
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label>Card Details</label>
|
|
<CardElement
|
|
options={{
|
|
style: {
|
|
base: {
|
|
fontSize: '16px',
|
|
color: '#424770',
|
|
'::placeholder': {
|
|
color: '#aab7c4',
|
|
},
|
|
},
|
|
invalid: {
|
|
color: '#9e2146',
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{error && <div className="error-message">{error}</div>}
|
|
|
|
<button type="submit" disabled={!stripe || loading}>
|
|
{loading ? 'Processing...' : `Deposit $${amount.toFixed(2)}`}
|
|
</button>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export const DepositForm: React.FC<DepositFormProps> = (props) => {
|
|
return (
|
|
<Elements stripe={stripePromise}>
|
|
<DepositFormContent {...props} />
|
|
</Elements>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Configuración
|
|
|
|
### 6.1 Variables de Entorno
|
|
|
|
```bash
|
|
# Stripe Keys
|
|
STRIPE_SECRET_KEY=sk_test_51abc...
|
|
STRIPE_PUBLISHABLE_KEY=pk_test_51abc...
|
|
STRIPE_WEBHOOK_SECRET=whsec_abc123...
|
|
|
|
# Stripe Settings
|
|
STRIPE_API_VERSION=2024-11-20.acacia
|
|
STRIPE_CURRENCY=usd
|
|
|
|
# Frontend
|
|
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_51abc...
|
|
```
|
|
|
|
### 6.2 Configuración de Webhook en Stripe Dashboard
|
|
|
|
1. Ir a Stripe Dashboard > Developers > Webhooks
|
|
2. Crear endpoint: `https://api.trading.com/webhooks/stripe`
|
|
3. Seleccionar eventos:
|
|
- `payment_intent.succeeded`
|
|
- `payment_intent.payment_failed`
|
|
- `payment_intent.canceled`
|
|
4. Copiar Webhook Secret y agregar a `.env`
|
|
|
|
---
|
|
|
|
## 7. Seguridad
|
|
|
|
### 7.1 Validación de Webhooks
|
|
|
|
```typescript
|
|
// Siempre verificar firma del webhook
|
|
const event = stripe.webhooks.constructEvent(
|
|
req.body,
|
|
signature,
|
|
webhookSecret
|
|
);
|
|
```
|
|
|
|
### 7.2 Idempotencia
|
|
|
|
```typescript
|
|
// Verificar que transacción no esté ya procesada
|
|
if (transaction.status === 'completed') {
|
|
logger.warn('Transaction already completed');
|
|
return;
|
|
}
|
|
```
|
|
|
|
### 7.3 Metadata Segura
|
|
|
|
```typescript
|
|
// No incluir información sensible en metadata
|
|
metadata: {
|
|
type: 'investment_deposit',
|
|
user_id: userId,
|
|
product_id: productId,
|
|
// NO incluir: passwords, tokens, PII
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Manejo de Errores
|
|
|
|
### 8.1 Errores de Stripe
|
|
|
|
```typescript
|
|
try {
|
|
// Stripe operation
|
|
} catch (error) {
|
|
if (error instanceof Stripe.errors.StripeCardError) {
|
|
// Card declined
|
|
return { error: 'Card was declined' };
|
|
} else if (error instanceof Stripe.errors.StripeInvalidRequestError) {
|
|
// Invalid parameters
|
|
return { error: 'Invalid request' };
|
|
} else if (error instanceof Stripe.errors.StripeAuthenticationError) {
|
|
// Authentication failed
|
|
logger.error('Stripe authentication error');
|
|
return { error: 'Payment service error' };
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8.2 Retry Logic
|
|
|
|
```typescript
|
|
// Implementar retry para webhooks fallidos
|
|
const MAX_RETRIES = 3;
|
|
let retries = 0;
|
|
|
|
while (retries < MAX_RETRIES) {
|
|
try {
|
|
await processWebhook(event);
|
|
break;
|
|
} catch (error) {
|
|
retries++;
|
|
if (retries === MAX_RETRIES) {
|
|
logger.error('Max retries reached', { event_id: event.id });
|
|
throw error;
|
|
}
|
|
await sleep(1000 * retries); // Exponential backoff
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Testing
|
|
|
|
### 9.1 Test Cards de Stripe
|
|
|
|
```typescript
|
|
// Test cards para diferentes escenarios
|
|
const TEST_CARDS = {
|
|
success: '4242424242424242',
|
|
declined: '4000000000000002',
|
|
insufficientFunds: '4000000000009995',
|
|
expiredCard: '4000000000000069',
|
|
processingError: '4000000000000119',
|
|
};
|
|
```
|
|
|
|
### 9.2 Test de Webhooks
|
|
|
|
```typescript
|
|
// tests/stripe/webhook.test.ts
|
|
|
|
import { StripeWebhookService } from '../../src/services/stripe/stripe-webhook.service';
|
|
import Stripe from 'stripe';
|
|
|
|
describe('Stripe Webhook Service', () => {
|
|
let webhookService: StripeWebhookService;
|
|
let stripe: Stripe;
|
|
|
|
beforeAll(() => {
|
|
webhookService = new StripeWebhookService();
|
|
stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
});
|
|
|
|
it('should process payment_intent.succeeded', async () => {
|
|
// Crear evento de prueba
|
|
const event = stripe.webhooks.generateTestHeaderString({
|
|
payload: JSON.stringify({
|
|
type: 'payment_intent.succeeded',
|
|
data: {
|
|
object: {
|
|
id: 'pi_test_123',
|
|
amount: 500000,
|
|
metadata: {
|
|
type: 'investment_deposit',
|
|
account_id: 'acc_123',
|
|
user_id: 'user_123',
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
});
|
|
|
|
// Procesar webhook
|
|
await webhookService.handleWebhook(mockRequest(event));
|
|
|
|
// Verificar que balance se actualizó
|
|
const account = await getAccount('acc_123');
|
|
expect(account.current_balance).toBe(5000);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Monitoreo
|
|
|
|
### 10.1 Logs Importantes
|
|
|
|
```typescript
|
|
logger.info('Payment Intent created', {
|
|
payment_intent_id: paymentIntent.id,
|
|
amount: paymentIntent.amount / 100,
|
|
account_id: accountId,
|
|
});
|
|
|
|
logger.info('Payment succeeded', {
|
|
payment_intent_id: paymentIntent.id,
|
|
transaction_id: transaction.id,
|
|
new_balance: newBalance,
|
|
});
|
|
|
|
logger.error('Payment failed', {
|
|
payment_intent_id: paymentIntent.id,
|
|
error: error.message,
|
|
user_id: userId,
|
|
});
|
|
```
|
|
|
|
### 10.2 Métricas
|
|
|
|
- Total de depósitos procesados
|
|
- Tasa de éxito de pagos
|
|
- Tiempo promedio de procesamiento
|
|
- Errores de webhook
|
|
|
|
---
|
|
|
|
## 11. Referencias
|
|
|
|
- [Stripe Payment Intents API](https://stripe.com/docs/payments/payment-intents)
|
|
- [Stripe Webhooks Guide](https://stripe.com/docs/webhooks)
|
|
- [Stripe React Elements](https://stripe.com/docs/stripe-js/react)
|
|
- [Best Practices for Stripe](https://stripe.com/docs/security/best-practices)
|