TicketFlow's purchase saga. No solutions.md before submitting.
Exercise 1 — The strong/eventual map
- Classify ALL the checkout's operations: availability, reservation+seats, charge, confirmation email, outgoing webhooks, sales dashboard, GDPR report (23). Strong or eventual, and the convergence guarantee of the eventual ones.
- Look in your code for the opposite sin: any email/charge in the critical request that should move, or any "eventual query" the user waits for? Document the finding.
Exercise 2 — The PurchaseSaga table
- Create the
PurchaseSaga(saga_id UUID, reserva FK, intent_id, estado, intentos, created_at, updated_at)model with the state machine (00b) asTextChoicesand transitions validated in the model. - Write the orchestrator
iniciar_compra(user, event, seats): local TX (reservation) + PENDING saga row + charge command with on_commit (29). Test: the payment fails BEFORE enqueueing if the reservation TX rolls back. - Resume test: kill the "process" (simulate an exception after the charge) and relaunch
reanudar_sagas()— the PENDING row with intent_id is picked up, the charge isn't duplicated (the fake gateway's idempotency, 14).
Exercise 3 — The UNKNOWN
- Implement the timeout handling:
except GatewayTimeout → Outcome.UNKNOWN, withreconciliar(intent_id)doingGET /charges/{intent}against the fake gateway. Three cases in tests: charged (→ CONFIRMED), declined (→ compensate), still UNKNOWN (→ retry with backoff). - Define the deadlines: maximum reconciliation retries and compensation deadline. What happens when the deadline expires with the saga still UNKNOWN? Write it as a 4-step runbook (the final state is human, not automatic).
- Break it on purpose: compensate blind on a timeout WITHOUT reconciling and demonstrate with the fake that the customer pays twice (charge + refund of a charge that existed... with the seat freed). Paste the red test as a warning.
Exercise 4 — The compensations
- Implement
compensar(saga): refund (ledger with the inverse entry, 08) + free seats +ReservationCancelledto the outbox + email. Idempotent: two calls → one refund. - The reverse-order test: a saga with 3 executed steps (charge, reservation, email) — in which order does your code compensate and why? Write it as a test assertion (the fake email is NOT "un-sent": document what happens with non-compensable effects).
- The daily job (31):
perseguir_compensaciones_pendientes()—compensation_pendingolder than 24 h → alert. Test with FakeClock.
Exercise 5 — Peripheral choreography
- The stream consumers (30) reacting to
PaymentSucceeded: emails, analytics. Write the analytics consumer that builds "sales per day" WITHOUT touching the purchase flow. What happens if analytics is down for 2 h? (answer: it converges via PEL/claim — verify it). - Write the full story's test (35's E2E ahead of time): happy purchase → CONFIRMED + 1 email + 1 webhook event; declined purchase → compensated + failure email; timeout-but-charged purchase → reconciled to CONFIRMED.
Submit
Paste the map, the saga model, the UNKNOWN's three cases and the saga's E2E test. Next: Lesson 33 — Well-made unit tests (module 7).