Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 144 additions & 92 deletions src/app/api/exchange-rate/route.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<NextResponse | null> {
const bridgeAPIKey = process.env.BRIDGE_API_KEY
async function getExchangeRate(from: string, to: string): Promise<number | null> {
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<number | null> {
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<number | null> {
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<NextResponse> {
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<number | null> {
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',
},
})
}
6 changes: 3 additions & 3 deletions src/constants/countryCurrencyMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading