PRD 09 — Storefront & Customer Booking
Status: draft · Priority tier: P0 · Size: L · Depends on: Foundation/auth (existing), Locations (PRD 01), Classes (PRD 02), Schedule (PRD 03), Appointments (PRD 04), Courses (PRD 05), Pricing Plans (PRD 06), Customers (PRD 07), Sales & Payments (PRD 08), Notifications (PRD 11), Website Builder (PRD 12)
1. Summary
The public, customer-facing site and the core customer loop: browse (home, timetable, classes, appointments, courses, packages/memberships, team, outlets, blog, FAQ, contact, legal), sign up / log in, then book a class session, book an appointment slot, or buy a package/membership, and manage it all from an account area. Without it the product has no customers; every admin PRD feeds this one.
2. Problem & goals
Customers must find a session or plan and pay/book with as few steps as possible, on a phone first. Goals:
- Guest reaches the login gate from the timetable in 1 click (original: 1).
- Signed-up customer books a free class in <= 3 clicks from the timetable after login (original: not observed).
- Public pages show only published, upcoming, store-visible content; zero admin-only data leaks.
- Pages usable at 390 px width with no horizontal scroll. Non-goals: theme/page editing (PRD 12); admin-side booking and sales (PRDs 03, 04, 08); reviews, referrals, cashback, loyalty; native apps; embed widget (PRD 15).
3. Users & roles
- Guest: browses all public pages; blocked at Book Now / Buy now.
- Customer: logged-in account holder; books, buys, cancels, edits profile.
- Family member (v2): dependant managed by a parent account.
- Owner/admin: configures content and rules elsewhere; sees results in Customers/Sales.
4. Scope
In (v1)
- must: public pages: home, timetable, classes list, packages, memberships, locate us (outlets), contact form.
- must: package/membership cards with Quick View and Buy now.
- must: customer signup (email, password rules, profile) and login using existing auth (assumption A1); login gate on Book Now / Buy now with return to the intended action.
- must: book a class session: free, with package/membership credits, or drop-in payment.
- must: buy a package/membership with a payment method, promo code, and (if enabled) pay later.
- must: account area: profile, upcoming/past bookings, cancel booking, credits/plans, purchases.
- must: mobile layout (menu drawer + fixed bottom bar); timezone chip in the footer.
- should: appointments page + slot booking; courses page; team; blog list/post; FAQ; legal pages (terms, privacy, refund and cancellation policies).
- should: waivers to sign before booking; mobile-number requirement/verification rules; bot check on signup/login/contact.
- should: class info popover on the timetable; filters (class, instructor, level); slots-left display.
-
could: family accounts; wallet balance; tracking pixels/UTM; floating WhatsApp button. Later (v2+)
-
should: waitlist ("Waitlisted" legend exists in the original timetable); class detail page; membership activation-date choice; saved cards.
-
could: reviews on class/appointment pages (toggles exist in PRD 02/04). Out
-
Shared cross-studio customer identity ("RezervPass" style): single-tenant product, no partner network; reason: privacy and scope.
- Mobile-OTP password reset as the only recovery path: replaced by existing email-OTP/Google login (A1).
- Branded native mobile app and ClassPass bookings.
5. User stories
- US-09-01 As a visitor, I want to browse the timetable, classes, plans, team and outlets without an account so that I can decide before committing.
- US-09-02 As a visitor, I want to sign up with email and a short profile so that I can book.
- US-09-03 As a customer, I want to log in and return to the thing I was about to book so that I do not start over.
- US-09-04 As a customer, I want to book a class using my package credit, or pay for a drop-in, so that I pay in the way I bought.
- US-09-05 As a customer, I want to pick an appointment slot (location, service, staff, time) so that I can book a private session.
- US-09-06 As a customer, I want to compare packages and memberships, preview details, and buy one with a promo code so that I get the best deal.
- US-09-07 As a customer, I want to see upcoming and past bookings and cancel one so that I stay in control.
- US-09-08 As a customer, I want to see my remaining credits, active plans, purchases and signed waivers so that I know what I own.
- US-09-09 As a parent, I want to book for my family members so that I manage everyone in one place.
- US-09-10 As a mobile visitor, I want a bottom bar for My Bookings, Book now, Packages and Memberships so that key actions are one tap away.
- US-09-11 As a visitor, I want to send the studio a message so that I can ask a question before booking.
- US-09-12 As a visitor in another timezone, I want to see the studio's timezone so that I read session times correctly.
6. Functional requirements
| ID | requirement | priority | evidence |
|---|---|---|---|
| FR-09-01 | Public shell: top nav (logo/name, Home, Classes, Pricing > Packages / Memberships, Timetable, Locate us, Team, Blog, Contact, login button), footer (contact, FAQ, legal links, timezone chip). Nav items appear only when that content type has published content or is enabled. | must | S160, S170 |
| FR-09-02 | Home: hero, about, packages/memberships CTA, featured classes, outlets, latest blog; each section hidden (not placeholder text) when it has no content. Content is configured via PRD 12. | must | S160, S209zh |
| FR-09-03 | Timetable: shows only published, upcoming sessions within the studio's timetable window (PRD 02 settings); horizontal date strip (14 days), per-day heading with count, row = time, duration, class, location/room, Book Now; empty day shows "0 classes"; empty timetable shows a friendly message. | must | S164, S236, S209zj |
| FR-09-04 | Timetable filters: class, instructor, level; month label with date jump. | should | S236 |
| FR-09-05 | Timetable shows slots left per PRD 02 rule (always / only when <= N; optional total capacity), "Full" when 0, and a Booked marker on sessions the logged-in customer holds. | must | 02 S24, S236 |
| FR-09-06 | Class info popover on a timetable row shows the class description (no separate class page in v1). | could | S238 |
| FR-09-07 | Classes page: lists only classes flagged for store listing (PRD 02) with category tabs and filters (type, sub-category, level); card = image, name, duration, mode tag. A listed class with no upcoming session still shows. | must | S161, S237 |
| FR-09-08 | Appointments page: pick location, then service (name, duration, from-price), then staff (or any) and slot; only slots that satisfy PRD 04 availability and booking rules are offered. | should | 07 features, S209m-q |
| FR-09-09 | Courses page: list of published courses with date, price, tickets left; ticket purchase follows the checkout in FR-09-24. | should | 07 features |
| FR-09-10 | Packages and Memberships pages: grouped by category; card = name, price, payment type (one-time / recurring), credits, location access, validity, Quick View, Buy now. Only active, store-visible plans whose restriction (all / segment / specific customers / new-members-only) allows the viewer; ineligible plans are hidden or marked (open question 6). | must | S162, S163, S245, S209zk |
| FR-09-11 | Quick View dialog: name, price, description, payment type, location access, validity, shareable flag, class credit count, onsite/online scope, Buy now. | must | S246, S209zm |
| FR-09-12 | Team page: staff cards (photo, name, bio) for staff flagged public. Locate us: outlet cards (name, address, phone, map link). Blog: list with type filter and post page. FAQ: accordion. Each shows an empty state when no content. | should | S165-S169 |
| FR-09-13 | Contact form: name, email, mobile (optional), subject (select), message; bot check; success message; studio is notified by email (PRD 11). Rate-limited per IP. | must | S168, F64 |
| FR-09-14 | Legal pages: terms and conditions, privacy policy, refund policy, class and appointment cancellation policies (text from settings); policies are shown at payment and linked on the booking confirmation. | must | S155, S243 |
| FR-09-15 | Login gate: Book Now / Buy now / My Bookings for a guest opens the login dialog over the current page (URL unchanged); after login the original action resumes. No guest checkout. | must | S239, S247, S209zn |
| FR-09-16 | Login uses existing auth (A1): email-OTP and Google. Dialog states: choose method, enter email, enter code, error/expired. | must | S171, existing auth |
| FR-09-17 | Signup: step 1 email; step 2 profile page. With A1 the "password" step becomes email-OTP verification; if a password option is later added the rule is >= 8 characters with letters, numbers and 1 symbol, with confirm-mismatch error. | must | S175, S177, S178, F61 |
| FR-09-18 | Profile step fields: first name, last name (required); mobile (optional unless the studio requires it), photo (optional), date of birth (optional), gender (optional: male / female / prefer not to say), marketing consent (email + SMS, unchecked by default), legal line linking studio terms and privacy. | must | S179, S243 |
| FR-09-19 | The customer record is created (or matched to an existing admin-created customer by email) when the profile is submitted, not earlier; abandoned signups leave nothing behind. | must | F61 |
| FR-09-20 | Signup errors (invalid/undeliverable email, duplicate, server error) show inline and persist until dismissed, never as an auto-hiding toast only. Email format and domain are validated before the final step, not at Submit. | must | S244, F83 |
| FR-09-21 | Mobile-number rules (studio setting): not required / required at signup / verify by code at first purchase of selected plans, first booking of selected classes or appointments. Verification needs an SMS provider (open question 8). | could | S154 |
| FR-09-22 | Book a class (logged in): Book Now > summary (class, time, location, attendees count if group booking is on) > choose how to pay > confirm > confirmation with booking reference. Options offered are only those valid: Free (class is free), Use credits (customer holds an eligible active package/membership with enough credits and the class accepts it), Pay drop-in (class has a direct price). | must | 08 S209h, S209o-q, F60 |
| FR-09-23 | Booking rules applied server-side: booking window (earliest/latest, PRD 02), capacity (no overbooking under concurrency), no duplicate booking of the same session by the same customer, session not cancelled/past, required waiver signed (if enforcement = must), customer eligibility. Failure gives a specific message. | must | 02 S24, S152b |
| FR-09-24 | Checkout page (plans, courses, drop-ins): order summary, promo code field (applies per PRD 06 rules, shows discount line, error for invalid/expired/used), payment method list (enabled gateway methods and manual methods with the studio's instructions), pay-later option when enabled, price total with tax per PRD 08, Pay button disabled until valid, and policy link. | must | S203-S207, S150, S133 |
| FR-09-25 | Pay later: if the studio enables it, the customer books/buys now with status Unpaid and a due rule (on fulfilment or within N days) and optional reminder email; unpaid plans can book only if "allow booking with unpaid plan" is on. | should | S150, S209t |
| FR-09-26 | Buying a package/membership: on success the plan appears in the account with start date, expiry, credits; credits are usable immediately for eligible classes; membership activation date = purchase date in v1. A purchase rule can require an active membership before a package may be bought. | must | S209ze, S150, F63 |
| FR-09-27 | Payment failure/abandon: no booking or plan is created as paid; the customer sees the error and can retry; a held seat is released after a short hold (open question 3). | must | gap |
| FR-09-28 | Confirmation screen + email (PRD 11): what, when, where (address, directions link), booking ref, how paid, cancellation policy, link to My Bookings. | must | 06 notification editor |
| FR-09-29 | Appointment booking (logged in): slot selection (FR-09-08) > summary > payment (credits / drop-in / pay later) > confirm; same rules engine as FR-09-23 plus staff/resource availability and minimum notice. | should | S209m-t |
| FR-09-30 | Account area (route behind login): Profile, Bookings, Plans & credits, Purchases, Waivers, Family (flagged). Guests are redirected to the login dialog. | must | gap |
| FR-09-31 | Profile: edit name, mobile, photo, DOB, gender, marketing consent; email change through existing auth flow; delete-account request (open question 7). | must | S179, existing user profile |
| FR-09-32 | Bookings: Upcoming and Past tabs; row = date/time, class or service, location, status (Booked, Completed, Cancelled, No show, Late cancelled), payment option; detail with ref and policy; empty states. | must | S209zf, gap |
| FR-09-33 | Cancel booking: Cancel on an upcoming booking when self-cancel is on; confirmation dialog states whether it is late (inside the late-cancel window) and what happens to credits/payment; result updates status and returns/forfeits credits per studio rule (open question 2). Inside the window is flagged late, not blocked, in v1. | must | 02 S24/S33, S209z |
| FR-09-34 | Plans & credits: each plan shows name, credits left/total, start, end, status (Active, Expired, Used up, Inactive), bookings made with it. | must | S209ze |
| FR-09-35 | Purchases: list of transactions (date, number, item, method, amount, status) with receipt/invoice download and pay-now button for unpaid items. | should | S209zg, S207 |
| FR-09-36 | Waivers: list of studio waiver forms with signed/unsigned state; sign inline (typed name + checkbox + timestamp); unsigned required waiver blocks booking with a link to sign. | should | S152, S209x |
| FR-09-37 | Family accounts (flag): parent adds dependants (name, DOB); can book/cancel/purchase on behalf and sees their bookings; dependants have no login. | could | S153 |
| FR-09-38 | Mobile (<= 640 px): hamburger drawer with the same nav plus login row; fixed bottom bar (My Bookings, Book now, Packages, Memberships); cards stack; login dialog is full-width; tap targets >= 44 px. Logged-in mobile account layout: open question 9. | must | S183, S184, S248-S250 |
| FR-09-39 | Timezone chip in the footer shows the studio timezone and offset; all session times render in the studio timezone with the zone label on booking confirmation (customer-local conversion is v2). | must | S160 |
| FR-09-40 | SEO and sharing: page title/description, readable slugs, 404 page; unpublished or hidden content returns 404 even by direct URL. | should | S100f, S22c |
| FR-09-41 | Bot protection on signup, login code request and contact form (any captcha, studio-neutral). | should | S171, F62 |
| FR-09-42 | Public API endpoints are separate from admin ones, never return admin fields (internal notes, other customers, costs), and every account endpoint is behind use: [auth] and scoped to the caller's own customer record. |
must | CLAUDE.md safety |
| FR-09-43 | Optional tracking: Meta/GA/GTM/TikTok ids and UTM capture on bookings; floating WhatsApp button. | could | 07 features |
7. Screens
| Our screen | Modelled on | States |
|---|---|---|
| Shell: top nav, footer, timezone chip | S160, S170 | desktop, mobile, logged-out, logged-in |
| Home | S160, S209zh | filled, sections hidden when empty |
| Timetable | S164, S236 | loading, filled, empty day, empty week, full session, booked, filters open, error |
| Classes list | S161, S237 | empty, filled, filtered-empty |
| Appointments | S209m-q (admin analog) | location pick, service pick, slots, no slots |
| Courses | none (not observed) | list, detail |
| Packages / Memberships | S162, S163, S245, S209zk | empty, filled, restricted/ineligible |
| Quick View dialog | S246, S209zm | open |
| Team, Locate us, Blog, FAQ | S165-S169 | empty, filled |
| Contact | S168 | idle, invalid, sending, sent, error |
| Legal pages | S155 (settings text) | filled |
| Login / signup dialog | S171, S175, S177, S239 | method, email, code, error |
| Complete profile | S179, S243 | blank, invalid, submitting, error (persistent) |
| Booking summary + confirmation | none (not observed) | free, credits, drop-in, error, success |
| Checkout | S203-S207 admin analog | cart, promo applied/error, payment method, pay later, paying, failed, complete |
| Account: profile, bookings, plans, purchases, waivers, family | S209ze-zg admin analogs | empty, filled, cancel dialog |
| Mobile drawer + bottom bar | S183, S184, S248-S250 | open, closed |
8. Flows
- Guest to booking: Timetable > Book Now (1, gate) > login (email, code) > resumes at booking summary > choose pay option (1) > Confirm (1). Target: <= 3 clicks after login, plus typing the code.
- Signup: Login > Sign up > email > code > profile (name) > Submit. Original: 6 clicks; target <= 5.
- Buy plan: Packages > Buy now (or Quick View > Buy now) > login if guest > checkout (promo optional) > pay > confirmation with credits. Target: <= 4 clicks after login.
- Cancel: Account > Bookings > booking > Cancel > confirm (<= 4 clicks).
- Edge cases: session fills while the customer is in checkout (clear "sold out" message, no charge); promo invalid; credits insufficient (offer drop-in or buy plan); customer already booked; login code expired; plan expires between selecting and booking; payment pending (manual method) shows Pending approval status; admin-created customer with same email is matched on signup; session cancelled by studio (customer notified, credits returned).
9. Data model (guesses marked *)
Reuses User, Customer (PRD 07), ClassSession, Booking (PRD 02/03), Plan, PlanPurchase, Transaction (PRD 06/08). New or extended:
- CustomerProfile(customer_id, mobile, dob, gender, photo_url, marketing_consent, consent_at, signup_source)
- Waiver(id, title, body, enforcement) and WaiverSignature(waiver_id, customer_id, signed_at, signed_name)
- ContactMessage(id, name, email, mobile, subject, body, created_at, handled*)
- StorefrontPage content (hero, about, FAQ, blog, team flags): owned by PRD 12.
- FamilyMember*(parent_customer_id, name, dob)
- CheckoutHold(session_id, customer_id, expires_at) for seat hold. Booking gains: payment_option (free / credits(purchase_id) / drop-in(transaction_id) / unpaid), late_cancelled bool, cancelled_at, cancelled_by (customer/staff), source (store/admin).
10. Business rules & edge cases
- Rule precedence: session override > class settings > none (PRD 02); appointment equivalents from PRD 04.
- Only published, upcoming, store-visible items are public; hidden class, past session, draft, inactive plan return 404.
- Slots left never reveals capacity unless "include total" is set.
- Credits are consumed at booking and returned on in-window cancel; late cancel follows the studio rule (open question 2). Free classes cannot use credits.
- One booking per customer per session unless group booking adds attendees.
- Booking window and capacity are checked again at confirm, atomically.
- Customer times: studio timezone in v1; stored UTC.
- Signup never creates a customer from an unverified email (A1); marketing consent defaults off and is stored with timestamp.
- Passwords (if ever enabled) are never logged; tokens live only in
web/src/lib/auth.ts. - Payment secrets stay server-side; card data never touches our server (gateway-hosted fields).
- Cancellation/refund policy text shown before paying; acceptance recorded on the transaction*.
11. Permissions
| Action | Guest | Customer | Staff/Admin |
|---|---|---|---|
| Browse public pages, contact form | yes | yes | yes |
| Book, buy, cancel | gate | own records | via admin PRDs |
| Account area | redirect to login | own | no (admin sees via Customers) |
| Family dependants | no | own | no |
12. Acceptance criteria
- Given a guest on the timetable, when they click Book Now, then the login dialog opens, the URL is unchanged, and after login the booking summary for that session opens.
- Given a session hidden/unpublished, when requested by direct URL, then 404.
- Given an empty studio, then Home hides empty sections and list pages show an empty state, with no placeholder text.
- Given slots mode "<= 3 remain" and 5 left, then no count; at 3 left, "3 left"; at 0, "Full" and Book disabled.
- Given a free class, then credit option is not offered; given a paid class and no credits, then only drop-in is offered.
- Given a customer with 1 eligible credit, when they book, then the credit decreases by 1 and the booking shows "credits".
- Given capacity 1 and two customers confirming at once, then exactly one booking succeeds and the other sees a sold-out message with no charge.
- Given booking window "latest 2 hours before" and a session in 1 hour, then Book is rejected with a window message.
- Given late_cancel_hours = 12 and a cancel 11 hours before, then the dialog warns it is late and the booking is marked late cancelled.
- Given an invalid promo code, then an error shows and the total is unchanged; a valid one shows a discount line and the reduced total.
- Given a failed payment, then no paid purchase or booking exists and the customer can retry.
- Given an undeliverable email at signup, then the error appears inline before the profile page and persists.
- Given an abandoned signup after email, then no customer record exists.
- Given a required unsigned waiver, then booking is blocked with a sign link.
- Given a logged-out request to any account endpoint, then 401; given customer A, then customer B's bookings are never returned.
- Given a 390 px viewport, then the bottom bar shows four actions and the page has no horizontal scroll.
- Given the footer, then it shows the studio timezone and offset.
13. Dependencies & open questions
Depends: PRDs 02, 03, 04, 05, 06, 07, 08, 11, 12, existing auth. Assumptions
- A1: customers use our existing email-OTP and Google login (one identity, this studio only). The original's email + password + shared "RezervPass" account is not copied; the password rules above apply only if a password option is added.
- A2: no guest checkout (original gates every booking/purchase behind login).
-
A3: studio timezone only in v1. Open questions (never observed in recon)
-
Payment step: methods, layout, saved cards, promo entry, tax display never observed (signup was blocked). Minimal v1 in FR-09-24; gateway choice is open in PRD 08.
- Late cancel: credits returned or forfeited? Customer self-cancel cutoff and refund of drop-in payments? Not observed.
- Seat hold during checkout and its length: not observed.
- Account area (profile, bookings, plans, purchases, waivers, family, wallet) never seen; all FR-09-30 to 37 are minimal v1 guesses modelled on the admin customer tabs.
- Booking confirmation screen and class/appointment/course detail pages never seen; original had only an info popover on the timetable.
- Ineligible plans: hidden or shown with a warning? (release notes mention eligibility warnings and new-member-only plans.)
- Delete-account / data export for customers (privacy law dependent).
- SMS provider for mobile verification; mobile as login identifier (original allowed it) is not in v1.
- Logged-in mobile layout and bottom-bar badge/active states not observed.
- Waitlist behaviour: legend only.
- Plan gating: family accounts, mobile verification, pay later in original were plan-gated; pricing tier: TBD.
- Original rejected the example.com domain at signup (server-side); rule and whether it is a real deliverability check unknown.
14. Evidence
See evidence/evidence.md.
