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; } } }