Files
unsupervised-scheduler/docs/features/lesson-booking.md
T
thatguygriffandClaude Fable 5 5888032ed7
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / PHPStan (pull_request) Successful in 2m46s
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Coding Standards (pull_request) Successful in 47s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m35s
CI / Build Plugin Zip (pull_request) Has been skipped
Skip payment step for unpriced bookings, confirm them immediately, show students their lessons
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]>
2026-07-05 17:02:38 -03:00

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

  1. 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).
  2. Student picks an offering (a 30 or 60-minute private-lesson type) and a slot.
  3. For a weekly reservation, the same weekday/time is held for the rest of the offering's term.
  4. Student answers the offering's questions (GET /offerings/{id}/questions).
  5. Student accepts the current published policy versions (GET /policies) — required to continue.
  6. Payment is taken per the student's billing method (card by default; pending for e-transfer; skipped for comp). See payments.md.
  7. POST /bookings creates the lesson row(s) (status = pending), records answers and policy acceptances, marks us_availability.is_booked = 1, and links the payment. A booking with nothing owed (no offering, or a free offering) creates no payment and is confirmed immediately.
  8. On successful payment (or comp) the lesson is confirmed and a receipt is emailed.
  9. Instructor sees the booking under My Lessons and may update status via PATCH /bookings/{id}/status.
  10. 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; requires book_lesson capability
  • [us_student_login] — front-end login form for students

Implementation

  • Repository: Unsupervised\Schedular\Booking\BookingRepository (insertSeries() builds a weekly series sharing a series_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 = pending and its payment linked via payment_id; the lesson is confirmed when the payment is settled (see payments.md) or manually via PATCH /bookings/{id}/status. Unpriced bookings skip the seam entirely and are confirmed at creation. GET /policies?scope=booking returns just the booking-gate policies the form must collect.

Tests

  • tests/Unit/Booking/BookingRepositoryTest.php
  • tests/Unit/Booking/LessonTest.php