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:
115
src/routes/api/cospend/exchange-rates/+server.ts
Normal file
115
src/routes/api/cospend/exchange-rates/+server.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ExchangeRate } from '../../../../models/ExchangeRate';
|
||||
import { dbConnect } from '../../../../utils/db';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ locals, url }) => {
|
||||
const auth = await locals.auth();
|
||||
if (!auth || !auth.user?.nickname) {
|
||||
throw error(401, 'Not logged in');
|
||||
}
|
||||
|
||||
const fromCurrency = url.searchParams.get('from')?.toUpperCase();
|
||||
const date = url.searchParams.get('date');
|
||||
const action = url.searchParams.get('action') || 'rate';
|
||||
|
||||
if (action === 'currencies') {
|
||||
return await getSupportedCurrencies();
|
||||
}
|
||||
|
||||
if (!fromCurrency || !date) {
|
||||
throw error(400, 'Missing required parameters: from and date');
|
||||
}
|
||||
|
||||
if (!isValidCurrencyCode(fromCurrency)) {
|
||||
throw error(400, 'Invalid currency code');
|
||||
}
|
||||
|
||||
try {
|
||||
const rate = await getExchangeRate(fromCurrency, date);
|
||||
return json({ rate, fromCurrency, toCurrency: 'CHF', date });
|
||||
} catch (e) {
|
||||
console.error('Error getting exchange rate:', e);
|
||||
throw error(500, 'Failed to get exchange rate');
|
||||
}
|
||||
};
|
||||
|
||||
async function getExchangeRate(fromCurrency: string, date: string): Promise<number> {
|
||||
if (fromCurrency === 'CHF') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
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 {
|
||||
// Connection will be reused
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function getSupportedCurrencies() {
|
||||
try {
|
||||
const response = await fetch('https://api.frankfurter.app/currencies');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const currencies = Object.keys(data);
|
||||
|
||||
return json({ currencies });
|
||||
} catch (e) {
|
||||
console.error('Error fetching supported currencies:', e);
|
||||
// Return common currencies as fallback
|
||||
const fallbackCurrencies = ['EUR', 'USD', 'GBP', 'JPY', 'CAD', 'AUD', 'SEK', 'NOK', 'DKK'];
|
||||
return json({ currencies: fallbackCurrencies });
|
||||
}
|
||||
}
|
||||
|
||||
function isValidCurrencyCode(currency: string): boolean {
|
||||
return /^[A-Z]{3}$/.test(currency);
|
||||
}
|
||||
Reference in New Issue
Block a user