# Resort Class Booking System

Build a secure one-resort class/activity booking system.

## Core stack

- Next.js App Router
- TypeScript
- Supabase Auth
- Supabase Postgres
- Supabase Storage
- Stripe Checkout
- Stripe Webhooks
- Resend for emails
- Tailwind CSS
- FullCalendar
- QR code generation
- English + Thai support

## Domains

Public website is separate static HTML.

Booking app:
- booking.resort.com

Admin app:
- admin.resort.com

Both should visually match the existing resort website, but simpler and more functional.

Do not build SaaS/multi-tenant logic. This is for one resort only.

---

## Main concept

This system handles scheduled public classes/activities only.

No private bookings.
No room booking integration.
Cloudbeds handles accommodation separately.

---

## User roles

Roles:
- super_admin
- admin
- staff
- customer

Super admin:
- full access
- manage admins/staff
- system-level access

Admin:
- manage activities
- manage sessions/classes
- manage instructors
- manage bookings
- promo codes
- reports

Staff:
- view all classes
- view their own classes
- view attendee lists
- add walk-ins
- optional QR check-in
- enter attendance after class

Customer:
- book classes
- buy packages
- view account
- view bookings/history/credits

---

## Activity templates

Activities are reusable templates.

Fields:
- id
- slug
- title_en
- title_th
- short_description_en
- short_description_th
- long_description_en
- long_description_th
- cover_image_url
- gallery_images
- category_id
- difficulty
- default_duration
- default_capacity
- regular_price_thb
- resort_guest_price_thb
- credit_cost
- booking_cutoff_minutes default 10
- default_primary_instructor_id
- active
- created_at
- updated_at

Activities should have proper public pages.

Example:
- /classes/yoga
- /classes/ice-bath

---

## Sessions/classes

Sessions are manually created from activity templates.

No repeating classes in MVP.

Fields:
- id
- activity_id
- title override optional
- start_time
- end_time
- capacity
- status: draft / published / cancelled / completed
- primary_instructor_id
- created_by
- actual_attendance_count
- created_at
- updated_at

Admins/staff should be able to:
- create session from template
- duplicate old session
- edit date/time/capacity/instructor/status
- save as draft
- publish
- cancel/reschedule and notify attendees

Draft sessions:
- visible only in admin
- not bookable

Published sessions:
- visible publicly
- bookable until cutoff

Cancelled:
- not bookable

Completed:
- used for reports/history

---

## Instructors

Instructors should have profiles.

Fields:
- id
- name
- slug
- photo_url
- bio_en
- bio_th
- specialties
- active
- linked_user_id optional

Sessions support:
- primary instructor
- supporting instructors

Use join table:
- session_instructors
- session_id
- instructor_id
- role

Public users can filter by instructor.

---

## Public booking flow

Customer can browse from:
- booking.resort.com/classes
- booking.resort.com/calendar
- booking.resort.com/packages
- activity-specific pages
- Today’s Classes section

Views:
- calendar view
- list view
- activity page upcoming sessions

Customer can:
- choose quantity before adding to cart
- edit quantity in cart
- add multiple classes to cart
- checkout once with Stripe

Cart persists for 3 days.

Cart does not reserve spots.

Capacity must be revalidated:
1. when adding to cart
2. before creating Stripe Checkout
3. after Stripe webhook confirms payment

Warn if classes overlap, but do not block checkout.

---

## Pricing

Currency:
- THB only

Prices are tax-inclusive.

Customer sees final price only.

Store:
- subtotal
- tax_amount
- total_price

Customer types:
- regular guest
- resort guest

Regular guests pay regular_price_thb.

Resort guests can unlock resort_guest_price_thb by entering:
- room number
- last name

Do not verify room number in MVP.
Store the entered room number and last name on the order.

Pricing must always be calculated server-side.

Never trust frontend price.

---

## Promo codes

Admins can create promo codes.

Fields:
- code
- discount_type: percentage / fixed_amount
- discount_value
- applies_to: all / selected_activities / selected_packages / both
- valid_from
- valid_until
- max_uses
- used_count
- active
- stack_with_resort_discount boolean

Default:
- promo codes can stack with resort guest pricing
- but admin can change this per promo/settings

Support 100% discount codes.

---

## Packages

Support both:

1. Credit packages
2. Fixed bundles

Package fields:
- id
- name_en
- name_th
- description_en
- description_th
- type: credit_package / fixed_bundle
- regular_price_thb
- resort_guest_price_thb
- active

Credit package:
- gives customer class credits
- credits do not expire in MVP

Activities can have different credit costs.

Example:
- Yoga = 1 credit
- Workshop = 2 credits

MVP rule:
An order uses either:
- normal payment
or
- package credits

Do not mix credits + payment in same order initially.

If customer has enough credits:
- suggest using credits
- customer confirms

---

## Orders and bookings

Use order/item model.

booking_orders:
- id
- customer_id nullable initially
- customer_name
- customer_email
- is_resort_guest
- room_number
- guest_last_name
- status: pending / confirmed / cancelled / refunded
- payment_status: unpaid / paid / offline_paid / refunded
- payment_method: stripe / offline / room_charge / promo
- stripe_checkout_session_id
- subtotal_thb
- tax_amount_thb
- total_thb
- promo_code_id
- created_at

booking_items:
- id
- order_id
- session_id
- quantity
- unit_price_thb
- total_price_thb
- credit_cost_total
- qr_token
- checked_in_at optional
- checked_in_by optional
- created_at

One order can contain multiple booking items.

Each booking item gets one QR code.

If quantity is 3 for one class:
- one QR code covers all 3 people

Only main booker name/email required.
No phone required.

---

## Checkout

Use Stripe Checkout only.

No custom card forms.
Do not store cards.
Do not save payment methods.

Flow:
1. Customer creates cart
2. Server validates prices/capacity/cutoff
3. Server creates pending booking_order and booking_items
4. Server creates Stripe Checkout Session
5. Customer pays
6. Stripe webhook verifies payment
7. Webhook marks order paid/confirmed
8. Webhook creates/finds customer account
9. Confirmation email sent
10. QR codes generated/sent

Never confirm payment from success page.
Only Stripe webhook can confirm payment.

Verify Stripe webhook signature.

---

## Accounts

Customers can checkout as guests.

After payment:
- create/find customer account by email
- send magic login link
- customer can optionally set password later

Customer login:
- magic link
- optional email/password

Staff/admin login:
- email/password

Customer portal:
- upcoming bookings
- past bookings
- attended classes
- package credits
- credit usage history
- invoices/receipts
- profile settings

Customers cannot self-cancel or reschedule.
They must contact staff.

---

## Staff/admin backend

Admin should be mobile-first.

admin.resort.com sections:
- Dashboard
- Calendar
- Today’s Classes
- Activities
- Sessions
- Instructors
- Bookings
- Packages
- Promo Codes
- Reports
- Staff Users

Staff dashboard:
- today’s classes
- my classes
- attendee lists
- add walk-in
- optional QR scan
- enter actual attendance after class

Staff can see all classes, but “My Classes” filters by assigned instructor.

Admin can create more users.

---

## Walk-ins/manual bookings

Staff can add walk-ins from Today view.

Fields:
- session
- name optional
- email optional
- quantity
- payment method: offline / room_charge / promo

Manual bookings reduce capacity.

If email is entered:
- send confirmation
- send QR code
- create/find customer account

If no email:
- internal booking only
- no confirmation
- no portal
- no reminders

---

## Attendance

QR check-in is optional.

Staff can:
- scan QR
- search by name
- mark booking item checked in

But MVP main attendance flow:
- after class, staff enters total actual attendance count

Store:
- booked quantity
- checked in quantity
- actual attendance count

Reports should show:
- booked
- attended
- no-show rate

---

## Notifications

Use Resend.

Send:
- booking confirmation
- 24h reminder
- 1h reminder
- cancellation notice
- reschedule notice

Reminder includes:
- class name
- date/time
- instructor
- location
- quantity
- QR code
- basic confirmation info

Use Vercel Cron or Supabase scheduled functions for reminders.

When staff cancels/reschedules:
- prompt to notify attendees
- send email to affected bookings

No automatic refunds in MVP.

---

## Images

Staff can upload images directly from phone.

Use Supabase Storage.

Images must be automatically compressed/resized before upload or during processing.

Recommended:
- cover image max 2000px wide
- gallery image max 1600px wide
- instructor photo max 1200px wide
- use WebP if possible

No approval workflow.
Uploads publish directly.

Only images needed.
No PDFs or file attachments.

---

## Calendar

Use FullCalendar.

Public calendar:
- shows only published upcoming sessions
- no private/customer data
- filters by category, difficulty, instructor, date
- click session to view/add to cart

Admin calendar:
- shows draft/published/cancelled/completed
- create/edit/duplicate sessions
- view bookings
- view capacity
- view instructor
- mobile-friendly

---

## Reports

Admin dashboard should show:
- today’s classes
- upcoming classes
- total bookings
- total revenue
- revenue by date
- revenue by activity
- revenue by instructor
- package sales
- promo code usage
- resort guest vs regular guest bookings
- checked-in vs no-show
- CSV export

---

## Security

Critical rules:

- Enable Supabase RLS on all tables.
- Never expose service role key to browser.
- Never trust frontend price.
- Never trust frontend capacity.
- Never trust frontend payment status.
- Public APIs only return safe public fields.
- Admin APIs require authenticated admin/staff role.
- Only Stripe webhook can mark Stripe payments as paid.
- Verify Stripe webhook signature.
- Customer cannot view other customers’ bookings.
- Staff/admin access must be role-protected.
- Admin subdomain should not be indexed.
- Use server-side validation for all booking operations.
- Revalidate capacity at checkout and webhook.
- Prevent overbooking as much as possible with transaction-safe database logic.

---

## Multilingual

Public frontend supports:
- English
- Thai

Activities/instructors/packages need English and Thai fields.

Use next-intl or similar.

Admin can enter both English and Thai text.

Do not overbuild translation management.

---

## MVP exclusions

Do not build:
- private bookings
- room booking integration
- Cloudbeds sync
- recurring classes
- waitlist
- gifting packages
- saved cards
- customer self-cancellation
- automatic refunds
- minimum participants
- favorites/wishlist
- full CMS/settings editor
- multi-resort SaaS logic
- complex mixed payment with credits + cash

Build cleanly so these can be added later if needed.