Files
fuel-price/docs/superpowers/specs/2026-04-14-eia-brent-price-fallback-design.md
Ovidiu U aec547cd86
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
refactor: restructure Stripe pricing config to support monthly and annual tiers
- Nest price IDs under `monthly` and `annual` keys for each tier (basic, plus, pro)
2026-04-14 19:26:01 +01:00

98 lines
3.2 KiB
Markdown

# 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