import { and, desc, eq, lte } from 'drizzle-orm';
import { fxRates } from '@rareindex/database';
import { toDateOnly, type CurrencyCode } from '@rareindex/shared';
import { db } from './db.ts';
/**
* Historical FX lookup (§136): rate quoted as 1 = rate , base USD in fx_rates.
* Converts `amount` in `currency` to USD using the latest rate on or before `date`
* (weekends/holidays fall back to the previous business day). Returns null when no rate exists —
* callers must then trigger an FX sync or leave price_usd unresolved, never guess.
*/
const cache = new Map();
export async function usdRateFor(currency: string, date: Date): Promise<{ rate: number; date: string } | null> {
if (currency === 'USD') return { rate: 1, date: toDateOnly(date) };
const key = `${currency}|${toDateOnly(date)}`;
if (cache.has(key)) return cache.get(key)!;
const [row] = await db()
.select({ rate: fxRates.rate, date: fxRates.date })
.from(fxRates)
.where(and(eq(fxRates.base, 'USD'), eq(fxRates.quote, currency), lte(fxRates.date, toDateOnly(date))))
.orderBy(desc(fxRates.date))
.limit(1);
const out = row ? { rate: row.rate, date: row.date } : null;
// Only cache misses briefly (map grows per date otherwise)
if (cache.size > 50_000) cache.clear();
cache.set(key, out);
return out;
}
export async function toUsd(amount: number, currency: string, date: Date): Promise<{ usd: number; rate: number; fxDate: string } | null> {
const r = await usdRateFor(currency, date);
if (!r || r.rate <= 0) return null;
// fx_rates.rate = quote units per 1 USD → USD = amount / rate
return { usd: amount / r.rate, rate: r.rate, fxDate: r.date };
}
export function pickRate(rates: Array<{ date: string; rate: number }>, date: string): { date: string; rate: number } | null {
let best: { date: string; rate: number } | null = null;
for (const r of rates) if (r.date <= date && (!best || r.date > best.date)) best = r;
return best;
}
export type { CurrencyCode };