Tom Butcher e98828bfc7
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Update configuration and enhance marketplace integration with new policies
This commit modifies the log level in the configuration file from "trace" to "debug" for improved logging clarity. It also introduces new routes for fulfillment, return, and payment policies in the index file, enhancing the marketplace integration capabilities. Additionally, the initialization process is updated to start the marketplace worker, ensuring that the application can handle marketplace operations effectively. Various schemas are updated to include new fields and relationships for policies, improving the overall functionality and flexibility of the marketplace management system.
2026-08-29 00:47:55 +01:00

271 lines
7.8 KiB
JavaScript

import config from '../../../config.js';
import log4js from 'log4js';
const logger = log4js.getLogger('eBay');
logger.level = config.server.logLevel;
const DEBUG_PAYLOAD_MAX_LENGTH = 4000;
function formatDebugPayload(value, { maxLength = DEBUG_PAYLOAD_MAX_LENGTH } = {}) {
if (value === null || value === undefined) {
return value;
}
let text;
try {
text = typeof value === 'string' ? value : JSON.stringify(value);
} catch {
text = String(value);
}
if (text.length <= maxLength) {
return text;
}
return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`;
}
export function getEbayMarketplaceId(marketplace) {
return marketplace?.config?.marketplaceId || 'EBAY_GB';
}
function getMarketplaceDebugContext(marketplace) {
return {
marketplace: marketplace?.name,
marketplaceId: getEbayMarketplaceId(marketplace),
sandbox: marketplace?.config?.sandbox ?? false,
};
}
const SANDBOX_API_URL = 'https://api.sandbox.ebay.com';
const PRODUCTION_API_URL = 'https://api.ebay.com';
const SANDBOX_MEDIA_API_URL = 'https://apim.sandbox.ebay.com';
const PRODUCTION_MEDIA_API_URL = 'https://apim.ebay.com';
const SANDBOX_AUTH_URL = 'https://auth.sandbox.ebay.com';
const PRODUCTION_AUTH_URL = 'https://auth.ebay.com';
const TOKEN_PATH = '/identity/v1/oauth2/token';
const DEFAULT_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.fulfillment',
'https://api.ebay.com/oauth/api_scope/sell.account',
'https://api.ebay.com/oauth/api_scope/commerce.notification.subscription',
'https://api.ebay.com/oauth/api_scope/commerce.media',
];
const MARKETPLACE_LANGUAGE_MAP = {
EBAY_US: 'en-US',
EBAY_GB: 'en-GB',
EBAY_AU: 'en-AU',
EBAY_CA: 'en-CA',
EBAY_DE: 'de-DE',
EBAY_FR: 'fr-FR',
EBAY_ES: 'es-ES',
EBAY_IT: 'it-IT',
EBAY_NL: 'nl-NL',
EBAY_BE: 'nl-BE',
};
export function getApiBaseUrl(marketplace) {
return marketplace.config.sandbox ? SANDBOX_API_URL : PRODUCTION_API_URL;
}
export function getMediaApiBaseUrl(marketplace) {
return marketplace.config.sandbox ? SANDBOX_MEDIA_API_URL : PRODUCTION_MEDIA_API_URL;
}
export function getAuthorizeBaseUrl(marketplace) {
return marketplace.config.sandbox ? SANDBOX_AUTH_URL : PRODUCTION_AUTH_URL;
}
export function getScopes(marketplace) {
if (Array.isArray(marketplace.config.scopes) && marketplace.config.scopes.length) {
return marketplace.config.scopes;
}
if (typeof marketplace.config.scopes === 'string' && marketplace.config.scopes.trim()) {
return marketplace.config.scopes.trim().split(/\s+/);
}
return DEFAULT_SCOPES;
}
export function getScopesString(marketplace) {
return getScopes(marketplace).join(' ');
}
function isValidLanguageTag(value) {
return /^[a-z]{2,3}(?:-[A-Z]{2})?$/.test(value || '');
}
export function getAcceptLanguage(marketplace) {
const configured =
marketplace.config?.acceptLanguage ||
marketplace.config?.locale ||
MARKETPLACE_LANGUAGE_MAP[marketplace.config?.marketplaceId];
if (typeof configured === 'string') {
const normalized = configured.replace('_', '-').trim();
if (isValidLanguageTag(normalized)) {
return normalized;
}
}
return 'en-GB';
}
export function getTokenExpiryDate(expiresInSeconds) {
return new Date(Date.now() + Math.max(Number(expiresInSeconds || 0) - 60, 0) * 1000);
}
export function isAccessTokenExpired(marketplace) {
const { accessToken, accessTokenExpiresAt } = marketplace.config || {};
if (!accessToken || !accessTokenExpiresAt) {
return true;
}
return new Date(accessTokenExpiresAt).getTime() <= Date.now();
}
export function getRequiredAuthConfig(marketplace) {
const { clientId, clientSecret } = marketplace.config || {};
if (!clientId || !clientSecret) {
throw new Error('eBay marketplace is missing required config (clientId, clientSecret)');
}
return { clientId, clientSecret };
}
export function getBasicAuthHeader(marketplace) {
const { clientId, clientSecret } = getRequiredAuthConfig(marketplace);
return `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
}
export async function makeRequest({
marketplace,
method = 'GET',
path,
params = {},
body = null,
acceptableStatuses = [],
extraHeaders = {},
logResponse = true,
rawBody = false,
contentType,
baseUrl,
} = {}) {
const { accessToken } = marketplace.config || {};
if (!accessToken) {
throw new Error(
'eBay marketplace is not authenticated. Complete marketplace authorization first.'
);
}
const queryString = Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
const apiBase = baseUrl || getApiBaseUrl(marketplace);
const url = queryString ? `${apiBase}${path}?${queryString}` : `${apiBase}${path}`;
const headers = {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Language': getAcceptLanguage(marketplace),
...extraHeaders,
};
headers['X-EBAY-C-MARKETPLACE-ID'] = getEbayMarketplaceId(marketplace);
const fetchOptions = {
method,
headers,
};
const isRawBody = rawBody === true || Boolean(contentType);
if (body != null && method !== 'GET') {
if (isRawBody) {
if (contentType) {
fetchOptions.headers['Content-Type'] = contentType;
}
fetchOptions.body = body;
} else {
fetchOptions.headers['Content-Type'] = 'application/json';
fetchOptions.headers['Content-Language'] = getAcceptLanguage(marketplace);
fetchOptions.body = JSON.stringify(body);
}
}
const startedAt = Date.now();
const debugBody = isRawBody
? `[binary ${body?.length ?? 0} bytes]`
: body
? formatDebugPayload(body)
: undefined;
logger.debug(`eBay API ${method} ${path}`, {
...getMarketplaceDebugContext(marketplace),
host: apiBase,
params: Object.keys(params).length ? params : undefined,
body: debugBody,
acceptableStatuses: acceptableStatuses.length ? acceptableStatuses : undefined,
});
const response = await fetch(url, fetchOptions);
const durationMs = Date.now() - startedAt;
if (response.status === 204) {
logger.debug(`eBay API ${method} ${path} -> 204 No Content (${durationMs}ms)`, {
...getMarketplaceDebugContext(marketplace),
});
return null;
}
const responseText = await response.text();
let data = null;
if (responseText) {
try {
data = JSON.parse(responseText);
} catch {
data = responseText;
}
}
if (!response.ok && acceptableStatuses.includes(response.status)) {
logger.debug(
`eBay API ${method} ${path} -> ${response.status} (acceptable) (${durationMs}ms)`,
{
...getMarketplaceDebugContext(marketplace),
response: data ? formatDebugPayload(data) : undefined,
}
);
return null;
}
if (!response.ok) {
const message =
(typeof data === 'object' &&
(data?.errors?.[0]?.longMessage ||
data?.errors?.[0]?.message ||
data?.error_description)) ||
(typeof data === 'string' && data) ||
response.statusText;
logger.error(`eBay API error: ${message}`, {
status: response.status,
path,
durationMs,
...getMarketplaceDebugContext(marketplace),
response: data ? formatDebugPayload(data) : undefined,
});
throw new Error(`eBay API error (${response.status}): ${message}`);
}
logger.debug(`eBay API ${method} ${path} -> ${response.status} (${durationMs}ms)`, {
...getMarketplaceDebugContext(marketplace),
response: logResponse && data ? formatDebugPayload(data) : undefined,
});
return data;
}
export { logger, formatDebugPayload, getMarketplaceDebugContext };