352 lines
10 KiB
JavaScript
352 lines
10 KiB
JavaScript
![]() |
const express = require('express');
|
||
|
const axios = require('axios');
|
||
|
const fs = require('fs');
|
||
|
const path = require('path');
|
||
|
const router = express.Router();
|
||
|
|
||
|
// Get API key from environment variables
|
||
|
const API_KEY = process.env.EXCHANGE_RATE_API_KEY;
|
||
|
if (!API_KEY) {
|
||
|
console.error('WARNING: EXCHANGE_RATE_API_KEY environment variable is not set!');
|
||
|
console.error('Exchange rate functionality will not work correctly.');
|
||
|
}
|
||
|
|
||
|
const BASE_URL = 'https://v6.exchangerate-api.com/v6';
|
||
|
|
||
|
// We'll use USD as our single base currency
|
||
|
const BASE_CURRENCY = 'USD';
|
||
|
|
||
|
// Path to the cache file
|
||
|
const CACHE_FILE_PATH = path.join(__dirname, 'exchange-rates-cache.json');
|
||
|
|
||
|
// In-memory storage for cached exchange rates
|
||
|
let exchangeRatesCache = {
|
||
|
USD: {
|
||
|
lastUpdated: null,
|
||
|
rates: {},
|
||
|
nextUpdateTime: null
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// one day in milliseconds
|
||
|
const UPDATE_INTERVAL = 1 * 24 * 60 * 60 * 1000;
|
||
|
|
||
|
// Load cached exchange rates from file
|
||
|
function loadCachedRates() {
|
||
|
try {
|
||
|
if (fs.existsSync(CACHE_FILE_PATH)) {
|
||
|
const data = fs.readFileSync(CACHE_FILE_PATH, 'utf8');
|
||
|
const parsedData = JSON.parse(data);
|
||
|
|
||
|
// Convert string dates back to Date objects
|
||
|
if (parsedData.USD) {
|
||
|
if (parsedData.USD.lastUpdated) {
|
||
|
parsedData.USD.lastUpdated = new Date(parsedData.USD.lastUpdated);
|
||
|
}
|
||
|
if (parsedData.USD.nextUpdateTime) {
|
||
|
parsedData.USD.nextUpdateTime = new Date(parsedData.USD.nextUpdateTime);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exchangeRatesCache = parsedData;
|
||
|
console.log('Loaded exchange rates from cache file');
|
||
|
} else {
|
||
|
console.log('No cache file found, will create one when rates are fetched');
|
||
|
}
|
||
|
} catch (error) {
|
||
|
console.error('Error loading cached exchange rates:', error.message);
|
||
|
// Continue with default cache if file can't be loaded
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Save exchange rates to cache file
|
||
|
function saveCachedRates() {
|
||
|
try {
|
||
|
fs.writeFileSync(CACHE_FILE_PATH, JSON.stringify(exchangeRatesCache, null, 2));
|
||
|
console.log('Exchange rates saved to cache file');
|
||
|
} catch (error) {
|
||
|
console.error('Error saving exchange rates to cache file:', error.message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Function to fetch and update exchange rates using USD as base
|
||
|
async function updateExchangeRates() {
|
||
|
if (!API_KEY) {
|
||
|
console.error('Cannot update exchange rates: API key is not set');
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
console.log(`Fetching latest exchange rates using ${BASE_CURRENCY} as base...`);
|
||
|
const response = await axios.get(`${BASE_URL}/${API_KEY}/latest/${BASE_CURRENCY}`);
|
||
|
|
||
|
if (response.data && response.data.result === 'success') {
|
||
|
exchangeRatesCache.USD = {
|
||
|
lastUpdated: new Date(),
|
||
|
rates: response.data.conversion_rates,
|
||
|
nextUpdateTime: new Date(Date.now() + UPDATE_INTERVAL)
|
||
|
};
|
||
|
|
||
|
// Save to file after updating
|
||
|
saveCachedRates();
|
||
|
|
||
|
console.log('Exchange rates updated successfully');
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
} catch (error) {
|
||
|
console.error('Failed to update exchange rates:', error.message);
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check if rates need updating and update if necessary
|
||
|
async function ensureRatesUpdated() {
|
||
|
if (!exchangeRatesCache.USD.lastUpdated ||
|
||
|
Date.now() > exchangeRatesCache.USD.nextUpdateTime.getTime()) {
|
||
|
return await updateExchangeRates();
|
||
|
}
|
||
|
console.log(`Using cached rates, next update: ${exchangeRatesCache.USD.nextUpdateTime}`);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Calculate conversion rate between any two currencies using USD as base
|
||
|
function calculateRate(from, to) {
|
||
|
const rates = exchangeRatesCache.USD.rates;
|
||
|
|
||
|
// If either currency is USD, we can use the rate directly
|
||
|
if (from === 'USD') return rates[to];
|
||
|
if (to === 'USD') return 1 / rates[from];
|
||
|
|
||
|
// Otherwise, calculate cross rate: from -> USD -> to
|
||
|
return rates[to] / rates[from];
|
||
|
}
|
||
|
|
||
|
// Load cached rates when the module is loaded
|
||
|
loadCachedRates();
|
||
|
|
||
|
// Initialize rates if needed
|
||
|
ensureRatesUpdated();
|
||
|
|
||
|
// Root endpoint
|
||
|
router.get('/', (req, res) => {
|
||
|
const availableCurrencies = exchangeRatesCache.USD.rates ?
|
||
|
Object.keys(exchangeRatesCache.USD.rates) : [];
|
||
|
|
||
|
res.json({
|
||
|
message: 'Exchange Rate API is running',
|
||
|
baseCurrency: BASE_CURRENCY,
|
||
|
availableCurrencies,
|
||
|
lastUpdated: exchangeRatesCache.USD.lastUpdated,
|
||
|
nextUpdate: exchangeRatesCache.USD.nextUpdateTime,
|
||
|
updateInterval: '1 day',
|
||
|
endpoints: {
|
||
|
latest: '/latest',
|
||
|
convert: '/convert/:from/:to/:amount',
|
||
|
currencies: '/currencies'
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Get all cached exchange rates
|
||
|
router.get('/latest', async (req, res) => {
|
||
|
await ensureRatesUpdated();
|
||
|
|
||
|
if (!exchangeRatesCache.USD.rates) {
|
||
|
return res.status(503).json({ error: 'Exchange rate data not yet available' });
|
||
|
}
|
||
|
|
||
|
res.json({
|
||
|
result: 'success',
|
||
|
base: BASE_CURRENCY,
|
||
|
lastUpdated: exchangeRatesCache.USD.lastUpdated,
|
||
|
nextUpdate: exchangeRatesCache.USD.nextUpdateTime,
|
||
|
rates: exchangeRatesCache.USD.rates
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Get rates for a specific currency as base
|
||
|
router.get('/latest/:currency', async (req, res) => {
|
||
|
const { currency } = req.params;
|
||
|
const currencyCode = currency.toUpperCase();
|
||
|
|
||
|
await ensureRatesUpdated();
|
||
|
|
||
|
if (!exchangeRatesCache.USD.rates) {
|
||
|
return res.status(503).json({ error: 'Exchange rate data not yet available' });
|
||
|
}
|
||
|
|
||
|
// Check if the currency is supported
|
||
|
if (!exchangeRatesCache.USD.rates[currencyCode] && currencyCode !== 'USD') {
|
||
|
return res.status(400).json({ error: `Currency '${currencyCode}' not supported` });
|
||
|
}
|
||
|
|
||
|
// Calculate rates with the requested currency as base
|
||
|
const rates = {};
|
||
|
const usdRates = exchangeRatesCache.USD.rates;
|
||
|
|
||
|
// If the requested base is USD, return rates directly
|
||
|
if (currencyCode === 'USD') {
|
||
|
res.json({
|
||
|
result: 'success',
|
||
|
base: currencyCode,
|
||
|
lastUpdated: exchangeRatesCache.USD.lastUpdated,
|
||
|
nextUpdate: exchangeRatesCache.USD.nextUpdateTime,
|
||
|
rates: usdRates
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Otherwise, calculate rates for all currencies with the requested currency as base
|
||
|
const baseRate = usdRates[currencyCode]; // Rate of 1 USD in the requested currency
|
||
|
|
||
|
// Add USD rate
|
||
|
rates['USD'] = 1 / baseRate;
|
||
|
|
||
|
// Add rates for all other currencies
|
||
|
for (const toCurrency in usdRates) {
|
||
|
if (toCurrency !== currencyCode) {
|
||
|
// Convert through USD: from -> USD -> to
|
||
|
rates[toCurrency] = usdRates[toCurrency] / baseRate;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add rate for the base currency itself
|
||
|
rates[currencyCode] = 1;
|
||
|
|
||
|
res.json({
|
||
|
result: 'success',
|
||
|
base: currencyCode,
|
||
|
lastUpdated: exchangeRatesCache.USD.lastUpdated,
|
||
|
nextUpdate: exchangeRatesCache.USD.nextUpdateTime,
|
||
|
rates: rates
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Get list of available currencies
|
||
|
router.get('/currencies', async (req, res) => {
|
||
|
await ensureRatesUpdated();
|
||
|
|
||
|
const availableCurrencies = exchangeRatesCache.USD.rates ?
|
||
|
Object.keys(exchangeRatesCache.USD.rates) : [];
|
||
|
|
||
|
res.json({
|
||
|
result: 'success',
|
||
|
baseCurrency: BASE_CURRENCY,
|
||
|
availableCurrencies,
|
||
|
lastUpdated: exchangeRatesCache.USD.lastUpdated,
|
||
|
nextUpdate: exchangeRatesCache.USD.nextUpdateTime
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Convert between currencies using cached rates
|
||
|
router.get('/convert/:from/:to/:amount', async (req, res) => {
|
||
|
const { from, to, amount } = req.params;
|
||
|
const fromCurrency = from.toUpperCase();
|
||
|
const toCurrency = to.toUpperCase();
|
||
|
|
||
|
await ensureRatesUpdated();
|
||
|
|
||
|
if (!exchangeRatesCache.USD.rates) {
|
||
|
return res.status(503).json({ error: 'Exchange rate data not yet available' });
|
||
|
}
|
||
|
|
||
|
// Check if currencies are supported
|
||
|
if (fromCurrency !== 'USD' && !exchangeRatesCache.USD.rates[fromCurrency]) {
|
||
|
return res.status(400).json({ error: `Currency '${fromCurrency}' not supported` });
|
||
|
}
|
||
|
|
||
|
if (toCurrency !== 'USD' && !exchangeRatesCache.USD.rates[toCurrency]) {
|
||
|
return res.status(400).json({ error: `Currency '${toCurrency}' not supported` });
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const numericAmount = parseFloat(amount);
|
||
|
|
||
|
if (isNaN(numericAmount)) {
|
||
|
return res.status(400).json({ error: 'Invalid amount' });
|
||
|
}
|
||
|
|
||
|
// Calculate conversion rate
|
||
|
const rate = calculateRate(fromCurrency, toCurrency);
|
||
|
const convertedAmount = numericAmount * rate;
|
||
|
|
||
|
res.json({
|
||
|
result: 'success',
|
||
|
from: fromCurrency,
|
||
|
to: toCurrency,
|
||
|
amount: numericAmount,
|
||
|
rate,
|
||
|
convertedAmount: parseFloat(convertedAmount.toFixed(4)),
|
||
|
lastUpdated: exchangeRatesCache.USD.lastUpdated,
|
||
|
nextUpdate: exchangeRatesCache.USD.nextUpdateTime
|
||
|
});
|
||
|
} catch (error) {
|
||
|
console.error('Conversion error:', error);
|
||
|
res.status(500).json({ error: 'Failed to convert currency' });
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Direct pair conversion (fallback to API if needed)
|
||
|
router.get('/pair/:from/:to/:amount', async (req, res) => {
|
||
|
const { from, to, amount } = req.params;
|
||
|
const fromCurrency = from.toUpperCase();
|
||
|
const toCurrency = to.toUpperCase();
|
||
|
|
||
|
// First try to use our cached rates
|
||
|
await ensureRatesUpdated();
|
||
|
|
||
|
if (exchangeRatesCache.USD.rates &&
|
||
|
(fromCurrency === 'USD' || exchangeRatesCache.USD.rates[fromCurrency]) &&
|
||
|
(toCurrency === 'USD' || exchangeRatesCache.USD.rates[toCurrency])) {
|
||
|
|
||
|
try {
|
||
|
const numericAmount = parseFloat(amount);
|
||
|
|
||
|
if (isNaN(numericAmount)) {
|
||
|
return res.status(400).json({ error: 'Invalid amount' });
|
||
|
}
|
||
|
|
||
|
// Calculate conversion rate
|
||
|
const rate = calculateRate(fromCurrency, toCurrency);
|
||
|
const convertedAmount = numericAmount * rate;
|
||
|
|
||
|
res.json({
|
||
|
result: 'success',
|
||
|
from: fromCurrency,
|
||
|
to: toCurrency,
|
||
|
amount: numericAmount,
|
||
|
rate,
|
||
|
convertedAmount: parseFloat(convertedAmount.toFixed(4)),
|
||
|
lastUpdated: exchangeRatesCache.USD.lastUpdated,
|
||
|
source: 'cache'
|
||
|
});
|
||
|
return;
|
||
|
} catch (error) {
|
||
|
console.error('Error using cached rates:', error);
|
||
|
// Fall through to API call
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we can't use cached rates, call the API directly
|
||
|
if (!API_KEY) {
|
||
|
return res.status(503).json({ error: 'Exchange rate API key is not configured' });
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const response = await axios.get(`${BASE_URL}/${API_KEY}/pair/${fromCurrency}/${toCurrency}/${amount}`);
|
||
|
|
||
|
// Update our cache with the latest USD rates if it's time
|
||
|
ensureRatesUpdated();
|
||
|
|
||
|
res.json({
|
||
|
...response.data,
|
||
|
source: 'api'
|
||
|
});
|
||
|
} catch (error) {
|
||
|
res.status(500).json({ error: 'Failed to convert currency' });
|
||
|
}
|
||
|
});
|
||
|
|
||
|
module.exports = router;
|