# EIA Brent Price Source — Primary with FRED Fallback **Date:** 2026-04-14 **Status:** Approved ## Problem FRED (`DCOILBRENTEU`) publishes Brent crude prices with a lag on top of the EIA's own publication delay. As of 2026-04-14, FRED's most recent data is 2026-04-02 — 12 days stale. EIA is the original data source; FRED mirrors it. Using EIA directly removes the mirroring lag. ## Goal Replace FRED as the primary source for `fetchBrentPrices()` with the EIA Open Data API, keeping FRED as a fallback so the system degrades gracefully if EIA is unavailable. --- ## Architecture All changes are confined to `app/Services/OilPriceService.php` and supporting config/env files. ### `fetchBrentPrices()` — updated flow ``` 1. $rows = fetchFromEia() 2. if ($rows === null): Log::warning('OilPriceService: EIA fetch failed, falling back to FRED') $rows = fetchFromFred() 3. if ($rows === null): Log::error('OilPriceService: both EIA and FRED fetch failed') return 4. BrentPrice::upsert($rows, ['date'], ['price_usd']) ``` ### `fetchFromEia(): ?array` (new private method) - Endpoint: `GET https://api.eia.gov/v2/petroleum/pri/spt/data/` - Params: `api_key`, `frequency=daily`, `data[0]=value`, `facets[series][]=RBRTE`, `sort[0][column]=period`, `sort[0][direction]=desc`, `length=30` - Returns `null` on: HTTP error, exception, empty/missing `response.data` array - Maps each row: `['date' => $row['period'], 'price_usd' => (float) $row['value']]` - Filters rows where `value` is `'.'` (EIA uses same placeholder as FRED) ### `fetchFromFred(): ?array` (extracted private method) - Existing FRED logic moved verbatim from the current `fetchBrentPrices()` body - Returns `null` on: HTTP error, exception, empty observations - Maps each row: `['date' => $obs['date'], 'price_usd' => (float) $obs['value']]` --- ## Configuration ### `config/services.php` Add under existing entries: ```php 'eia' => [ 'api_key' => env('EIA_API_KEY'), ], ``` ### `.env.example` Add: ``` EIA_API_KEY= # US EIA Open Data API key — register free at eia.gov/opendata ``` FRED key stays in `.env.example` (still used as fallback). --- ## Logging | Event | Level | Message | |---|---|---| | EIA fetch succeeded | — | (no log — normal path) | | EIA fetch failed, FRED used | `warning` | `OilPriceService: EIA fetch failed, falling back to FRED` | | Both failed | `error` | `OilPriceService: both EIA and FRED fetch failed` | | No rows after filter | `warning` | `OilPriceService: no valid EIA observations returned` (existing pattern) | --- ## Tests Existing `OilPriceService` fetch tests are updated (not replaced): 1. **EIA succeeds** — `Http::fake()` returns valid EIA response; assert upsert called with correct rows; assert FRED endpoint never called. 2. **EIA fails, FRED succeeds** — EIA returns 500; assert FRED endpoint called; assert upsert called with FRED rows; assert warning logged. 3. **Both fail** — both return 500; assert upsert never called; assert error logged. 4. **EIA returns empty data** — `response.data` is `[]`; assert falls back to FRED. --- ## Out of Scope - No changes to `generatePrediction()`, `PredictOilPrices` command, or scheduler - No new service classes or interfaces - No changes to `brent_prices` schema