Module kit / Calendar and booking
Booking Confirmation Flow
calendar-booking/booking-confirmation-flow
| Kind | backend |
|---|---|
| Domain | calendar-booking |
| Maturity | prod |
| Reusable | extract |
| Stack | fastapi-0.118, sqlalchemy-2.0-sync, postgres, apscheduler-3.11, google-api-python-client, icalendar-6.1, cryptography-fernet, twilio |
| Depends on | _shared/fastapi-project-layout, _shared/alembic-migrations, _shared/workspace-scoping, _shared/scheduler-and-background-work, _shared/error-envelope |
| Frontends | calendar-booking/ui/calendar-settings-panel |
| Reference implementation |
|
| Measured on | alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12 |
| Version | 1 |
Purpose
A stranger with no account picks a slot on an embeddable widget and gets a real, held appointment: the create path takes a row lock on any slot-holding booking for the exact (meeting_type_id, starts_at) before insert, and a partial UNIQUE index (uq_booking_slot_meeting_type_starts_at, calendar_integration_model.py 362 to 371) is the database backstop that lets exactly one INSERT win when the lock finds nothing to lock (the genuine empty-slot race). The invariant that makes it trustworthy is that "slot taken" is always a clean 409, never a 500 and never two calendar events for one time: the router-level lock catches the common case (_lock_slot_or_409, calendar_router.py 1452 to 1485) and the index catches the race the lock cannot see, with the router translating the resulting IntegrityError back into the same 409 (calendar_router.py 1571 to 1583).
When to use
Read it before touching create_public_booking, _lock_slot_or_409, CalendarIntegrationService.create_booking/cancel_booking, _dispatch_booking_messages, _send_ics_invites, run_booking_reminders/send_booking_reminder/cancel_unconfirmed_bookings, or confirm_booking_by_token; before changing what counts as a slot-holding status (SLOT_HOLDING_STATUSES); and when someone asks "why did this slot double-book", "why didn't the reminder SMS go out", "why was this booking auto-cancelled", or "what does the confirmation link actually do". Trigger phrases: "public booking", "slot lock", "double-book", "partial unique index", "meeting type", "iCal feed", "CalDAV", "reconfirmation", "SMS reminder", "confirmation token", "message steps", ".ics invite".
Not this recipe: the admin/workspace CRUD for meeting types and calendar connections (/admin/meeting-types, /workspace/meeting-types, /integrations/ical, /integrations/caldav, OAuth connect/callback, calendar_router.py 249 to 1345) - configuration, not the booking flow itself; get_available_slots/get_availability_slots (calendar_integration_service.py 517 to 1044) - the availability computation this flow depends on but does not own, a neighbouring recipe; the embed widget HTML/JS generator (get_embed_widget, calendar_router.py 1943 to roughly 2390) - the rendered page around this flow, not the flow; services/calendar/booking_lead_bridge.py's target, the CRM lead itself (this recipe cites the call site and its own tests but the lead model is out of scope); the shared messaging_service engine (resolve_steps, dispatch, drain_due, cancel_pending) - this recipe is one of its callers, not its owner; Google/iCal/CalDAV OAuth and feed-fetch mechanics (get_oauth_authorization_url, handle_oauth_callback, services/calendar/ical_provider.py, services/calendar/caldav_provider.py) - connection setup, read for context only.
Stack assumptions
At 62ba4fc: fastapi==0.118.2, synchronous handlers on sqlalchemy.orm.Session (SQLAlchemy==2.0.43), APScheduler==3.11.0 for the reminder/auto-cancel job, google-api-python-client==2.154.0/google-auth==2.41.1/google-auth-oauthlib==1.2.1 for the Google Calendar write path, icalendar==6.1.0 for .ics generation, cryptography==46.0.0 (Fernet) for OAuth-token and iCal/CalDAV-secret encryption at rest, Twilio (via services/sms_service.py) for the reconfirmation SMS, psycopg2-binary==2.9.10, alembic==1.16.5. Foundations assumed: S4 (fastapi-project-layout) for the router mount (mount_router(app, calendar_router.router, prefix="/api/calendar", ...), app_fastapi.py 845); S8 (alembic-migrations) for the slot-lock and message-step columns under Deploy & ops; S6/workspace-scoping for _has_workspace_access/_assert_calendar_connect_access gating the non-public routes (calendar_router.py 959 to 998, out of this recipe's public-flow scope but the pattern this recipe's public routes deliberately skip); S12 (scheduler-and-background-work) for the run_booking_reminders job.
Config/env this flow reads (core/config.py, not independently verified line-by-line beyond the usages cited): API_BASE_URL (builds the /api/calendar/confirm/{token} link, scheduled_jobs.py 1400); CALENDAR_ENCRYPTION_KEY (Fernet key for stored OAuth/feed secrets, calendar_integration_service.py 78 to 112, falls back to a dev-only local key file, raises in non-development without it); TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN/TWILIO_PHONE_NUMBER (sms_service.py 27 to 148, unconfigured Twilio answers {"status": "failed", ...} rather than raising). Fail behaviour: create_public_booking wraps the whole handler in try/except Exception and answers 500 "Failed to create booking. Please try again." for anything not already an HTTPException (calendar_router.py 1649 to 1653); the CAPI block, the lead-bridge call, _dispatch_booking_messages, _notify_host_of_booking and _send_ics_invites are each individually wrapped so a failure in any one of them never fails the booking response (calendar_router.py 1588 to 1633; calendar_integration_service.py 1295 to 1318).
Data model
Three tables carry this flow (models/calendar_integration_model.py); columns not touched by this recipe (OAuth token fields, iCal/CalDAV credentials, style/embed settings) are omitted here - see When to use for the recipe that owns them.
calendar_integrations (
id uuid PRIMARY KEY,
workspace_id uuid REFERENCES workspaces(id) ON DELETE CASCADE, -- NULL = platform-level
provider varchar(50), -- google | outlook | apple | ical_feed | caldav
provider_account_id varchar(255),
timezone varchar(100) DEFAULT 'Europe/Zurich',
is_active boolean DEFAULT true,
default_messages jsonb -- tri-state fallback for every meeting type's `messages`
);
booking_meeting_types (
id uuid PRIMARY KEY,
calendar_integration_id uuid NOT NULL REFERENCES calendar_integrations(id) ON DELETE CASCADE,
slug varchar(100) NOT NULL,
duration_minutes integer NOT NULL DEFAULT 60,
buffer_before_minutes integer DEFAULT 0,
buffer_after_minutes integer DEFAULT 15,
min_notice_hours integer DEFAULT 24,
availability_schedule jsonb,
location_type varchar(50) DEFAULT 'google_meet',
booking_questions jsonb DEFAULT '[]',
confirmation_message text,
send_confirmation_email boolean DEFAULT true,
send_reminder_email boolean DEFAULT true,
reminder_hours_before integer DEFAULT 24,
messages jsonb, -- NULL = inherit calendar_integrations.default_messages, [] = explicitly none
embed_settings jsonb,
is_active boolean DEFAULT true,
UNIQUE (calendar_integration_id, slug) -- uq_meeting_type_integration_slug
);
calendar_bookings (
id uuid PRIMARY KEY,
meeting_type_id uuid REFERENCES booking_meeting_types(id) ON DELETE SET NULL, -- nullable: direct/onboarding bookings
calendar_integration_id uuid NOT NULL REFERENCES calendar_integrations(id) ON DELETE CASCADE,
google_event_id varchar(255),
contact_id uuid REFERENCES tracking_contacts(id) ON DELETE SET NULL,
attendee_email varchar(255) NOT NULL,
attendee_name varchar(255),
attendee_phone varchar(50),
attendee_timezone varchar(100) DEFAULT 'Europe/Zurich',
starts_at timestamptz NOT NULL,
ends_at timestamptz NOT NULL,
status varchar(50) DEFAULT 'confirmed', -- pending | pending_payment | confirmed | cancelled | rescheduled | completed | no_show
cancelled_at timestamptz, cancellation_reason text, cancelled_by varchar(50),
source varchar(100) DEFAULT 'direct', -- direct | embed | onboarding | project | api
sms_reminder_24h_sent_at timestamptz,
sms_reminder_1h_sent_at timestamptz,
confirmation_token varchar(64) UNIQUE, -- ix_calendar_bookings_confirmation_token
reconfirmation_status varchar(20) DEFAULT 'pending', -- pending | confirmed | cancelled_no_confirm | no_response
reconfirmed_at timestamptz,
sms_opt_in boolean DEFAULT false
);
-- Partial UNIQUE index, the double-book backstop:
CREATE UNIQUE INDEX uq_booking_slot_meeting_type_starts_at
ON calendar_bookings (meeting_type_id, starts_at)
WHERE meeting_type_id IS NOT NULL AND status IN ('confirmed', 'pending_payment');
SLOT_HOLDING_STATUSES = (CONFIRMED, PENDING_PAYMENT) (calendar_integration_model.py 63 to 66) is the single source of truth both the availability query and the slot-lock use for "this slot is taken" - cancelled/rescheduled/completed/no_show never hold a slot. What is NOT stored: no card or payment detail (Payrexx/pending_payment handling itself is out of this recipe's scope); no raw PSP or Google API response body, only google_event_id/google_event_link; the reminder/confirmation email body is not persisted on the booking row, it is rendered from the meeting type's/calendar's messages steps at dispatch time (messaging_service.dispatch).
API surface
| Route | Auth | Tenancy | Codes | Notes |
|---|---|---|---|---|
POST /api/calendar/public/book/{slug} (create_public_booking, calendar_router.py 1487 to 1653) | none (public) | integration_id query param selects the workspace's calendar, or falls back to the platform-level integration | 200 BookingResponse; 400 invalid attendee_timezone; 404 meeting type/integration not found; 409 slot taken; 429 anti-abuse (form_protection.validate_submission); 500 unhandled | Body is BookingRequest (calendar_router.py 161 to 197): starts_at, attendee_email, attendee_name, optional attendee_phone/sms_opt_in/answers/notes/tracking fields |
GET /api/calendar/public/booking/{booking_id}?email= (get_public_booking, 1656 to 1682) | none | attendee_email query param is the only lookup key (email-as-secret) | 200 BookingResponse; 404 no match | |
POST /api/calendar/public/cancel/{booking_id}?email= (cancel_public_booking, 1685 to 1711) | none | same email check | 200 {"success": true, ...}; 400 already cancelled; 404 no match | Calls calendar_service.cancel_booking(..., cancelled_by="attendee") |
GET /api/calendar/confirm/{token} (confirm_booking_by_token, 1714 to 1781) | none | token is CalendarBooking.confirmation_token, a 64-char unguessable value from secrets.token_urlsafe(32) | always 200, an HTML page (_confirmation_page); no JSON error path | States: token not found ("Link ungültig"), already confirmed, cancelled, in the past, or freshly confirmed (flips reconfirmation_status='confirmed', stamps reconfirmed_at) |
GET /api/calendar/public/meeting-type/{slug} / GET /api/calendar/public/availability/{slug} | none | same integration_id fallback pattern | 200 / 400 / 403 (embed disallowed) / 404 | Read-only prerequisites this flow depends on, not detailed here (see When to use) |
_lock_slot_or_409 (calendar_router.py 1452 to 1485) is not a route; it is the function the create path calls before create_booking, documented under Application logic.
Application logic
create_public_booking, in commit order:
- Extracts client IP (
X-Forwarded-Forfirst hop, elserequest.client.host) and runsform_protection.validate_submissionwhen a protection token or PoW challenge is present,429on failure (calendar_router.py1502 to 1520). - Validates
attendee_timezonewithvalidate_timezone,400on failure (1523 to 1524). - Resolves the meeting type via
integration_idor the platform-level integration;404if either is missing (1526 to 1535). - Derives
sourcefromreferrer_url("embed" substring),onboarding_session_id, orproject_id, defaulting todirect(1537 to 1545). - Lock:
_lock_slot_or_409runsSELECT ... FOR UPDATEon any existingCalendarBookingfor(meeting_type_id, starts_at)whose status is inSLOT_HOLDING_STATUSES; if one is found,409immediately (calendar_router.py1452 to 1485). Scoped tomeeting_type.id, which is itself workspace-isolated through itsCalendarIntegration; direct/onboarding bookings (no meeting type) are never slot-locked. - Insert:
calendar_service.create_booking(...)(see below). If the empty-slot race let a second racer past the lock check, the partial UNIQUE index raisesIntegrityErroron commit; the router catches it, rolls back, logs, and answers the same409"This time slot is no longer available" (1571 to 1583). - Conversion (best-effort, never fails the booking):
fire_booking_conversion(services/calendar/booking_conversion.py) sends a server-side Meta CAPI event -Scheduleby default, or a per-slug mapping viaget_calendar_conversion_mapping- and always returns anevent_ideven when the send is skipped (test mode, consent denied, mapping disabled), so the widget always has something to dedupe a browser-side pixel against (1585 to 1617). - CRM bridge (best-effort):
create_or_update_booking_lead(services/calendar/booking_lead_bridge.py) creates or updates a workspace-scopedLeadkeyed on(workspace_id, email), stitches the visitor's tracking session via the sharedlink_lead_to_tracking_contactprimitive, stampsbooking.contact_id, and logs ameeting_scheduledClientActivityLogrow; it commits its own transaction and never raises out to the router (calendar_router.py1619 to 1633;booking_lead_bridge.py44 to 163). Skipped entirely for a platform-level (tenant-less) integration. - Returns
BookingResponseincluding the CAPIconversionpayload when one was sent.
CalendarIntegrationService.create_booking (calendar_integration_service.py 1050 to 1320), the function step 6 calls:
- Resolves
integration/duration/ends_at/location_typefrommeeting_type, or fromintegration_id+ends_atfor a direct booking (1081 to 1102). - Builds the event description from
meeting_type.description+ configuredbooking_questions(matched by id) + any unmatched answer keys, so a custom front-end's own question keys are never silently dropped (_build_event_description, 1663 to 1712). - Provider branch: Google gets a real Calendar API
events().insert(..., sendUpdates='all')with a Google MeetconferenceDatarequest whenlocation_type == google_meet(1176 to 1199); CalDAV gets a remote write viaservices/calendar/caldav_provider.create_event(1200 to 1219); an iCal-feed integration performs no remote write at all - the booking exists only in this database, and step 5 below ships the.icsinvite instead (1221 to 1227). - Inserts the
CalendarBookingrow withstatus=CONFIRMED(1254 to 1282) - this call itself does not construct apending_paymentrow; that status exists for a separate paid-appointment path this recipe does not own (see the slot-lock migration's own docstring under Deploy & ops). - Message fan-out (best-effort):
_dispatch_booking_messagesresolves the step list via_resolve_message_steps(meeting type's ownmessages, else the calendar'sdefault_messages, via the sharedmessaging_service.resolve_stepstri-state:Noneinherits,[]means explicitly none,[...]is used as-is), drops anybefore_event-anchored step whenmeeting_type.send_reminder_emailisFalse, and callsmessaging_service.dispatch(..., surface_kind="booking", anchor_at=booking.starts_at, ref_id=booking.id, variables={name, meeting, date, time, location, meeting_link})(1286 to 1389).dispatchfires anyon_eventstep immediately and enqueuesbefore_event/after_eventsteps intoscheduled_messages, drained every 5 minutes byrun_message_drain(scheduled_jobs.py593 to 603, 211 to 214). - Host notification (best-effort):
_notify_host_of_bookingemails the workspace owner (preferred) or the connected provider account (fallback) a plain summary table of the booking, including any pre-check answers keyed by their configured labels (1391 to 1481). Exists because Google's ownsendUpdates='all'notifies the attendee, never the calendar owner about an event on their own calendar. .icsinvites (best-effort, iCal-feed and CalDAV only):_send_ics_invitesbuilds aMETHOD:REQUEST.icsviabuild_booking_icsand emails it to the attendee always, and additionally to the calendar owner when the integration is iCal-feed (which wrote no remote event at all) (1310 to 1318, 1483 to 1564).
Cancellation, cancel_booking (1566 to 1620): deletes the remote event when one exists (Google events().delete(sendUpdates='all'), or CalDAV cancel_event); flips status=CANCELLED, stamps cancelled_at/cancelled_by/cancellation_reason; calls messaging_service.cancel_pending(surface_kind="booking", ref_id=booking.id) to drop any not-yet-sent queued reminder/follow-up (so a cancelled attendee never gets "see you tomorrow"); for iCal-feed/CalDAV integrations, emails a METHOD:CANCEL .ics reusing the original event UID so the attendee's own calendar client resolves it as a cancellation of the same event, not a new one (_send_cancel_ics, 1622 to 1661).
SMS reconfirmation-by-token, run_booking_reminders (scheduled_jobs.py 1304 to 1485, IntervalTrigger(minutes=15), scheduled_jobs.py 202 to 208):
- Selects CONFIRMED bookings with
sms_opt_in=True, a non-emptyattendee_phone, starting 23 to 25 hours out andsms_reminder_24h_sent_at IS NULL; and separately, starting 50 to 70 minutes out withsms_reminder_1h_sent_at IS NULL(1324 to 1359). send_booking_reminder: for the 24h reminder only, mintsconfirmation_token = secrets.token_urlsafe(32)if the booking does not already have one and flushes it before sending, then builds{API_BASE_URL}/api/calendar/confirm/{token}and sends viasms_service.send_booking_reminder(1376 to 1413). Onstatus in ('sent', 'mock')it stamps the matchingsms_reminder_*_sent_atcolumn and commits (1416 to 1421); a send is only ever selected while its own_sent_atcolumn is stillNULL, so a resend can never fire twice for the same window.cancel_unconfirmed_bookings: separately selects CONFIRMED bookings starting within 3 hours whose 24h reminder was sent (sms_reminder_24h_sent_at IS NOT NULL) but whosereconfirmation_status != 'confirmed', callscancel_booking(cancelled_by="system", reason="Termin wurde nicht bestätigt (automatische Absage)"), setsreconfirmation_status = 'cancelled_no_confirm', and sends a plain cancellation SMS (1430 to 1485).confirm_booking_by_token(see API surface) is the only writer ofreconfirmation_status='confirmed'; it 404-equivalent-HTML's an unknown token, and refuses (with an HTML message, not an error status) an already-confirmed, cancelled, or past booking before flipping status.
Invariants
- At most one slot-holding (
CONFIRMEDorPENDING_PAYMENT) booking exists per(meeting_type_id, starts_at), enforced by the database itself, not application logic alone (ck/indexuq_booking_slot_meeting_type_starts_at;test_second_confirmed_same_slot_rejected,test_pending_payment_conflicts_with_confirmed_slot). - Two genuinely concurrent bookers for the same empty slot: exactly one wins, the other gets a clean rejection, and the database ends with exactly one surviving slot-holding row (
test_two_racing_bookings_one_wins, real threads + athreading.Barrieragainst a committed Postgres session, not the shared rolled-backdb_sessionfixture). - A cancelled (or rescheduled/completed/no_show) booking never blocks a rebooking of the same slot (
test_cancelled_slot_can_be_rebooked), because those statuses are excluded from the partial index'sWHEREclause. - A reminder SMS is sent at most once per window: selection and the
_sent_at IS NULLwrite are the same guard, so neither a second scheduler tick nor a second worker can re-select an already-sent booking (module-level comment,scheduled_jobs.py1316 to 1321; no dedicated concurrency test for this path was found - see Tests). - Cancelling a booking always drops its not-yet-sent queued messages,
messaging_service.cancel_pendingis called unconditionally insidecancel_booking, not left to the caller (calendar_integration_service.py1602 to 1610). messages: Noneon a meeting type always falls back to the calendar'sdefault_messages;messages: []never does. This ismessaging_service.resolve_steps's contract, and_resolve_message_stepscalls it rather than re-implementing the tri-state (calendar_integration_service.py1323 to 1335).
Frontend
The module that would render this, by id:
calendar-booking/ui/calendar-settings-panel the workspace-facing configuration surface (meeting
types, connected calendar, default messages) whose
settings this flow reads at booking time; at 62ba4fc
nothing in this recipe's own flow has a dedicated
admin UI beyond that settings panel - the booking
widget itself (embed HTML) is a neighbouring
recipe (see When to use)
Minimum admin/debug surface (no dedicated route exists for these at this commit, they are queries a human runs by hand): SELECT id, status, reconfirmation_status, sms_reminder_24h_sent_at, sms_reminder_1h_sent_at, confirmation_token FROM calendar_bookings WHERE starts_at > now() AND status = 'confirmed' ORDER BY starts_at (the live reminder queue); SELECT count(*) FROM calendar_bookings WHERE reconfirmation_status = 'cancelled_no_confirm' AND created_at > now() - interval '7 days' (auto-cancel rate, a spike here means the SMS or the confirm link itself is broken); the log lines SCHEDULER: Processed %s 24h reminders, %s 1h reminders, %s auto-cancelled (scheduled_jobs.py 1367) and SCHEDULER: Auto-cancelled unconfirmed booking %s (1480).
Security & tenancy
Auth dependency: none on any route in this recipe - create_public_booking, get_public_booking, cancel_public_booking, and confirm_booking_by_token all take only db (and the public Query params), by design, this is the embeddable-widget surface. Tenancy is carried by integration_id (an opaque UUID query param, not validated against any session) rather than a workspace-scoped session; a platform-level booking (no integration_id) falls back to the single platform integration. get_public_booking/cancel_public_booking use attendee_email as the entire access credential - the query filters WHERE id = :id AND attendee_email = :email, so knowing the booking id alone is not enough, but the email is transmitted and compared in plaintext over the query string, not a signed token. confirm_booking_by_token uses confirmation_token, a secrets.token_urlsafe(32) value (scheduled_jobs.py 1396) with a database-level UNIQUE constraint - a lookup miss and an already-consumed token both render the same class of HTML page rather than distinguishing "wrong" from "used".
Fail direction: the slot-lock is closed on the database (a raised IntegrityError becomes a 409, never a silently-accepted second booking); every side effect that is NOT the booking-and-slot itself (CAPI, CRM lead, message dispatch, host notification, .ics email) is wrapped so its failure can only be logged, never surfaces as a failed booking or a 500 - meaning a production incident in any one of those systems is invisible to the attendee and to the booking's own success/failure signal, visible only in logs (Gotchas).
Secrets and PII at rest: OAuth tokens and iCal/CalDAV feed secrets are Fernet-encrypted with a key from CALENDAR_ENCRYPTION_KEY (or a dev-only local file, calendar_integration_service.py 78 to 112); attendee_email/attendee_name/attendee_phone/free-text answers/notes are stored in plain columns with no field-level encryption - this recipe does not add any beyond what the base table already does. Rate limits: form_protection.validate_submission (honeypot + optional proof-of-work + IP) gates create_public_booking only when the client supplies a protection token/challenge; there is no server-mandated minimum, so a caller that omits those fields bypasses the check entirely (calendar_router.py 1509 to 1520, not independently verified beyond this call site - form_protection's own enforcement is out of this recipe's scope). Attacker-controlled inputs: starts_at (must still pass the slot-lock/index, so a malicious or clock-skewed value cannot create a second booking for a taken slot, but nothing in this flow revalidates it against the meeting type's own availability window at insert time - that check lives in get_available_slots, read at quote time, not enforced again at booking time); attendee_email/attendee_phone/answers/notes (rendered into the Google Calendar event description and the host-notification email as HTML via simple _html.escape, calendar_integration_service.py 1465 to 1470); integration_id (any UUID is accepted and looked up directly, with no ownership check tying it to the request's origin).
Tests
Command, run 2026-09-12 against the alchemy_labs .venv at 62ba4fc, exact output not reproduced here (not executed as part of authoring this recipe - the process instructed reading, not running, these tests; treat the following as sourced from the test files' own structure, not a captured run):
pytest backend/tests/v2/coverage/test_booking_g1_public_booking_flow_availability_book_cancel_confirm_.py \
backend/tests/v2/coverage/test_booking_g5_slot_lock_double_book.py \
backend/tests/test_booking_lead_bridge.py -q
test_booking_g1_public_booking_flow_availability_book_cancel_confirm_.py (738 lines): 30 def test_ functions against the unauthenticated client fixture, covering get_public_meeting_type (5 tests, including embed-disabled 403 and inactive-invisible), get_public_availability (5), create_public_booking (7, including test_create_public_booking_persists_booking, test_create_public_booking_capi_failure_swallowed - proves a CAPI exception never fails the booking response), get_public_booking (4, email-match/mismatch), cancel_public_booking (3), confirm_booking_by_token (5: invalid token, test_confirm_success_flips_status_and_stamps_time, already-confirmed, cancelled, past-booking), plus 3 direct service-level tests (test_service_create_booking_computes_ends_at_and_status, test_service_get_meeting_type_by_slug_is_integration_scoped, test_service_get_meeting_type_by_slug_excludes_inactive, test_service_get_available_slots_shape_and_timezone). Uses an iCal-feed integration and an autouse fixture that monkeypatches _send_ics_invites/_send_cancel_ics to no-ops, so the suite is fully hermetic (no outbound network).
test_booking_g5_slot_lock_double_book.py (297 lines): 5 tests, hermetic against a real Postgres (not the shared rolled-back db_session fixture - this file opens its own create_engine/sessionmaker because two racing connections cannot be modeled inside one rolled-back transaction, per its own module docstring). test_pending_payment_status_exists_and_holds_slot (enum/set assertions, no DB); test_second_confirmed_same_slot_rejected and test_pending_payment_conflicts_with_confirmed_slot (both expect IntegrityError on the second commit); test_cancelled_slot_can_be_rebooked (second commit must NOT raise); test_two_racing_bookings_one_wins (two real threading.Threads synchronized on a threading.Barrier, mirrors the router's own lock-then-insert sequence, asserts exactly one "won" and one "conflict_*" outcome, then independently re-queries the database and asserts exactly 1 surviving slot-holding row).
test_booking_lead_bridge.py (257 lines): 7 tests against create_or_update_booking_lead directly - test_new_email_creates_lead_in_integration_workspace, test_existing_lead_email_no_duplicate_and_no_source_flip (an existing non-booking-sourced lead keeps its original source), test_workspace_isolation_same_email_two_workspaces, test_platform_level_integration_skips (a None workspace_id returns None, writes nothing), test_session_stitch_and_aggregate_recompute, test_fingerprint_stitch_is_workspace_scoped, test_no_tracking_payload_still_creates_lead.
What this recipe genuinely does not know: whether these three files pass at 62ba4fc (not run during authoring); whether a dedicated test exists for the SMS-reconfirmation concurrency guarantee described under Invariants (scheduled_jobs.py's run_booking_reminders/send_booking_reminder/cancel_unconfirmed_bookings have no test file among the three named in this recipe's source list, and a search for one was not performed beyond those three files - do not assume its absence, only that this recipe did not locate one); the exact pytest total/pass count and any skip conditions (unlike the billing-credits recipe's bar, no PRIMA_CREDITS_TEST_DATABASE_URL-style env-gate marker was found on these three files, but this was not exhaustively verified against conftest.py).
Deploy & ops
Config keys: API_BASE_URL, CALENDAR_ENCRYPTION_KEY, TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN/TWILIO_PHONE_NUMBER (all above). Migrations touching this flow's schema, in the order they were introduced (migrations/versions/, revision IDs and down_revision read directly from each file): baseline_prod_schema_001 (revision baseline_prod_schema_001, down_revision: None) already contains calendar_bookings.confirmation_token/sms_reminder_24h_sent_at/sms_reminder_1h_sent_at/reconfirmation_status and the calendar_bookings_confirmation_token_key UNIQUE constraint plus ix_calendar_bookings_confirmation_token - meaning the SMS-reconfirmation columns predate this repo's incremental Alembic history, they were already live in production at the baseline snapshot; 20260427b_add_ical_caldav.py (ical_caldav_001, revises content_review_001) adds the iCal/CalDAV integration columns this flow's provider branch depends on; 20260610_0003_contact_spine_001.py (contact_spine_001, revises encrypt_ad_oauth_tokens_001) adds calendar_bookings.contact_id; 20260610_0004_booking_slot_lock.py (booking_slot_lock_001, revises commerce_subscriptions_001) adds the partial UNIQUE index this recipe's core invariant depends on, additive and reversible per its own docstring; 20260901_0001_booking_message_steps.py (booking_msg_steps_001, revises sparks_team_001) adds booking_meeting_types.messages and calendar_integrations.default_messages, both nullable with no server default, deliberately, so the tri-state (inherit / explicit-none / explicit-list) cannot collapse; 20260907_0001_leadsource_add_booking.py (leadsource_booking_001, revises desk_email_ingest_001) adds 'booking' to the Postgres leadsource enum the CRM bridge writes.
Scheduler: run_booking_reminders on IntervalTrigger(minutes=15) (scheduled_jobs.py 202 to 208, job id booking_reminders); the message-step fan-out this flow triggers at booking time is drained separately by run_message_drain on IntervalTrigger(minutes=5) (211 to 214, job id message_drain) via messaging_service.drain_due. Both run inside the same BackgroundScheduler a singleton-lock promotes one worker to own (_start_full_scheduler, 191 to 199) - not independently re-verified beyond this recipe's own reading of _start_full_scheduler's surrounding singleton-lock comments.
Rollback: the slot-lock index (booking_slot_lock_001) has an explicit, reversible downgrade() per its own docstring; leadsource_booking_001's downgrade() is a documented no-op (Postgres cannot drop an enum value without recreating the type) - rolling back the code after that migration has run leaves the 'booking' enum value in place harmlessly. No Argus-style check for this flow was found in the reference implementation; the closest measured proxy is the SELECT under Frontend.
Gotchas
- 2026-09-12 (measured at 62ba4fc): every side effect after the booking is committed - Meta CAPI, the CRM lead bridge, message dispatch, host notification,
.icsemail - is individually try/excepted and only logged on failure (calendar_router.py1588 to 1633;calendar_integration_service.py1295 to 1318). This is the same trade-off the auto-topup recipe's outer catch-all makes for money: a production outage in any of these five systems produces a normal 200 booking response and a log line, nothing else. There is no dead-letter queue or retry for a failed_dispatch_booking_messagesor_notify_host_of_bookingcall at this commit. - 2026-09-12 (measured at 62ba4fc,
calendar_integration_service.py1221 to 1227): an iCal-feed integration performs no remote calendar write at all when a booking is created - the booking exists only in this app's database until the.icsemail is opened and manually added. A rebuild that assumes every confirmed booking has a corresponding external calendar event will be wrong for this provider. - 2026-09-12 (measured at 62ba4fc):
create_booking's direct-booking mode (meeting_type=None,integration_id+ends_atsupplied instead) is not slot-locked by_lock_slot_or_409at all - that guard only runs increate_public_booking's meeting-type path. Any other caller ofcreate_bookingin direct mode gets no double-book protection beyond the partial index's ownmeeting_type_id IS NOT NULLcondition, which does not even apply to a row with no meeting type. - 2026-09-12 (measured at 62ba4fc):
confirm_booking_by_token's "invalid or expired" HTML page ("Link ungültig") is returned identically whether the token was never valid or was already consumed on an unrelated booking (calendar_router.py1730 to 1735) - an attacker or a support agent cannot distinguish "wrong token" from "this token belongs to a different, already-resolved booking" from the response alone. - 2026-09-12 (measured at 62ba4fc,
calendar_router.py1509 to 1520): the anti-abuse check oncreate_public_bookingonly runs when the request supplies_protection_tokenor_protection_pow_challenge; a client that omits both fields entirely skips the check, it is not a server-mandated minimum.
Reference implementation
Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.
alchemy_labs/backend/api/routers/calendar_router.py(2486 lines):BookingRequest/BookingResponse/CancelBookingRequest(161 to 224);_lock_slot_or_409(1452 to 1485);create_public_booking(1487 to 1653);get_public_booking(1656 to 1682);cancel_public_booking(1685 to 1711);confirm_booking_by_token(1714 to 1781);_confirmation_page(1784 to 1867).alchemy_labs/backend/services/calendar_integration_service.py(1717 lines):get_platform_integration/get_primary_workspace_integration(413 to 457);get_meeting_type_by_slug(500 to 511);create_booking(1050 to 1320);_resolve_message_steps(1323 to 1335);_dispatch_booking_messages(1337 to 1389);_notify_host_of_booking(1391 to 1481);_send_ics_invites(1483 to 1564);cancel_booking(1566 to 1620);_send_cancel_ics(1622 to 1661);_build_event_description(1663 to 1712).alchemy_labs/backend/services/calendar/booking_conversion.py(146 lines, whole file in scope):fire_booking_conversion(38 to 145).alchemy_labs/backend/services/calendar/booking_lead_bridge.py(164 lines, whole file in scope):create_or_update_booking_lead(44 to 163).alchemy_labs/backend/services/calendar/ics_builder.py(98 lines, whole file in scope):build_booking_ics(19 to 86).alchemy_labs/backend/models/calendar_integration_model.py(440 lines):BookingStatus/SLOT_HOLDING_STATUSES(43 to 66);CalendarIntegration(82 to 154);BookingMeetingType(161 to 253);CalendarBookingincluding the partial unique index (260 to 372).alchemy_labs/backend/services/messaging_service.py:resolve_steps(184 to 202);dispatch(460 to 522);cancel_pending(596 onward, whole-function range not fully re-verified beyond the call site).alchemy_labs/backend/services/scheduled_jobs.py: job registration (191 to 214);run_booking_reminders(1304 to 1373);send_booking_reminder(1376 to 1427);cancel_unconfirmed_bookings(1430 to 1485).alchemy_labs/backend/app_fastapi.py: router mount (845).alchemy_labs/backend/migrations/versions/:baseline_prod_schema_001.py,20260427b_add_ical_caldav.py,20260610_0003_contact_spine_001.py,20260610_0004_booking_slot_lock.py,20260901_0001_booking_message_steps.py,20260907_0001_leadsource_add_booking.py.- Tests as listed under Tests:
alchemy_labs/backend/tests/v2/coverage/test_booking_g1_public_booking_flow_availability_book_cancel_confirm_.py(738 lines),test_booking_g5_slot_lock_double_book.py(297 lines),alchemy_labs/backend/tests/test_booking_lead_bridge.py(257 lines).
Deviations of the reference from an ideal implementation, all dated 2026-09-12: direct/onboarding bookings bypass the application-level slot lock entirely (Gotchas); every post-commit side effect is best-effort with no retry or dead-letter path; the confirmation-token flow cannot distinguish an invalid token from an already-resolved one in its response; the anti-abuse check is opt-in per request rather than server-enforced.
Acceptance checks
These are read from the source and test files, not captured from a live run (the authoring process specified reading the tests, not executing them; a verifier should run the below and record real output).
grep -n "uq_booking_slot_meeting_type_starts_at" backend/models/calendar_integration_model.pyshows theIndex(...)definition at line 362, withpostgresql_wherecontainingmeeting_type_id IS NOT NULL AND status IN ('confirmed', 'pending_payment').grep -c "^def test_" backend/tests/v2/coverage/test_booking_g1_public_booking_flow_availability_book_cancel_confirm_.pyprints30; the same ontest_booking_g5_slot_lock_double_book.pyprints5; onbackend/tests/test_booking_lead_bridge.pyprints7.pytest backend/tests/v2/coverage/test_booking_g5_slot_lock_double_book.py -qagainst a real (not sqlite/mocked) Postgres test database is expected to pass all 5, withtest_two_racing_bookings_one_winsin particular printing noIntegrityErrorescaping the thread (it is caught and recorded, not re-raised) and the final DB re-query returningcount == 1. Not run as part of authoring this recipe; a verifier should confirm the actual output.grep -n "confirmation_token character varying(64)" backend/migrations/versions/baseline_prod_schema_001.pymatches line 719, confirming the SMS-reconfirmation columns are baseline, not a later addition.grep -n "IntervalTrigger(minutes=15)" backend/services/scheduled_jobs.py | head -1matches line 204, therun_booking_remindersregistration.
Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.