Rocky_Mountain_Vending/app/api/ebay/search/route.ts

248 lines
7.1 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
/**
* eBay API Proxy Route
* Proxies requests to eBay Finding API to avoid CORS issues
*/
interface eBaySearchParams {
keywords: string
categoryId?: string
sortOrder?: string
maxResults?: number
}
interface eBaySearchResult {
itemId: string
title: string
price: string
currency: string
imageUrl?: string
viewItemUrl: string
condition?: string
shippingCost?: string
affiliateLink: string
}
type MaybeArray<T> = T | T[]
const SEARCH_CACHE_TTL = 15 * 60 * 1000 // 15 minutes
const searchResponseCache = new Map<string, { results: eBaySearchResult[]; timestamp: number }>()
const inFlightSearchResponses = new Map<string, Promise<eBaySearchResult[]>>()
// Affiliate campaign ID for generating links
const AFFILIATE_CAMPAIGN_ID = process.env.EBAY_AFFILIATE_CAMPAIGN_ID?.trim() || ''
// Generate eBay affiliate link
function generateAffiliateLink(viewItemUrl: string): string {
if (!AFFILIATE_CAMPAIGN_ID) {
return viewItemUrl
}
try {
const url = new URL(viewItemUrl)
url.searchParams.set('mkcid', '1')
url.searchParams.set('mkrid', '711-53200-19255-0')
url.searchParams.set('siteid', '0')
url.searchParams.set('campid', AFFILIATE_CAMPAIGN_ID)
url.searchParams.set('toolid', '10001')
url.searchParams.set('mkevt', '1')
return url.toString()
} catch {
return viewItemUrl
}
}
function first<T>(value: MaybeArray<T> | undefined): T | undefined {
if (!value) {
return undefined
}
return Array.isArray(value) ? value[0] : value
}
function normalizeItem(item: any): eBaySearchResult {
const currentPrice = first(item.sellingStatus?.currentPrice)
const shippingCost = first(item.shippingInfo?.shippingServiceCost)
const condition = first(item.condition)
const viewItemUrl = item.viewItemURL || item.viewItemUrl || ''
return {
itemId: item.itemId || '',
title: item.title || 'Unknown Item',
price: `${currentPrice?.value || '0'} ${currentPrice?.currencyId || 'USD'}`,
currency: currentPrice?.currencyId || 'USD',
imageUrl: first(item.galleryURL) || undefined,
viewItemUrl,
condition: condition?.conditionDisplayName || undefined,
shippingCost: shippingCost?.value
? `${shippingCost.value} ${shippingCost.currencyId || currentPrice?.currencyId || 'USD'}`
: undefined,
affiliateLink: generateAffiliateLink(viewItemUrl),
}
}
async function readEbayErrorMessage(response: Response) {
const text = await response.text().catch(() => '')
if (!text.trim()) {
return `eBay API error: ${response.status}`
}
try {
const parsed = JSON.parse(text) as any
const messages = parsed?.errorMessage?.[0]?.error?.[0]
const message = Array.isArray(messages?.message) ? messages.message[0] : messages?.message
if (typeof message === 'string' && message.trim()) {
const errorId = Array.isArray(messages?.errorId) ? messages.errorId[0] : messages?.errorId
return errorId ? `eBay API error ${errorId}: ${message}` : `eBay API error: ${message}`
}
} catch {
// Fall through to returning the raw text below.
}
return text.trim() || `eBay API error: ${response.status}`
}
function buildCacheKey(
keywords: string,
categoryId: string | undefined,
sortOrder: string,
maxResults: number,
): string {
return [
keywords.trim().toLowerCase(),
categoryId || '',
sortOrder || 'BestMatch',
String(maxResults),
].join('|')
}
function getCachedSearchResults(cacheKey: string): eBaySearchResult[] | null {
const cached = searchResponseCache.get(cacheKey)
if (!cached) {
return null
}
if (Date.now() - cached.timestamp > SEARCH_CACHE_TTL) {
searchResponseCache.delete(cacheKey)
return null
}
return cached.results
}
function setCachedSearchResults(cacheKey: string, results: eBaySearchResult[]) {
searchResponseCache.set(cacheKey, {
results,
timestamp: Date.now(),
})
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const keywords = searchParams.get('keywords')
const categoryId = searchParams.get('categoryId') || undefined
const sortOrder = searchParams.get('sortOrder') || 'BestMatch'
const maxResults = parseInt(searchParams.get('maxResults') || '6', 10)
const cacheKey = buildCacheKey(keywords || '', categoryId, sortOrder, maxResults)
if (!keywords) {
return NextResponse.json({ error: 'Keywords parameter is required' }, { status: 400 })
}
const appId = process.env.EBAY_APP_ID?.trim()
if (!appId) {
console.error('EBAY_APP_ID not configured')
return NextResponse.json(
{ error: 'eBay API not configured. Please set EBAY_APP_ID environment variable.' },
{ status: 503 }
)
}
const cachedResults = getCachedSearchResults(cacheKey)
if (cachedResults) {
return NextResponse.json(cachedResults)
}
const inFlight = inFlightSearchResponses.get(cacheKey)
if (inFlight) {
try {
const results = await inFlight
return NextResponse.json(results)
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch products from eBay' },
{ status: 500 }
)
}
}
// Build eBay Finding API URL
const baseUrl = 'https://svcs.ebay.com/services/search/FindingService/v1'
const url = new URL(baseUrl)
url.searchParams.set('OPERATION-NAME', 'findItemsAdvanced')
url.searchParams.set('SERVICE-VERSION', '1.0.0')
url.searchParams.set('SECURITY-APPNAME', appId)
url.searchParams.set('RESPONSE-DATA-FORMAT', 'JSON')
url.searchParams.set('REST-PAYLOAD', 'true')
url.searchParams.set('keywords', keywords)
url.searchParams.set('sortOrder', sortOrder)
url.searchParams.set('paginationInput.entriesPerPage', maxResults.toString())
if (categoryId) {
url.searchParams.set('categoryId', categoryId)
}
try {
const request = (async () => {
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json',
},
})
if (!response.ok) {
const errorMessage = await readEbayErrorMessage(response)
throw new Error(errorMessage)
}
const data = await response.json()
// Parse eBay API response
const findItemsAdvancedResponse = data.findItemsAdvancedResponse?.[0]
if (!findItemsAdvancedResponse) {
return []
}
const searchResult = findItemsAdvancedResponse.searchResult?.[0]
if (!searchResult || !searchResult.item || searchResult.item.length === 0) {
return []
}
const items = Array.isArray(searchResult.item) ? searchResult.item : [searchResult.item]
return items.map((item: any) => normalizeItem(item))
})()
inFlightSearchResponses.set(cacheKey, request)
const results = await request
setCachedSearchResults(cacheKey, results)
return NextResponse.json(results)
} catch (error) {
console.error('Error fetching from eBay API:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch products from eBay' },
{ status: 500 }
)
} finally {
inFlightSearchResponses.delete(cacheKey)
}
}