diff --git a/CHANGELOG.md b/CHANGELOG.md index 36eea1a..0f46119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ each change under the current top section as you work. ## [1.2.3] ### Added -- Every price a student sees now says **when** it is due. Lesson types in the booking form read `50.00 CAD at booking`, and group-class cards read `120.00 CAD up front`, `40.00 CAD weekly` or `40.00 CAD monthly` — the offering's billing mode, in the student's words. A free offering still just reads **Free**. +- Every price a student sees now says **when** it is due. Lesson types in the booking form read `50.00 CAD at booking`, and group-class cards read `120.00 CAD up front`, `40.00 CAD weekly` or `40.00 CAD monthly` — the offering's billing mode, in the student's words. A monthly **private lesson** is quoted per lesson (`50.00 CAD per lesson monthly`), since its monthly charge covers every lesson booked that month; a monthly group class is quoted as the monthly figure it is. A free offering still just reads **Free**. - Booking a lesson and enrolling in a class now take a **second confirmation that the student agrees to pay**. Above the Confirm button the form restates the price with its cadence, spells out how it is collected ("Charged on the 1st of each month, for that month's lessons"), adds the studio's HST so the figure matches the total actually billed, and requires a tick on "I agree to pay 56.50 CAD at booking." before it will submit — separate from, and in addition to, the studio policies the student accepts above it. Reserving a time weekly quotes the per-lesson fee and the most it can add up to ("up to 12 lessons, 678.00 CAD in total"), since a week another student takes first is simply not booked. Free offerings have nothing to agree to and show no price block. ## [1.2.2] diff --git a/assets/js/booking.js b/assets/js/booking.js index ed1c219..11261a9 100644 --- a/assets/js/booking.js +++ b/assets/js/booking.js @@ -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, }) : ''; diff --git a/assets/js/pricing.js b/assets/js/pricing.js index ccd7e73..6e3c105 100644 --- a/assets/js/pricing.js +++ b/assets/js/pricing.js @@ -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 @@

Price

${escHtml(money(price, currency))} - ${escHtml(CADENCE[billingMode])} + ${escHtml(cadenceLabel(billingMode, opts.kind))}

${taxLine}

${escHtml(count > 1 @@ -104,18 +116,31 @@ : CADENCE_NOTE[billingMode])}

`; } + // 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) { diff --git a/docs/features/payments.md b/docs/features/payments.md index 45a5718..9adc382 100644 --- a/docs/features/payments.md +++ b/docs/features/payments.md @@ -102,16 +102,30 @@ After booking, the destination on a payment can be corrected per booking: Every price a student is shown on the front end carries its **cadence** — the offering's `billing_mode` in the words the student needs: -| `billing_mode` | Shown as | Explained beneath as | -|----------------|--------------|------------------------------------------------------------| -| `one_time` | `at booking` | Charged once, when you book. | -| `full_term` | `up front` | Charged once, up front, for the whole term. | -| `weekly` | `weekly` | Charged for each lesson, 24 hours before it starts. | -| `monthly` | `monthly` | Charged on the 1st of each month, for that month's lessons.| +| `billing_mode` | Shown as | Explained beneath as | +|----------------|-----------------------------------|------------------------------------------------------------| +| `one_time` | `at booking` | Charged once, when you book. | +| `full_term` | `up front` | Charged once, up front, for the whole term. | +| `weekly` | `weekly` | Charged for each lesson, 24 hours before it starts. | +| `monthly` | `per lesson monthly` / `monthly` | Charged on the 1st of each month, for that month's lessons.| So a lesson type reads `50.00 CAD at booking` in the booking form's type picker, and a group class card reads `120.00 CAD up front`. A free offering shows `Free`. +**`monthly` reads differently per offering kind.** A private lesson's monthly +charge is that month's lessons × the fee, so the fee is quoted **per lesson** +(`50.00 CAD per lesson monthly`). A group class is enrolled in once, as one +schedule, so its figure is quoted as it stands (`120.00 CAD monthly`) — see +`isPerLessonMonthly()` in `assets/js/pricing.js`. + +> **Caveat.** `ScheduledBillingRunner::billGroupMonthly()` bills a monthly group +> class `price × sessions in the month`, the same per-session multiplication used +> for private lessons — so a class meeting weekly is charged roughly four times +> the quoted `120.00 CAD monthly` figure. The display deliberately quotes the +> price as a monthly figure; closing the gap means either billing a monthly group +> class once per month regardless of session count, or restoring the per-lesson +> wording for group classes too. + Before a booking or enrolment can be submitted, the form shows the price again as a summary block with a **required agreement checkbox** — the second confirmation, distinct from the policy acceptances above it: @@ -133,6 +147,8 @@ Cadence-specific wording: simply not claimed, so the real charge can come in under it. - **`weekly` / `monthly`** — nothing is taken at registration, so the agreement is to the recurring charge: "I agree to pay 56.50 CAD per lesson, billed monthly." + A monthly **group class** agrees to its monthly figure instead ("I agree to pay + 138.00 CAD monthly."), matching how its price is quoted on the card. All of this lives in `assets/js/pricing.js` (`window.usPricing`), shared by the booking and group-class flows so a price reads the same wherever it is met. The