diff --git a/src/app/api/exchange-rate/route.ts b/src/app/api/exchange-rate/route.ts index 459f5b803..8131a2493 100644 --- a/src/app/api/exchange-rate/route.ts +++ b/src/app/api/exchange-rate/route.ts @@ -1,17 +1,12 @@ import { NextRequest, NextResponse } from 'next/server' +import { getCurrencyPrice } from '@/app/actions/currency' interface ExchangeRateResponse { rate: number } -interface BridgeExchangeRateResponse { - midmarket_rate: string - buy_rate: string - sell_rate: string -} - -// Currency pairs that should use Bridge API (USD to these currencies only) -const BRIDGE_PAIRS = new Set(['USD-EUR', 'USD-MXN', 'USD-BRL']) +// LATAM currencies that should use Manteca API +const MANTECA_CURRENCIES = new Set(['ARS', 'BRL', 'COP', 'CRC', 'PUSD', 'GTQ', 'PHP', 'BOB']) export async function GET(request: NextRequest) { try { @@ -39,122 +34,179 @@ export async function GET(request: NextRequest) { ) } - const pairKey = `${fromUc}-${toUc}` - const reversePairKey = `${toUc}-${fromUc}` - - // Check if we should use Bridge for this pair or its reverse - const shouldUseBridge = BRIDGE_PAIRS.has(pairKey) - const shouldUseBridgeReverse = BRIDGE_PAIRS.has(reversePairKey) - - if (shouldUseBridge || shouldUseBridgeReverse) { - // For Bridge pairs, we need to determine which rate to use - let bridgeResult - if (shouldUseBridge) { - // Direct pair (e.g., USD→EUR): use sell_rate - bridgeResult = await fetchFromBridge(fromUc, toUc, 'sell_rate', false) - } else { - // Reverse pair (e.g., EUR→USD): fetch USD→EUR and use buy_rate, then invert - bridgeResult = await fetchFromBridge(toUc, fromUc, 'buy_rate', true) + // If either currency is USD, handle direct conversion + if (fromUc === 'USD' || toUc === 'USD') { + const pairKey = `${fromUc}-${toUc}` + + // Check if either currency uses getCurrencyPrice (Manteca or Bridge currencies) + if ( + MANTECA_CURRENCIES.has(fromUc) || + MANTECA_CURRENCIES.has(toUc) || + ['EUR', 'MXN'].includes(fromUc) || + ['EUR', 'MXN'].includes(toUc) + ) { + const currencyPriceRate = await fetchFromCurrencyPrice(fromUc, toUc) + if (currencyPriceRate !== null) { + return NextResponse.json( + { rate: currencyPriceRate }, + { + headers: { + 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', + }, + } + ) + } + // Fall back to other providers if getCurrencyPrice fails + console.warn(`getCurrencyPrice failed for ${pairKey}, falling back to other providers`) } - if (bridgeResult) { - return bridgeResult + // Use Frankfurter for all other pairs or as fallback + const frankfurterRate = await fetchFromFrankfurter(fromUc, toUc) + if (frankfurterRate !== null) { + return NextResponse.json( + { rate: frankfurterRate }, + { + headers: { + 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', + }, + } + ) } - // Fall back to Frankfurter if Bridge fails - console.warn(`Bridge failed for ${pairKey}, falling back to Frankfurter`) + return NextResponse.json({ error: 'Failed to fetch exchange rates' }, { status: 500 }) } - // Use Frankfurter for all other pairs or as fallback - return await fetchFromFrankfurter(fromUc, toUc) + // For non-USD pairs, convert through USD: from → USD → to + const fromToUsdRate = await getExchangeRate(fromUc, 'USD') + const usdToToRate = await getExchangeRate('USD', toUc) + + if (!fromToUsdRate || !usdToToRate) { + return NextResponse.json({ error: 'Failed to fetch intermediate USD rates' }, { status: 500 }) + } + + const combinedRate = fromToUsdRate * usdToToRate + + return NextResponse.json( + { rate: combinedRate }, + { + headers: { + 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', + }, + } + ) } catch (error) { console.error('Exchange rate API error:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } -async function fetchFromBridge( - from: string, - to: string, - rateType: 'buy_rate' | 'sell_rate', - shouldInvert: boolean -): Promise { - const bridgeAPIKey = process.env.BRIDGE_API_KEY +async function getExchangeRate(from: string, to: string): Promise { + try { + // Check if either currency uses getCurrencyPrice (Manteca or Bridge currencies) + if ( + MANTECA_CURRENCIES.has(from) || + MANTECA_CURRENCIES.has(to) || + ['EUR', 'MXN'].includes(from) || + ['EUR', 'MXN'].includes(to) + ) { + return await fetchFromCurrencyPrice(from, to) + } - if (!bridgeAPIKey) { - console.warn('Bridge API key not set') + // Use Frankfurter for all other pairs or as fallback + return await fetchFromFrankfurter(from, to) + } catch (error) { + console.error(`Failed to get exchange rate for ${from}-${to}:`, error) return null } +} +async function fetchFromCurrencyPrice(from: string, to: string): Promise { + console.log('Fetching from getCurrencyPrice') try { - const url = `https://api.bridge.xyz/v0/exchange_rates?from=${from.toLowerCase()}&to=${to.toLowerCase()}` - const options: RequestInit & { next?: { revalidate?: number } } = { - method: 'GET', - // Bridge expects header name 'Api-Key' - headers: { 'Api-Key': bridgeAPIKey }, - next: { revalidate: 300 }, // Cache for 5 minutes - } - - const response = await fetch(url, options) + if (from === 'USD' && (MANTECA_CURRENCIES.has(to) || ['EUR', 'MXN'].includes(to))) { + // USD → other currency: use sell rate (selling USD to get other currency) + const { sell } = await getCurrencyPrice(to) + if (!isFinite(sell) || sell <= 0) { + console.error(`Invalid sell rate from getCurrencyPrice for ${to}: ${sell}`) + return null + } + return sell + } else if ((MANTECA_CURRENCIES.has(from) || ['EUR', 'MXN'].includes(from)) && to === 'USD') { + // Other currency → USD: use buy rate (buying USD with other currency) + const { buy } = await getCurrencyPrice(from) + if (!isFinite(buy) || buy <= 0) { + console.error(`Invalid buy rate from getCurrencyPrice for ${from}: ${buy}`) + return null + } + return 1 / buy + } else if ( + (MANTECA_CURRENCIES.has(from) || ['EUR', 'MXN'].includes(from)) && + (MANTECA_CURRENCIES.has(to) || ['EUR', 'MXN'].includes(to)) + ) { + // Other currency → Other currency: convert through USD + const fromPrices = await getCurrencyPrice(from) + const toPrices = await getCurrencyPrice(to) + + if (!isFinite(fromPrices.buy) || fromPrices.buy <= 0 || !isFinite(toPrices.sell) || toPrices.sell <= 0) { + console.error(`Invalid prices for ${from}-${to}: buy=${fromPrices.buy}, sell=${toPrices.sell}`) + return null + } - if (!response.ok) { - console.error(`Bridge API error: ${response.status} ${response.statusText}`) + // from → USD → to + const fromToUsd = 1 / fromPrices.buy + const usdToTo = toPrices.sell + return fromToUsd * usdToTo + } else { + // Unsupported conversion + console.warn(`Unsupported getCurrencyPrice conversion: ${from} → ${to}`) return null } + } catch (error) { + console.error(`getCurrencyPrice error for ${from}-${to}:`, error) + return null + } +} - const bridgeData: BridgeExchangeRateResponse = await response.json() - - // Validate the response structure - if (!bridgeData[rateType]) { - console.error(`Invalid Bridge response: missing ${rateType}`) - return null +async function fetchFromFrankfurter(from: string, to: string): Promise { + try { + // If either currency is USD, do direct conversion + if (from === 'USD' || to === 'USD') { + return await fetchDirectFromFrankfurter(from, to) } - let rate = parseFloat(bridgeData[rateType]) + // For non-USD pairs, convert through USD: from → USD → to + const fromToUsdRate = await fetchDirectFromFrankfurter(from, 'USD') + const usdToToRate = await fetchDirectFromFrankfurter('USD', to) - // If we fetched the reverse pair (e.g., fetched USD→EUR for EUR→USD request), - // we need to invert the rate - if (shouldInvert) { - rate = 1 / rate - } - - const exchangeRate: ExchangeRateResponse = { - rate, + if (!fromToUsdRate || !usdToToRate) { + return null } - return NextResponse.json(exchangeRate, { - headers: { - 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', - }, - }) + return fromToUsdRate * usdToToRate } catch (error) { - console.error('Bridge API exception:', error) + console.error(`Frankfurter API exception for ${from}-${to}:`, error) return null } } -async function fetchFromFrankfurter(from: string, to: string): Promise { - const url = `https://api.frankfurter.app/latest?from=${from}&to=${to}` - const options: RequestInit & { next?: { revalidate?: number } } = { - method: 'GET', - next: { revalidate: 300 }, // Cache for 5 minutes - } - - const response = await fetch(url, options) +async function fetchDirectFromFrankfurter(from: string, to: string): Promise { + try { + const url = `https://api.frankfurter.app/latest?from=${from}&to=${to}` + const options: RequestInit & { next?: { revalidate?: number } } = { + method: 'GET', + next: { revalidate: 300 }, // Cache for 5 minutes + } - if (!response.ok) { - console.error(`Frankfurter API error: ${response.status} ${response.statusText}`) - return NextResponse.json({ error: 'Failed to fetch exchange rates from API' }, { status: response.status }) - } + const response = await fetch(url, options) - const data = await response.json() + if (!response.ok) { + console.error(`Frankfurter API error: ${response.status} ${response.statusText}`) + return null + } - const exchangeRate: ExchangeRateResponse = { - rate: data.rates[to] * 0.995, // Subtract 50bps + const data = await response.json() + return data.rates[to] * 0.995 // Subtract 50bps + } catch (error) { + console.error(`Frankfurter direct API exception for ${from}-${to}:`, error) + return null } - - return NextResponse.json(exchangeRate, { - headers: { - 'Cache-Control': 's-maxage=300, stale-while-revalidate=600', - }, - }) } diff --git a/src/constants/countryCurrencyMapping.ts b/src/constants/countryCurrencyMapping.ts index 5c5e93f7a..baa92e918 100644 --- a/src/constants/countryCurrencyMapping.ts +++ b/src/constants/countryCurrencyMapping.ts @@ -42,9 +42,9 @@ export const countryCurrencyMappings: CountryCurrencyMapping[] = [ // Mexico { currencyCode: 'MXN', currencyName: 'Mexican Peso', country: 'Mexico', flagCode: 'mx', path: 'mexico' }, - // Coming Soon - { currencyCode: 'BRL', currencyName: 'Brazilian Real', country: 'Brazil', flagCode: 'br', comingSoon: true }, - { currencyCode: 'ARS', currencyName: 'Argentine Peso', country: 'Argentina', flagCode: 'ar', comingSoon: true }, + // LATAM Countries + { currencyCode: 'BRL', currencyName: 'Brazilian Real', country: 'Brazil', flagCode: 'br' }, + { currencyCode: 'ARS', currencyName: 'Argentine Peso', country: 'Argentina', flagCode: 'ar' }, ] export default countryCurrencyMappings