Students could cancel a lesson right up to its start time. This adds a cancellation cutoff: once a lesson starts within the configured window,
student-initiated online cancellation is blocked. Instructors and studio admins
are never affected — they can still cancel at any time via the lesson-status and
student-management flows.
How the window is resolved
Per lesson: the offering's own cutoff when it sets one, otherwise the studio
default. Everything is computed in hours.
Studio default — us_cancellation_cutoff_hours, stored/computed in hours,
but entered and displayed in days under Studio Settings → Cancellations
(default 1 day). 0 = students may cancel any time.
Per-offering override — new nullable column us_offerings.cancellation_cutoff_hours, entered in hours on the offering
form. Blank inherits the studio default; 0 allows anytime cancellation.
Enforcement
Booking\CancellationPolicy resolves the effective cutoff and decides
(studentMayCancel), comparing against WordPress-local time like the rest of
the upcoming-lesson logic. Zero cutoff always allows; unparseable data fails
open so a student is never trapped.
BookingEndpoint::cancel() (POST /bookings/{id}/cancel) returns cancellation_closed (HTTP 403) with a humanised window ("2 days", "12 hours")
when it's too late. The student booking UI already surfaces this error message.
PATCH /bookings/{id}/status and Auth\StudentActions::cancelLesson() (admin
paths) intentionally bypass the policy.
Schema / versioning
Adds a nullable cancellation_cutoff_hours column to us_offerings. Ships within
the current unreleased 1.1.0; the version header is bumped at release time.
composer test — 440 tests, all passing (new CancellationPolicyTest, cutoff
cases in BookingEndpointTest, studio-default cases in StudioSettingsTest,
offering column round-trips).
composer lint — PHPStan, no errors.
composer cs — PHPCS, clean.
Closes #93
## What & why
Students could cancel a lesson right up to its start time. This adds a
**cancellation cutoff**: once a lesson starts within the configured window,
student-initiated online cancellation is blocked. Instructors and studio admins
are never affected — they can still cancel at any time via the lesson-status and
student-management flows.
## How the window is resolved
Per lesson: the offering's own cutoff when it sets one, otherwise the studio
default. Everything is computed in **hours**.
- **Studio default** — `us_cancellation_cutoff_hours`, stored/computed in hours,
but entered and displayed in **days** under *Studio Settings → Cancellations*
(default 1 day). `0` = students may cancel any time.
- **Per-offering override** — new nullable column
`us_offerings.cancellation_cutoff_hours`, entered in **hours** on the offering
form. Blank inherits the studio default; `0` allows anytime cancellation.
## Enforcement
- `Booking\CancellationPolicy` resolves the effective cutoff and decides
(`studentMayCancel`), comparing against WordPress-local time like the rest of
the upcoming-lesson logic. Zero cutoff always allows; unparseable data fails
open so a student is never trapped.
- `BookingEndpoint::cancel()` (`POST /bookings/{id}/cancel`) returns
`cancellation_closed` (HTTP 403) with a humanised window ("2 days", "12 hours")
when it's too late. The student booking UI already surfaces this error message.
- `PATCH /bookings/{id}/status` and `Auth\StudentActions::cancelLesson()` (admin
paths) intentionally bypass the policy.
## Schema / versioning
Adds a nullable `cancellation_cutoff_hours` column to `us_offerings`. Ships within
the current unreleased `1.1.0`; the version header is bumped at release time.
## Docs
- New `docs/features/cancellation-cutoff.md`.
- `docs/features/offerings.md` data-model table updated.
## Tests run
- `composer test` — 440 tests, all passing (new `CancellationPolicyTest`, cutoff
cases in `BookingEndpointTest`, studio-default cases in `StudioSettingsTest`,
offering column round-trips).
- `composer lint` — PHPStan, no errors.
- `composer cs` — PHPCS, clean.
Students can no longer cancel their own lesson online once it starts within a
configured window; instructors and studio admins can always cancel.
- Studio default `us_cancellation_cutoff_hours` (stored/computed in hours,
entered and displayed in days under Studio Settings → Cancellations).
- Optional per-offering override `cancellation_cutoff_hours` (entered in hours);
blank inherits the studio default, 0 allows anytime cancellation.
- `Booking\CancellationPolicy` resolves the effective window and decides;
`BookingEndpoint::cancel()` returns a 403 `cancellation_closed` when too late.
The instructor status endpoint and studio-admin student actions bypass it.
Closes#93
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Studio Settings cutoff field now takes an integer number of days (step 1,
coerced with Val::int) instead of allowing half-day fractions, and displays the
stored hours rounded to whole days.
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.
Closes #93
What & why
Students could cancel a lesson right up to its start time. This adds a
cancellation cutoff: once a lesson starts within the configured window,
student-initiated online cancellation is blocked. Instructors and studio admins
are never affected — they can still cancel at any time via the lesson-status and
student-management flows.
How the window is resolved
Per lesson: the offering's own cutoff when it sets one, otherwise the studio
default. Everything is computed in hours.
us_cancellation_cutoff_hours, stored/computed in hours,but entered and displayed in days under Studio Settings → Cancellations
(default 1 day).
0= students may cancel any time.us_offerings.cancellation_cutoff_hours, entered in hours on the offeringform. Blank inherits the studio default;
0allows anytime cancellation.Enforcement
Booking\CancellationPolicyresolves the effective cutoff and decides(
studentMayCancel), comparing against WordPress-local time like the rest ofthe upcoming-lesson logic. Zero cutoff always allows; unparseable data fails
open so a student is never trapped.
BookingEndpoint::cancel()(POST /bookings/{id}/cancel) returnscancellation_closed(HTTP 403) with a humanised window ("2 days", "12 hours")when it's too late. The student booking UI already surfaces this error message.
PATCH /bookings/{id}/statusandAuth\StudentActions::cancelLesson()(adminpaths) intentionally bypass the policy.
Schema / versioning
Adds a nullable
cancellation_cutoff_hourscolumn tous_offerings. Ships withinthe current unreleased
1.1.0; the version header is bumped at release time.Docs
docs/features/cancellation-cutoff.md.docs/features/offerings.mddata-model table updated.Tests run
composer test— 440 tests, all passing (newCancellationPolicyTest, cutoffcases in
BookingEndpointTest, studio-default cases inStudioSettingsTest,offering column round-trips).
composer lint— PHPStan, no errors.composer cs— PHPCS, clean.