Repository navigation
feat(payment): pay bookings and settle them through a saga, closing US-005, US-006 and EPIC-03 - #26
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A passenger can now pay a booking, and the outcome travels back through the system until the booking reflects it.
POST /api/v1/paymentson the new payment-service asks booking-service what is owed, charges a deterministic gateway that decides by card number, and records the payment together with an outbox row in a single transaction. A relay carries that row to Kafka aspayment.succeeded.v1orpayment.failed.v1, keyed by booking id. booking-service consumes both topics: a success confirms the booking (US-005), a failure marks itFAILEDand returns the seats to flight-service through a new release endpoint (US-006). Every consumer claims the event id in aprocessed_eventstable before doing any work, so the at-least-once delivery an outbox implies costs nothing on redelivery.Changes
payment-service (new)
Three Maven modules in the ADR-006 shape. The domain holds
Payment,CardNumberwith Luhn validation,Moneythat rejects zero and negative amounts, andPaymentStatus.PayBookingServicereads the amount from booking-service over Feign, because an amount taken from the request body would let anyone pay what they liked. It charges the gateway and hands the result toPaymentRecorder, a separate bean so@UnitOfWorkis not self-invoked.DecliningCardGatewayrefuses4000000000000002and accepts everything else, so either saga path can be shown on purpose.The outbox:
PaymentSettledis published as a domain event, aBEFORE_COMMITlistener writes the row so it joins the payment's transaction, andOutboxRelayclaims pending rows withFOR UPDATE SKIP LOCKED, sends withacks=all, waits for the broker's acknowledgement, and only then marks them published.booking-service
GET /api/v1/bookings/{id}, which is where payment-service learns the amount and the status.One Kafka listener per topic, feeding
ConfirmBookingServiceandFailBookingService. Both claim the event id first throughprocessed_events(INSERT ... ON CONFLICT DO NOTHING), so a redelivered message is a no-op.Failing is two steps that deliberately do not share a transaction: mark the booking
FAILED, then release the seats over HTTP and recordseats_released_at. In that order, because a crash in between leaves seats held on a failed booking, which a sweep can find, and never aPENDINGbooking with no seats.BookingStatusgrowsCONFIRMEDandFAILED; migrations addseats_released_atand theprocessed_eventstable.flight-service
DELETE /api/v1/flights/{flightId}/seat-blocks/{seatBlockId}releases a hold and returns its seats, answering 204 whether or not the block still exists, so the compensation can be retried safely.Outbound ports regrouped by aggregate into
port/out/flightandport/out/seatblock.Infrastructure and docs
Compose gains a single-node Kafka in KRaft mode with two advertised listeners, one for the network and one for the host; both services that use it wait on its health check.
scripts/smoke-payment.ps1walks the whole journey against the running stack;-Declinewalks the failing one.PaymentJourneyE2ETestjoins the e2e module, which now starts seven containers: three databases, the broker and the three services.ARCHITECTURE.mdgains ADR-013 (the payment saga) and ADR-014 (consuming a payment event once), and ADR-001 is amended: only payment owns an outbox today.Notes
Decisions worth reading before the code, argued in full in the ADRs:
Boot 4 and Testcontainers 2 renames that cost time this round:
spring-boot-kafkaspring-kafkaalonespring-kafkaalone putsKafkaTemplateon the classpath with nothing wiring ittestcontainers-kafkakafkaorg.testcontainers.kafka.KafkaContainerKafkaContainerwith@ServiceConnectionin both smoke test configurationsbootstrap-servers@MockitoBean KafkaTemplatein the hand-built outbox sliceLeft out on purpose, and documented in the ADR consequences rather than papered over:
FAILEDwithseats_released_atunset; the partial index the sweep would use exists, the sweep does not.processed_eventsis never purged, and a message naming a booking that does not exist redelivers forever for want of a dead letter topic.PENDING. A read of a booking is a snapshot, not a verdict.Testing
./mvnw -B clean verifyruns 267 tests with zero failures; the two skips are the e2e classes staying behind their flag../mvnw -B verify -pl e2e-tests "-Dairline.e2e=true"runs the 20 e2e tests against the real stack, including both saga journeys: pay and poll untilCONFIRMED, pay with the declining card and poll untilFAILEDwith the seats back on the flight.scripts/smoke-payment.ps1was run both ways against compose: the happy path endsCONFIRMEDwith the hold alive and 118 seats left, and-DeclineendsFAILEDwith 120 seats, zero holds andseats_released_atset.The tests that carry the new guarantees:
PaymentOutboxSliceTestproves the outbox row shares the payment's transaction (a failed save announces nothing) and that the payload carries nothing about the card;OutboxRelayTestproves the relay sends what waits, keys by aggregate, and marks what it sent so a second sweep sends nothing;SettleBookingSliceTestproves confirming, failing with its compensation, and that a second delivery of a claimed event does nothing.Closes #8 (US-005)
Closes #9 (US-006)
Closes #7 (EPIC-03)