When a lesson that was already paid for is cancelled, the student is now credited that money instead of it being a manual refund, and the daily scheduled-billing scan applies any available credit against their upcoming charges before emailing the notice.
How it works
Credit on cancel — PaymentService::creditForCancelledLesson issues one lesson's share of the covering payment's total (whole amount for a single/weekly-scheduled lesson; total ÷ N for a monthly charge or a full-term series). The original payment is left untouched. Wired into all three cancel paths: student self-cancel (BookingEndpoint::cancel), instructor status update (BookingEndpoint::updateStatus), and admin student-detail cancel (StudentActions::cancelLesson). Guards prevent double-crediting and only credit genuinely-paid payments.
Apply at billing — PaymentService::applyCredits draws the balance down FIFO across the run's charges; each payment's new credit_applied column reduces Payment::netDue(), a fully-covered charge is marked paid-by-credit and leaves the admin queue, and the notice email shows the credit applied and reduced total.
Admin visibility — the student detail page shows the student's available credit balance and a table of every credit (date, reason, amount, remaining, status), gated by manage_billing.
Data model
New us_credits table and us_payments.credit_applied column. Ships as part of the unreleased 1.2.0 (same release as scheduled billing) — version intentionally not bumped.
Notes
Reporting counts gross (documented in docs/features/credits.md): the original paid lesson and a later credit-covered charge both count as paid, a direct consequence of leaving the original payment intact rather than representing a partial refund.
Group-enrolment cancellations are not auto-credited (scope was private lessons).
## What
When a lesson that was **already paid for** is cancelled, the student is now credited that money instead of it being a manual refund, and the daily scheduled-billing scan applies any available credit against their upcoming charges before emailing the notice.
## How it works
- **Credit on cancel** — `PaymentService::creditForCancelledLesson` issues one lesson's share of the covering payment's total (whole amount for a single/weekly-scheduled lesson; `total ÷ N` for a monthly charge or a full-term series). The original payment is left untouched. Wired into all three cancel paths: student self-cancel (`BookingEndpoint::cancel`), instructor status update (`BookingEndpoint::updateStatus`), and admin student-detail cancel (`StudentActions::cancelLesson`). Guards prevent double-crediting and only credit genuinely-paid payments.
- **Apply at billing** — `PaymentService::applyCredits` draws the balance down FIFO across the run's charges; each payment's new `credit_applied` column reduces `Payment::netDue()`, a fully-covered charge is marked paid-by-credit and leaves the admin queue, and the notice email shows the credit applied and reduced total.
- **Admin visibility** — the student detail page shows the student's available credit balance and a table of every credit (date, reason, amount, remaining, status), gated by `manage_billing`.
## Data model
- New `us_credits` table and `us_payments.credit_applied` column. Ships as part of the **unreleased 1.2.0** (same release as scheduled billing) — version intentionally not bumped.
## Notes
- Reporting counts gross (documented in `docs/features/credits.md`): the original paid lesson and a later credit-covered charge both count as `paid`, a direct consequence of leaving the original payment intact rather than representing a partial refund.
- Group-enrolment cancellations are not auto-credited (scope was private lessons).
## Tests
- `composer test` — 585 tests pass
- `composer lint` (PHPStan level 10) — clean
- `composer cs` (PHPCS) — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Cancelling a lesson that was already paid for now credits the student
that money instead of leaving it as a manual refund, and the daily
scheduled-billing scan applies any available credit against their due
charges before emailing the notice.
- New us_credits ledger + us_payments.credit_applied column (Payment::netDue).
- PaymentService::creditForCancelledLesson issues a per-lesson share of the
covering payment's total; wired into all three cancel paths (student
self-cancel, instructor status update, admin student-detail cancel).
- PaymentService::applyCredits draws credit down FIFO across a run's charges,
marking a fully-covered charge paid-by-credit; the notice shows the credit
applied and reduced total, and the admin queue shows net due.
- Student detail page shows a student's credit balance and history.
Ships as part of the unreleased 1.2.0 (same release as scheduled billing).
Tests: composer test (585), composer lint, composer cs all pass.
Co-Authored-By: Claude Opus 4.8 <[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.
What
When a lesson that was already paid for is cancelled, the student is now credited that money instead of it being a manual refund, and the daily scheduled-billing scan applies any available credit against their upcoming charges before emailing the notice.
How it works
PaymentService::creditForCancelledLessonissues one lesson's share of the covering payment's total (whole amount for a single/weekly-scheduled lesson;total ÷ Nfor a monthly charge or a full-term series). The original payment is left untouched. Wired into all three cancel paths: student self-cancel (BookingEndpoint::cancel), instructor status update (BookingEndpoint::updateStatus), and admin student-detail cancel (StudentActions::cancelLesson). Guards prevent double-crediting and only credit genuinely-paid payments.PaymentService::applyCreditsdraws the balance down FIFO across the run's charges; each payment's newcredit_appliedcolumn reducesPayment::netDue(), a fully-covered charge is marked paid-by-credit and leaves the admin queue, and the notice email shows the credit applied and reduced total.manage_billing.Data model
us_creditstable andus_payments.credit_appliedcolumn. Ships as part of the unreleased 1.2.0 (same release as scheduled billing) — version intentionally not bumped.Notes
docs/features/credits.md): the original paid lesson and a later credit-covered charge both count aspaid, a direct consequence of leaving the original payment intact rather than representing a partial refund.Tests
composer test— 585 tests passcomposer lint(PHPStan level 10) — cleancomposer cs(PHPCS) — clean🤖 Generated with Claude Code