--- id: "ET-PAY-002" title: "Integración Stripe API Completa" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-005" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-PAY-002: Integración Stripe API Completa **Epic:** OQI-005 Pagos y Stripe **Versión:** 1.0 **Fecha:** 2025-12-05 **Responsable:** Requirements-Analyst --- ## 1. Descripción Integración completa con Stripe API para: - Gestión de Customers - Payment Intents (one-time payments) - Subscriptions (pagos recurrentes) - Payment Methods - Invoices - Refunds - Stripe Elements (frontend) --- ## 2. Arquitectura de Integración ``` ┌─────────────────────────────────────────────────────────────────┐ │ Stripe Integration Stack │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Frontend Backend Stripe API │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Stripe │ │ Stripe │ │ Stripe │ │ │ │ Elements │─────►│ Service │───►│ Platform │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ React │ │ Database │ │ Webhooks │ │ │ │ Components │ │ Persist │◄───│ Handler │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Stripe Service Implementation ### 3.1 Core Stripe Service ```typescript // src/services/stripe/stripe.service.ts import Stripe from 'stripe'; import { AppError } from '../../utils/errors'; import { logger } from '../../utils/logger'; export class StripeService { private stripe: Stripe; constructor() { const secretKey = process.env.STRIPE_SECRET_KEY!; this.stripe = new Stripe(secretKey, { apiVersion: '2024-11-20.acacia', typescript: true, maxNetworkRetries: 3, timeout: 30000, }); } // ============ CUSTOMERS ============ /** * Crea un customer en Stripe */ async createCustomer(params: { email: string; name?: string; phone?: string; metadata?: Record; }): Promise { try { const customer = await this.stripe.customers.create({ email: params.email, name: params.name, phone: params.phone, metadata: params.metadata || {}, }); logger.info('Stripe customer created', { customer_id: customer.id }); return customer; } catch (error: any) { logger.error('Failed to create Stripe customer', { error: error.message }); throw new AppError('Failed to create customer', 500); } } /** * Actualiza un customer */ async updateCustomer( customerId: string, params: Stripe.CustomerUpdateParams ): Promise { try { return await this.stripe.customers.update(customerId, params); } catch (error: any) { throw new AppError('Failed to update customer', 500); } } /** * Obtiene un customer */ async getCustomer(customerId: string): Promise { try { return await this.stripe.customers.retrieve(customerId) as Stripe.Customer; } catch (error: any) { throw new AppError('Customer not found', 404); } } // ============ PAYMENT INTENTS ============ /** * Crea un Payment Intent */ async createPaymentIntent(params: { amount: number; currency?: string; customer_id?: string; payment_method?: string; metadata?: Record; description?: string; }): Promise { try { const paymentIntent = await this.stripe.paymentIntents.create({ amount: Math.round(params.amount * 100), // Convertir a centavos currency: params.currency || 'usd', customer: params.customer_id, payment_method: params.payment_method, confirmation_method: 'manual', confirm: false, metadata: params.metadata || {}, description: params.description, }); logger.info('Payment Intent created', { payment_intent_id: paymentIntent.id, amount: params.amount, }); return paymentIntent; } catch (error: any) { logger.error('Failed to create Payment Intent', { error: error.message }); throw new AppError('Failed to create payment intent', 500); } } /** * Confirma un Payment Intent */ async confirmPaymentIntent( paymentIntentId: string, paymentMethodId?: string ): Promise { try { const params: Stripe.PaymentIntentConfirmParams = {}; if (paymentMethodId) { params.payment_method = paymentMethodId; } return await this.stripe.paymentIntents.confirm(paymentIntentId, params); } catch (error: any) { throw new AppError('Payment confirmation failed', 400); } } /** * Cancela un Payment Intent */ async cancelPaymentIntent(paymentIntentId: string): Promise { try { return await this.stripe.paymentIntents.cancel(paymentIntentId); } catch (error: any) { throw new AppError('Failed to cancel payment', 400); } } // ============ PAYMENT METHODS ============ /** * Adjunta Payment Method a Customer */ async attachPaymentMethod( paymentMethodId: string, customerId: string ): Promise { try { return await this.stripe.paymentMethods.attach(paymentMethodId, { customer: customerId, }); } catch (error: any) { throw new AppError('Failed to attach payment method', 400); } } /** * Desvincula Payment Method */ async detachPaymentMethod(paymentMethodId: string): Promise { try { return await this.stripe.paymentMethods.detach(paymentMethodId); } catch (error: any) { throw new AppError('Failed to detach payment method', 400); } } /** * Lista Payment Methods de un Customer */ async listPaymentMethods( customerId: string, type: 'card' | 'bank_account' = 'card' ): Promise { try { const paymentMethods = await this.stripe.paymentMethods.list({ customer: customerId, type, }); return paymentMethods.data; } catch (error: any) { throw new AppError('Failed to list payment methods', 500); } } /** * Establece Payment Method como default */ async setDefaultPaymentMethod( customerId: string, paymentMethodId: string ): Promise { try { return await this.stripe.customers.update(customerId, { invoice_settings: { default_payment_method: paymentMethodId, }, }); } catch (error: any) { throw new AppError('Failed to set default payment method', 400); } } // ============ SUBSCRIPTIONS ============ /** * Crea una suscripción */ async createSubscription(params: { customer_id: string; price_id: string; trial_days?: number; metadata?: Record; }): Promise { try { const subscriptionParams: Stripe.SubscriptionCreateParams = { customer: params.customer_id, items: [{ price: params.price_id }], metadata: params.metadata || {}, payment_behavior: 'default_incomplete', expand: ['latest_invoice.payment_intent'], }; if (params.trial_days) { subscriptionParams.trial_period_days = params.trial_days; } const subscription = await this.stripe.subscriptions.create(subscriptionParams); logger.info('Subscription created', { subscription_id: subscription.id, customer_id: params.customer_id, }); return subscription; } catch (error: any) { logger.error('Failed to create subscription', { error: error.message }); throw new AppError('Failed to create subscription', 500); } } /** * Actualiza una suscripción */ async updateSubscription( subscriptionId: string, params: Stripe.SubscriptionUpdateParams ): Promise { try { return await this.stripe.subscriptions.update(subscriptionId, params); } catch (error: any) { throw new AppError('Failed to update subscription', 500); } } /** * Cancela una suscripción */ async cancelSubscription( subscriptionId: string, immediate: boolean = false ): Promise { try { if (immediate) { return await this.stripe.subscriptions.cancel(subscriptionId); } else { return await this.stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true, }); } } catch (error: any) { throw new AppError('Failed to cancel subscription', 500); } } /** * Reactiva una suscripción cancelada */ async resumeSubscription(subscriptionId: string): Promise { try { return await this.stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: false, }); } catch (error: any) { throw new AppError('Failed to resume subscription', 500); } } // ============ INVOICES ============ /** * Obtiene una factura */ async getInvoice(invoiceId: string): Promise { try { return await this.stripe.invoices.retrieve(invoiceId); } catch (error: any) { throw new AppError('Invoice not found', 404); } } /** * Lista facturas de un customer */ async listInvoices( customerId: string, limit: number = 10 ): Promise { try { const invoices = await this.stripe.invoices.list({ customer: customerId, limit, }); return invoices.data; } catch (error: any) { throw new AppError('Failed to list invoices', 500); } } /** * Paga una factura manualmente */ async payInvoice(invoiceId: string): Promise { try { return await this.stripe.invoices.pay(invoiceId); } catch (error: any) { throw new AppError('Failed to pay invoice', 400); } } // ============ REFUNDS ============ /** * Crea un reembolso */ async createRefund(params: { payment_intent_id: string; amount?: number; reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'; metadata?: Record; }): Promise { try { const refundParams: Stripe.RefundCreateParams = { payment_intent: params.payment_intent_id, metadata: params.metadata || {}, }; if (params.amount) { refundParams.amount = Math.round(params.amount * 100); } if (params.reason) { refundParams.reason = params.reason; } const refund = await this.stripe.refunds.create(refundParams); logger.info('Refund created', { refund_id: refund.id, payment_intent: params.payment_intent_id, amount: params.amount, }); return refund; } catch (error: any) { logger.error('Failed to create refund', { error: error.message }); throw new AppError('Failed to create refund', 500); } } /** * Obtiene un reembolso */ async getRefund(refundId: string): Promise { try { return await this.stripe.refunds.retrieve(refundId); } catch (error: any) { throw new AppError('Refund not found', 404); } } // ============ PRICES ============ /** * Lista precios de un producto */ async listPrices(productId?: string): Promise { try { const params: Stripe.PriceListParams = { active: true }; if (productId) { params.product = productId; } const prices = await this.stripe.prices.list(params); return prices.data; } catch (error: any) { throw new AppError('Failed to list prices', 500); } } // ============ SETUP INTENTS ============ /** * Crea Setup Intent para guardar payment method */ async createSetupIntent(customerId: string): Promise { try { return await this.stripe.setupIntents.create({ customer: customerId, payment_method_types: ['card'], }); } catch (error: any) { throw new AppError('Failed to create setup intent', 500); } } } ``` --- ## 4. Frontend Integration ### 4.1 Stripe Elements Configuration ```typescript // src/config/stripe.config.ts export const STRIPE_ELEMENTS_OPTIONS = { fonts: [ { cssSrc: 'https://fonts.googleapis.com/css?family=Roboto', }, ], locale: 'en' as const, }; export const CARD_ELEMENT_OPTIONS = { style: { base: { fontSize: '16px', color: '#424770', '::placeholder': { color: '#aab7c4', }, fontFamily: '"Roboto", sans-serif', }, invalid: { color: '#9e2146', iconColor: '#9e2146', }, }, hidePostalCode: false, }; ``` ### 4.2 Payment Form Component ```typescript // src/components/payments/PaymentForm.tsx import React, { useState } from 'react'; import { loadStripe } from '@stripe/stripe-js'; import { Elements, CardElement, useStripe, useElements, } from '@stripe/react-stripe-js'; import { paymentApi } from '../../api/payment.api'; import { CARD_ELEMENT_OPTIONS } from '../../config/stripe.config'; const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY!); interface PaymentFormProps { amount: number; onSuccess: (paymentIntentId: string) => void; onError: (error: string) => void; } const PaymentFormContent: React.FC = ({ amount, onSuccess, onError, }) => { const stripe = useStripe(); const elements = useElements(); const [loading, setLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!stripe || !elements) return; setLoading(true); 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 Payment Intent en backend const response = await paymentApi.createPaymentIntent({ amount, payment_method_id: paymentMethod.id, }); const { client_secret } = response.data.payment_intent; // Confirmar pago const { error: confirmError, paymentIntent } = await stripe.confirmCardPayment( client_secret ); if (confirmError) { throw new Error(confirmError.message); } if (paymentIntent?.status === 'succeeded') { onSuccess(paymentIntent.id); } } catch (err: any) { onError(err.message); } finally { setLoading(false); } }; return (
); }; export const PaymentForm: React.FC = (props) => { return ( ); }; ``` ### 4.3 Subscription Form Component ```typescript // src/components/payments/SubscriptionForm.tsx import React, { useState } from 'react'; import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; import { subscriptionApi } from '../../api/subscription.api'; interface SubscriptionFormProps { priceId: string; planName: string; onSuccess: () => void; } export const SubscriptionForm: React.FC = ({ priceId, planName, onSuccess, }) => { const stripe = useStripe(); const elements = useElements(); const [loading, setLoading] = useState(false); const [error, setError] = useState(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 suscripción en backend const response = await subscriptionApi.create({ price_id: priceId, payment_method_id: paymentMethod.id, }); const { subscription, client_secret } = response.data; // Si requiere confirmación if (client_secret) { const { error: confirmError } = await stripe.confirmCardPayment(client_secret); if (confirmError) { throw new Error(confirmError.message); } } onSuccess(); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; return (

Subscribe to {planName}

{error &&
{error}
} ); }; ``` --- ## 5. Configuración ### 5.1 Variables de Entorno ```bash # Backend STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_API_VERSION=2024-11-20.acacia # Frontend REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_... ``` --- ## 6. Testing ### 6.1 Test con Stripe Test Mode ```typescript // tests/stripe/payment-intent.test.ts import { StripeService } from '../../src/services/stripe/stripe.service'; describe('Stripe Payment Intents', () => { let stripeService: StripeService; beforeAll(() => { stripeService = new StripeService(); }); it('should create payment intent', async () => { const paymentIntent = await stripeService.createPaymentIntent({ amount: 100.00, metadata: { test: 'true' }, }); expect(paymentIntent.amount).toBe(10000); // 100 * 100 centavos expect(paymentIntent.currency).toBe('usd'); }); it('should confirm payment intent with test card', async () => { // Usar tarjeta de prueba: 4242424242424242 // Implementar lógica de confirmación }); }); ``` --- ## 7. Referencias - [Stripe API Documentation](https://stripe.com/docs/api) - [Stripe React Elements](https://stripe.com/docs/stripe-js/react) - [Payment Intents Guide](https://stripe.com/docs/payments/payment-intents) - [Subscriptions Guide](https://stripe.com/docs/billing/subscriptions/overview)