refactor: split oil price ingestion and prediction into separate services + commands

- BrentPriceFetcher owns ingestion (fetchFromEia / fetchFromFred, each throws on failure)
- BrentPricePredictor owns prediction and marks latest brent_prices row as generated
- oil:fetch command tries EIA, falls back to FRED, fails loudly if both fail
- oil:predict command prompts if latest price already has a prediction; --force bypasses
- add prediction_generated_at column to brent_prices
- delete OilPriceService (replaced by the two focused services)
This commit is contained in:
Ovidiu U
2026-04-14 16:59:43 +01:00
parent 1a0381265e
commit 486f0e689c
10 changed files with 415 additions and 306 deletions

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Services;
use App\Models\BrentPrice;
use App\Services\BrentPriceSources\BrentPriceFetchException;
use App\Services\BrentPriceSources\EiaBrentPriceSource;
use App\Services\BrentPriceSources\FredBrentPriceSource;
final readonly class BrentPriceFetcher
{
public function __construct(
private readonly EiaBrentPriceSource $eia,
private readonly FredBrentPriceSource $fred,
) {}
/**
* Fetch from EIA and persist. Throws on failure.
*/
public function fetchFromEia(): void
{
$rows = $this->eia->fetch();
if ($rows === null) {
throw new BrentPriceFetchException('EIA fetch returned no data');
}
BrentPrice::upsert($rows, ['date'], ['price_usd']);
}
/**
* Fetch from FRED and persist. Throws on failure.
*/
public function fetchFromFred(): void
{
$rows = $this->fred->fetch();
if ($rows === null) {
throw new BrentPriceFetchException('FRED fetch returned no data');
}
BrentPrice::upsert($rows, ['date'], ['price_usd']);
}
}