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>
504 lines
17 KiB
Markdown
504 lines
17 KiB
Markdown
---
|
|
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)
|