add Stripe
This commit is contained in:
90
docs/progress/2026-05-02-booking-lifecycle.md
Normal file
90
docs/progress/2026-05-02-booking-lifecycle.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 2026-05-02 — Booking Lifecycle (Phase C)
|
||||
|
||||
> Companion to `BuildLog.md`. Predecessor: `2026-05-02-therapist-self-serve.md`. Closes Phase C of the post-payments UX plan.
|
||||
|
||||
## Milestone
|
||||
|
||||
The booking lifecycle has all the verbs the practice needs: create, view, **reschedule, mark complete, mark no-show**, cancel — for both customer and admin. Reschedule is atomic (cancel-old + create-new in one transaction; rollback on conflict so the customer never loses their old slot to a phantom new booking). Single "rescheduled" email replaces the cancel + new-confirmation pair. Booking detail pages exist for both customer and admin.
|
||||
|
||||
## What landed
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `src/lib/booking.ts` | +`rescheduleBooking()` (atomic, transactional, surfaces `BookingConflictError` if new slot collides — old booking stays intact). +`markComplete()` and +`markNoShow()` (CONFIRMED→target only, idempotent). |
|
||||
| `src/lib/email.ts` | +`sendBookingRescheduled()` template (one email summarizing old → new). |
|
||||
| `src/app/admin/bookings/[id]/page.tsx` | Detail page — all booking fields incl. cancel-audit, payment-status placeholder, room-released-at, action buttons (Mark complete / Mark no-show / Cancel). Mark-* buttons only appear on past CONFIRMED bookings. |
|
||||
| `src/app/account/bookings/[id]/page.tsx` | Customer detail page — service / when / therapist / room / price + Reschedule + Cancel. Owner-or-admin authorization on the action. |
|
||||
| `src/app/book/page.tsx` | Accepts `fromBookingId` param — shows "Rescheduling your X — pick a new time" banner; threads param through to slot links and the date picker form. |
|
||||
| `src/app/book/confirm/page.tsx` | Detects `fromBookingId` and routes through `rescheduleBooking` + `sendBookingRescheduled` instead of `createHold` + `sendBookingConfirmation`. Page heading and copy adapt. Conflict redirects back to `/book` with `taken=1` and the `fromBookingId` preserved. |
|
||||
| `src/app/account/bookings/page.tsx` | Per-row `Reschedule` link → `/book?serviceId=…&fromBookingId=…`. Booking row is now itself a link to the detail page. |
|
||||
| `src/app/admin/bookings/page.tsx` | Booking row links to detail page. Therapist-name fallback for null. |
|
||||
| `test/booking.test.ts` | +8 tests: reschedule happy path, atomic rollback on conflict, rejects rescheduling CANCELLED, can change therapist/room on reschedule, markComplete CONFIRMED→COMPLETED, markComplete idempotent, markComplete rejects HOLD, markNoShow CONFIRMED→NO_SHOW. |
|
||||
|
||||
## What's verified
|
||||
|
||||
- `pnpm test` — **79/79 green** (was 71; +8 new in this session)
|
||||
- `pnpm lint` — clean
|
||||
- `pnpm exec tsc --noEmit` — clean
|
||||
- Live smoke test (curl + Mailpit, dev server):
|
||||
- **Customer (alex@example.com)**: signed in via magic link → `/account/bookings` shows Reschedule + Cancel buttons → click Reschedule → `/book?fromBookingId=…` shows "Rescheduling your..." banner and slot links carry `fromBookingId` → click new slot → `/book/confirm` shows "Confirm reschedule" heading, "previous booking will be cancelled" copy, hidden `fromBookingId` field
|
||||
- **Admin (admin@touchbase.local)**: `/admin/bookings/{id}` renders full detail with customer name, therapist link, room link, all fields, Cancel button (Mark complete / no-show buttons hidden because the booking is in the future — they appear only after the booking end time)
|
||||
|
||||
## Decisions ratified
|
||||
|
||||
| Decision | Resolution |
|
||||
|---|---|
|
||||
| Reschedule semantics | **Atomic** — cancel-old + create-new in one `db.$transaction`. If the new insert fails (conflict, FK, etc.), the entire transaction rolls back; old booking stays intact. Reason: customer should never lose their slot to a phantom new booking. |
|
||||
| Reschedule email | **One** rescheduled email, not cancel + new-confirmation pair. Reason: cleaner customer experience; one notification with old-vs-new framing. |
|
||||
| Mark complete / no-show timing | Buttons only render when `status === "CONFIRMED" && booking.endsAt < now`. Reason: marking a future booking complete is meaningless and almost always a misclick. |
|
||||
| Mark complete / no-show idempotency | Re-marking returns `{ ok: true }` (no-op). Switching from one final state to another is rejected. Reason: prevents accidental status flips after the fact. |
|
||||
| Cancel reason on reschedule | `"rescheduled"` literal in `Booking.cancelReason`. Reason: distinguishable from customer/admin cancellation in audit; could drive metrics later. |
|
||||
| Authorization on customer detail page | Owner or admin can view; otherwise redirect to `/account/bookings`. Same on cancel action. |
|
||||
| Authorization on admin detail page | Layout already requires `role=ADMIN`; actions re-check on the safe side. |
|
||||
| `/account/bookings/[id]` reachable by admins | Yes — they may want to debug from a customer's POV. The cancel button still works. |
|
||||
| Reschedule preserves service by default | `rescheduleBooking` defaults newServiceId/therapist/room to the old booking's. The customer flow always passes the same serviceId; admin could pass different ones if/when we build admin-side reschedule. |
|
||||
| Mark-* helpers return discriminated unions | `{ ok: true }` or `{ ok: false; reason: string }`. Reason: same shape as `cancelBooking` so the UI can react without exceptions. |
|
||||
|
||||
## Gotchas hit
|
||||
|
||||
None — clean session.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. Customer-visible brand name (still pending)
|
||||
2. Currency
|
||||
3. Stripe account ownership
|
||||
4. **NEW**: should reschedule require explicit confirmation if the old booking was CONFIRMED with a deposit? Not relevant until 5d Stripe.
|
||||
5. **NEW**: admin "reschedule on customer's behalf" UI flow — currently admins cancel and the customer rebooks. We have the `rescheduleBooking` helper; the admin UI for it is a future enhancement.
|
||||
|
||||
## Roadmap status
|
||||
|
||||
UX-completeness:
|
||||
|
||||
- A — Admin CRUD: done 2026-05-02
|
||||
- B — Therapist self-serve: done 2026-05-02
|
||||
- **C — Booking lifecycle UX: done 2026-05-02 (this session)**
|
||||
- D — PWA shell (per `BuildLog.md` §11)
|
||||
- E — Polish — 404, empty states, mobile review, dark-mode glance
|
||||
|
||||
## Recommended next step
|
||||
|
||||
**Phase D — PWA shell**. Per `BuildLog.md` §11, this is the "Stage 1" mobile story we promised at bootstrap:
|
||||
|
||||
- `manifest.webmanifest` with name, icons, `display: "standalone"`, theme color, start URL
|
||||
- A minimal service worker (`next-pwa` or hand-rolled with Workbox) — **network-first for HTML, stale-while-revalidate for static assets, no caching of API/booking endpoints**
|
||||
- iOS Safari quirks: separate `apple-touch-icon`, `apple-mobile-web-app-capable`, status bar style
|
||||
- Test on a phone or in DevTools "Add to Home Screen"
|
||||
|
||||
~half-day to a full day. After D, Phase E (~half-day): 404 page, empty-state polish, mobile-first audit, dark-mode glance.
|
||||
|
||||
## How to resume
|
||||
|
||||
```bash
|
||||
cd /Users/noise/Documents/code/touchbase
|
||||
docker-compose up -d postgres mailpit
|
||||
pnpm db:seed
|
||||
pnpm tsx scripts/book-on-behalf.ts alex@example.com "60-minute Swedish" 2026-05-05T10:00
|
||||
pnpm dev
|
||||
# As alex: sign in → /account/bookings → click row to see detail → Reschedule → pick new time → confirm
|
||||
# As admin: sign in → /admin/bookings → click row → view detail with action buttons
|
||||
```
|
||||
Reference in New Issue
Block a user