# 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.orbiquant.com/checkout/success?session_id={CHECKOUT_SESSION_ID}",
  "cancelUrl": "https://app.orbiquant.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.orbiquant.com
```

### Stripe Checkout URLs

```typescript
// success_url
https://app.orbiquant.com/checkout/success?session_id={CHECKOUT_SESSION_ID}

// cancel_url
https://app.orbiquant.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)
