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>
17 KiB
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:
- Exponer endpoint público:
POST /api/v1/payments/webhook - NO requerir autenticación JWT
- Validar firma de Stripe usando
STRIPE_WEBHOOK_SECRET - Parsear evento con
stripe.webhooks.constructEvent() - Responder
200 OKinmediatamente (< 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:
- Verificar
stripe-signatureheader - Usar secret específico del webhook endpoint
- Rechazar eventos sin firma válida (400)
- 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:
- Responder
200 OKantes de procesar evento - Agregar evento a cola (Bull/BullMQ)
- Procesar en background worker
- Retry automático con backoff exponencial (3 intentos)
- 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:
- Guardar
event.iden tablawebhook_events - Verificar si evento ya fue procesado
- Skip procesamiento si
processed = true - 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:
- Registrar todos los eventos recibidos en
webhook_events - Incluir payload completo del evento
- Timestamp de recepción y procesamiento
- Estado de procesamiento (pending, success, failed)
- 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
createdllega antes deupdated
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
- Ir a Developers → Webhooks
- Click "Add endpoint"
- URL:
https://api.trading.com/payments/webhook - 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
- 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/webhookrecibe 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