Files
unsupervised-scheduler/docs/features/lesson-booking.md
T
thatguygriffandClaude Fable 5 e324c5d585
CI / Tests (PHP 8.1) (pull_request) Successful in 42s
CI / Tests (PHP 8.2) (pull_request) Successful in 39s
CI / No Debug Code (pull_request) Successful in 1s
CI / Coding Standards (pull_request) Successful in 2m46s
CI / PHPStan (pull_request) Successful in 2m49s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m34s
CI / Build Plugin Zip (pull_request) Skipped
Hide the My Lessons menu for users who already see Scheduler
Scheduler (view_all_lessons) is a superset of My Lessons — same template,
every instructor's lessons, same payment edit forms — so for an
owner-operator both menu items showed the same data twice. The My Lessons
menu item is now only registered for users without view_all_lessons;
instructors are unaffected.

Closes #85

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 11:11:22 -03:00

8.1 KiB
Raw Permalink Blame History

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 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).
  2. 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.
  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 (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) 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 upfront as a single payment linked to the series' first (anchor) lesson:

  • billing_mode = full_term — the offering's price already covers the term and is charged once.
  • billing_mode = one_time — the per-lesson price is charged once per occurrence actually claimed (price × N).

Settling that payment (Stripe webhook, e-transfer confirmation, comp) confirms every non-cancelled lesson in the series (BookingRepository::updateStatusForSeries()), not just the anchor row.

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. Hidden for users who also hold view_all_lessons — Scheduler is a superset, so the menu item would only duplicate it.

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; 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