54 lines
1.7 KiB
PHP
54 lines
1.7 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services\Runware;
|
||
|
|
|
||
|
|
use Illuminate\Support\Facades\Cache;
|
||
|
|
use Illuminate\Support\Facades\Http;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Keeps a live-ish USD->EUR rate cached for BudgetGuard to convert Runware's
|
||
|
|
* USD costs against the EUR monthly cap. Refreshed daily by the scheduler
|
||
|
|
* (see routes/console.php); falls back to a static configured rate if no
|
||
|
|
* live fetch has ever succeeded, so the cap never stops enforcing.
|
||
|
|
*/
|
||
|
|
class CurrencyRateService
|
||
|
|
{
|
||
|
|
protected const CACHE_KEY = 'runware:usd_eur_rate';
|
||
|
|
|
||
|
|
// Refresh runs daily; keep the cached value alive well past that so a
|
||
|
|
// single failed fetch doesn't drop back to the fallback rate.
|
||
|
|
protected const CACHE_TTL_SECONDS = 60 * 60 * 30;
|
||
|
|
|
||
|
|
public function usdToEur(): float
|
||
|
|
{
|
||
|
|
return Cache::get(self::CACHE_KEY) ?? (float) config('runware.usd_eur_fallback_rate');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function refresh(): ?float
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$response = Http::timeout(10)->get('https://api.frankfurter.app/latest', [
|
||
|
|
'from' => 'USD',
|
||
|
|
'to' => 'EUR',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$rate = $response->json('rates.EUR');
|
||
|
|
|
||
|
|
if (! $response->successful() || ! is_numeric($rate)) {
|
||
|
|
throw new \RuntimeException('Unexpected FX response: '.$response->body());
|
||
|
|
}
|
||
|
|
|
||
|
|
Cache::put(self::CACHE_KEY, (float) $rate, self::CACHE_TTL_SECONDS);
|
||
|
|
|
||
|
|
return (float) $rate;
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
Log::warning('Failed to refresh USD/EUR rate, keeping previous cached/fallback value.', [
|
||
|
|
'exception' => $e->getMessage(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|