305 lines
10 KiB
JavaScript
305 lines
10 KiB
JavaScript
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const envPath = join(__dirname, '..', '.env');
|
|
|
|
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) {
|
|
let content = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
|
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
const line = `${key}=${value}`;
|
|
if (regex.test(content)) content = content.replace(regex, line);
|
|
else content = content.trimEnd() + '\n' + line + '\n';
|
|
writeFileSync(envPath, content, 'utf-8');
|
|
process.env[key] = value;
|
|
}
|
|
|
|
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' });
|
|
}
|