firstOrFail(); $bookings = DB::table('bookings') ->whereBetween('date', [now()->subDay()->toDateString(), now()->addYear()->toDateString()]) ->orderBy('date') ->get(); $events = []; foreach ($bookings as $b) { $slot = DB::table('slots')->where('id', $b->slot_id)->first(); if (!$slot) { continue; } // (optional guard) $startAt = Carbon::parse($b->date.' '.$slot->time, 'Europe/London')->utc(); $endAt = (clone $startAt)->addMinutes(60); $uid = 'booking-'.$b->id.'@tutoring.richardjolley.co.uk'; // IMPORTANT: build UTC DateTime objects that will serialize as ...Z (no TZID) $start = new DateTime(new \DateTimeImmutable($startAt->format('Y-m-d\TH:i:s')), true); $end = new DateTime(new \DateTimeImmutable($endAt->format('Y-m-d\TH:i:s')), true); $event = (new IcalEvent(new UniqueIdentifier($uid))) ->setSummary('Tutoring Booking') ->setOccurrence(new TimeSpan($start, $end)); $events[] = $event; } $calendar = new Calendar($events); $ics = (new CalendarFactory())->createCalendar($calendar); return response($ics) ->header('Content-Type', 'text/calendar; charset=utf-8') ->header('Content-Disposition', 'attachment; filename="tutoring-'.$teacher->id.'.ics"'); } }