feat: add multi-currency support to cospend payments
- Add ExchangeRate model for currency conversion tracking - Implement currency utility functions for formatting and conversion - Add exchange rates API endpoint with caching and fallback rates - Update Payment and RecurringPayment models to support multiple currencies - Enhanced payment forms with currency selection and conversion display - Update split method selector with better currency handling - Add currency-aware payment display and balance calculations - Support for EUR, USD, GBP, and CHF with automatic exchange rate fetching 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
94
src/lib/utils/currency.ts
Normal file
94
src/lib/utils/currency.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ExchangeRate } from '../../models/ExchangeRate';
|
||||
import { dbConnect, dbDisconnect } from '../../utils/db';
|
||||
|
||||
/**
|
||||
* Convert amount from foreign currency to CHF using direct database/API access
|
||||
*/
|
||||
export async function convertToCHF(
|
||||
amount: number,
|
||||
fromCurrency: string,
|
||||
date: string,
|
||||
fetch?: typeof globalThis.fetch
|
||||
): Promise<{
|
||||
convertedAmount: number;
|
||||
exchangeRate: number;
|
||||
}> {
|
||||
if (fromCurrency.toUpperCase() === 'CHF') {
|
||||
return {
|
||||
convertedAmount: amount,
|
||||
exchangeRate: 1
|
||||
};
|
||||
}
|
||||
|
||||
const rate = await getExchangeRate(fromCurrency.toUpperCase(), date);
|
||||
|
||||
return {
|
||||
convertedAmount: amount * rate,
|
||||
exchangeRate: rate
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exchange rate from database cache or fetch from API
|
||||
*/
|
||||
async function getExchangeRate(fromCurrency: string, date: string): Promise<number> {
|
||||
const dateStr = date.split('T')[0]; // Extract YYYY-MM-DD
|
||||
|
||||
await dbConnect();
|
||||
|
||||
try {
|
||||
// Try cache first
|
||||
const cachedRate = await ExchangeRate.findOne({
|
||||
fromCurrency,
|
||||
toCurrency: 'CHF',
|
||||
date: dateStr
|
||||
});
|
||||
|
||||
if (cachedRate) {
|
||||
return cachedRate.rate;
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
const rate = await fetchFromFrankfurterAPI(fromCurrency, dateStr);
|
||||
|
||||
// Cache the result
|
||||
await ExchangeRate.create({
|
||||
fromCurrency,
|
||||
toCurrency: 'CHF',
|
||||
rate,
|
||||
date: dateStr
|
||||
});
|
||||
|
||||
return rate;
|
||||
} finally {
|
||||
await dbDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch exchange rate from Frankfurter API
|
||||
*/
|
||||
async function fetchFromFrankfurterAPI(fromCurrency: string, date: string): Promise<number> {
|
||||
const url = `https://api.frankfurter.app/${date}?from=${fromCurrency}&to=CHF`;
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Frankfurter API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.rates || !data.rates.CHF) {
|
||||
throw new Error(`No exchange rate found for ${fromCurrency} to CHF on ${date}`);
|
||||
}
|
||||
|
||||
return data.rates.CHF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate currency code (3-letter ISO code)
|
||||
*/
|
||||
export function isValidCurrencyCode(currency: string): boolean {
|
||||
return /^[A-Z]{3}$/.test(currency.toUpperCase());
|
||||
}
|
||||
Reference in New Issue
Block a user