year = $year ?? Date::now()->addYear()->year; throw_if( $this->year < 2026, \InvalidArgumentException::class ); } public function handle(): void { $hollydayList = $this->getHollydaysForYear($this->year); foreach ($hollydayList as $holiday) { $exception = PickupException::query() ->firstOrNew(['label' => $holiday->label]); $exception->fill([ 'date' => $holiday->date->format('Y-m-d'), 'repeatable' => true, 'repeat_frequency' => RepeatFrequency::YEARLY, ]); $exception->setAttribute('auto_updated', true); $exception->save(); } // * Sync repeatables api pickup exception for all shops $repeatablesFromApi = PickupException::query() ->whereIn('label', array_map( fn ($v) => $v->label, $hollydayList ))->get(); Shop::query()->get() ->each(fn ($shop) => $shop->pickupExceptions() ->syncWithoutDetaching($repeatablesFromApi)); } /** @return object{date:\DateTimeInterface,label:string}[] */ private function getHollydaysForYear(int $year): array { $url = $this->getCalendarApiUrlFor($year); $response = Http::timeout(self::API_CALL_TIMEOUT)->get($url); throw_unless( $response->successful(), \UnexpectedValueException::class, 'Failed to fetch calendar from api' ); $outputList = $response ->collect() ->map(fn ($label, $date) => [ 'date' => $date, 'label' => $label, ])->all(); $rules = [ '*' => 'array', '*.date' => ['required', 'date'], '*.label' => ['required', 'string'], ]; $validator = Validator::make($outputList, $rules); if ($validator->fails()) { $errors = $validator->errors(); throw new \UnexpectedValueException( 'Invalid response: ' . json_encode($errors->messages()) ); } return array_map(fn (array $entry) => (object) [ 'date' => Date::parse( $entry['date'], new \DateTimeZone('Europe/Paris') ), 'label' => Arr::string($entry, 'label'), ], $outputList); } private function getCalendarApiUrlFor(int $year): string { return str_replace( ':year', strval($year), self::CALENDAR_API_URL ); } }