Booking lifecycle: create (race-proof), confirm, cancel — plus holds, waitlist, unit auto-assignment, pricing, and the queries that feed pickers and admin lists.
Double-booking protection
Engine.validate_request/5 is pure and advisory. create_booking/4
therefore re-runs it inside a transaction that locks the service row
(FOR UPDATE): concurrent requests serialize, each re-reads the active
occupancy (bookings + unexpired holds) and re-validates, so the last
seat can only be taken once. When the service has a provider, ALL of
that provider's services are locked in uuid order (deadlock-safe), and
the provider's other bookings block the request absolutely.
Lifecycle
pending (services with require_approval — RSVP-style) → confirmed
→ cancelled. Pending bookings HOLD capacity. A cancellation frees
capacity immediately, emails the customer, and notifies open waitlist
entries for the freed dates (notify-all-first-to-book).
Live updates
Every mutation broadcasts {:bookings_changed, service_uuid} on both
service_topic(service_uuid) and admin_topic/0. Minimal payload.
Summary
Functions
Resolves a manage token back to its booking; nil when invalid/expired.
Cancels a pending or confirmed booking; frees capacity immediately.
Whether a CUSTOMER may still self-cancel: active, upcoming, and outside
the service's cancel_notice window (admins cancel unrestricted).
Approves a pending booking (capacity was already held).
Booking counts per status — powers the admin filter tabs.
opts[:service_uuids] restricts to visible services (own-only admins).
Creates a booking of service for the validated range.
Reserves the picked range for 5 minutes while the visitor fills the details form. Advisory (created after the advisory validation passed) — the locked create is still the authority.
Fetches a booking by UUID; nil on miss or forged id.
Joins the waitlist for a date. One open entry per email+date+service — repeat joins are collapsed.
Active (pending + confirmed) bookings of a service that overlap the
given window. window is {from_utc, until_utc} or
{:dates, from, until}, both exclusive-end.
Lists bookings for the admin. Options: :service_uuid,
:service_uuids, :status, :upcoming (from today, site frame),
:limit (default 200).
The full occupancy the validator sees: active bookings PLUS unexpired
holds (mapped to pseudo-bookings), minus opts[:exclude_hold] (the
caller's own hold). This is what pickers and the locked create both
consume, so a held slot greys out everywhere.
Waitlist entries of a service, open first, newest first.
Signed token a guest uses to view/cancel their booking without an account.
Releases a hold (back button, LiveView terminate). Idempotent.
Per-service PubSub topic — public pickers subscribe to exactly one.
Open-entry counts per service — admin visibility.
Functions
Resolves a manage token back to its booking; nil when invalid/expired.
Cancels a pending or confirmed booking; frees capacity immediately.
Whether a CUSTOMER may still self-cancel: active, upcoming, and outside
the service's cancel_notice window (admins cancel unrestricted).
Approves a pending booking (capacity was already held).
Booking counts per status — powers the admin filter tabs.
opts[:service_uuids] restricts to visible services (own-only admins).
Creates a booking of service for the validated range.
range: {starts_at_utc, ends_at_utc} or {:dates, starts_on, ends_on}
— must match the service's time_unit (the engine rejects a mismatch).
customer_attrs: name / email / phone / notes (the only cast fields).
Options: :user_uuid (REQUIRED when signup_policy is
"login_required"), :source ("public" default | "admin"),
:actor_uuid, :hold_uuid (the caller's own hold — excluded from the
capacity check and consumed on success), :now/:today (test
injection).
On success the booking carries the computed total_price/currency
(priced services) and an auto-assigned unit_uuid (unit-tracked
services); the confirmation email + reminder job fire best-effort.
Returns {:ok, booking}, {:error, %Ecto.Changeset{}} or
{:error, reason_atom, message}.
Reserves the picked range for 5 minutes while the visitor fills the details form. Advisory (created after the advisory validation passed) — the locked create is still the authority.
Fetches a booking by UUID; nil on miss or forged id.
Joins the waitlist for a date. One open entry per email+date+service — repeat joins are collapsed.
Active (pending + confirmed) bookings of a service that overlap the
given window. window is {from_utc, until_utc} or
{:dates, from, until}, both exclusive-end.
Lists bookings for the admin. Options: :service_uuid,
:service_uuids, :status, :upcoming (from today, site frame),
:limit (default 200).
The full occupancy the validator sees: active bookings PLUS unexpired
holds (mapped to pseudo-bookings), minus opts[:exclude_hold] (the
caller's own hold). This is what pickers and the locked create both
consume, so a held slot greys out everywhere.
Waitlist entries of a service, open first, newest first.
Signed token a guest uses to view/cancel their booking without an account.
Releases a hold (back button, LiveView terminate). Idempotent.
Per-service PubSub topic — public pickers subscribe to exactly one.
Open-entry counts per service — admin visibility.