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

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]>
This commit is contained in:
2026-07-28 15:07:46 -03:00
co-authored by Claude Opus 5
parent 9344ab7193
commit a276d53c1b
4 changed files with 57 additions and 15 deletions
+1
View File
@@ -489,6 +489,7 @@
price: offering.price,
currency: offering.currency,
billing_mode: offering.billing_mode,
kind: offering.kind,
occurrences: weeklyEl && weeklyEl.checked ? weeklyOccurrences(slot) : 1,
})
: '';
+33 -8
View File
@@ -36,6 +36,14 @@
return CADENCE[billingMode] ? billingMode : 'one_time';
}
// A monthly charge rolls up every lesson that falls in the month, so a
// private lesson's monthly price is quoted *per lesson* — the fee is
// multiplied by the lessons booked that month. A group class is enrolled in
// once, as one schedule, so its monthly figure is quoted as it stands.
function isPerLessonMonthly(billingMode, kind) {
return 'monthly' === billingMode && 'group_class' !== kind;
}
// "50.00 CAD" — amount then currency code, the format used throughout the
// ledger, receipts and payment notices.
function money(amount, currency) {
@@ -57,22 +65,26 @@
return (Number(amount) || 0) + tax(amount);
}
// "50.00 CAD at booking" / "Free" — the catalogue label, always carrying the
// cadence so a price is never shown without saying when it is due.
// "50.00 CAD at booking" / "50.00 CAD per lesson monthly" / "Free" — the
// catalogue label, always carrying the cadence so a price is never shown
// without saying when it is due.
function priceLabel(offering) {
const price = Number(offering.price) || 0;
if (price <= 0) {
return 'Free';
}
return `${money(price, offering.currency)} ${CADENCE[mode(offering.billing_mode)]}`;
const billingMode = mode(offering.billing_mode);
const perLesson = isPerLessonMonthly(billingMode, offering.kind) ? 'per lesson ' : '';
return `${money(price, offering.currency)} ${perLesson}${CADENCE[billingMode]}`;
}
// The price block shown on a booking/enrolment form, followed by the
// agreement the student must tick to confirm they will pay it. A free
// offering has nothing to agree to, so it renders nothing at all.
//
// opts: { price, currency, billing_mode, occurrences }
// opts: { price, currency, billing_mode, kind, occurrences }
// `occurrences` is how many lessons a one-time price is charged for in this
// one registration (a weekly reservation claims several at once); it is
// ignored for the other modes, whose price is charged per period regardless.
@@ -96,7 +108,7 @@
<h4>Price</h4>
<p class="us-price-amount">
<strong>${escHtml(money(price, currency))}</strong>
<span class="us-price-cadence">${escHtml(CADENCE[billingMode])}</span>
<span class="us-price-cadence">${escHtml(cadenceLabel(billingMode, opts.kind))}</span>
</p>
${taxLine}
<p class="us-price-note">${escHtml(count > 1
@@ -104,18 +116,31 @@
: CADENCE_NOTE[billingMode])}</p>
<label class="us-price-agree">
<input type="checkbox" class="us-price-accept" required>
${escHtml(agreeText(each, currency, billingMode, count))}
${escHtml(agreeText(each, currency, billingMode, count, opts.kind))}
</label>
</div>`;
}
// The cadence as it reads beside an amount: a private lesson billed monthly
// adds "per lesson", since the month's charge is that fee times the lessons
// it covers.
function cadenceLabel(billingMode, kind) {
return isPerLessonMonthly(billingMode, kind)
? `per lesson ${CADENCE[billingMode]}`
: CADENCE[billingMode];
}
// What the student is ticking: the amount actually billed (tax included),
// and when. A weekly reservation is charged per lesson for every week it
// claims, and the claim can come up short when another student takes one of
// the times first — so its total is stated as a ceiling, never a promise.
function agreeText(each, currency, billingMode, count) {
function agreeText(each, currency, billingMode, count, kind) {
if (RECURRING.indexOf(billingMode) !== -1) {
return `I agree to pay ${money(each, currency)} per lesson, billed ${CADENCE[billingMode]}.`;
// A monthly group class is enrolled in once and quoted as it stands;
// everything else recurring is a per-lesson fee.
return 'monthly' === billingMode && !isPerLessonMonthly(billingMode, kind)
? `I agree to pay ${money(each, currency)} monthly.`
: `I agree to pay ${money(each, currency)} per lesson, billed ${CADENCE[billingMode]}.`;
}
if (count > 1) {