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:
2025-09-14 19:54:31 +02:00
parent ac84de43e1
commit 90ea22497f
13 changed files with 936 additions and 59 deletions

View File

@@ -0,0 +1,47 @@
import mongoose from 'mongoose';
export interface IExchangeRate {
_id?: string;
fromCurrency: string; // e.g., "USD"
toCurrency: string; // Always "CHF" for our use case
rate: number;
date: string; // Date in YYYY-MM-DD format
createdAt?: Date;
updatedAt?: Date;
}
const ExchangeRateSchema = new mongoose.Schema(
{
fromCurrency: {
type: String,
required: true,
uppercase: true,
trim: true
},
toCurrency: {
type: String,
required: true,
uppercase: true,
trim: true,
default: 'CHF'
},
rate: {
type: Number,
required: true,
min: 0
},
date: {
type: String,
required: true,
match: /^\d{4}-\d{2}-\d{2}$/
}
},
{
timestamps: true
}
);
// Create compound index for efficient lookups
ExchangeRateSchema.index({ fromCurrency: 1, toCurrency: 1, date: 1 }, { unique: true });
export const ExchangeRate = mongoose.model<IExchangeRate>("ExchangeRate", ExchangeRateSchema);

View File

@@ -4,8 +4,10 @@ export interface IPayment {
_id?: string;
title: string;
description?: string;
amount: number;
currency: string;
amount: number; // Always in CHF (converted if necessary)
currency: string; // Currency code (CHF if no conversion, foreign currency if converted)
originalAmount?: number; // Amount in foreign currency (only if currency != CHF)
exchangeRate?: number; // Exchange rate used for conversion (only if currency != CHF)
paidBy: string; // username/nickname of the person who paid
date: Date;
image?: string; // path to uploaded image
@@ -36,7 +38,17 @@ const PaymentSchema = new mongoose.Schema(
type: String,
required: true,
default: 'CHF',
enum: ['CHF'] // For now only CHF as requested
uppercase: true
},
originalAmount: {
type: Number,
required: false,
min: 0
},
exchangeRate: {
type: Number,
required: false,
min: 0
},
paidBy: {
type: String,

View File

@@ -4,16 +4,16 @@ export interface IRecurringPayment {
_id?: string;
title: string;
description?: string;
amount: number;
currency: string;
amount: number; // Amount in the original currency
currency: string; // Original currency code
paidBy: string; // username/nickname of the person who paid
category: 'groceries' | 'shopping' | 'travel' | 'restaurant' | 'utilities' | 'fun' | 'settlement';
splitMethod: 'equal' | 'full' | 'proportional' | 'personal_equal';
splits: Array<{
username: string;
amount?: number;
amount?: number; // Amount in original currency
proportion?: number;
personalAmount?: number;
personalAmount?: number; // Amount in original currency
}>;
frequency: 'daily' | 'weekly' | 'monthly' | 'custom';
cronExpression?: string; // For custom frequencies using cron syntax
@@ -47,7 +47,7 @@ const RecurringPaymentSchema = new mongoose.Schema(
type: String,
required: true,
default: 'CHF',
enum: ['CHF']
uppercase: true
},
paidBy: {
type: String,