Booking a slot without a priced offering succeeded server-side but the front end unconditionally called POST /payments/intent, which 400ed ("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 — and had no way to see their booked session anywhere.
Changes
1. Payment step is skipped when nothing is owed
POST /bookings and POST /enrollments now include a payment field in the response: a {id, method, status} summary (new Payment::toSummaryArray()), or null when no payment was created.
booking.js / group-classes.js only run usPayment.collect() when the response carries a payment.
2. Unpriced bookings are confirmed immediately
With no payment there is no later payment-finalization step to confirm the lesson, so BookingEndpoint::book() confirms the created lesson row(s) at creation. Priced bookings keep the existing pending → paid → confirmed flow, and comped payments (paid at creation) now report confirmed in the response.
3. Students can see their upcoming lessons
The booking page gets a "Your upcoming lessons" panel backed by GET /bookings, with a status badge (Pending payment / Confirmed). It refreshes on load and after a successful booking, and never blocks slot browsing if the fetch fails.
GET /bookings for students now returns upcoming non-cancelled lessons (new BookingRepository::findUpcomingForStudent(), mirroring the instructor query) instead of the full history, and every returned lesson includes the slot's start_dt/end_dt.
composer test (271 tests), composer lint, and composer cs all pass. Feature docs updated (lesson-booking.md, payments.md, group-classes.md), including the stale "payment seam deferred to #7" note.
Fixes #53.
Booking a slot without a priced offering succeeded server-side but the front end unconditionally called `POST /payments/intent`, which 400ed ("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` — and had no way to see their booked session anywhere.
## Changes
**1. Payment step is skipped when nothing is owed**
- `POST /bookings` and `POST /enrollments` now include a `payment` field in the response: a `{id, method, status}` summary (new `Payment::toSummaryArray()`), or `null` when no payment was created.
- `booking.js` / `group-classes.js` only run `usPayment.collect()` when the response carries a payment.
**2. Unpriced bookings are confirmed immediately**
- With no payment there is no later payment-finalization step to confirm the lesson, so `BookingEndpoint::book()` confirms the created lesson row(s) at creation. Priced bookings keep the existing pending → paid → confirmed flow, and comped payments (paid at creation) now report `confirmed` in the response.
**3. Students can see their upcoming lessons**
- The booking page gets a "Your upcoming lessons" panel backed by `GET /bookings`, with a status badge (Pending payment / Confirmed). It refreshes on load and after a successful booking, and never blocks slot browsing if the fetch fails.
- `GET /bookings` for students now returns upcoming non-cancelled lessons (new `BookingRepository::findUpcomingForStudent()`, mirroring the instructor query) instead of the full history, and every returned lesson includes the slot's `start_dt`/`end_dt`.
## Tests
- `BookingEndpointTest`: unpriced booking → confirmed + `payment: null`; priced → pending + summary; comped → confirmed; `myLessons` includes slot times.
- New `EnrollmentEndpointTest` covering free vs priced enrolment responses.
- `BookingRepositoryTest::testFindUpcomingForStudentJoinsSlotAndExcludesCancelled`, `PaymentTest::testToSummaryArrayContainsOnlyClientFacingFields`.
`composer test` (271 tests), `composer lint`, and `composer cs` all pass. Feature docs updated (`lesson-booking.md`, `payments.md`, `group-classes.md`), including the stale "payment seam deferred to #7" note.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
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 #53.
Booking a slot without a priced offering succeeded server-side but the front end unconditionally called
POST /payments/intent, which 400ed ("Could not start payment for this registration"). The student saw an error while the backend held a claimed slot and a lesson stuck atpending— and had no way to see their booked session anywhere.Changes
1. Payment step is skipped when nothing is owed
POST /bookingsandPOST /enrollmentsnow include apaymentfield in the response: a{id, method, status}summary (newPayment::toSummaryArray()), ornullwhen no payment was created.booking.js/group-classes.jsonly runusPayment.collect()when the response carries a payment.2. Unpriced bookings are confirmed immediately
BookingEndpoint::book()confirms the created lesson row(s) at creation. Priced bookings keep the existing pending → paid → confirmed flow, and comped payments (paid at creation) now reportconfirmedin the response.3. Students can see their upcoming lessons
GET /bookings, with a status badge (Pending payment / Confirmed). It refreshes on load and after a successful booking, and never blocks slot browsing if the fetch fails.GET /bookingsfor students now returns upcoming non-cancelled lessons (newBookingRepository::findUpcomingForStudent(), mirroring the instructor query) instead of the full history, and every returned lesson includes the slot'sstart_dt/end_dt.Tests
BookingEndpointTest: unpriced booking → confirmed +payment: null; priced → pending + summary; comped → confirmed;myLessonsincludes slot times.EnrollmentEndpointTestcovering free vs priced enrolment responses.BookingRepositoryTest::testFindUpcomingForStudentJoinsSlotAndExcludesCancelled,PaymentTest::testToSummaryArrayContainsOnlyClientFacingFields.composer test(271 tests),composer lint, andcomposer csall pass. Feature docs updated (lesson-booking.md,payments.md,group-classes.md), including the stale "payment seam deferred to #7" note.🤖 Generated with Claude Code
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]>