Cancellation cutoff: limit how close to a lesson a student can cancel #94

Merged
thatguygriff merged 2 commits from feature/cancellation-cutoff into main 2026-07-23 15:14:56 +00:00
Owner

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 defaultus_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.
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.
thatguygriff added 1 commit 2026-07-23 14:58:00 +00:00
Add cancellation cutoff limiting how close to a lesson a student can cancel
CI / Tests (PHP 8.1) (pull_request) Successful in 41s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Coding Standards (pull_request) Successful in 2m54s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m38s
CI / Build Plugin Zip (pull_request) Skipped
8b90b8d78d
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]>
thatguygriff added 1 commit 2026-07-23 15:10:58 +00:00
Accept whole days only for the studio cancellation cutoff
CI / Tests (PHP 8.2) (pull_request) Successful in 38s
CI / Tests (PHP 8.1) (pull_request) Successful in 49s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m54s
CI / Coding Standards (pull_request) Successful in 3m2s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m40s
CI / Build Plugin Zip (pull_request) Skipped
169f7b6a13
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]>
thatguygriff merged commit 90fdde8c06 into main 2026-07-23 15:14:56 +00:00
thatguygriff deleted branch feature/cancellation-cutoff 2026-07-23 15:14:56 +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#94