/** * CAD → USD foreign exchange conversion. * * Source: Bank of Canada daily exchange rate (Valet API). * Caches the rate for 24 hours. * * cadToUsdCents(cadCents): * 1. Fetch current CAD/USD rate (e.g., 1 CAD = 0.73 USD) * 2. Convert CAD cents to USD cents * 3. Add 10% buffer to cover fluctuations * 4. Round UP to the nearest whole dollar (100 cents) */ let cachedRate: { rate: number; fetchedAt: number } | null = null; const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours const BUFFER_PCT = 0.10; // 10% buffer on top of spot rate const FALLBACK_RATE = 0.72; // conservative fallback if API is unreachable /** * Fetch the current CAD/USD exchange rate from the Bank of Canada. * Returns how many USD 1 CAD buys (e.g., 0.73). */ async function fetchCadUsdRate(): Promise { // Bank of Canada Valet API — FXCADUSD is the official daily rate const url = "https://www.bankofcanada.ca/valet/observations/FXCADUSD/json?recent=1"; try { const resp = await fetch(url, { signal: AbortSignal.timeout(8000), headers: { "Accept": "application/json" }, }); if (!resp.ok) { throw new Error(`Bank of Canada API returned ${resp.status}`); } const data = await resp.json() as { observations: Array<{ d: string; FXCADUSD: { v: string } }>; }; const obs = data.observations; if (!obs || obs.length === 0) { throw new Error("No observations returned from Bank of Canada"); } const rate = parseFloat(obs[obs.length - 1].FXCADUSD.v); if (isNaN(rate) || rate <= 0 || rate > 2) { throw new Error(`Invalid rate value: ${obs[obs.length - 1].FXCADUSD.v}`); } console.log(`[fx] Bank of Canada CAD/USD rate: ${rate} (date: ${obs[obs.length - 1].d})`); return rate; } catch (err) { console.warn(`[fx] Failed to fetch Bank of Canada rate: ${err}. Using fallback ${FALLBACK_RATE}`); return FALLBACK_RATE; } } /** * Get the current CAD/USD rate (cached for 24h). */ async function getCadUsdRate(): Promise { const now = Date.now(); if (cachedRate && (now - cachedRate.fetchedAt) < CACHE_TTL_MS) { return cachedRate.rate; } const rate = await fetchCadUsdRate(); cachedRate = { rate, fetchedAt: now }; return rate; } /** * Convert CAD cents to USD cents with 10% buffer, rounded UP to nearest whole dollar. * * Example: C$350 (35000 cents) at rate 0.73: * 35000 * 0.73 = 25550 USD cents * 25550 * 1.10 = 28105 (with 10% buffer) * Round up to $282.00 = 28200 cents */ export async function cadToUsdCents(cadCents: number): Promise { const rate = await getCadUsdRate(); const usdCentsRaw = cadCents * rate; const withBuffer = usdCentsRaw * (1 + BUFFER_PCT); // Round UP to nearest 100 (whole dollar) const rounded = Math.ceil(withBuffer / 100) * 100; return rounded; } /** * Synchronous version using cached rate (returns 0 if not yet fetched). * Use for display purposes only — call cadToUsdCents() for order pricing. */ export function cadToUsdCentsSync(cadCents: number): number { if (!cachedRate) return 0; const usdCentsRaw = cadCents * cachedRate.rate; const withBuffer = usdCentsRaw * (1 + BUFFER_PCT); return Math.ceil(withBuffer / 100) * 100; } /** * Get the current cached rate info (for display/debugging). */ export async function getFxInfo(): Promise<{ rate: number; withBuffer: number; fetchedAt: string }> { const rate = await getCadUsdRate(); return { rate, withBuffer: rate * (1 + BUFFER_PCT), fetchedAt: cachedRate ? new Date(cachedRate.fetchedAt).toISOString() : "never", }; } /** * Pre-warm the cache at startup. */ export async function warmFxCache(): Promise { await getCadUsdRate(); }