trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-003-stripe.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
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>
2026-01-07 09:31:29 -06:00

26 KiB

id title type status priority epic project version created_date updated_date
ET-INV-003 Integración Stripe para Depósitos Technical Specification Done Alta OQI-004 trading-platform 1.0.0 2025-12-05 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

// 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

// 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

// 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

// 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

// 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

# 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

// Siempre verificar firma del webhook
const event = stripe.webhooks.constructEvent(
  req.body,
  signature,
  webhookSecret
);

7.2 Idempotencia

// Verificar que transacción no esté ya procesada
if (transaction.status === 'completed') {
  logger.warn('Transaction already completed');
  return;
}

7.3 Metadata Segura

// 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

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

// 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

// Test cards para diferentes escenarios
const TEST_CARDS = {
  success: '4242424242424242',
  declined: '4000000000000002',
  insufficientFunds: '4000000000009995',
  expiredCard: '4000000000000069',
  processingError: '4000000000000119',
};

9.2 Test de Webhooks

// 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

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