Availability slots created without a tied offering could be booked with no offering at all: the lesson was free, confirmed immediately, and asked no intake questions. The booking UI never let the student choose an offering, contradicting the documented flow.
Changes
Server — Booking\BookingEndpoint
POST /bookings now rejects bookings with no resolvable offering (400 offering_required).
A student-chosen offering (generic slot) must be active, of kind private_lesson, owned by the slot's instructor, and its duration_minutes must match the slot — implementing the duration-matching rule from docs/features/availability-management.md.
A slot-tied offering remains authoritative over anything the client sends (existing anti-tamper behaviour unchanged).
Front end — assets/js/booking.js
Registration form gains a Lesson type field:
Slot tied to an offering → locked select showing title, duration, and price, so the student sees exactly what they are booking.
Generic slot → required picker of the instructor's active private-lesson offerings that fit the slot length (catalog cached per instructor).
Intake questions now load for whichever offering is selected.
Generic slots with no fitting offering show a "cannot be booked online" message instead of a doomed form.
Tests & docs
BookingEndpointTest: offering-less booking rejected; slot-tied offering charged when the request omits it; inactive / group-class / duration-mismatched offerings rejected.
docs/features/lesson-booking.md updated to match.
Deployment note
Existing generic slots become unbookable until the instructor ties an offering to them or has at least one active private-lesson offering matching their duration — the intended consequence of closing the free-booking hole.
composer test (286 tests), composer lint (PHPStan level 10), and composer cs all pass.
Fixes #55
## Problem
Availability slots created without a tied offering could be booked with no offering at all: the lesson was **free, confirmed immediately, and asked no intake questions**. The booking UI never let the student choose an offering, contradicting the documented flow.
## Changes
**Server — `Booking\BookingEndpoint`**
- `POST /bookings` now rejects bookings with no resolvable offering (`400 offering_required`).
- A student-chosen offering (generic slot) must be **active**, of kind **`private_lesson`**, owned by the slot's instructor, and its `duration_minutes` must match the slot — implementing the duration-matching rule from `docs/features/availability-management.md`.
- A slot-tied offering remains authoritative over anything the client sends (existing anti-tamper behaviour unchanged).
**Front end — `assets/js/booking.js`**
- Registration form gains a **Lesson type** field:
- Slot tied to an offering → locked select showing title, duration, and price, so the student sees exactly what they are booking.
- Generic slot → required picker of the instructor's active private-lesson offerings that fit the slot length (catalog cached per instructor).
- Intake questions now load for whichever offering is selected.
- Generic slots with no fitting offering show a "cannot be booked online" message instead of a doomed form.
**Tests & docs**
- `BookingEndpointTest`: offering-less booking rejected; slot-tied offering charged when the request omits it; inactive / group-class / duration-mismatched offerings rejected.
- `docs/features/lesson-booking.md` updated to match.
## Deployment note
Existing generic slots become unbookable until the instructor ties an offering to them or has at least one active private-lesson offering matching their duration — the intended consequence of closing the free-booking hole.
`composer test` (286 tests), `composer lint` (PHPStan level 10), and `composer cs` all pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Generic slots (no tied offering) were bookable with no offering at all:
free, instantly confirmed, and with no intake questions. POST /bookings
now rejects offering-less bookings (400 offering_required), and a
student-chosen offering must be an active private-lesson type owned by
the slot's instructor whose duration matches the slot.
The registration form gains a Lesson type field: locked to the slot's
tied offering (title, duration, price) so the student sees what they
are booking, or a required picker of fitting offerings for generic
slots, with intake questions following the selection.
Fixes#55
Co-Authored-By: Claude Fable 5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #55
Problem
Availability slots created without a tied offering could be booked with no offering at all: the lesson was free, confirmed immediately, and asked no intake questions. The booking UI never let the student choose an offering, contradicting the documented flow.
Changes
Server —
Booking\BookingEndpointPOST /bookingsnow rejects bookings with no resolvable offering (400 offering_required).private_lesson, owned by the slot's instructor, and itsduration_minutesmust match the slot — implementing the duration-matching rule fromdocs/features/availability-management.md.Front end —
assets/js/booking.jsTests & docs
BookingEndpointTest: offering-less booking rejected; slot-tied offering charged when the request omits it; inactive / group-class / duration-mismatched offerings rejected.docs/features/lesson-booking.mdupdated to match.Deployment note
Existing generic slots become unbookable until the instructor ties an offering to them or has at least one active private-lesson offering matching their duration — the intended consequence of closing the free-booking hole.
composer test(286 tests),composer lint(PHPStan level 10), andcomposer csall pass.🤖 Generated with Claude Code