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]>
3.4 KiB
Feature: Cancellation Cutoff
Overview
Students may cancel their own lessons online — but not indefinitely close to the start time. A cancellation cutoff closes student-initiated cancellation once a lesson begins within a configured window. Instructors and studio admins are never subject to the cutoff: they can cancel a lesson at any time through the lesson-status and student-management flows.
The window is resolved per lesson:
- If the lesson's offering sets its own cutoff, that value is used.
- Otherwise the studio default applies.
Both values are expressed and computed in hours. The studio default is entered and displayed to the admin in days for convenience; a per-offering override is entered directly in hours (a finer-grained "time").
Data Model
Option us_cancellation_cutoff_hours
Studio-wide default cutoff, stored as an integer number of hours. Defaults to
24 (one day) when unset. 0 means students may cancel at any time.
Column {prefix}us_offerings.cancellation_cutoff_hours
Nullable SMALLINT UNSIGNED. NULL means "inherit the studio default"; any set
value (including 0 — cancel any time) overrides it for that offering.
Resolution & Enforcement
Booking\CancellationPolicy owns the logic:
cutoffHours(?int $offeringCutoffHours): int— the offering's override when it is a non-negative value, otherwise the studio default.studentMayCancel(string $slotStartDt, ?int $offeringCutoffHours, ?string $now): bool— false oncenowis within the effective cutoff of the slot start. A zero cutoff always allows cancellation; unparseable datetimes fail open so a student is never trapped by bad data. Comparisons use WordPress-local time (current_time('mysql')), matching how upcoming lessons are computed.describeCutoff(int $hours): string— humanises a cutoff for messages (whole days as days, otherwise hours).
Booking\BookingEndpoint::cancel() (the student endpoint,
POST /bookings/{id}/cancel) consults the policy before cancelling and returns a
cancellation_closed (HTTP 403) error explaining the window when it is too late.
The instructor status endpoint (PATCH /bookings/{id}/status) and
Auth\StudentActions::cancelLesson() (studio-admin student view) bypass the
policy entirely.
Admin Interface
- Studio Settings → Cancellations: "Cancellation cutoff (days)" — the studio default, entered/displayed in days, stored in hours.
- Offerings add/edit form: "Cancellation cutoff (hours)" — an optional
per-offering override; blank inherits the studio default,
0allows anytime cancellation.
Implementation
- Service:
Unsupervised\Schedular\Booking\CancellationPolicy - Studio default:
Unsupervised\Schedular\Payment\StudioSettings::cancellationCutoffHours()(optionus_cancellation_cutoff_hours) - Per-offering value:
Unsupervised\Schedular\Offering\Offering::$cancellationCutoffHours - Enforcement:
Unsupervised\Schedular\Booking\BookingEndpoint::cancel() - Wiring:
RestRegistrarconstructsnew CancellationPolicy( new StudioSettings() )
Tests
tests/Unit/Booking/CancellationPolicyTest.phptests/Unit/Booking/BookingEndpointTest.php(cutoff cases incancel())tests/Unit/Payment/StudioSettingsTest.php(default getter)tests/Unit/Offering/OfferingTest.php,tests/Unit/Offering/OfferingRepositoryTest.php(new column round-trips)