Files
vendoo/lib/ebay-api.mjs
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00

294 lines
9.6 KiB
JavaScript

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' });
}