trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-005-webhooks.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

566 lines
17 KiB
Markdown

---
id: "RF-PAY-005"
title: "Sistema de Webhooks de Stripe"
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-005: Sistema de Webhooks de Stripe
**Version:** 1.0.0
**Fecha:** 2025-12-05
**Estado:** ✅ Implementado
**Prioridad:** P0 (Crítica)
**Story Points:** 5
**Épica:** [OQI-005](../_MAP.md)
---
## Descripción
El sistema debe recibir, validar y procesar webhooks de Stripe para mantener sincronizado el estado de pagos, suscripciones y eventos críticos, garantizando integridad de datos y acciones automatizadas.
---
## Objetivo de Negocio
- Mantener estado de pagos sincronizado en tiempo real
- Automatizar acciones post-pago (otorgar acceso, enviar emails)
- Detectar fraudes y chargebacks inmediatamente
- Manejar fallos de renovación de suscripciones
- Cumplir con flujo asíncrono de Stripe
---
## Webhooks Implementados
### Webhooks de Payment Intent
| Evento | Prioridad | Acción |
|--------|-----------|--------|
| `payment_intent.succeeded` | P0 | Actualizar pago a succeeded, otorgar acceso a recurso |
| `payment_intent.payment_failed` | P0 | Actualizar pago a failed, enviar email de error |
| `payment_intent.canceled` | P1 | Actualizar pago a canceled |
| `payment_intent.requires_action` | P1 | Log para análisis (3DS pendiente) |
### Webhooks de Subscription
| Evento | Prioridad | Acción |
|--------|-----------|--------|
| `customer.subscription.created` | P0 | Crear registro en BD, enviar email de bienvenida |
| `customer.subscription.updated` | P0 | Sincronizar plan, status, period_end |
| `customer.subscription.deleted` | P0 | Marcar como canceled, revocar acceso premium |
| `customer.subscription.trial_will_end` | P1 | Enviar email recordatorio 3 días antes |
### Webhooks de Invoice
| Evento | Prioridad | Acción |
|--------|-----------|--------|
| `invoice.payment_succeeded` | P0 | Generar factura PDF, crear Payment record |
| `invoice.payment_failed` | P0 | Actualizar suscripción a past_due, enviar email de fallo |
| `invoice.finalized` | P1 | Preparar factura antes de cobro |
| `invoice.updated` | P2 | Sincronizar cambios de invoice |
### Webhooks de Chargebacks/Disputas
| Evento | Prioridad | Acción |
|--------|-----------|--------|
| `charge.dispute.created` | P0 | Alertar admin, marcar pago en disputa |
| `charge.dispute.closed` | P0 | Actualizar estado de disputa (won/lost) |
| `charge.refunded` | P0 | Crear nota de crédito, actualizar Payment |
### Webhooks de Customer
| Evento | Prioridad | Acción |
|--------|-----------|--------|
| `customer.updated` | P2 | Sincronizar datos de customer |
| `customer.deleted` | P2 | Log para auditoría |
---
## Requisitos Funcionales
### RF-PAY-005.1: Endpoint de Webhook
**DEBE:**
1. Exponer endpoint público: `POST /api/v1/payments/webhook`
2. **NO requerir** autenticación JWT
3. Validar firma de Stripe usando `STRIPE_WEBHOOK_SECRET`
4. Parsear evento con `stripe.webhooks.constructEvent()`
5. Responder `200 OK` inmediatamente (< 5s)
**Implementación:**
```typescript
@Post('webhook')
@HttpCode(200)
async handleWebhook(@Req() req: Request) {
const sig = req.headers['stripe-signature'];
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
throw new BadRequestException(`Webhook signature verification failed`);
}
// Procesar evento de forma asíncrona
await this.webhookQueue.add('process-webhook', { event });
return { received: true };
}
```
### RF-PAY-005.2: Validación de Firma
**DEBE:**
1. Verificar `stripe-signature` header
2. Usar secret específico del webhook endpoint
3. Rechazar eventos sin firma válida (400)
4. Prevenir replay attacks (timestamp tolerance 5 min)
**Seguridad:**
```typescript
// Stripe valida automáticamente:
// 1. Firma HMAC-SHA256 correcta
// 2. Timestamp no más antiguo de 5 minutos
// 3. Payload no modificado
```
### RF-PAY-005.3: Procesamiento Asíncrono
**DEBE:**
1. Responder `200 OK` antes de procesar evento
2. Agregar evento a cola (Bull/BullMQ)
3. Procesar en background worker
4. Retry automático con backoff exponencial (3 intentos)
5. Dead Letter Queue (DLQ) para eventos fallidos
**Flujo:**
```
Webhook → Validar Firma → Responder 200 → Queue → Worker → Procesar
Retry (si falla)
DLQ (después de 3 intentos)
```
### RF-PAY-005.4: Idempotencia
**DEBE:**
1. Guardar `event.id` en tabla `webhook_events`
2. Verificar si evento ya fue procesado
3. Skip procesamiento si `processed = true`
4. Marcar como procesado después de completar
**Implementación:**
```typescript
async processWebhook(event: Stripe.Event) {
const existing = await db.webhookEvent.findOne({ stripeEventId: event.id });
if (existing?.processed) {
logger.info(`Event ${event.id} already processed, skipping`);
return;
}
// Procesar evento...
await db.webhookEvent.upsert({
stripeEventId: event.id,
type: event.type,
processed: true,
processedAt: new Date(),
});
}
```
### RF-PAY-005.5: Manejo de Eventos Específicos
#### `payment_intent.succeeded`
```typescript
async handlePaymentIntentSucceeded(paymentIntent: Stripe.PaymentIntent) {
// 1. Actualizar Payment en BD
await db.payment.update(
{ stripePaymentIntentId: paymentIntent.id },
{ status: 'succeeded', updatedAt: new Date() }
);
// 2. Otorgar acceso al recurso
if (paymentIntent.metadata.type === 'course_purchase') {
await this.courseService.grantAccess(
paymentIntent.metadata.userId,
paymentIntent.metadata.courseId
);
}
// 3. Generar factura
await this.invoiceService.generate(paymentIntent.id);
// 4. Enviar email de confirmación
await this.emailService.sendPaymentConfirmation(paymentIntent);
}
```
#### `customer.subscription.updated`
```typescript
async handleSubscriptionUpdated(subscription: Stripe.Subscription) {
await db.subscription.update(
{ stripeSubscriptionId: subscription.id },
{
status: subscription.status,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
}
);
// Si cambió a canceled, revocar acceso
if (subscription.status === 'canceled') {
await this.subscriptionService.revokeAccess(subscription.metadata.userId);
}
}
```
#### `invoice.payment_failed`
```typescript
async handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
// 1. Actualizar suscripción a past_due
await db.subscription.update(
{ stripeSubscriptionId: invoice.subscription },
{ status: 'past_due' }
);
// 2. Enviar email urgente
await this.emailService.sendPaymentFailedAlert(invoice);
// 3. Alertar admin si es cliente high-value
if (invoice.amount_due > 10000) { // $100 USD
await this.slackService.notifyAdmin(`Payment failed for high-value customer: ${invoice.customer_email}`);
}
}
```
### RF-PAY-005.6: Logging y Auditoría
**DEBE:**
1. Registrar todos los eventos recibidos en `webhook_events`
2. Incluir payload completo del evento
3. Timestamp de recepción y procesamiento
4. Estado de procesamiento (pending, success, failed)
5. Error message si falló
**Modelo:**
```typescript
@Entity({ name: 'webhook_events', schema: 'billing' })
class WebhookEvent {
id: string; // UUID
stripeEventId: string; // evt_xxx (UNIQUE)
type: string; // payment_intent.succeeded
payload: object; // JSON completo del evento
processed: boolean; // true si se procesó exitosamente
processedAt?: Date;
attempts: number; // Número de intentos de procesamiento
lastError?: string; // Último error si falló
receivedAt: Date;
createdAt: Date;
}
```
---
## Flujo de Webhook
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Stripe │ │ Webhook │ │ Queue │ │ Worker │
│ │ │ Endpoint │ │ │ │ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │
│ POST /webhook │ │ │
│ { event } │ │ │
│──────────────────▶│ │ │
│ │ │ │
│ │ 1. Validate │ │
│ │ signature │ │
│ │ │ │
│ │ 2. Parse event │ │
│ │ │ │
│ │ │ 3. Add to queue │
│ │──────────────────▶│ │
│ │ │ │
│◀──────────────────│ │ │
│ 200 OK │ │ │
│ { received: true }│ │ │
│ │ │ │
│ │ │ 4. Dequeue event │
│ │ │──────────────────▶│
│ │ │ │
│ │ │ │ 5. Check
│ │ │ │ idempotency
│ │ │ │
│ │ │ │ 6. Process
│ │ │ │ event
│ │ │ │
│ │ │ │ 7. Update DB
│ │ │ │
│ │ │◀──────────────────│
│ │ │ Done │
│ │ │ │
```
---
## Reglas de Negocio
### RN-001: Orden de Eventos
Stripe **NO garantiza** orden de eventos. Sistema DEBE:
- Usar timestamps de Stripe para ordenar
- Manejar eventos fuera de orden
- No asumir que `created` llega antes de `updated`
### RN-002: Retry de Stripe
Stripe reintenta webhooks automáticamente:
- Cada 1h durante 3 días si respuesta != 200
- Sistema debe ser idempotente (procesar mismo evento N veces sin problema)
### RN-003: Testing de Webhooks
- Usar Stripe CLI para testing local: `stripe listen --forward-to localhost:3000/payments/webhook`
- Usar eventos de test en staging
- Nunca usar producción para testing
---
## Configuración de Webhooks en Stripe
### Dashboard de Stripe
1. Ir a **Developers → Webhooks**
2. Click "Add endpoint"
3. URL: `https://api.trading.com/payments/webhook`
4. Seleccionar eventos:
**Eventos esenciales (P0):**
```
payment_intent.succeeded
payment_intent.payment_failed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.payment_succeeded
invoice.payment_failed
charge.dispute.created
```
**Eventos opcionales (P1-P2):**
```
customer.subscription.trial_will_end
invoice.finalized
charge.refunded
customer.updated
```
5. Copiar **Signing secret:** `whsec_...`
---
## Seguridad
### Validación Obligatoria
- **SIEMPRE** validar firma de Stripe
- **NUNCA** confiar en payload sin validar
- Rechazar eventos con timestamp > 5 min (prevenir replay)
### Rate Limiting
- Stripe puede enviar múltiples eventos simultáneos
- No aplicar rate limiting al endpoint de webhook
- Aplicar rate limiting en el worker si es necesario
### Secrets Management
```env
# Desarrollo
STRIPE_WEBHOOK_SECRET=whsec_test_xxx
# Producción
STRIPE_WEBHOOK_SECRET=whsec_xxx
# NUNCA commitear secrets al repo
```
---
## Monitoreo y Alertas
### Métricas a Rastrear
- Webhooks recibidos/min
- Tasa de procesamiento exitoso (%)
- Eventos en DLQ
- Latencia promedio de procesamiento
- Eventos duplicados detectados (idempotencia)
### Alertas Críticas
| Condición | Alerta | Canal |
|-----------|--------|-------|
| > 10 eventos en DLQ | High | Slack + Email |
| Tasa de fallo > 5% | Medium | Slack |
| Latencia > 30s | Low | Logs |
| Disputa de pago | High | Slack + SMS |
---
## Manejo de Errores
### Errores Recuperables (Retry)
- Timeout de DB
- Error de red temporal
- Servicio externo no disponible
**Estrategia:** Retry con backoff exponencial (1s, 5s, 25s)
### Errores No Recuperables (DLQ)
- Evento malformado
- Usuario no existe
- Pago ya procesado (idempotencia)
- Validación de negocio falla
**Estrategia:** Mover a DLQ, alertar admin, investigar manualmente
### Logging de Errores
```typescript
try {
await this.processEvent(event);
} catch (error) {
logger.error('Webhook processing failed', {
eventId: event.id,
eventType: event.type,
error: error.message,
stack: error.stack,
});
await db.webhookEvent.update(
{ stripeEventId: event.id },
{
lastError: error.message,
attempts: db.raw('attempts + 1'),
}
);
throw error; // Para que Bull/BullMQ reintente
}
```
---
## Testing
### Stripe CLI
```bash
# Escuchar webhooks localmente
stripe listen --forward-to localhost:3000/api/v1/payments/webhook
# Trigger evento específico
stripe trigger payment_intent.succeeded
# Trigger con metadata
stripe trigger payment_intent.succeeded \
--add payment_intent:metadata.userId=user_123 \
--add payment_intent:metadata.type=course_purchase
```
### Tests de Integración
```typescript
describe('Webhook Handler', () => {
it('should process payment_intent.succeeded', async () => {
const event = stripe.webhooks.constructEvent(
mockPayload,
mockSignature,
WEBHOOK_SECRET
);
await webhookHandler.handle(event);
const payment = await db.payment.findOne({ ... });
expect(payment.status).toBe('succeeded');
});
it('should be idempotent', async () => {
await webhookHandler.handle(event);
await webhookHandler.handle(event); // Procesar 2 veces
const events = await db.webhookEvent.find({ stripeEventId: event.id });
expect(events).toHaveLength(1); // Solo 1 registro
});
});
```
---
## Configuración Requerida
```env
# Stripe Webhook
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Queue (Redis)
REDIS_HOST=localhost
REDIS_PORT=6379
WEBHOOK_QUEUE_NAME=stripe-webhooks
# Retry Config
WEBHOOK_MAX_ATTEMPTS=3
WEBHOOK_RETRY_DELAY=1000 # ms
WEBHOOK_RETRY_BACKOFF=exponential
```
---
## Criterios de Aceptación
- [ ] Endpoint `/payments/webhook` recibe eventos de Stripe
- [ ] Firma de Stripe se valida correctamente
- [ ] Eventos inválidos son rechazados con 400
- [ ] Endpoint responde 200 en < 5 segundos
- [ ] Eventos se procesan de forma asíncrona en cola
- [ ] Sistema es idempotente (mismos eventos no duplican acciones)
- [ ] payment_intent.succeeded otorga acceso a recurso
- [ ] customer.subscription.updated sincroniza estado en BD
- [ ] invoice.payment_failed envía email de alerta
- [ ] Eventos fallidos se mueven a DLQ después de 3 intentos
- [ ] Todos los eventos se registran en `webhook_events`
- [ ] Alertas se disparan para eventos críticos
---
## Especificación Técnica Relacionada
- [ET-PAY-005: Webhook Processing](../especificaciones/ET-PAY-005-webhooks.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)