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[] const SEARCH_CACHE_TTL = 15 * 60 * 1000 // 15 minutes const searchResponseCache = new Map< string, { results: eBaySearchResult[]; timestamp: number } >() const inFlightSearchResponses = new Map>() // 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(value: MaybeArray | 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) } }