Fix missing historical currency rates (#344)
## Summary - fall back to previous historical currency rate dates when exact API release is missing - keep latest rate lookups unchanged - add regression coverage for missing historical release Fixes PHP-LARAVEL-18 ## Tests - vendor/bin/pint --dirty --format agent - php artisan test --compact tests/Feature/CurrencyConversionServiceTest.php
This commit is contained in:
parent
f6c20576b5
commit
fe3b32395c
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RuntimeException;
|
||||
|
|
@ -12,6 +13,8 @@ class CurrencyConversionService
|
|||
|
||||
private const FALLBACK_URL = 'https://currency-api.pages.dev/v1/';
|
||||
|
||||
private const HISTORICAL_LOOKBACK_DAYS = 7;
|
||||
|
||||
/** @var array<string, array<string, float>> Keyed by "{currency}:{date}" */
|
||||
private array $rateCache = [];
|
||||
|
||||
|
|
@ -77,25 +80,48 @@ class CurrencyConversionService
|
|||
*/
|
||||
private function fetchRates(string $currency, string $date): array
|
||||
{
|
||||
$primaryUrl = self::PRIMARY_URL."{$date}/v1/currencies/{$currency}.min.json";
|
||||
$fallbackUrl = self::FALLBACK_URL."{$date}/currencies/{$currency}.min.json";
|
||||
$lastException = null;
|
||||
|
||||
try {
|
||||
$response = Http::timeout(10)->get($primaryUrl);
|
||||
$response->throw();
|
||||
foreach ($this->candidateDates($date) as $candidateDate) {
|
||||
foreach ($this->rateUrls($currency, $candidateDate) as $url) {
|
||||
try {
|
||||
$response = Http::timeout(10)->get($url);
|
||||
$response->throw();
|
||||
|
||||
return $response->json($currency) ?? [];
|
||||
} catch (\Throwable) {
|
||||
// Primary failed, try fallback
|
||||
return $response->json($currency) ?? [];
|
||||
} catch (\Throwable $e) {
|
||||
$lastException = $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(10)->get($fallbackUrl);
|
||||
$response->throw();
|
||||
throw new RuntimeException("Failed to fetch currency rates for {$currency} on {$date}: {$lastException?->getMessage()}", 0, $lastException);
|
||||
}
|
||||
|
||||
return $response->json($currency) ?? [];
|
||||
} catch (\Throwable $e) {
|
||||
throw new RuntimeException("Failed to fetch currency rates for {$currency} on {$date}: {$e->getMessage()}", 0, $e);
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function candidateDates(string $date): array
|
||||
{
|
||||
if ($date === 'latest') {
|
||||
return [$date];
|
||||
}
|
||||
|
||||
$parsedDate = Carbon::createFromFormat('Y-m-d', $date);
|
||||
|
||||
return collect(range(0, self::HISTORICAL_LOOKBACK_DAYS))
|
||||
->map(fn (int $days): string => $parsedDate->copy()->subDays($days)->toDateString())
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function rateUrls(string $currency, string $date): array
|
||||
{
|
||||
return [
|
||||
self::PRIMARY_URL."{$date}/v1/currencies/{$currency}.min.json",
|
||||
self::FALLBACK_URL."{$date}/currencies/{$currency}.min.json",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,24 @@ test('throws when both primary and fallback fail', function () {
|
|||
$service->convert('BTC', 'EUR', 1.0, '2026-01-15');
|
||||
})->throws(RuntimeException::class);
|
||||
|
||||
test('falls back to previous historical date when requested release is missing', function () {
|
||||
Http::fake([
|
||||
'cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@2025-12-10/*' => Http::response('Not Found', 404),
|
||||
'currency-api.pages.dev/v1/2025-12-10/*' => Http::response('Not Found', 404),
|
||||
'cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@2025-12-09/*' => Http::response([
|
||||
'usd' => [
|
||||
'eur' => 0.9,
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$service = new CurrencyConversionService;
|
||||
$result = $service->convert('EUR', 'USD', 90.0, '2025-12-10');
|
||||
|
||||
expect($result)->toBe(100.0);
|
||||
Http::assertSentCount(3);
|
||||
});
|
||||
|
||||
test('caches rates so same currency and date makes only one HTTP request', function () {
|
||||
Http::fake([
|
||||
'cdn.jsdelivr.net/*currencies/eur*' => Http::response([
|
||||
|
|
|
|||
Loading…
Reference in New Issue