feat: Add currency selection and conversion support in PDF generation, enhance expense processing with target currency handling

This commit is contained in:
2025-07-10 14:02:14 -04:00
parent 2928d9a7ed
commit 3ba8542e4f
4 changed files with 405 additions and 65 deletions

View File

@@ -303,6 +303,80 @@ export const convertToUSD = async (amount: number, fromCurrency: string): Promis
}
};
/**
* Convert amount from one currency to EUR
*/
export const convertToEUR = async (amount: number, fromCurrency: string): Promise<{
eurAmount: number;
rate: number;
conversionDate: string;
} | null> => {
// If already EUR, no conversion needed
if (fromCurrency.toUpperCase() === 'EUR') {
return {
eurAmount: amount,
rate: 1.0,
conversionDate: new Date().toISOString()
};
}
try {
const rateCache = await getExchangeRates();
if (!rateCache) {
console.error('[currency] No exchange rates available for conversion');
return null;
}
const fromCurrencyUpper = fromCurrency.toUpperCase();
// Get USD -> EUR rate
const usdToEurRate = rateCache.rates['EUR'];
if (!usdToEurRate) {
console.error('[currency] EUR rate not available');
return null;
}
// If converting from USD to EUR
if (fromCurrencyUpper === 'USD') {
const eurAmount = amount * usdToEurRate;
console.log(`[currency] Converted ${amount} USD to ${eurAmount.toFixed(2)} EUR (rate: ${usdToEurRate.toFixed(4)})`);
return {
eurAmount: parseFloat(eurAmount.toFixed(2)),
rate: parseFloat(usdToEurRate.toFixed(4)),
conversionDate: rateCache.lastUpdated
};
}
// For other currencies, convert through USD first
const usdToSourceRate = rateCache.rates[fromCurrencyUpper];
if (!usdToSourceRate) {
console.error(`[currency] Currency ${fromCurrencyUpper} not supported`);
return null;
}
// Calculate: Source -> USD -> EUR
// Source -> USD: amount / usdToSourceRate
// USD -> EUR: (amount / usdToSourceRate) * usdToEurRate
const sourceToEurRate = usdToEurRate / usdToSourceRate;
const eurAmount = amount * sourceToEurRate;
console.log(`[currency] Converted ${amount} ${fromCurrencyUpper} to ${eurAmount.toFixed(2)} EUR (rate: ${sourceToEurRate.toFixed(4)})`);
return {
eurAmount: parseFloat(eurAmount.toFixed(2)),
rate: parseFloat(sourceToEurRate.toFixed(4)),
conversionDate: rateCache.lastUpdated
};
} catch (error) {
console.error('[currency] Error during EUR conversion:', error);
return null;
}
};
/**
* Format price with currency symbol
*/
@@ -403,46 +477,160 @@ export const getCacheStatus = async (): Promise<{
}
};
/**
* Convert amount from any currency to target currency
*/
export const convertToTargetCurrency = async (
amount: number,
fromCurrency: string,
targetCurrency: string
): Promise<{
targetAmount: number;
rate: number;
conversionDate: string;
} | null> => {
// If same currency, no conversion needed
if (fromCurrency.toUpperCase() === targetCurrency.toUpperCase()) {
return {
targetAmount: amount,
rate: 1.0,
conversionDate: new Date().toISOString()
};
}
// Use existing functions for specific conversions
if (targetCurrency.toUpperCase() === 'USD') {
const result = await convertToUSD(amount, fromCurrency);
if (result) {
return {
targetAmount: result.usdAmount,
rate: result.rate,
conversionDate: result.conversionDate
};
}
return null;
}
if (targetCurrency.toUpperCase() === 'EUR') {
const result = await convertToEUR(amount, fromCurrency);
if (result) {
return {
targetAmount: result.eurAmount,
rate: result.rate,
conversionDate: result.conversionDate
};
}
return null;
}
// For other currencies, convert through USD
try {
const rateCache = await getExchangeRates();
if (!rateCache) {
console.error('[currency] No exchange rates available for conversion');
return null;
}
const fromCurrencyUpper = fromCurrency.toUpperCase();
const targetCurrencyUpper = targetCurrency.toUpperCase();
// Get rates
const usdToFromRate = rateCache.rates[fromCurrencyUpper];
const usdToTargetRate = rateCache.rates[targetCurrencyUpper];
if (!usdToFromRate || !usdToTargetRate) {
console.error(`[currency] Currency not supported: ${!usdToFromRate ? fromCurrencyUpper : targetCurrencyUpper}`);
return null;
}
// Calculate: Source -> USD -> Target
const fromToTargetRate = usdToTargetRate / usdToFromRate;
const targetAmount = amount * fromToTargetRate;
console.log(`[currency] Converted ${amount} ${fromCurrencyUpper} to ${targetAmount.toFixed(2)} ${targetCurrencyUpper} (rate: ${fromToTargetRate.toFixed(4)})`);
return {
targetAmount: parseFloat(targetAmount.toFixed(2)),
rate: parseFloat(fromToTargetRate.toFixed(4)),
conversionDate: rateCache.lastUpdated
};
} catch (error) {
console.error('[currency] Error during currency conversion:', error);
return null;
}
};
/**
* Enhanced expense processing with currency conversion
*/
export const processExpenseWithCurrency = async (expense: any): Promise<any> => {
export const processExpenseWithCurrency = async (expense: any, targetCurrency: string = 'EUR'): Promise<any> => {
const processedExpense = { ...expense };
// Parse price number
const priceNumber = parseFloat(expense.Price?.toString().replace(/[^\d.-]/g, '')) || 0;
processedExpense.PriceNumber = priceNumber;
// Get currency symbol
// Get currency code and symbol
const currencyCode = expense.currency || 'USD';
processedExpense.Currency = currencyCode;
processedExpense.CurrencySymbol = getCurrencySymbol(currencyCode);
// Convert to USD if not already USD
if (currencyCode.toUpperCase() !== 'USD') {
const conversion = await convertToUSD(priceNumber, currencyCode);
// Convert to target currency if not already in target
const targetCurrencyUpper = targetCurrency.toUpperCase();
const targetField = `Price${targetCurrencyUpper}`;
if (currencyCode.toUpperCase() !== targetCurrencyUpper) {
const conversion = await convertToTargetCurrency(priceNumber, currencyCode, targetCurrency);
if (conversion) {
processedExpense.PriceUSD = conversion.usdAmount;
processedExpense[targetField] = conversion.targetAmount;
processedExpense.ConversionRate = conversion.rate;
processedExpense.ConversionDate = conversion.conversionDate;
processedExpense.TargetCurrency = targetCurrencyUpper;
}
} else {
// If already USD, set USD amount to original amount
processedExpense.PriceUSD = priceNumber;
// If already in target currency, set target amount to original amount
processedExpense[targetField] = priceNumber;
processedExpense.ConversionRate = 1.0;
processedExpense.ConversionDate = new Date().toISOString();
processedExpense.TargetCurrency = targetCurrencyUpper;
}
// Also convert to USD and EUR for compatibility
if (currencyCode.toUpperCase() !== 'USD') {
const usdConversion = await convertToUSD(priceNumber, currencyCode);
if (usdConversion) {
processedExpense.PriceUSD = usdConversion.usdAmount;
}
} else {
processedExpense.PriceUSD = priceNumber;
}
if (currencyCode.toUpperCase() !== 'EUR') {
const eurConversion = await convertToEUR(priceNumber, currencyCode);
if (eurConversion) {
processedExpense.PriceEUR = eurConversion.eurAmount;
}
} else {
processedExpense.PriceEUR = priceNumber;
}
// Create display prices
processedExpense.DisplayPrice = createDisplayPrice(
priceNumber,
currencyCode,
processedExpense.PriceUSD
);
processedExpense.DisplayPrice = formatPriceWithCurrency(priceNumber, currencyCode);
processedExpense.DisplayPriceUSD = formatPriceWithCurrency(
processedExpense.PriceUSD || priceNumber,
'USD'
// Create display price with target currency conversion
const targetAmount = processedExpense[targetField];
if (currencyCode.toUpperCase() !== targetCurrencyUpper && targetAmount) {
const targetSymbol = getCurrencySymbol(targetCurrency);
processedExpense.DisplayPriceWithTarget = `${formatPriceWithCurrency(priceNumber, currencyCode)} (${targetSymbol}${targetAmount.toFixed(2)})`;
} else {
processedExpense.DisplayPriceWithTarget = formatPriceWithCurrency(priceNumber, currencyCode);
}
processedExpense.DisplayPriceTarget = formatPriceWithCurrency(
targetAmount || priceNumber,
targetCurrency
);
return processedExpense;