CI / No Debug Code (push) Successful in 3s
CI / Coding Standards (push) Successful in 46s
CI / Tests (PHP 8.1) (push) Successful in 45s
CI / Tests (PHP 8.2) (push) Successful in 45s
CI / PHPStan (push) Successful in 1m11s
CI / Tests (PHP 8.3) (push) Successful in 1m0s
CI / Build Plugin Zip (push) Successful in 1m10s
Adds POST /bookings/{id}/cancel (owner-only, idempotent): marks the lesson
cancelled, releases the availability slot for rebooking, and voids a
still-pending payment so it leaves the admin confirmation queue. Paid
payments are untouched — refunds stay a manual admin decision.
The instructor PATCH /bookings/{id}/status path now does the same slot
release and payment voiding on cancellation (previously cancelled lessons
left their slot permanently booked), and reinstating a cancelled lesson
re-claims the slot, rejecting with 409 if the freed time was rebooked.
The "Your upcoming lessons" panel gets a Cancel button with a confirm
prompt; on success both the lesson list and the slot calendar refresh.
Co-Authored-By: Claude Fable 5 <[email protected]>
97 lines
6.6 KiB
Markdown
97 lines
6.6 KiB
Markdown
# 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) 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).
|
|
|
|
`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`
|