Files
unsupervised-scheduler/docs/features/cancellation-cutoff.md
thatguygriffandClaude Opus 4.8 8b90b8d78d
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
Add cancellation cutoff limiting how close to a lesson a student can cancel
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]>
2026-07-23 11:57:18 -03:00

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:

  1. If the lesson's offering sets its own cutoff, that value is used.
  2. 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 once now is 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, 0 allows anytime cancellation.

Implementation

  • Service: Unsupervised\Schedular\Booking\CancellationPolicy
  • Studio default: Unsupervised\Schedular\Payment\StudioSettings::cancellationCutoffHours() (option us_cancellation_cutoff_hours)
  • Per-offering value: Unsupervised\Schedular\Offering\Offering::$cancellationCutoffHours
  • Enforcement: Unsupervised\Schedular\Booking\BookingEndpoint::cancel()
  • Wiring: RestRegistrar constructs new CancellationPolicy( new StudioSettings() )

Tests

  • tests/Unit/Booking/CancellationPolicyTest.php
  • tests/Unit/Booking/BookingEndpointTest.php (cutoff cases in cancel())
  • tests/Unit/Payment/StudioSettingsTest.php (default getter)
  • tests/Unit/Offering/OfferingTest.php, tests/Unit/Offering/OfferingRepositoryTest.php (new column round-trips)