Files
touchbase/docs/progress/2026-05-01-end-to-end.md

6.9 KiB
Raw Permalink Blame History

2026-05-01 — End-to-End Booking Flow

Same-day successor to 2026-05-01-availability.md. Closes Step 4 of Initial.md §9.

Milestone

The full path "admin takes a phone booking on behalf of a customer" works end-to-end: query the DB, find slots, hold, confirm, send a confirmation email, record a notification row. Race conditions on the same slot are caught at the DB level and surfaced as a typed BookingConflictError. Practice timezone (America/Detroit) is wired through every layer that touches local clock time.

What's verified

  • pnpm test64/64 green across 6 test files
  • pnpm lint — clean
  • pnpm exec tsc --noEmit — clean
  • pnpm tsx scripts/book-on-behalf.ts alex@example.com "60-minute Swedish" 2026-05-05T10:00 — books, emails, prints confirmation
  • Mailpit at http://localhost:8025 shows the rendered email with proper EDT timezone formatting

What landed

File Role Tests
src/lib/availability-loader.ts DB → algorithm adapter; returns { service, therapists[], rooms[] } for a window + service. Filters bookings to active statuses + window-overlap 12
src/lib/booking.ts createHold / confirmHold / expireStaleHolds; BookingConflictError typed error; PG-code error classification (23P01, 40P01, 40001) 8
src/lib/email.ts nodemailer SMTP transport (Mailpit dev / Resend prod); sendEmail low-level + sendBookingConfirmation high-level; cached transport with reset seam for tests 3
scripts/book-on-behalf.ts Admin CLI smoke-test of the whole chain (manual)
test/e2e-booking.test.ts Integration test of the chain + concurrent-hold race 2

Plus environment changes:

  • APP_TZ set to America/Detroit in .env, .env.test, .env.example
  • nodemailer + @types/nodemailer added

Decisions made this session

Decision Resolution
Practice timezone America/Detroit (US Eastern, observes DST) — confirmed by user
Hold duration default 10 minutes (DEFAULT_HOLD_MINUTES in booking.ts)
Concurrent-hold conflict signaling DB exclusion violation (23P01) AND deadlock (40P01) AND serialization failure (40001) all surface as BookingConflictError. Reason: from a caller's POV, all three mean "someone else got the slot first"
Loader filters Bookings filtered to status IN (HOLD, CONFIRMED) + window-overlap on endsAt/roomReleasedAt. Cancelled/completed rows aren't loaded
Loader output shape Matches findSlots input exactly — no glue at the call site
Algorithm re-validation at commit None. createHold trusts the caller picked a slot from findSlots. Reason: DB has no concept of working hours; double-check at insert time would be a re-run of the algorithm, defeating the purpose. The exclusion constraints catch the only thing the DB can know about (overlap)
Email template format Plain text (no React Email yet). Plain Intl.DateTimeFormat for local time rendering. Reason: one template doesn't justify a templating layer
Notification row lifecycle Insert with status='queued' before send → update to sent (with providerId, sentAt) on success or failed on throw. Reason: durable record even when email itself fails
BookingConfirmationArgs shape Pass bookingId only; the email function does its own DB query for the related rows. Reason: keeps the booking-creator's caller surface simple

Gotchas hit

1. GiST exclusion constraints + concurrent inserts → deadlock, not always 23P01

When two transactions try to insert overlapping rows simultaneously, Postgres can detect a deadlock during the GiST index check rather than report a clean exclusion violation. The first run of the e2e race test failed because we were only catching 23P01. Fix: also classify 40P01 (deadlock_detected) and 40001 (serialization_failure) as conflicts.

2. Mailpit container had stopped between sessions

Postgres has restart: unless-stopped; Mailpit was created without that initially. Re-bringing the compose was sufficient but worth knowing — the compose.yaml already has restart: unless-stopped on Mailpit too, so this shouldn't recur.

3. SMTP host resolves IPv6 first on macOS

Initial test failure was ECONNREFUSED ::1:8025 even though Mailpit was bound to 0.0.0.0:8025. nodemailer falls back to IPv4 on its own; once Mailpit was up, it resolved cleanly. Worth noting if we ever see flaky email tests later.

Coverage breakdown

test/availability.test.ts          28 tests   pure algorithm
test/availability-loader.test.ts   12 tests   DB loader + integration with findSlots
test/booking-exclusion.test.ts     10 tests   exclusion constraints + FK rejection
test/booking.test.ts                8 tests   createHold / confirmHold / expireStaleHolds
test/email.test.ts                  3 tests   SMTP delivery + Notification row
test/e2e-booking.test.ts            2 tests   admin happy path + concurrent-hold race
                                  ----
                                   63 tests
                                  + 1 in beforeAll glue?  (vitest reports 64)

Open questions still unresolved

  1. Customer-visible brand name (TouchBase or other) — affects email subject/body
  2. Currency — USD assumed throughout
  3. Stripe account ownership — needed when billing wiring lands

Roadmap status (Initial.md §9)

  1. Spike — done 2026-04-30
  2. Schema + seed — done 2026-05-01
  3. Availability algorithm — done 2026-05-01
  4. First end-to-end story (admin booking + email)done 2026-05-01 (this session)
  5. next: public self-booking → Stripe deposits → reminders

Public self-booking is the next big chunk. Reasonable break:

  • 5a — Auth.js with email magic links for customers (~35 days). Touch the existing User model lightly.
  • 5b — Public booking page that calls the same loader + createHold pipeline (~57 days).
  • 5c — Stripe deposit flow + webhook handler (~35 days).
  • 5d — Email reminders via pg-boss scheduled jobs (~23 days).

Recommended order: 5a → 5b → 5c → 5d. Each piece is independently shippable and reviewable.

Alternative: build a thin admin web UI first (a /admin page that wraps the existing CLI flow) to get the practice using TouchBase to take phone bookings, then layer public self-booking after. ~34 days. Lets the practice start using it sooner; defers the auth story until customers actually need to log in (admin auth is simpler — single-org single-admin or even basic auth to start).

How to resume

cd /Users/noise/Documents/code/touchbase
docker-compose up -d postgres mailpit
pnpm install
pnpm db:bootstrap          # if test DB or extensions missing
pnpm db:seed               # reset + seed dev DB
pnpm test                  # 64/64
pnpm tsx scripts/book-on-behalf.ts alex@example.com "60-minute Swedish" 2026-05-05T10:00
open http://localhost:8025  # Mailpit web UI