# 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)