tz); $slotStartUtc = $slotStartLocal->clone()->utc(); $slotEndUtc = $slotStartUtc->clone()->addMinutes($durationMins); // Expand comparison bounds by buffer $slotStartMinusBuffer = $slotStartUtc->clone()->subMinutes($this->bufferMins); $slotEndPlusBuffer = $slotEndUtc->clone()->addMinutes($this->bufferMins); // Overlap: busyStart < slotEnd+buffer AND busyEnd > slotStart-buffer return ExternalBusyWindow::query() ->where('starts_at', '<', $slotEndPlusBuffer) ->where('ends_at', '>', $slotStartMinusBuffer) ->exists(); } /** * For block-booking: given a set of future candidate dates for a given time, * return the subset that are blocked (with reasons). * * @param array $dateListYmd e.g. ['2025-10-27','2025-11-03', ...] * @return array */ public function checkSeriesConflicts(array $dateListYmd, string $timeHi, int $durationMins = 60): array { $results = []; foreach ($dateListYmd as $ymd) { $blocked = $this->isSlotBlockedByExternalBusy($ymd, $timeHi, $durationMins); $results[] = [ 'date' => $ymd, 'blocked' => $blocked, ]; } return $results; } }