The front-end booking calendar now opens in the Week view (anchored to the week of the earliest open slot) with List still available. The Scheduler and My Lessons admin pages gain a week calendar (usc_view/usc_week, bucketed via a new generic WeekCalendar::bucket()) and open in it by default; the original table remains as the List view since it carries the HST / e-transfer forms. Closes #76 Co-Authored-By: Claude Fable 5 <[email protected]>
7.6 KiB
Feature: Lesson Booking
Overview
Students register for a private lesson by choosing an offering, picking a time (or reserving a weekly slot for the term), answering the offering's intake questions, accepting current policies, and paying. Instructors confirm or cancel from wp-admin or via the REST API. A lesson becomes confirmed only once its payment is paid (or the student is comp'd).
Data Model — {prefix}us_lessons
| Column | Type | Notes |
|---|---|---|
id |
BIGINT UNSIGNED | Primary key |
slot_id |
BIGINT UNSIGNED | FK → us_availability.id |
offering_id |
BIGINT UNSIGNED | FK → us_offerings.id (the private-lesson type booked) |
student_id |
BIGINT UNSIGNED | WordPress user ID |
instructor_id |
BIGINT UNSIGNED | WordPress user ID (denormalised for fast queries) |
recurrence |
VARCHAR(10) | single or weekly |
series_id |
BIGINT UNSIGNED | Nullable — groups the lesson rows of one weekly reservation |
status |
VARCHAR(20) | pending / confirmed / cancelled |
payment_id |
BIGINT UNSIGNED | Nullable FK → us_payments.id |
notes |
TEXT | Optional student notes |
created_at |
DATETIME | Insertion time |
Registration Flow
- Student opens the page with the
[us_booking]shortcode and browses open slots as a weekly calendar (the default, anchored to the week of the earliest open slot) or an agenda list (view toggle with previous/next-week navigation; times shown in 12-hour AM/PM form). - Student picks a slot and an offering (a 30 or 60-minute private-lesson type). When the slot is tied to an offering the form shows it locked (the student sees exactly what they are booking); otherwise the form presents the instructor's active private-lesson offerings whose duration fits the slot. Every booking requires an offering — a generic slot with no fitting offering cannot be booked online.
- For a
weeklyreservation, the same weekday/time is held for the rest of the offering's term. - Student answers the offering's questions (
GET /offerings/{id}/questions). - Student accepts the current published policy versions (
GET /policies) — required to continue. - Payment is taken per the student's billing method (card by default;
pendingfor e-transfer; skipped for comp). Seepayments.md. POST /bookingscreates the lesson row(s) (status = pending), records answers and policy acceptances, marksus_availability.is_booked = 1, and links the payment. A booking with nothing owed (a free offering) creates no payment and isconfirmedimmediately.- On successful payment (or comp) the lesson is
confirmedand a receipt is emailed. - Instructor sees the booking under My Lessons and may update status via
PATCH /bookings/{id}/status. - The booking page also shows the student their upcoming lessons (
GET /bookings) with a per-lesson status badge (pending payment / confirmed) and a Cancel button.
Cancellation
Students cancel their own lessons via POST /bookings/{id}/cancel (idempotent).
Cancelling marks the lesson cancelled, frees the availability slot for
rebooking, and voids a still-pending payment (marked failed so it leaves the
admin confirmation queue). A paid payment is never touched — refunds are a
manual, admin-side decision. Instructors cancelling via
PATCH /bookings/{id}/status get the same slot release and payment voiding;
reinstating a cancelled lesson re-claims its slot and fails with 409 slot_taken if the freed time was booked by someone else in the meantime.
Weekly Reservations
A weekly reservation creates one series_id shared across N lesson rows (one per
week in the term) and reserves the matching availability windows. It is billed
full-term upfront as a single payment (billing_mode = full_term on the
offering).
REST API
| Method | Endpoint | Permission |
|---|---|---|
GET |
/wp-json/us-scheduler/v1/bookings |
Any logged-in user |
POST |
/wp-json/us-scheduler/v1/bookings |
book_lesson |
POST |
/wp-json/us-scheduler/v1/bookings/{id}/cancel |
Logged-in owner of the lesson |
PATCH |
/wp-json/us-scheduler/v1/bookings/{id}/status |
manage_availability or admin |
POST /bookings body: offering_id, slot_id, recurrence, answers[]
(question_id → value), accepted_policy_version_ids[], and payment data
(see payments.md). The response includes ids, the resulting lesson
status, and payment — a {id, method, status} summary, or null when
nothing is owed (the front end then skips the payment step).
An offering is always required (400 offering_required otherwise): a slot tied
to an offering uses that offering regardless of the request, while a generic
slot uses the student's offering_id, which must be one of the instructor's
active private_lesson offerings whose duration_minutes matches the slot.
GET /bookings returns the caller's upcoming, non-cancelled lessons (their own
for students; the instructor's for callers with manage_availability), each
with the slot's start_dt/end_dt.
Group classes follow the same registration flow but enrol against an offering of
kind group_class; see group-classes.md.
Admin Interface
- Scheduler (
view_all_lessons— studio admin / administrators): all upcoming lessons across all instructors - My Lessons (
view_own_lessons): upcoming lessons for the logged-in instructor
Both pages open in a Week calendar view by default (usc_view/usc_week
query params, same pattern as the availability page, bucketed via
Availability\WeekCalendar), with the original table available as the List
view — the list is where the per-lesson HST and e-transfer edit forms live.
Frontend Shortcodes
[us_booking]— student calendar + registration flow; requiresbook_lessoncapability[us_student_login]— front-end login form for students
Implementation
- Repository:
Unsupervised\Schedular\Booking\BookingRepository(insertSeries()builds a weekly series sharing aseries_id) - Model:
Unsupervised\Schedular\Booking\Lesson - Registration gate:
Unsupervised\Schedular\Registration\RegistrationGate— validates and records intake answers + booking-scoped policy acceptances; shared with group enrolment - Admin controller:
Unsupervised\Schedular\Booking\LessonController - REST endpoint:
Unsupervised\Schedular\Booking\BookingEndpoint - Frontend:
Unsupervised\Schedular\Booking\BookingPage,Unsupervised\Schedular\Auth\LoginPage
Payment seam: a priced booking is created with
status = pendingand its payment linked viapayment_id; the lesson is confirmed when the payment is settled (seepayments.md) or manually viaPATCH /bookings/{id}/status. Unpriced bookings skip the seam entirely and are confirmed at creation.GET /policies?scope=bookingreturns just the booking-gate policies the form must collect.
Tests
tests/Unit/Booking/BookingRepositoryTest.phptests/Unit/Booking/LessonTest.php