Show price cadence on the front end and require a pay agreement at booking #125

Merged
thatguygriff merged 3 commits from feature/price-cadence-pay-agreement into main 2026-07-28 18:35:36 +00:00
Owner

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

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)
thatguygriff added 1 commit 2026-07-28 17:48:12 +00:00
Show price cadence and require a pay agreement at booking
CI / Tests (PHP 8.2) (pull_request) Successful in 46s
CI / Tests (PHP 8.1) (pull_request) Successful in 56s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m56s
CI / Coding Standards (pull_request) Successful in 2m59s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m41s
CI / Build Plugin Zip (pull_request) Skipped
9344ab7193
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]>
thatguygriff added 1 commit 2026-07-28 18:07:49 +00:00
Quote a monthly private lesson per lesson, a group class monthly
CI / Tests (PHP 8.1) (pull_request) Successful in 53s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 54s
CI / Coding Standards (pull_request) Successful in 2m52s
CI / PHPStan (pull_request) Successful in 3m0s
a276d53c1b
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]>
thatguygriff added 1 commit 2026-07-28 18:22:49 +00:00
Bill a monthly group class its fee once per month
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / Tests (PHP 8.2) (pull_request) Successful in 52s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m50s
CI / Coding Standards (pull_request) Successful in 2m58s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m38s
CI / Build Plugin Zip (pull_request) Skipped
bfdc3b3380
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]>
Author
Owner

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.
thatguygriff merged commit 907f665876 into main 2026-07-28 18:35:36 +00:00
thatguygriff deleted branch feature/price-cadence-pay-agreement 2026-07-28 18:35:36 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Unsupervised/unsupervised-scheduler#125