New shared helper assets/js/pricing.js (window.usPricing), registered as us-scheduler-pricing and made a dependency of both the booking and group-class
scripts. Every price a student sees now says when it is due:
billing_mode
Shown as
Explained beneath as
one_time
50.00 CAD at booking
Charged once, when you book.
full_term
400.00 CAD up front
Charged once, up front, for the whole term.
weekly
50.00 CAD weekly
Charged for each lesson, 24 hours before it starts.
monthly
50.00 CAD monthly
Charged on the 1st of each month, for that month's lessons.
Applied to the booking form's lesson-type picker and the group-class cards
(previously a bare 120.00 CAD). Free offerings still read Free. The block
editor's sample card was updated to match.
The pay agreement
Above the Confirm button, both forms now render a price block followed by a
required checkbox — separate from, and in addition to, the policy acceptances:
☐ I agree to pay 56.50 CAD at booking.
Submit is blocked until it is ticked. Cadence-specific wording: a weekly
reservation quotes the per-lesson fee and a ceiling on the total
(up to 12 lessons, 678.00 CAD in total), since a week another student claims
first is simply not booked; weekly / monthly agree to the recurring charge
rather than a one-off.
Two judgement calls worth a reviewer's eye
HST is folded into the agreed figure. Quoting the pre-tax price and then
billing Payment::total() would make the agreement inaccurate, so the studio
rate is localized as usScheduler.taxRate and the checkbox states the
tax-inclusive amount, with the breakdown above it. Consequence: comped students
are not taxed or charged at all, so for them the quoted figure is an upper
bound — the client cannot know a student's billing method without a new
endpoint, so this is not special-cased.
The weekly total is a ceiling, not a promise. The browser counts unbooked
slots in the recurrence group and caps at 12 to mirror BookingEndpoint::MAX_WEEKLY_OCCURRENCES; the server only charges for slots it
actually claims, so the real charge can come in under the quote. The wording
says so.
No schema change, so no version bump — the changelog entry lands under the open 1.2.3 section.
Tests
composer test — 645 tests, 1841 assertions, all passing
composer lint — PHPStan level 10, no errors
composer cs — PHPCS clean
node --check on each changed JS file, plus a scratch harness exercising usPricing output across all four billing modes, the weekly-reservation case,
a zero price, and a zero tax rate
Docs
docs/features/payments.md gains a Price Display and the Pay Agreement
section; lesson-booking.md, group-classes.md and offerings.md cross-
reference it from their flows.
Closes #124.
## Price cadence on the front end
New shared helper `assets/js/pricing.js` (`window.usPricing`), registered as
`us-scheduler-pricing` and made a dependency of both the booking and group-class
scripts. Every price a student sees now says when it is due:
| `billing_mode` | Shown as | Explained beneath as |
|---|---|---|
| `one_time` | `50.00 CAD at booking` | Charged once, when you book. |
| `full_term` | `400.00 CAD up front` | Charged once, up front, for the whole term. |
| `weekly` | `50.00 CAD weekly` | Charged for each lesson, 24 hours before it starts. |
| `monthly` | `50.00 CAD monthly` | Charged on the 1st of each month, for that month's lessons. |
Applied to the booking form's lesson-type picker and the group-class cards
(previously a bare `120.00 CAD`). Free offerings still read **Free**. The block
editor's sample card was updated to match.
## The pay agreement
Above the Confirm button, both forms now render a price block followed by a
required checkbox — separate from, and in addition to, the policy acceptances:
> ☐ I agree to pay 56.50 CAD at booking.
Submit is blocked until it is ticked. Cadence-specific wording: a weekly
reservation quotes the per-lesson fee and a ceiling on the total
(`up to 12 lessons, 678.00 CAD in total`), since a week another student claims
first is simply not booked; `weekly` / `monthly` agree to the recurring charge
rather than a one-off.
## Two judgement calls worth a reviewer's eye
- **HST is folded into the agreed figure.** Quoting the pre-tax price and then
billing `Payment::total()` would make the agreement inaccurate, so the studio
rate is localized as `usScheduler.taxRate` and the checkbox states the
tax-inclusive amount, with the breakdown above it. Consequence: comped students
are not taxed or charged at all, so for them the quoted figure is an upper
bound — the client cannot know a student's billing method without a new
endpoint, so this is not special-cased.
- **The weekly total is a ceiling, not a promise.** The browser counts unbooked
slots in the recurrence group and caps at 12 to mirror
`BookingEndpoint::MAX_WEEKLY_OCCURRENCES`; the server only charges for slots it
actually claims, so the real charge can come in under the quote. The wording
says so.
No schema change, so no version bump — the changelog entry lands under the open
`1.2.3` section.
## Tests
- `composer test` — 645 tests, 1841 assertions, all passing
- `composer lint` — PHPStan level 10, no errors
- `composer cs` — PHPCS clean
- `node --check` on each changed JS file, plus a scratch harness exercising
`usPricing` output across all four billing modes, the weekly-reservation case,
a zero price, and a zero tax rate
## Docs
`docs/features/payments.md` gains a **Price Display and the Pay Agreement**
section; `lesson-booking.md`, `group-classes.md` and `offerings.md` cross-
reference it from their flows.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Every price a student meets on the front end now carries the cadence it is
billed on — at booking, up front, weekly, monthly — so a bare amount can no
longer read as a one-off when it is a recurring charge.
Both registration forms then restate the price and require a second, separate
tick agreeing to pay it, distinct from the policy acceptances above it. The
agreed figure includes the studio HST so it matches Payment::total(), the amount
actually billed; the rate reaches the browser as a new localized `taxRate`.
A weekly reservation is charged per lesson for every week it claims, and a week
another student takes first is simply not claimed, so its total is quoted as a
ceiling ("up to 12 lessons") rather than a promise. Free offerings have nothing
to agree to and show no price block at all.
The formatting and the agreement live in one shared helper (`window.usPricing`,
registered as `us-scheduler-pricing`) so a price reads the same in the booking
form, the class catalogue and the editor preview.
Closes#124
Co-Authored-By: Claude Opus 5 <[email protected]>
A monthly charge covers every lesson that falls in the month, so a private
lesson's fee reads "50.00 CAD per lesson monthly" — the figure on its own would
suggest the whole month costs 50.00. A group class is enrolled in once, as a
single schedule, so its price is quoted as the monthly figure it is.
The pay agreement follows the same split: per-lesson for a monthly private
lesson, the monthly figure for a monthly group class. Weekly, full-term and
at-booking wording is unchanged.
Co-Authored-By: Claude Opus 5 <[email protected]>
A monthly group class multiplied its price by the sessions falling in the month,
the same rule private lessons use — so a class priced at 40.00 CAD meeting
weekly was billed 160.00 CAD on the 1st, and no studio could quote the price on
a class card without lying about it.
A group class is now billed its fee once for the month however many times it
meets, which is what the card quotes and what the student ticks to agree to.
Private lessons keep the per-lesson rule: their price is a per-lesson fee, and
that is why the card quotes it per lesson.
The session count still labels the month on the student's payment notice; it no
longer prices it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Two follow-up commits, one of which changes billing behaviour — worth a look before merging.
a276d53 — monthly reads per offering kind. A monthly private lesson is quoted 50.00 CAD per lesson monthly (its month is that month's lessons × the fee); a monthly group class is quoted 50.00 CAD monthly. The agreement checkbox follows the same split. Weekly, full-term and at-booking wording is unchanged.
bfdc3b3 — a monthly group class is now billed its fee once per month. This closes the gap the previous commit would otherwise have opened. ScheduledBillingRunner::billGroupMonthly() was charging price × sessions in the month, the same rule private lessons use, so a class priced at 40.00 CAD meeting weekly was billed 160.00 CAD on the 1st — not something a card could quote honestly. It now charges the fee once for the month however many times the class meets. Private lessons are untouched. The session count still labels the month on the student's payment notice ("March 2026: 4 sessions"); it no longer prices it.
Upgrade note for existing studios: any live monthly group class should have its price re-checked — a studio that set 40.00 meaning "per session" will now collect 40.00 for the month, not 160.00. Called out in the changelog under Changed.
Tests: composer test 646 tests / 1842 assertions, composer lint, composer cs all pass. testGroupMonthlyBillsMonthTotal became testGroupMonthlyBillsTheMonthlyFeeOnceHoweverManySessions, plus a new single-session case.
Two follow-up commits, one of which changes billing behaviour — worth a look before merging.
**`a276d53` — monthly reads per offering kind.** A monthly private lesson is quoted `50.00 CAD per lesson monthly` (its month is that month's lessons × the fee); a monthly group class is quoted `50.00 CAD monthly`. The agreement checkbox follows the same split. Weekly, full-term and at-booking wording is unchanged.
**`bfdc3b3` — a monthly group class is now billed its fee once per month.** This closes the gap the previous commit would otherwise have opened. `ScheduledBillingRunner::billGroupMonthly()` was charging `price × sessions in the month`, the same rule private lessons use, so a class priced at `40.00 CAD` meeting weekly was billed `160.00 CAD` on the 1st — not something a card could quote honestly. It now charges the fee once for the month however many times the class meets. Private lessons are untouched. The session count still labels the month on the student's payment notice ("March 2026: 4 sessions"); it no longer prices it.
**Upgrade note for existing studios:** any live monthly group class should have its price re-checked — a studio that set `40.00` meaning "per session" will now collect `40.00` for the month, not `160.00`. Called out in the changelog under **Changed**.
Tests: `composer test` 646 tests / 1842 assertions, `composer lint`, `composer cs` all pass. `testGroupMonthlyBillsMonthTotal` became `testGroupMonthlyBillsTheMonthlyFeeOnceHoweverManySessions`, plus a new single-session case.
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 #124.
Price cadence on the front end
New shared helper
assets/js/pricing.js(window.usPricing), registered asus-scheduler-pricingand made a dependency of both the booking and group-classscripts. Every price a student sees now says when it is due:
billing_modeone_time50.00 CAD at bookingfull_term400.00 CAD up frontweekly50.00 CAD weeklymonthly50.00 CAD monthlyApplied to the booking form's lesson-type picker and the group-class cards
(previously a bare
120.00 CAD). Free offerings still read Free. The blockeditor's sample card was updated to match.
The pay agreement
Above the Confirm button, both forms now render a price block followed by a
required checkbox — separate from, and in addition to, the policy acceptances:
Submit is blocked until it is ticked. Cadence-specific wording: a weekly
reservation quotes the per-lesson fee and a ceiling on the total
(
up to 12 lessons, 678.00 CAD in total), since a week another student claimsfirst is simply not booked;
weekly/monthlyagree to the recurring chargerather than a one-off.
Two judgement calls worth a reviewer's eye
billing
Payment::total()would make the agreement inaccurate, so the studiorate is localized as
usScheduler.taxRateand the checkbox states thetax-inclusive amount, with the breakdown above it. Consequence: comped students
are not taxed or charged at all, so for them the quoted figure is an upper
bound — the client cannot know a student's billing method without a new
endpoint, so this is not special-cased.
slots in the recurrence group and caps at 12 to mirror
BookingEndpoint::MAX_WEEKLY_OCCURRENCES; the server only charges for slots itactually claims, so the real charge can come in under the quote. The wording
says so.
No schema change, so no version bump — the changelog entry lands under the open
1.2.3section.Tests
composer test— 645 tests, 1841 assertions, all passingcomposer lint— PHPStan level 10, no errorscomposer cs— PHPCS cleannode --checkon each changed JS file, plus a scratch harness exercisingusPricingoutput across all four billing modes, the weekly-reservation case,a zero price, and a zero tax rate
Docs
docs/features/payments.mdgains a Price Display and the Pay Agreementsection;
lesson-booking.md,group-classes.mdandofferings.mdcross-reference it from their flows.
🤖 Generated with Claude Code
Every price a student meets on the front end now carries the cadence it is billed on — at booking, up front, weekly, monthly — so a bare amount can no longer read as a one-off when it is a recurring charge. Both registration forms then restate the price and require a second, separate tick agreeing to pay it, distinct from the policy acceptances above it. The agreed figure includes the studio HST so it matches Payment::total(), the amount actually billed; the rate reaches the browser as a new localized `taxRate`. A weekly reservation is charged per lesson for every week it claims, and a week another student takes first is simply not claimed, so its total is quoted as a ceiling ("up to 12 lessons") rather than a promise. Free offerings have nothing to agree to and show no price block at all. The formatting and the agreement live in one shared helper (`window.usPricing`, registered as `us-scheduler-pricing`) so a price reads the same in the booking form, the class catalogue and the editor preview. Closes #124 Co-Authored-By: Claude Opus 5 <[email protected]>Two follow-up commits, one of which changes billing behaviour — worth a look before merging.
a276d53— monthly reads per offering kind. A monthly private lesson is quoted50.00 CAD per lesson monthly(its month is that month's lessons × the fee); a monthly group class is quoted50.00 CAD monthly. The agreement checkbox follows the same split. Weekly, full-term and at-booking wording is unchanged.bfdc3b3— a monthly group class is now billed its fee once per month. This closes the gap the previous commit would otherwise have opened.ScheduledBillingRunner::billGroupMonthly()was chargingprice × sessions in the month, the same rule private lessons use, so a class priced at40.00 CADmeeting weekly was billed160.00 CADon the 1st — not something a card could quote honestly. It now charges the fee once for the month however many times the class meets. Private lessons are untouched. The session count still labels the month on the student's payment notice ("March 2026: 4 sessions"); it no longer prices it.Upgrade note for existing studios: any live monthly group class should have its price re-checked — a studio that set
40.00meaning "per session" will now collect40.00for the month, not160.00. Called out in the changelog under Changed.Tests:
composer test646 tests / 1842 assertions,composer lint,composer csall pass.testGroupMonthlyBillsMonthTotalbecametestGroupMonthlyBillsTheMonthlyFeeOnceHoweverManySessions, plus a new single-session case.