From the user demo: booking a recurring weekly lesson only required payment for the first week. Two related defects:
Amount — BookingEndpoint::book() claimed up to 12 weekly occurrences but created the payment with $offering->price once. For a per-lesson-priced offering (billing_mode = one_time) the student paid one week while holding the whole term. It now charges price × the occurrences actually claimed; a full_term price is still charged once, since that price already covers the term. The Stripe intent, e-transfer display amount, and HST all follow automatically because they derive from the payment row.
Confirmation — settling the payment (Stripe webhook, e-transfer confirmation, comp) confirmed only the anchor lesson; the rest of the series stayed pending forever. PaymentService::confirmRegistration() now resolves the anchor's series_id and confirms every non-cancelled lesson in the series via a new BookingRepository::updateStatusForSeries().
Tests
BookingEndpointTest: weekly booking charges price × claimed occurrences (one_time), and a full_term price exactly once.
PaymentServiceTest: markPaid confirms the whole weekly series, not just the anchor.
Closes #79
## What & why
From the user demo: booking a recurring weekly lesson only required payment for the first week. Two related defects:
1. **Amount** — `BookingEndpoint::book()` claimed up to 12 weekly occurrences but created the payment with `$offering->price` once. For a per-lesson-priced offering (`billing_mode = one_time`) the student paid one week while holding the whole term. It now charges **price × the occurrences actually claimed**; a `full_term` price is still charged once, since that price already covers the term. The Stripe intent, e-transfer display amount, and HST all follow automatically because they derive from the payment row.
2. **Confirmation** — settling the payment (Stripe webhook, e-transfer confirmation, comp) confirmed only the anchor lesson; the rest of the series stayed `pending` forever. `PaymentService::confirmRegistration()` now resolves the anchor's `series_id` and confirms **every non-cancelled lesson in the series** via a new `BookingRepository::updateStatusForSeries()`.
## Tests
- `BookingEndpointTest`: weekly booking charges price × claimed occurrences (`one_time`), and a `full_term` price exactly once.
- `PaymentServiceTest`: `markPaid` confirms the whole weekly series, not just the anchor.
- `BookingRepositoryTest`: `updateStatusForSeries` SQL guards (valid status, skips cancelled rows).
- `composer test` — 370 tests pass; `composer lint` (PHPStan level 10) and `composer cs` clean.
Docs updated: `docs/features/lesson-booking.md`, `docs/features/payments.md`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A weekly booking on a per-lesson (one_time) priced offering was creating its
single upfront payment for one week's price while reserving up to 12 weeks,
and settling that payment confirmed only the anchor lesson, leaving the rest
of the series pending forever.
- BookingEndpoint now charges price x claimed occurrences for one_time
billing; a full_term price is still charged once since it covers the term.
- PaymentService::confirmRegistration resolves the anchor lesson's series and
confirms every non-cancelled row via the new
BookingRepository::updateStatusForSeries().
Closes#79
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.
Closes #79
What & why
From the user demo: booking a recurring weekly lesson only required payment for the first week. Two related defects:
BookingEndpoint::book()claimed up to 12 weekly occurrences but created the payment with$offering->priceonce. For a per-lesson-priced offering (billing_mode = one_time) the student paid one week while holding the whole term. It now charges price × the occurrences actually claimed; afull_termprice is still charged once, since that price already covers the term. The Stripe intent, e-transfer display amount, and HST all follow automatically because they derive from the payment row.pendingforever.PaymentService::confirmRegistration()now resolves the anchor'sseries_idand confirms every non-cancelled lesson in the series via a newBookingRepository::updateStatusForSeries().Tests
BookingEndpointTest: weekly booking charges price × claimed occurrences (one_time), and afull_termprice exactly once.PaymentServiceTest:markPaidconfirms the whole weekly series, not just the anchor.BookingRepositoryTest:updateStatusForSeriesSQL guards (valid status, skips cancelled rows).composer test— 370 tests pass;composer lint(PHPStan level 10) andcomposer csclean.Docs updated:
docs/features/lesson-booking.md,docs/features/payments.md.🤖 Generated with Claude Code