From bd7a55c6d373cad3ceb60ef75c50d3d6593cbea3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 16:45:30 +0000 Subject: [PATCH] fix: always fetch exchange rate by expense date Agent-Logs-Url: https://github.com/ClementLSW/osps/sessions/7f23da57-fd59-4ff9-8e7d-b17be954e069 Co-authored-by: ClementLSW <24468433+ClementLSW@users.noreply.github.com> --- src/lib/exchangeRate.js | 66 +++++++++------------------------------- src/pages/AddExpense.jsx | 12 ++++---- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/src/lib/exchangeRate.js b/src/lib/exchangeRate.js index f980be8..7bab37c 100644 --- a/src/lib/exchangeRate.js +++ b/src/lib/exchangeRate.js @@ -1,69 +1,33 @@ /** - * Exchange rate utilities — calls /api/exchange-rate (Netlify Function proxy). - * - * The proxy calls api.frankfurter.dev server-side, avoiding browser CORS - * restrictions. Rate convention: 1 unit of `from` = X units of `to`. - * - * ECB publish window: - * Rates update daily around 16:00 CET (22:00 SGT). During this window - * /latest can be unstable. If the call fails without an explicit date, - * we retry once with yesterday's date as a fallback. + * Exchange rate utilities — fetches from frankfurter.dev. + * Always fetches by explicit date so rate reflects when money was spent. */ +const BASE_URL = 'https://api.frankfurter.dev/v1' + /** - * Fetch exchange rate. - * @param {string} from - Source currency (ISO 4217, e.g. 'MYR') - * @param {string} to - Target currency (ISO 4217, e.g. 'SGD') - * @param {string} [date] - ISO date string (YYYY-MM-DD) for historical rate - * @returns {Promise<{ from, to, rate, date, fetchedAt, usedFallbackDate? }>} + * @param {string} from - Source currency (e.g. 'MYR') + * @param {string} to - Target currency (e.g. 'SGD') + * @param {string} date - ISO date YYYY-MM-DD (the expense date) + * @returns {Promise<{ from, to, rate, date, fetchedAt }>} */ export async function fetchExchangeRate(from, to, date) { if (from === to) { - return { - from, - to, - rate: 1, - date: date || new Date().toISOString().slice(0, 10), - fetchedAt: new Date().toISOString(), - } + return { from, to, rate: 1, date, fetchedAt: new Date().toISOString() } } - // First attempt - try { - return await _fetchOnce(from, to, date) - } catch (err) { - // Only retry /latest — historical dates don't have publish window issues - if (date) throw err - - const yesterday = new Date() - yesterday.setUTCDate(yesterday.getUTCDate() - 1) - const fallbackDate = yesterday.toISOString().slice(0, 10) - - const result = await _fetchOnce(from, to, fallbackDate) - return { ...result, usedFallbackDate: true } - } -} - -async function _fetchOnce(from, to, date) { - const params = new URLSearchParams({ from, to }) - if (date) params.set('date', date) - - const res = await fetch(`/api/exchange-rate?${params}`) + const res = await fetch(`${BASE_URL}/${date}?from=${from}&to=${to}`) if (!res.ok) { - const body = await res.json().catch(() => ({})) - const err = new Error(body.error || `Exchange rate fetch failed: ${res.status}`) + const err = new Error(`Exchange rate fetch failed: ${res.status}`) err.status = res.status throw err } const data = await res.json() + const rate = data.rates?.[to] - return { - from: data.from, - to: data.to, - rate: data.rate, - date: data.date, - fetchedAt: new Date().toISOString(), - } + if (rate == null) throw new Error(`No rate found for ${from} → ${to}`) + + return { from, to, rate, date: data.date, fetchedAt: new Date().toISOString() } } diff --git a/src/pages/AddExpense.jsx b/src/pages/AddExpense.jsx index 6345efa..528ec22 100644 --- a/src/pages/AddExpense.jsx +++ b/src/pages/AddExpense.jsx @@ -65,33 +65,33 @@ export default function AddExpense() { // Fetch exchange rate when currency or date changes useEffect(() => { const groupCurrency = group?.currency || 'SGD' - if (!expenseCurrency || expenseCurrency === groupCurrency || manualRate) return + if (!expenseCurrency || expenseCurrency === groupCurrency || manualRate) { + setRateLoading(false) + return + } let cancelled = false async function fetchRate() { setRateLoading(true) try { - const isToday = expenseDate === new Date().toISOString().slice(0, 10) const result = await fetchExchangeRate( expenseCurrency, groupCurrency, - isToday ? undefined : expenseDate + expenseDate, ) if (!cancelled) setExchangeRate(result) } catch (err) { - const isHistorical = expenseDate !== new Date().toISOString().slice(0, 10) await log('fx.error', { from: expenseCurrency, to: group?.currency || 'SGD', date: expenseDate, - is_historical: isHistorical, http_status: err.status ?? null, error: err.message, }) console.error('Exchange rate fetch failed:', err) if (!cancelled) toast.error('Could not fetch exchange rate') } finally { - if (!cancelled) setRateLoading(false) + setRateLoading(false) } } fetchRate()