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

17 KiB

id title type status priority epic project version created_date updated_date
RF-PAY-005 Sistema de Webhooks de Stripe Requirement Done Alta OQI-005 trading-platform 1.0.0 2025-12-05 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


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:

@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:

// 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:

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

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

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

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:

@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
  1. 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

# 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

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

# 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

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

# 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

Historias de Usuario Relacionadas