--- id: "RF-PAY-002" title: "Checkout con Stripe Elements" type: "Requirement" status: "Done" priority: "Alta" epic: "OQI-005" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # RF-PAY-002: Checkout con Stripe Elements **Version:** 1.0.0 **Fecha:** 2025-12-05 **Estado:** ✅ Implementado **Prioridad:** P0 (Crítica) **Story Points:** 8 **Épica:** [OQI-005](../_MAP.md) --- ## Descripción El sistema debe proporcionar interfaces de checkout seguras y optimizadas usando Stripe Elements y Stripe Checkout para procesar pagos únicos y suscripciones, cumpliendo con estándares PCI-DSS sin manejar datos de tarjeta directamente. --- ## Objetivo de Negocio - Maximizar conversión con UX optimizada de checkout - Eliminar responsabilidad PCI-DSS - Reducir fraude con validaciones nativas de Stripe - Soportar múltiples métodos de pago (tarjetas, wallets) - Optimizar para mobile y desktop --- ## Modos de Checkout ### Modo 1: Embedded Checkout (Stripe Elements) **Uso:** Compras de cursos, pagos únicos dentro de la app **Ventajas:** - Control total de UX - Integración nativa en SPA - Customización de estilos - Mejor para flujos complejos **Componentes:** ```javascript - CardNumberElement - CardExpiryElement - CardCvcElement - PaymentElement (todo-en-uno) ``` ### Modo 2: Hosted Checkout (Stripe Checkout) **Uso:** Suscripciones, compras rápidas **Ventajas:** - Implementación rápida - Optimización automática de conversión - Soporte automático de wallets (Apple Pay, Google Pay) - Reducción de fricción --- ## Requisitos Funcionales ### RF-PAY-002.1: Inicialización de Stripe Elements **DEBE:** 1. Cargar Stripe.js desde CDN (`https://js.stripe.com/v3/`) 2. Inicializar con Publishable Key 3. Crear instancia de Elements con opciones de estilo 4. Montar elementos en contenedores DOM específicos 5. Aplicar tema según modo claro/oscuro de la app **Ejemplo de configuración:** ```typescript const stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY); const elements = stripe.elements({ appearance: { theme: 'stripe', variables: { colorPrimary: '#0066ff', colorBackground: '#ffffff', colorText: '#1a1a1a', colorDanger: '#df1b41', fontFamily: 'Inter, system-ui, sans-serif', spacingUnit: '4px', borderRadius: '8px', } }, clientSecret: paymentIntent.clientSecret }); ``` ### RF-PAY-002.2: Creación de Payment Intent **Backend DEBE:** 1. Recibir request con `amount`, `currency`, `type`, `metadata` 2. Validar monto mínimo ($0.50 USD) 3. Crear PaymentIntent en Stripe 4. Guardar registro en `billing.payments` (status: `pending`) 5. Retornar `clientSecret` al frontend **Request:** ```json POST /api/v1/payments/create-payment-intent { "amount": 4900, "currency": "usd", "type": "course_purchase", "courseId": "uuid-curso", "description": "Curso de Trading Avanzado" } ``` **Response:** ```json { "clientSecret": "pi_3Abc123_secret_xyz", "paymentIntentId": "pi_3Abc123", "amount": 4900, "currency": "usd" } ``` ### RF-PAY-002.3: Procesamiento de Pago **Frontend DEBE:** 1. Recopilar información de tarjeta (via Stripe Elements) 2. Validar campos antes de submit 3. Llamar `stripe.confirmCardPayment(clientSecret, { payment_method })` 4. Manejar 3D Secure (SCA) automáticamente 5. Mostrar estados de loading/success/error **Flujo de confirmación:** ```javascript const { error, paymentIntent } = await stripe.confirmCardPayment( clientSecret, { payment_method: { card: cardElement, billing_details: { name: userName, email: userEmail, } } } ); if (error) { // Mostrar error } else if (paymentIntent.status === 'succeeded') { // Pago exitoso } ``` ### RF-PAY-002.4: Hosted Checkout Session **Backend DEBE:** 1. Crear Checkout Session en Stripe 2. Configurar `success_url` y `cancel_url` 3. Incluir line items con productos 4. Configurar modo (`payment` o `subscription`) 5. Retornar URL de checkout **Request:** ```json POST /api/v1/payments/create-checkout-session { "priceId": "price_1Sb3k64dPtEGmLmpm5n5bbJH", "mode": "subscription", "successUrl": "https://app.trading.com/checkout/success?session_id={CHECKOUT_SESSION_ID}", "cancelUrl": "https://app.trading.com/pricing" } ``` **Response:** ```json { "url": "https://checkout.stripe.com/c/pay/cs_test_abc123...", "sessionId": "cs_test_abc123" } ``` ### RF-PAY-002.5: Validación de Formulario **DEBE validar:** - Número de tarjeta completo y válido (Luhn algorithm) - Fecha de expiración futura - CVC de 3-4 dígitos - Nombre del titular no vacío - Email válido **Feedback en tiempo real:** - Indicador visual de validez de campo - Detección automática de marca de tarjeta (Visa, Mastercard, etc.) - Mensajes de error específicos ### RF-PAY-002.6: Manejo de 3D Secure (SCA) **DEBE:** 1. Detectar automáticamente si tarjeta requiere SCA 2. Mostrar modal/iframe de autenticación del banco 3. Esperar confirmación del usuario 4. Continuar procesamiento si autenticación exitosa 5. Rechazar pago si autenticación falla --- ## Flujo Completo de Checkout ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Usuario │ │ Frontend │ │ Backend │ │ Stripe │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ Inicia compra │ │ │ │──────────────────▶│ │ │ │ │ │ │ │ │ POST /create- │ │ │ │ payment-intent │ │ │ │──────────────────▶│ │ │ │ │ │ │ │ │ Create │ │ │ │ PaymentIntent │ │ │ │──────────────────▶│ │ │ │◀──────────────────│ │ │ │ clientSecret │ │ │ │ │ │ │ │ Save to DB │ │ │ │ (pending) │ │ │ │ │ │ │◀──────────────────│ │ │ │ { clientSecret } │ │ │ │ │ │ │◀──────────────────│ │ │ │ Muestra form │ │ │ │ de tarjeta │ │ │ │ │ │ │ │ Ingresa datos │ │ │ │ de tarjeta │ │ │ │──────────────────▶│ │ │ │ │ │ │ │ Click "Pagar" │ │ │ │──────────────────▶│ │ │ │ │ │ │ │ │ confirmCardPayment│ │ │ │ (clientSecret) │ │ │ │──────────────────────────────────────▶│ │ │ │ │ │ │ │ │ Requiere │◀──────────────────────────────────────────────────────────│ 3DS? │ Modal 3DS │ │ │ │ del banco │ │ │ │ │ │ │ │ Autentica │ │ │ │──────────────────────────────────────────────────────────▶│ │ │ │ │ │ │◀──────────────────────────────────────│ │ │ { paymentIntent: │ │ │ │ status: │ │ │ │ 'succeeded' } │ │ │ │ │ │ │ │ │◀──────────────────│ │ │ │ Webhook: │ │ │ │ payment_intent. │ │ │ │ succeeded │ │ │ │ │ │ │ │ Update DB │ │ │ │ (succeeded) │ │ │ │ Grant access │ │ │ │ │ │◀──────────────────│ │ │ │ "Pago exitoso!" │ │ │ │ │ │ │ ``` --- ## Reglas de Negocio ### RN-001: Montos Mínimos - Pago único: **$0.50 USD mínimo** - Suscripción: según plan ($19, $49, $99) - Bloquear pagos inferiores con mensaje claro ### RN-002: Monedas Soportadas - **USD:** Moneda principal - **MXN, COP, ARS, CLP, PEN:** LATAM (futuro) - Conversión automática con tasas de Stripe ### RN-003: Reintentos de Pago - **No reintentar automáticamente** en frontend - Mostrar error específico al usuario - Permitir editar datos de tarjeta e intentar de nuevo - Backend registra intentos fallidos para análisis ### RN-004: Timeouts - Payment Intent válido por **24 horas** - Checkout Session válido por **24 horas** - Expirar clientSecret después de uso exitoso --- ## Métodos de Pago Soportados | Método | Embedded | Hosted | Región | |--------|----------|--------|--------| | Tarjetas (Visa, MC, Amex) | ✅ | ✅ | Global | | Apple Pay | ⚠️ via PaymentRequest | ✅ | USA, Mx | | Google Pay | ⚠️ via PaymentRequest | ✅ | Global | | OXXO | ❌ | ✅ | México | | Efecty | ❌ | ✅ | Colombia | | PSE | ❌ | ✅ | Colombia | --- ## Estilos y UX ### Customización de Stripe Elements ```typescript const appearance = { theme: 'stripe', // 'stripe' | 'night' | 'flat' variables: { colorPrimary: '#0066ff', colorBackground: '#ffffff', colorText: '#1a1a1a', colorDanger: '#df1b41', fontFamily: 'Inter, system-ui, sans-serif', spacingUnit: '4px', borderRadius: '8px', fontSizeBase: '16px', }, rules: { '.Input': { border: '1px solid #e0e0e0', boxShadow: 'none', }, '.Input:focus': { border: '1px solid #0066ff', boxShadow: '0 0 0 3px rgba(0, 102, 255, 0.1)', }, '.Input--invalid': { border: '1px solid #df1b41', } } }; ``` ### Estados del Formulario | Estado | Indicador Visual | |--------|------------------| | Idle | Campos vacíos, botón habilitado | | Typing | Validación en tiempo real | | Valid | Checkmark verde, botón resaltado | | Invalid | Mensaje de error rojo | | Processing | Spinner, botón deshabilitado | | Success | Checkmark animado, redirect | | Error | Mensaje de error, retry habilitado | --- ## Manejo de Errores ### Errores de Stripe Elements | Error Code | Mensaje Usuario | Acción | |------------|-----------------|--------| | `card_declined` | Tu tarjeta fue rechazada. Intenta con otra. | Permitir cambiar tarjeta | | `insufficient_funds` | Fondos insuficientes. | Sugerir otra tarjeta | | `expired_card` | Tarjeta expirada. Verifica la fecha. | Validar fecha | | `incorrect_cvc` | Código de seguridad incorrecto. | Reintentar CVC | | `processing_error` | Error de procesamiento. Intenta de nuevo. | Retry | | `rate_limit` | Demasiados intentos. Espera un momento. | Backoff 30s | ### Errores de Backend | Error | Código HTTP | Mensaje Usuario | |-------|-------------|-----------------| | Monto inválido | 400 | El monto debe ser mayor a $0.50 USD | | Producto no encontrado | 404 | El curso no existe | | Ya comprado | 409 | Ya tienes acceso a este curso | | Stripe API error | 502 | Error de procesamiento. Contacta soporte. | --- ## Seguridad ### PCI Compliance - **NUNCA** enviar datos de tarjeta a backend - Usar Stripe.js para tokenización - Validar solo en frontend con Stripe Elements - Backend solo maneja tokens `pm_xxx` ### Validación de Origen - Validar `userId` del JWT contra `metadata.userId` del PaymentIntent - Verificar que el usuario no haya comprado ya el producto - Rate limiting: máximo 5 intentos por 15 minutos ### Prevención de Fraude - Stripe Radar activado (detección automática) - Requerir CVC siempre - Habilitar 3D Secure para transacciones > $30 USD - Bloquear IPs con alto índice de rechazo --- ## Configuración Requerida ```env # Frontend (.env.local) VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51Sb3k64dPtEGmLmp... # Backend (.env) STRIPE_SECRET_KEY=sk_test_51Sb3k64dPtEGmLmp... STRIPE_WEBHOOK_SECRET=whsec_... FRONTEND_URL=https://app.trading.com ``` ### Stripe Checkout URLs ```typescript // success_url https://app.trading.com/checkout/success?session_id={CHECKOUT_SESSION_ID} // cancel_url https://app.trading.com/pricing ``` --- ## Webhooks Relacionados | Evento | Acción | |--------|--------| | `payment_intent.succeeded` | Actualizar pago a succeeded, otorgar acceso | | `payment_intent.payment_failed` | Actualizar pago a failed, enviar email | | `payment_intent.canceled` | Actualizar pago a canceled | | `checkout.session.completed` | Confirmar suscripción/compra | | `checkout.session.expired` | Notificar expiración | --- ## Performance ### Optimizaciones - **Lazy load** Stripe.js solo en páginas de checkout - **Prefetch** clientSecret al mostrar producto - **Cache** Price IDs en memoria (Redis) - **Timeout** de 30s para confirmCardPayment ### Métricas a Rastrear - Tiempo de carga de Stripe.js - Tasa de conversión por paso (form → submit → success) - Tasa de rechazo por tipo de error - Tiempo promedio de checkout --- ## Criterios de Aceptación - [ ] Stripe Elements se carga sin errores - [ ] Formulario valida tarjeta en tiempo real - [ ] Marcas de tarjeta se detectan automáticamente - [ ] Payment Intent se crea correctamente desde backend - [ ] 3D Secure funciona para tarjetas que lo requieren - [ ] Errores de Stripe se muestran claramente al usuario - [ ] Hosted Checkout redirige a Stripe correctamente - [ ] Success/cancel URLs funcionan después de Checkout - [ ] Estilos de Elements coinciden con tema de app - [ ] Checkout es responsive en mobile y desktop --- ## Especificación Técnica Relacionada - [ET-PAY-002: Stripe Elements Integration](../especificaciones/ET-PAY-002-stripe-elements.md) ## Historias de Usuario Relacionadas - [US-PAY-002: Suscribirse a Plan](../historias-usuario/US-PAY-002-suscribirse.md) - [US-PAY-005: Comprar Curso](../historias-usuario/US-PAY-005-comprar-curso.md) - [US-PAY-006: Agregar Método de Pago](../historias-usuario/US-PAY-006-agregar-metodo-pago.md)