import refreshTokenHelper from "../../../utils/refreshTokenHelper"; import errorHandlingHelper from "../../../utils/errorHandlingHelper"; import fetchHelper from "../../../utils/fetchHelper"; // GET /api/ffn/products?orderSourceId=123&$top=50&$skip=0 // Proxies to {base}/api/v1/fulfiller/products using Order Source credentials const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const query = getQuery(event) const orderSourceId = query.orderSourceId as string if(!orderSourceId) return { status: 400, message: 'orderSourceId is required' } const os: any = await fetchHelper(event, `models/c_ordersource/${orderSourceId}`, 'GET', token, null) if(!os) return { status: 404, message: 'Order Source not found' } if(!(os?.Marketplace?.identifier === 'jtl-ffn' || os?.marketplace === '7' || os?.Marketplace?.id === '7')) { return { status: 400, message: 'Selected Order Source is not JTL-FFN' } } // Fall back to the global fulfiller credentials when the Order Source has none of its own. const cfg: any = useRuntimeConfig() const baseUrl: string = os?.marketplace_url || cfg?.api?.jtlFulfillerUrl || '' if(!baseUrl) return { status: 400, message: 'No marketplace_url configured for this Order Source' } const ffnToken: string = os?.marketplace_token || cfg?.api?.jtlFulfillerToken || '' const headers: any = { 'content-type': 'application/json', 'accept': 'application/json' } if(ffnToken) { headers['Authorization'] = `ffn ${ffnToken}` headers['x-application-id'] = 'JPCB0XLOGYOU' headers['x-application-version'] = '0.1' } if(os?.marketplace_key) headers['x-api-key'] = os.marketplace_key if(os?.marketplace_secret) headers['x-api-secret'] = os.marketplace_secret if(os?.jtl_merchantID) headers['x-merchant-id'] = os.jtl_merchantID const params = new URLSearchParams() ;['$top', '$skip', '$orderBy'].forEach(k => { const v = (query as any)[k] if(v !== undefined) params.append(k, String(v)) }) // CRITICAL: the JTL-FFN fulfiller /products endpoint is fulfiller-wide — it returns // products for EVERY merchant the fulfiller serves and IGNORES the x-merchant-id header // set above. Without an explicit filter, selecting one Order Source (e.g. kfp/CX45) // also lists another merchant's articles (e.g. wunderhome/LA28). Scope to this Order // Source's merchant with an OData $filter on merchantId, combined with any client filter. const merchantId: string = os?.jtl_merchantID ? String(os.jtl_merchantID).trim() : '' const clientFilter: string = (query as any)['$filter'] ? String((query as any)['$filter']).trim() : '' const filters: string[] = [] if(clientFilter) filters.push(`(${clientFilter})`) if(merchantId) filters.push(`merchantId eq '${merchantId.replace(/'/g, "''")}'`) if(filters.length) params.set('$filter', filters.join(' and ')) const qs = params.toString() try { let host = baseUrl.trim() if(!/^https?:\/\//i.test(host)) host = `https://${host}` host = host.replace(/\/$/, '') let endpoint = '/api/v1/fulfiller/products' if(/\/api\/v1\/fulfiller\/products\/?$/i.test(host)) { endpoint = '' } else if(/\/api\/v1\/fulfiller\/?$/i.test(host)) { endpoint = '/products' } const url = `${host}${endpoint}${qs ? `?${qs}` : ''}` const res = await $fetch(url, { method: 'GET', headers }) return { status: 200, ...res } } catch (err: any) { const data = errorHandlingHelper(err?.data ?? err, err?.data ?? err) const status = Number(data?.status || err?.status || err?.response?.status || 500) const message = data?.message || err?.data?.message || err?.message || 'Failed to fetch FFN products' return { status, message } } } export default defineEventHandler(async (event) => { try { return await handleFunc(event) } catch (err: any) { try { const authToken = await refreshTokenHelper(event) return await handleFunc(event, authToken) } catch (error: any) { const data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) if([401, 402, 403, 407].includes(Number(data.status))) { //@ts-ignore setCookie(event, 'user', null) } return data } } })