PhoenixKitBookings.Bookings (PhoenixKitBookings v0.1.1)

Copy Markdown View Source

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) → confirmedcancelled. 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

admin_topic()

booking_from_token(token)

Resolves a manage token back to its booking; nil when invalid/expired.

cancel_booking(booking, opts \\ [])

Cancels a pending or confirmed booking; frees capacity immediately.

cancellable_by_customer?(booking, service)

Whether a CUSTOMER may still self-cancel: active, upcoming, and outside the service's cancel_notice window (admins cancel unrestricted).

confirm_booking(booking, opts \\ [])

Approves a pending booking (capacity was already held).

count_by_status(opts \\ [])

Booking counts per status — powers the admin filter tabs. opts[:service_uuids] restricts to visible services (own-only admins).

create_booking(service, range, customer_attrs, opts \\ [])

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}.

create_hold(service, range, opts \\ [])

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.

get_booking(uuid)

Fetches a booking by UUID; nil on miss or forged id.

join_waitlist(service, attrs)

Joins the waitlist for a date. One open entry per email+date+service — repeat joins are collapsed.

list_active_overlapping(service_uuid, arg)

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.

list_bookings(opts \\ [])

Lists bookings for the admin. Options: :service_uuid, :service_uuids, :status, :upcoming (from today, site frame), :limit (default 200).

list_occupancy(service_uuid, window, opts \\ [])

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.

list_waitlist(service_uuid)

Waitlist entries of a service, open first, newest first.

manage_token(booking)

Signed token a guest uses to view/cancel their booking without an account.

release_hold(hold_uuid)

Releases a hold (back button, LiveView terminate). Idempotent.

service_topic(service_uuid)

Per-service PubSub topic — public pickers subscribe to exactly one.

waitlist_counts()

Open-entry counts per service — admin visibility.