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

111 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 test`**64/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
## Recommended next step
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
```bash
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
```