Booking a slot with no priced offering created the lesson but no payment,
yet the front end still called POST /payments/intent, which 400ed with
"Could not start payment for this registration" — the student saw an error
while the backend held a claimed slot and a lesson stuck at pending.
- POST /bookings and POST /enrollments now return a `payment` summary
({id, method, status}) or null when nothing is owed; the JS only runs
the payment step when a payment exists.
- Bookings with nothing owed are confirmed at creation — there is no
payment step that would ever confirm them later.
- The booking page now shows the student's upcoming lessons (GET /bookings,
now scoped to upcoming non-cancelled lessons with slot start/end times)
with a pending-payment/confirmed status badge.
Fixes #53
Co-Authored-By: Claude Fable 5 <[email protected]>
5.9 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 an agenda list or a weekly calendar (view toggle with previous/next-week navigation; times shown in 12-hour AM/PM form). - Student picks an offering (a 30 or 60-minute private-lesson type) and a slot.
- 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 (no offering, or 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).
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 |
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).
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
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