- Made `/api/auth/me` public and return explicit allowlist (name, email, two_factor_confirmed_at, tier, subscription fields) instead of spreading `$user->toArray()` which leaked is_admin, stripe_id, pm_type, pm_last_four, postcode. Returns `null` when unauthenticated rather than 401. - Moved `/auth/logout` to remain behind auth:sanctum gate. - Added 3×200ms retry with exponential backoff to EiaBrentPriceSource and FredBrentPriceSource on ConnectionException or 5xx responses. Timeout raised from 10s to 30s. - Both sources now throw typed BrentPriceFetchException on exhausted retries instead of silently returning null + logging. Updated tests to assert exception message includes HTTP status or "connection failed".
58 lines
2.0 KiB
PHP
58 lines
2.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\BrentPriceSources;
|
|
|
|
use App\Services\ApiLogger;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Throwable;
|
|
|
|
final class FredBrentPriceSource
|
|
{
|
|
private const string URL = 'https://api.stlouisfed.org/fred/series/observations';
|
|
|
|
public function __construct(private readonly ApiLogger $apiLogger) {}
|
|
|
|
/**
|
|
* @return array{date: string, price_usd: float}[]|null null only when the response carried no usable rows
|
|
*
|
|
* @throws BrentPriceFetchException on network failure or non-2xx response after retries
|
|
*/
|
|
public function fetch(): ?array
|
|
{
|
|
try {
|
|
$response = $this->apiLogger->send('fred', 'GET', self::URL, fn () => Http::timeout(30)
|
|
->retry(3, 200, fn (Throwable $e) => $this->shouldRetry($e))
|
|
->throw()
|
|
->get(self::URL, [
|
|
'series_id' => 'DCOILBRENTEU',
|
|
'api_key' => config('services.fred.api_key'),
|
|
'sort_order' => 'desc',
|
|
'limit' => 30,
|
|
'file_type' => 'json',
|
|
]));
|
|
} 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());
|
|
}
|
|
}
|