chore: move to Frankfurter API for exchange rate (#1214)

This commit is contained in:
jubnl
2026-06-16 20:59:08 +02:00
committed by GitHub
parent ac211d93c8
commit 79057ea603
7 changed files with 38 additions and 17 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ export function applyGlobalMiddleware(
"https://en.wikipedia.org", "https://commons.wikimedia.org",
"https://*.basemaps.cartocdn.com", "https://*.tile.openstreetmap.org",
"https://unpkg.com", "https://open-meteo.com", "https://api.open-meteo.com",
"https://geocoding-api.open-meteo.com", "https://api.exchangerate-api.com",
"https://geocoding-api.open-meteo.com", "https://api.frankfurter.dev",
"https://router.project-osrm.org/route/v1/", "https://routing.openstreetmap.de/",
"https://api.mapbox.com", "https://*.tiles.mapbox.com", "https://events.mapbox.com"
],
+11 -4
View File
@@ -1,7 +1,7 @@
/**
* Live exchange rates for the Costs/Budget money conversion.
*
* Fetches from exchangerate-api.com (no key, already CSP-allowlisted for the
* Fetches from api.frankfurter.dev (no key, already CSP-allowlisted for the
* dashboard widget) and caches per base currency in-memory for a few hours so a
* settlement request never hammers the upstream. Rates are "units of X per 1
* base", so an amount in currency C converts to base as `amount / rates[C]`.
@@ -17,10 +17,17 @@ const inflight = new Map<string, Promise<Record<string, number> | null>>();
async function fetchRates(base: string): Promise<Record<string, number> | null> {
try {
const res = await fetch(`https://api.exchangerate-api.com/v4/latest/${encodeURIComponent(base)}`);
const res = await fetch(`https://api.frankfurter.dev/v2/rates?base=${encodeURIComponent(base)}`);
if (!res.ok) return null;
const data = (await res.json()) as { rates?: Record<string, number> };
return data.rates && typeof data.rates === 'object' ? data.rates : null;
// Frankfurter returns an array of { date, base, quote, rate } and omits the
// base's own self-rate, so seed the map with `base = 1` then index by quote.
const data = (await res.json()) as Array<{ quote?: string; rate?: number }>;
if (!Array.isArray(data)) return null;
const rates: Record<string, number> = { [base.toUpperCase()]: 1 };
for (const r of data) {
if (r && typeof r.quote === 'string' && typeof r.rate === 'number') rates[r.quote] = r.rate;
}
return Object.keys(rates).length > 1 ? rates : null;
} catch {
return null;
}