Módulo 6 · Procesamiento asíncrono y mensajería

Lección 32 — Consistencia eventual y saga

El pago, la reserva y el correo: tres sistemas, una historia coherente.

Publicada
En esta lección
  1. Ejercicio 1 — El mapa fuerte/eventual
  2. Ejercicio 2 — La tabla PurchaseSaga
  3. Ejercicio 3 — El UNKNOWN
  4. Ejercicio 4 — Las compensaciones
  5. Ejercicio 5 — Coreografía periférica
  6. Entrega

La saga de compra de TicketFlow. Sin solutions.md hasta entregar.

Ejercicio 1 — El mapa fuerte/eventual

  1. Clasifica TODAS las operaciones del checkout: disponibilidad, reserva+asientos, cobro, email de confirmación, webhooks salientes, dashboard de ventas, informe RGPD (23). Fuerte o eventual, y la garantía de convergencia de las eventuales.
  2. Busca en tu código el pecado contrario: ¿algún email/cobro en el request crítico que deba moverse, o alguna "consulta eventual" que el usuario espera? Documenta el finding.

Ejercicio 2 — La tabla PurchaseSaga

  1. Crea el modelo PurchaseSaga(saga_id UUID, reserva FK, intent_id, estado, intentos, created_at, updated_at) con la máquina de estados (00b) como TextChoices y transiciones validadas en el modelo.
  2. Escribe el orquestador iniciar_compra(user, event, seats): TX local (reserva) + fila saga PENDING + comando de cobro con on_commit (29). Test: el pago falla ANTES de encolarse si la TX de reserva hace rollback.
  3. Test de reanudación: mata el "proceso" (simula excepción tras el cobro) y relanza reanudar_sagas() — la fila PENDING con intent_id se retoma, no se duplica el cobro (idempotencia de la pasarela fake, 14).

Ejercicio 3 — El UNKNOWN

  1. Implementa el manejo del timeout: except GatewayTimeout → Outcome.UNKNOWN, con reconciliar(intent_id) que hace GET /charges/{intent} a la pasarela fake. Tres casos en test: cobró (→ CONFIRMED), declinó (→ compensar), sigue UNKNOWN (→ reintento con backoff).
  2. Define los plazos: máximo de reintentos de reconciliación y plazo de compensación. ¿Qué pasa al expirar el plazo con la saga aún UNKNOWN? Escríbelo como runbook de 4 pasos (el estado final es humano, no automático).
  3. Rompe a propósito: compensa en ciego ante un timeout SIN reconciliar y demostrando con el fake que el cliente paga dos veces (cargo + reembolso de un cargo que existió... con el asiento liberado). Pega el test rojo como advertencia.

Ejercicio 4 — Las compensaciones

  1. Implementa compensar(saga): reembolso (ledger con entrada inversa, 08) + liberar asientos + ReservationCancelled al outbox + email. Idempotente: dos llamadas → un reembolso.
  2. Test del orden inverso: saga con 3 pasos ejecutados (cobro, reserva, email) — ¿en qué orden compensa tu código y por qué? Escríbelo como assertion del test (el email fake NO se "desenvía": documenta qué pasa con los efectos no compensables).
  3. El job diario (31): perseguir_compensaciones_pendientes() — las compensation_pending con más de 24 h → alerta. Test con FakeClock.

Ejercicio 5 — Coreografía periférica

  1. Los consumidores del stream (30) que reaccionan a PaymentSucceeded: emails, analytics. Escribe el consumidor de analytics que construye "ventas por día" SIN tocar el flujo de compra. ¿Qué pasa si analytics está caído 2 h? (respuesta: converge por PEL/reclamo — verifícalo).
  2. Escribe el test de la historia completa (la E2E de la 35 adelantada): compra feliz → CONFIRMED + 1 email + 1 evento webhook; compra con declinación → compensada + email de fallo; compra con timeout-cobró → reconciliada a CONFIRMED.

Entrega

Pega el mapa, el modelo saga, los tres casos del UNKNOWN y el test E2E de la saga. Después: Lección 33 — Pruebas unitarias bien hechas (módulo 7).