call([ 'sort_order' => 'desc', 'limit' => 30, ]); } /** * Backfill range (inclusive). FRED's `observation_start` / * `observation_end` parameters expect ISO dates (YYYY-MM-DD). * Returns null when the range is empty (e.g. all weekends/holidays). * * @return array{date: string, price_usd: float}[]|null * * @throws BrentPriceFetchException */ public function fetchRange(string $from, string $to): ?array { return $this->call([ 'observation_start' => $from, 'observation_end' => $to, 'sort_order' => 'asc', 'limit' => 100000, ]); } /** * @param array $extraParams * @return array{date: string, price_usd: float}[]|null * * @throws BrentPriceFetchException */ private function call(array $extraParams): ?array { $params = array_merge([ 'series_id' => 'DCOILBRENTEU', 'api_key' => config('services.fred.api_key'), 'file_type' => 'json', ], $extraParams); try { $response = $this->apiLogger->send('fred', 'GET', self::URL, fn () => Http::timeout(60) ->retry(3, 200, fn (Throwable $e) => $this->shouldRetry($e)) ->throw() ->get(self::URL, $params)); } catch (ConnectionException $e) { throw new BrentPriceFetchException("FRED connection failed: {$e->getMessage()}", previous: $e); } catch (RequestException $e) { throw new BrentPriceFetchException("FRED returned HTTP {$e->response->status()}", previous: $e); } $rows = collect($response->json('observations') ?? []) ->filter(fn (array $obs) => $obs['value'] !== '.') ->map(fn (array $obs) => [ 'date' => $obs['date'], 'price_usd' => (float) $obs['value'], ]) ->all(); return $rows === [] ? null : $rows; } private function shouldRetry(Throwable $e): bool { return $e instanceof ConnectionException || ($e instanceof RequestException && $e->response->serverError()); } }