01 — Booking Creation Flow
Overview
A booking creation request arrives from Treatwell's mp-booking service and must be converted
into one or more Appointment records in :booked state, linked to a TransactionCode,
optionally to an AppointmentMarketplaceDetail, and to an AppointmentRequest.
Key files:
tw-integration/app/controllers/bookings_controller.rbtw-integration/app/services/treatwell/booking_service.rbtw-integration/app/entities/treatwell/base_booking.rbuala-main/app/services/availabilities/finder_over_matrices_service.rb
Sequence Diagram
Step-by-step narrative
1. Parameter ingestion (Controller)
BookingsController#create permits and snake_cases the incoming JSON payload. No idempotency
key is checked at this stage — a network retry from Treatwell will create a second booking.
2. Order validation
Order.valid? checks currency (EUR/GBP/CHF only), payment method, and reference format.
Failure rolls back cleanly with no side effects.
3. Slot resolution (BaseBooking._slots)
For each booking item, FinderOverMatricesService reads the precomputed bitmask matrix
(VenueAvailabilityDynamicContent) for the requested venue and date.
It decodes the bitmask, finds the position matching exact_start_time, and returns the slot.
The matrix is a background-job artefact — it is not updated on booking creation. See 03 — Availability Management for full details.
4. Customer resolution (BaseBooking._retrieve_customer!)
Looks up a customer by (phone, email) within the venue scope. If not found, creates one.
The find-then-create pattern has a race window: two simultaneous requests with the same
customer data can both find zero results and both issue create!.
5. In-memory availability validation
_validate_availabilities calls has_room_for? and has_enough_space? on the
StaffMember and Workstation objects respectively. These checks operate on in-memory
availability vectors derived from the same precomputed matrix. No database row is locked.
At 2 events/second, two requests for the same slot at the same venue can both pass this check with no error.
6. Sequential saves
# booking_service.rb:77
self.appointments.each(&:save!)
Appointments are saved one by one inside the outer Appointment.transaction block.
If save! raises for the N-th appointment, the first N-1 are already written to the
database. The rescue ActiveRecord::Rollback at the outer level should roll these back,
but this relies on Rails transaction semantics operating correctly with no nested saves
or callbacks that open sub-transactions.
7. TransactionCode
TransactionCode.for_appointments! creates the link between the Treatwell order reference
and the saved appointment IDs. A UNIQUE constraint on code provides a last-resort
collision guard, but a collision here causes a rollback after the appointments are already
saved (step 6), resulting in orphaned :requested appointments.
8. State transition to :booked
Appointment.multiple_book_by_customer! triggers the state machine, which fires
after_commit hooks that enqueue Sidekiq jobs. Jobs are enqueued after commit —
if the commit fails, no jobs are enqueued (correct). However, if Sidekiq is unavailable
at commit time, the jobs are lost with no retry at the enqueue level.
9. Marketing promotions
_use_marketing_promotions runs at the end of the transaction. Any exception here is
not rescued within the transaction block, so it would propagate and roll back the
entire booking. This is actually the correct behaviour, but it means a promotion
misconfiguration can block valid bookings.
Risk summary for this flow
| Step | Risk | Severity |
|---|---|---|
| No idempotency check | Duplicate bookings on Treatwell retry | P0 |
| No slot lock during validation | Double-booking same slot | P0 |
Sequential save! calls | Orphaned :requested appointments on partial failure | P0 |
| Stale availability matrix | Slot appears free when already booked | P1 |
| Customer find-then-create | Duplicate customer records | P1 |
TransactionCode UNIQUE collision | Rollback after saves, orphaned appointments | P2 |
| Jobs enqueued at commit time | Silent notification loss if Sidekiq unavailable | P2 |