Skip to main content

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.rb
  • tw-integration/app/services/treatwell/booking_service.rb
  • tw-integration/app/entities/treatwell/base_booking.rb
  • uala-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​

StepRiskSeverity
No idempotency checkDuplicate bookings on Treatwell retryP0
No slot lock during validationDouble-booking same slotP0
Sequential save! callsOrphaned :requested appointments on partial failureP0
Stale availability matrixSlot appears free when already bookedP1
Customer find-then-createDuplicate customer recordsP1
TransactionCode UNIQUE collisionRollback after saves, orphaned appointmentsP2
Jobs enqueued at commit timeSilent notification loss if Sidekiq unavailableP2