feat: Add currency selection and conversion support in PDF generation, enhance expense processing with target currency handling
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user