= 0 ) { return $offeringCutoffHours; } return $this->settings->cancellationCutoffHours(); } /** * Whether a student may still cancel a lesson starting at $slotStartDt * (WordPress-local `Y-m-d H:i:s`), given the offering's optional cutoff * override. A zero cutoff always allows cancellation; unparseable input * fails open so a student is never trapped by bad data. Pass $now to make * the comparison deterministic in tests. */ public function studentMayCancel( string $slotStartDt, ?int $offeringCutoffHours, ?string $now = null ): bool { $hours = $this->cutoffHours( $offeringCutoffHours ); if ( $hours <= 0 ) { return true; } $start = strtotime( $slotStartDt ); $current = strtotime( $now ?? current_time( 'mysql' ) ); if ( false === $start || false === $current ) { return true; } return ( $start - $current ) >= $hours * 3600; } /** * A human-readable description of a cutoff for student-facing messages: * whole days as days, anything else as hours. */ public function describeCutoff( int $hours ): string { if ( $hours > 0 && 0 === $hours % 24 ) { $days = $hours / 24; /* translators: %d: number of days. */ return sprintf( _n( '%d day', '%d days', $days, 'unsupervised-schedular' ), $days ); } /* translators: %d: number of hours. */ return sprintf( _n( '%d hour', '%d hours', $hours, 'unsupervised-schedular' ), $hours ); } }