import { setSecret } from './secret-store.mjs'; const EBAY_AUTH_SANDBOX = 'https://auth.sandbox.ebay.com/oauth2/authorize'; const EBAY_AUTH_PROD = 'https://auth.ebay.com/oauth2/authorize'; const EBAY_TOKEN_SANDBOX = 'https://api.sandbox.ebay.com/identity/v1/oauth2/token'; const EBAY_TOKEN_PROD = 'https://api.ebay.com/identity/v1/oauth2/token'; const EBAY_API_SANDBOX = 'https://api.sandbox.ebay.com'; const EBAY_API_PROD = 'https://api.ebay.com'; function isSandbox() { return process.env.EBAY_SANDBOX === 'true'; } function authUrl() { return isSandbox() ? EBAY_AUTH_SANDBOX : EBAY_AUTH_PROD; } function tokenUrl() { return isSandbox() ? EBAY_TOKEN_SANDBOX : EBAY_TOKEN_PROD; } function apiBase() { return isSandbox() ? EBAY_API_SANDBOX : EBAY_API_PROD; } function setEnvVar(key, value) { setSecret(key, value, { source: 'oauth' }); } function getBasicAuth() { const clientId = process.env.EBAY_CLIENT_ID; const clientSecret = process.env.EBAY_CLIENT_SECRET; if (!clientId || !clientSecret) throw new Error('EBAY_CLIENT_ID oder EBAY_CLIENT_SECRET nicht gesetzt'); return Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); } const SCOPES = [ 'https://api.ebay.com/oauth/api_scope', 'https://api.ebay.com/oauth/api_scope/sell.inventory', 'https://api.ebay.com/oauth/api_scope/sell.account', 'https://api.ebay.com/oauth/api_scope/sell.fulfillment', 'https://api.ebay.com/oauth/api_scope/sell.marketing', ].join(' '); let oauthState = null; export function getOAuthUrl(redirectUri) { if (!process.env.EBAY_CLIENT_ID) throw new Error('EBAY_CLIENT_ID nicht gesetzt'); oauthState = Math.random().toString(36).substring(2); const params = new URLSearchParams({ client_id: process.env.EBAY_CLIENT_ID, response_type: 'code', redirect_uri: process.env.EBAY_RU_NAME || redirectUri, scope: SCOPES, state: oauthState, }); return `${authUrl()}?${params}`; } export async function handleCallback(code, state, redirectUri) { if (oauthState && state !== oauthState) throw new Error('Ungültiger OAuth-State'); oauthState = null; const res = await fetch(tokenUrl(), { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${getBasicAuth()}`, }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: process.env.EBAY_RU_NAME || redirectUri, }), }); if (!res.ok) { const err = await res.text(); throw new Error(`eBay Token-Fehler: ${err}`); } const tokens = await res.json(); setEnvVar('EBAY_ACCESS_TOKEN', tokens.access_token); setEnvVar('EBAY_REFRESH_TOKEN', tokens.refresh_token); return tokens; } async function refreshToken() { const refreshTkn = process.env.EBAY_REFRESH_TOKEN; if (!refreshTkn) throw new Error('Kein eBay Refresh-Token'); const res = await fetch(tokenUrl(), { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${getBasicAuth()}`, }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshTkn, scope: SCOPES, }), }); if (!res.ok) throw new Error('eBay Token-Refresh fehlgeschlagen'); const tokens = await res.json(); setEnvVar('EBAY_ACCESS_TOKEN', tokens.access_token); if (tokens.refresh_token) setEnvVar('EBAY_REFRESH_TOKEN', tokens.refresh_token); return tokens.access_token; } async function ebayFetch(path, opts = {}) { let token = process.env.EBAY_ACCESS_TOKEN; if (!token) throw new Error('Nicht mit eBay verbunden'); const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'Content-Language': 'de-DE', 'X-EBAY-C-MARKETPLACE-ID': 'EBAY_DE', ...opts.headers, }; let res = await fetch(`${apiBase()}${path}`, { ...opts, headers }); if (res.status === 401) { token = await refreshToken(); headers.Authorization = `Bearer ${token}`; res = await fetch(`${apiBase()}${path}`, { ...opts, headers }); } if (res.status === 204) return null; if (!res.ok) { const err = await res.text(); throw new Error(`eBay API (${res.status}): ${err}`); } const ct = res.headers.get('content-type') || ''; return ct.includes('json') ? res.json() : res.text(); } function mapCondition(condition) { const map = { 'neu': 'NEW', 'new': 'NEW', 'wie neu': 'LIKE_NEW', 'like new': 'LIKE_NEW', 'sehr gut': 'VERY_GOOD', 'very good': 'VERY_GOOD', 'gut': 'GOOD', 'good': 'GOOD', 'akzeptabel': 'ACCEPTABLE', 'acceptable': 'ACCEPTABLE', 'defekt': 'FOR_PARTS_OR_NOT_WORKING', }; return map[(condition || '').toLowerCase()] || 'GOOD'; } export async function createInventoryItem(listing, sku) { const images = (listing.photos || []).map(p => `http://localhost:${process.env.PORT || 8124}/uploads/${p}`); const item = { availability: { shipToLocationAvailability: { quantity: 1 }, }, condition: mapCondition(listing.condition), product: { title: listing.title, description: listing.description || '', aspects: {}, imageUrls: images, }, }; if (listing.brand) item.product.aspects['Marke'] = [listing.brand]; if (listing.color) item.product.aspects['Farbe'] = [listing.color]; if (listing.size) item.product.aspects['Größe'] = [listing.size]; await ebayFetch(`/sell/inventory/v1/inventory_item/${encodeURIComponent(sku)}`, { method: 'PUT', body: JSON.stringify(item), }); return sku; } export async function createOffer(listing, sku) { const price = listing.price || listing.suggested_price || 1; const offer = { sku, marketplaceId: 'EBAY_DE', format: 'FIXED_PRICE', listingDescription: listing.description || '', availableQuantity: 1, pricingSummary: { price: { value: String(price), currency: 'EUR' }, }, listingPolicies: {}, merchantLocationKey: process.env.EBAY_LOCATION_KEY || undefined, }; const categoryId = listing.ebay_category_id || process.env.EBAY_DEFAULT_CATEGORY || '175673'; offer.categoryId = categoryId; const fulfillmentId = process.env.EBAY_FULFILLMENT_POLICY_ID; const paymentId = process.env.EBAY_PAYMENT_POLICY_ID; const returnId = process.env.EBAY_RETURN_POLICY_ID; if (fulfillmentId) offer.listingPolicies.fulfillmentPolicyId = fulfillmentId; if (paymentId) offer.listingPolicies.paymentPolicyId = paymentId; if (returnId) offer.listingPolicies.returnPolicyId = returnId; const result = await ebayFetch('/sell/inventory/v1/offer', { method: 'POST', body: JSON.stringify(offer), }); return result; } export async function publishOffer(offerId) { const result = await ebayFetch(`/sell/inventory/v1/offer/${offerId}/publish`, { method: 'POST', }); return result; } export async function createAndPublish(listing, sku) { await createInventoryItem(listing, sku); const offer = await createOffer(listing, sku); const offerId = offer.offerId; const published = await publishOffer(offerId); return { offerId, listingId: published.listingId, url: `https://www.ebay.de/itm/${published.listingId}`, }; } export async function getFulfillmentPolicies() { return ebayFetch('/sell/account/v1/fulfillment_policy?marketplace_id=EBAY_DE'); } export async function getPaymentPolicies() { return ebayFetch('/sell/account/v1/payment_policy?marketplace_id=EBAY_DE'); } export async function getReturnPolicies() { return ebayFetch('/sell/account/v1/return_policy?marketplace_id=EBAY_DE'); } export async function getPolicies() { const [fulfillment, payment, returns] = await Promise.all([ getFulfillmentPolicies().catch(() => ({ fulfillmentPolicies: [] })), getPaymentPolicies().catch(() => ({ paymentPolicies: [] })), getReturnPolicies().catch(() => ({ returnPolicies: [] })), ]); return { fulfillment: fulfillment.fulfillmentPolicies || [], payment: payment.paymentPolicies || [], returns: returns.returnPolicies || [], }; } export function isConnected() { return !!(process.env.EBAY_ACCESS_TOKEN && process.env.EBAY_CLIENT_ID); } export function hasCredentials() { return !!(process.env.EBAY_CLIENT_ID && process.env.EBAY_CLIENT_SECRET); } export async function getOffer(offerId) { if (!offerId) throw new Error('eBay Offer-ID fehlt'); return ebayFetch(`/sell/inventory/v1/offer/${encodeURIComponent(offerId)}`); } export async function updateOffer(listing, offerId, sku) { if (!offerId) throw new Error('eBay Offer-ID fehlt'); await createInventoryItem(listing, sku); const price = listing.price || listing.suggested_price || 1; const body = { sku, marketplaceId: 'EBAY_DE', format: 'FIXED_PRICE', listingDescription: listing.description || '', availableQuantity: 1, pricingSummary: { price: { value: String(price), currency: 'EUR' } }, listingPolicies: {}, categoryId: listing.ebay_category_id || process.env.EBAY_DEFAULT_CATEGORY || '175673', merchantLocationKey: process.env.EBAY_LOCATION_KEY || undefined, }; if (process.env.EBAY_FULFILLMENT_POLICY_ID) body.listingPolicies.fulfillmentPolicyId = process.env.EBAY_FULFILLMENT_POLICY_ID; if (process.env.EBAY_PAYMENT_POLICY_ID) body.listingPolicies.paymentPolicyId = process.env.EBAY_PAYMENT_POLICY_ID; if (process.env.EBAY_RETURN_POLICY_ID) body.listingPolicies.returnPolicyId = process.env.EBAY_RETURN_POLICY_ID; await ebayFetch(`/sell/inventory/v1/offer/${encodeURIComponent(offerId)}`, { method: 'PUT', body: JSON.stringify(body) }); return getOffer(offerId); } export async function withdrawOffer(offerId) { if (!offerId) throw new Error('eBay Offer-ID fehlt'); return ebayFetch(`/sell/inventory/v1/offer/${encodeURIComponent(offerId)}/withdraw`, { method: 'POST' }); }