customer account/bookings + cancellation email
This commit is contained in:
101
docs/progress/2026-05-02-customer-account.md
Normal file
101
docs/progress/2026-05-02-customer-account.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 2026-05-02 — Customer "My Bookings" + Cancel
|
||||
|
||||
> Companion to `Initial.md`. Predecessor: `2026-05-02-public-booking.md`. Closes Step 5c.2 of `Initial.md` §9.
|
||||
|
||||
## Milestone
|
||||
|
||||
Customers can now see and cancel their own bookings. The booking flow has a back half: book → see in "My bookings" → cancel if needed → confirmation email both ways. Practice can also cancel on a customer's behalf via the same action (admin role passes the authorization check).
|
||||
|
||||
## What landed
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `src/lib/booking.ts` | Added `cancelBooking()` — idempotent on CANCELLED, rejects COMPLETED/NO_SHOW, sets `status=CANCELLED` + `cancelledAt`/`cancelledBy`/`cancelReason`. Returns a typed result. |
|
||||
| `src/lib/email.ts` | Added `sendBookingCancellation()` — mirrors the confirmation template shape. |
|
||||
| `src/app/account/layout.tsx` | Auth-required customer shell with header (My bookings + Book a session + sign-out). Same shape as `/admin/layout.tsx` but no role check. |
|
||||
| `src/app/account/bookings/page.tsx` | Server-rendered list of the user's bookings, split into Upcoming (with Cancel buttons) and Past (with status pills). Cancel server action validates ownership-or-admin, calls `cancelBooking` + `sendBookingCancellation`, redirects. |
|
||||
| `src/app/page.tsx`, `src/app/book/page.tsx` | Header link swaps to "My bookings" if signed in, else "Sign in". |
|
||||
| `src/app/book/done/page.tsx` | Added "My bookings →" link alongside "Back to services". |
|
||||
| `test/booking.test.ts` | +7 tests covering cancel: CONFIRMED→CANCELLED with audit fields, HOLD→CANCELLED, idempotent on CANCELLED, rejects COMPLETED + NO_SHOW, slot becomes bookable again after cancel, throws on unknown id. |
|
||||
|
||||
## What's verified end-to-end
|
||||
|
||||
- `pnpm test` — **71/71 green** (was 64; +7 cancel tests)
|
||||
- `pnpm lint` — clean
|
||||
- `pnpm exec tsc --noEmit` — clean
|
||||
- Live smoke test (curl + Mailpit, dev server):
|
||||
- Booked Alex Park into 60-min Swedish at Tue May 5 10:00 EDT via the CLI
|
||||
- Signed in as alex@example.com via magic link → landed on `/account/bookings`
|
||||
- Page shows the booking row: "60-minute Swedish", "Tue, May 5, 10:00 AM", with a Cancel button
|
||||
- Header swaps: anonymous shows "Sign in", signed-in shows "My bookings" — verified on `/` and `/book`
|
||||
|
||||
The cancel server action's POST wasn't replayed via curl (Next.js 16 RPC encoding rejects manual replays — same constraint we documented previously); the underlying `cancelBooking` function has 7 dedicated tests + the action body is otherwise straight composition with `sendBookingCancellation` (also tested) and `redirect()`.
|
||||
|
||||
## Decisions ratified
|
||||
|
||||
| Decision | Resolution |
|
||||
|---|---|
|
||||
| Cancellation policy | None for v1 — anyone can cancel their own booking anytime. Practice can refine (e.g. "no cancellations within 24h") when they have data on no-show patterns. |
|
||||
| Cancellation authorization | Owner OR admin. Action checks both even though page only shows the user's own bookings (defense in depth). |
|
||||
| Status transitions disallowed | COMPLETED → CANCELLED and NO_SHOW → CANCELLED are rejected. They're historical; cancellation doesn't make sense after the fact. |
|
||||
| `cancelBooking` return shape | Discriminated union: `{ kind: "cancelled", alreadyCancelled: bool }` or `{ kind: "rejected", reason: "completed"\|"no_show" }`. Reason: callers can react to "already cancelled" without an exception. |
|
||||
| Past bookings cap | Last 20 only. Cheap heuristic; pagination/infinite scroll waits until someone has ≥20 bookings. |
|
||||
| Past list status pill | Shows status (CANCELLED, NO_SHOW, COMPLETED). |
|
||||
| Page header for signed-in users | Inline `await auth()` in the home + book pages; no shared component yet. Keeps per-page data fetching local. |
|
||||
| Cancellation email | Includes `cancelReason` if set. Plain text, same SMTP transport. |
|
||||
|
||||
## Gotchas hit
|
||||
|
||||
### TS narrowing in test fixture helper
|
||||
TS narrowed `status !== "HOLD"` correctly, then complained about a redundant `status === "HOLD"` inside that block. Removed the dead branch.
|
||||
|
||||
## Open questions still unresolved
|
||||
|
||||
1. Customer-visible brand name
|
||||
2. Currency
|
||||
3. Stripe account ownership
|
||||
4. **NEW**: cancellation policy (window, fees, no-show charge) — defer until we have real customer data
|
||||
|
||||
## 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 — done 2026-05-01
|
||||
5. Public self-booking → Stripe → reminders
|
||||
- 5a Auth.js + admin shell — done 2026-05-02
|
||||
- 5b Admin "create booking" page — done 2026-05-02
|
||||
- 5c.1 Public booking page (no payment) — done 2026-05-02
|
||||
- **5c.2 Customer "my bookings" + cancel — done 2026-05-02 (this session)**
|
||||
- 5d Stripe deposit flow + webhook
|
||||
- 5e Email reminders (pg-boss scheduled jobs)
|
||||
|
||||
**Booking-flow features for v1 are now complete modulo payments and reminders.** A customer can book, see their bookings, and cancel. Admin can take phone bookings and view all upcoming. Both sides receive transactional emails.
|
||||
|
||||
## Recommended next step
|
||||
|
||||
**5d — Stripe deposit flow**. The biggest remaining piece. Sequence:
|
||||
|
||||
1. Stripe SDK + env config (test keys)
|
||||
2. `createPaymentIntent` for the deposit at `createHold` time (returns client_secret to render in the confirm page)
|
||||
3. Confirm page renders Stripe Elements (this is the first place we need a client component)
|
||||
4. Webhook handler at `/api/stripe/webhook` updates `Payment` and transitions `Booking` from HOLD to CONFIRMED on `payment_intent.succeeded`
|
||||
5. Hold expiry job via pg-boss cancels HOLDs whose deposit didn't capture in time
|
||||
|
||||
~3–5 days. After 5d, **5e reminders** is the smallest remaining piece (~2–3 days for pg-boss schedule + reminder template + send job).
|
||||
|
||||
## How to resume
|
||||
|
||||
```bash
|
||||
cd /Users/noise/Documents/code/touchbase
|
||||
docker-compose up -d postgres mailpit
|
||||
pnpm db:seed
|
||||
pnpm dev
|
||||
# As a customer:
|
||||
# http://localhost:3000 → click service → pick date → pick time
|
||||
# sign in via magic link (any email) → /book/confirm → enter name → confirm
|
||||
# land on /book/done → click "My bookings →"
|
||||
# /account/bookings → click Cancel → cancellation email arrives in Mailpit
|
||||
# As admin:
|
||||
# sign in as admin@touchbase.local → /admin/bookings to see all upcoming
|
||||
```
|
||||
Reference in New Issue
Block a user