diff --git a/src/integrations/marketplaces/ebay/__tests__/orders.sync.test.js b/src/integrations/marketplaces/ebay/__tests__/orders.sync.test.js
new file mode 100644
index 0000000..9ffcbef
--- /dev/null
+++ b/src/integrations/marketplaces/ebay/__tests__/orders.sync.test.js
@@ -0,0 +1,81 @@
+import { afterEach, describe, expect, it, jest } from '@jest/globals';
+import { getOrder, syncOrders } from '../orders.js';
+
+const successXml = `
+
+ Success
+ false
+
+
+ 12-34567-89012
+ Completed
+ 10.00
+ 12.50
+ ebay-buyer
+
+
+`;
+
+const marketplace = {
+ name: 'eBay UK',
+ config: { accessToken: 'token', marketplaceId: 'EBAY_GB' },
+};
+
+function mockTradingResponse(xml = successXml) {
+ return jest.spyOn(global, 'fetch').mockResolvedValue({
+ ok: true,
+ status: 200,
+ text: async () => xml,
+ });
+}
+
+describe('eBay Trading API order fetch', () => {
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('syncs orders with GetOrders and a default lookback', async () => {
+ const fetchSpy = mockTradingResponse();
+
+ const orders = await syncOrders(marketplace);
+
+ expect(fetchSpy).toHaveBeenCalledWith(
+ 'https://api.ebay.com/ws/api.dll',
+ expect.objectContaining({
+ method: 'POST',
+ headers: expect.objectContaining({
+ 'X-EBAY-API-CALL-NAME': 'GetOrders',
+ 'X-EBAY-API-IAF-TOKEN': 'token',
+ 'X-EBAY-API-SITEID': '3',
+ }),
+ })
+ );
+ const body = fetchSpy.mock.calls[0][1].body;
+ expect(body).toContain('30');
+ expect(body).toContain('Seller');
+ expect(orders).toHaveLength(1);
+ expect(orders[0].orderId).toBe('12-34567-89012');
+ });
+
+ it('uses CreateTimeFrom/To when a sync window is provided', async () => {
+ const fetchSpy = mockTradingResponse();
+
+ await syncOrders(marketplace, { startTime: 1756512000, endTime: 1756598400 });
+
+ const body = fetchSpy.mock.calls[0][1].body;
+ expect(body).toContain('2025-08-30T00:00:00.000Z');
+ expect(body).toContain('2025-08-31T00:00:00.000Z');
+ expect(body).not.toContain('');
+ });
+
+ it('loads a single order by OrderID', async () => {
+ const fetchSpy = mockTradingResponse();
+
+ const order = await getOrder(marketplace, '12-34567-89012');
+
+ const body = fetchSpy.mock.calls[0][1].body;
+ expect(body).toContain('12-34567-89012');
+ expect(order.orderId).toBe('12-34567-89012');
+ });
+});
diff --git a/src/integrations/marketplaces/ebay/__tests__/orders.test.js b/src/integrations/marketplaces/ebay/__tests__/orders.test.js
index 77b7355..28e670f 100644
--- a/src/integrations/marketplaces/ebay/__tests__/orders.test.js
+++ b/src/integrations/marketplaces/ebay/__tests__/orders.test.js
@@ -6,56 +6,106 @@ import {
mapOrderLineItems,
mapOrderShipments,
handleWebhook,
+ parseGetOrdersResponse,
} from '../orders.js';
-const ebayOrder = {
- orderId: '12-34567-89012',
- orderFulfillmentStatus: 'NOT_STARTED',
- pricingSummary: {
- priceSubtotal: { value: '10.00' },
- deliveryCost: { value: '2.50' },
- tax: { value: '1.20' },
- total: { value: '13.70' },
- },
- buyer: {
- username: 'ebay-buyer',
- buyerRegistrationAddress: { email: 'buyer@example.com' },
- },
- fulfillmentStartInstructions: [
- {
- shippingStep: {
- shipTo: {
- fullName: 'Jane Buyer',
- primaryPhone: { phoneNumber: '012345' },
- contactAddress: {
- addressLine1: '1 High St',
- city: 'London',
- postalCode: 'SW1A 1AA',
- countryCode: 'GB',
- },
- },
- },
- },
- ],
- lineItems: [
- {
- lineItemId: 'line-1',
+const sampleGetOrdersXml = `
+
+ Success
+ false
+
+
+ 12-34567-89012
+ 12-34567-89012
+ Completed
+ 13.70
+
+ Complete
+
+
+
+ 1.20
+
+
+ Royal Mail
+ TRACK123
+
+
+ 2026-08-01T09:00:00.000Z
+
+ Jane Buyer
+ 1 High St
+ London
+ SW1A 1AA
+ GB
+ 012345
+
+
+ UK_RoyalMailFirstClassStandard
+ 2.50
+
+ 10.00
+ 13.70
+
+
+
+ buyer@example.com
+ Jane
+ Buyer
+
+ -
+ 111
+ Red widget
+ SKU-RED
+
+ 2
+ line-1
+ 5.00
+
+ 1.00
+
+ line-1
+
+
+ ebay-buyer
+ 2026-08-01T09:05:00.000Z
+ 2026-08-01T10:00:00.000Z
+ NotApplicable
+
+
+
+`;
+
+const ebayOrder = parseGetOrdersResponse(sampleGetOrdersXml).orders[0];
+
+describe('eBay Trading API GetOrders parsing', () => {
+ it('parses order totals, buyer, lines, and tracking', () => {
+ expect(ebayOrder).toMatchObject({
+ orderId: '12-34567-89012',
+ orderStatus: 'Completed',
+ buyerUserId: 'ebay-buyer',
+ buyerEmail: 'buyer@example.com',
+ subtotal: 10,
+ shippingAmount: 2.5,
+ salesTaxAmount: 1.2,
+ total: 13.7,
+ });
+ expect(ebayOrder.shippingAddress.city).toBe('London');
+ expect(ebayOrder.transactions[0]).toMatchObject({
sku: 'SKU-RED',
- title: 'Red widget',
quantity: 2,
- lineItemCost: { value: '5.00' },
- taxes: [{ amount: { value: '1.00' } }],
- },
- ],
- shippingFulfillments: [
- {
- fulfillmentId: 'ful-1',
- shipmentTrackingNumber: 'TRACK123',
- shippedDate: '2026-08-01T10:00:00.000Z',
- lineItems: [{ lineItemId: 'line-1' }],
- },
- ],
-};
+ transactionPrice: 5,
+ });
+ expect(ebayOrder.shipments[0].trackingNumber).toBe('TRACK123');
+ });
+
+ it('treats HasMoreOrders as false when missing', () => {
+ expect(parseGetOrdersResponse('Success')).toEqual({
+ orders: [],
+ hasMoreOrders: false,
+ });
+ });
+});
describe('eBay order mappers', () => {
it('maps sales order totals and externalReference', () => {
@@ -64,13 +114,18 @@ describe('eBay order mappers', () => {
expect(mapped.totalAmount).toBe(10);
expect(mapped.shippingAmount).toBe(2.5);
expect(mapped.totalTaxAmount).toBe(1.2);
- expect(mapped.state.type).toBe('draft');
+ expect(mapped.state.type).toBe('shipped');
});
it('maps cancelled orders', () => {
- expect(
- mapOrderStatus({ cancelStatus: { cancelState: 'CANCELED' } })
- ).toBe('cancelled');
+ expect(mapOrderStatus({ cancelStatus: 'CancelClosedWithRefund' })).toBe('cancelled');
+ expect(mapOrderStatus({ orderStatus: 'Cancelled' })).toBe('cancelled');
+ });
+
+ it('maps paid but unshipped orders as confirmed', () => {
+ expect(mapOrderStatus({ orderStatus: 'Completed', checkoutStatus: 'Complete' })).toBe(
+ 'confirmed'
+ );
});
it('maps buyer to client with marketplace buyer id', () => {
@@ -93,10 +148,10 @@ describe('eBay order mappers', () => {
});
});
- it('maps shipments from fulfillments', () => {
+ it('maps shipments from tracking details', () => {
const shipments = mapOrderShipments(ebayOrder);
expect(shipments[0]).toMatchObject({
- externalReference: 'ful-1',
+ externalReference: 'TRACK123',
trackingNumber: 'TRACK123',
});
expect(shipments[0].state.type).toBe('shipped');
@@ -105,7 +160,10 @@ describe('eBay order mappers', () => {
it('classifies commerce notification topics', async () => {
const sold = await handleWebhook(
{ name: 'eBay' },
- { metadata: { topic: 'ORDER_CONFIRMATION' }, notification: { data: { orderId: 'o1' }, notificationId: 'n1' } }
+ {
+ metadata: { topic: 'ORDER_CONFIRMATION' },
+ notification: { data: { orderId: 'o1' }, notificationId: 'n1' },
+ }
);
expect(sold).toMatchObject({ action: 'orderUpdate', orderId: 'o1', notificationId: 'n1' });
diff --git a/src/integrations/marketplaces/ebay/orders.js b/src/integrations/marketplaces/ebay/orders.js
index 2569e97..abf3602 100644
--- a/src/integrations/marketplaces/ebay/orders.js
+++ b/src/integrations/marketplaces/ebay/orders.js
@@ -1,49 +1,217 @@
import { makeRequest, logger } from './shared.js';
+import { makeTradingRequest, xmlBlocks, xmlEscape, xmlNumber, xmlText } from './trading.js';
+
+const ENTRIES_PER_PAGE = 100;
+const DEFAULT_LOOKBACK_DAYS = 30;
+const MAX_RANGE_MS = 90 * 24 * 60 * 60 * 1000;
+const INVALID_EMAILS = new Set(['', 'invalid request']);
+
+const CANCELLED_STATUSES = new Set([
+ 'Cancelled',
+ 'CancelClosedNoRefund',
+ 'CancelClosedWithRefund',
+ 'CancelClosedUnknownRefund',
+ 'CancelClosedForCommitment',
+]);
+
+function headerXml(orderXml) {
+ return String(orderXml || '').replace(/[\s\S]*?<\/TransactionArray>/i, '');
+}
+
+function parseShippingAddress(xml) {
+ const block = xmlBlocks(xml, 'ShippingAddress')[0] || '';
+ return {
+ name: xmlText(block, 'Name'),
+ street1: xmlText(block, 'Street1'),
+ street2: xmlText(block, 'Street2'),
+ city: xmlText(block, 'CityName'),
+ state: xmlText(block, 'StateOrProvince'),
+ postalCode: xmlText(block, 'PostalCode'),
+ country: xmlText(block, 'Country'),
+ phone: xmlText(block, 'Phone'),
+ };
+}
+
+function parseTrackingDetails(xml) {
+ return xmlBlocks(xml, 'ShipmentTrackingDetails')
+ .map((block) => ({
+ trackingNumber: xmlText(block, 'ShipmentTrackingNumber'),
+ carrier: xmlText(block, 'ShippingCarrierUsed'),
+ }))
+ .filter((detail) => detail.trackingNumber);
+}
+
+function parseTradingTransaction(xml) {
+ const item = xmlBlocks(xml, 'Item')[0] || '';
+ const buyer = xmlBlocks(xml, 'Buyer')[0] || '';
+ const taxes = xmlBlocks(xml, 'Taxes')[0] || '';
+ return {
+ transactionId: xmlText(xml, 'TransactionID'),
+ orderLineItemId: xmlText(xml, 'OrderLineItemID'),
+ sku: xmlText(item, 'SKU'),
+ title: xmlText(item, 'Title'),
+ quantity: xmlNumber(xml, 'QuantityPurchased') || 1,
+ transactionPrice: xmlNumber(xml, 'TransactionPrice'),
+ totalTaxAmount: xmlNumber(taxes, 'TotalTaxAmount'),
+ email: xmlText(buyer, 'Email'),
+ firstName: xmlText(buyer, 'UserFirstName'),
+ lastName: xmlText(buyer, 'UserLastName'),
+ tracking: parseTrackingDetails(xml),
+ };
+}
+
+function firstValidEmail(transactions) {
+ for (const transaction of transactions) {
+ const email = (transaction.email || '').trim();
+ if (email && !INVALID_EMAILS.has(email.toLowerCase())) {
+ return email;
+ }
+ }
+ return '';
+}
+
+function dedupeTracking(details) {
+ const seen = new Set();
+ const unique = [];
+ for (const detail of details) {
+ if (seen.has(detail.trackingNumber)) continue;
+ seen.add(detail.trackingNumber);
+ unique.push(detail);
+ }
+ return unique;
+}
+
+export function parseTradingOrder(orderXml) {
+ const header = headerXml(orderXml);
+ const shippingSelected = xmlBlocks(header, 'ShippingServiceSelected')[0] || '';
+ const salesTaxBlock = xmlBlocks(header, 'SalesTax')[0] || '';
+ const checkout = xmlBlocks(header, 'CheckoutStatus')[0] || '';
+ const transactions = xmlBlocks(orderXml, 'Transaction').map(parseTradingTransaction);
+ const taxFromTransactions = transactions.reduce((sum, line) => sum + line.totalTaxAmount, 0);
+
+ return {
+ orderId: xmlText(header, 'OrderID') || xmlText(header, 'ExtendedOrderID'),
+ extendedOrderId: xmlText(header, 'ExtendedOrderID'),
+ orderStatus: xmlText(header, 'OrderStatus'),
+ cancelStatus: xmlText(header, 'CancelStatus'),
+ checkoutStatus: xmlText(checkout, 'Status'),
+ paidTime: xmlText(header, 'PaidTime'),
+ shippedTime: xmlText(header, 'ShippedTime'),
+ createdTime: xmlText(header, 'CreatedTime'),
+ buyerUserId: xmlText(header, 'BuyerUserID'),
+ buyerEmail: firstValidEmail(transactions),
+ shippingAddress: parseShippingAddress(header),
+ subtotal: xmlNumber(header, 'Subtotal'),
+ total: xmlNumber(header, 'Total'),
+ amountPaid: xmlNumber(header, 'AmountPaid'),
+ shippingAmount: xmlNumber(shippingSelected, 'ShippingServiceCost'),
+ salesTaxAmount: xmlNumber(salesTaxBlock, 'SalesTaxAmount') || taxFromTransactions,
+ transactions,
+ shipments: dedupeTracking([
+ ...parseTrackingDetails(header),
+ ...transactions.flatMap((line) => line.tracking),
+ ]),
+ };
+}
+
+export function parseGetOrdersResponse(xml = '') {
+ return {
+ orders: xmlBlocks(xml, 'Order').map(parseTradingOrder),
+ hasMoreOrders: xmlText(xml, 'HasMoreOrders').toLowerCase() === 'true',
+ };
+}
+
+function buildGetOrdersRequest({
+ startIso,
+ endIso,
+ numberOfDays,
+ pageNumber = 1,
+ orderIds = [],
+} = {}) {
+ const filters = [];
+ if (orderIds.length) {
+ filters.push(
+ ` \n${orderIds
+ .map((id) => ` ${xmlEscape(id)}`)
+ .join('\n')}\n `
+ );
+ } else if (numberOfDays) {
+ filters.push(` ${numberOfDays}`);
+ } else {
+ if (startIso) filters.push(` ${startIso}`);
+ if (endIso) filters.push(` ${endIso}`);
+ }
+
+ return `
+
+ en_GB
+ High
+ ReturnAll
+ Seller
+ All
+${filters.join('\n')}
+
+ ${ENTRIES_PER_PAGE}
+ ${pageNumber}
+
+`;
+}
+
+function createTimeWindows(startTime, endTime) {
+ if (!startTime && !endTime) {
+ return [{ numberOfDays: DEFAULT_LOOKBACK_DAYS }];
+ }
+
+ const nowSec = Math.floor(Date.now() / 1000);
+ const startSec = startTime || endTime - DEFAULT_LOOKBACK_DAYS * 86400;
+ const endSec = endTime || nowSec;
+ const windows = [];
+ let cursor = Math.min(startSec, endSec) * 1000;
+ const endMs = Math.max(startSec, endSec) * 1000;
+
+ while (cursor < endMs) {
+ const windowEnd = Math.min(cursor + MAX_RANGE_MS, endMs);
+ windows.push({
+ startIso: new Date(cursor).toISOString(),
+ endIso: new Date(windowEnd).toISOString(),
+ });
+ cursor = windowEnd;
+ }
+
+ return windows.length ? windows : [{ numberOfDays: DEFAULT_LOOKBACK_DAYS }];
+}
+
+async function fetchOrdersPage(marketplace, requestOptions) {
+ const xml = await makeTradingRequest({
+ marketplace,
+ callName: 'GetOrders',
+ body: buildGetOrdersRequest(requestOptions),
+ });
+ return parseGetOrdersResponse(xml);
+}
+
+async function fetchOrdersInWindow(marketplace, windowOptions) {
+ const orders = [];
+ let pageNumber = 1;
+ let hasMore = true;
+
+ while (hasMore) {
+ const page = await fetchOrdersPage(marketplace, { ...windowOptions, pageNumber });
+ if (page.orders.length) {
+ orders.push(...page.orders);
+ }
+ hasMore = page.hasMoreOrders && page.orders.length > 0;
+ pageNumber += 1;
+ }
+
+ return orders;
+}
async function fetchAllOrders(marketplace, { startTime, endTime } = {}) {
const orders = [];
- let offset = 0;
- const limit = 50;
-
- const filterParts = [];
- if (startTime) {
- const isoStart = new Date(startTime * 1000).toISOString();
- filterParts.push(`creationdate:[${isoStart}..`);
+ for (const window of createTimeWindows(startTime, endTime)) {
+ orders.push(...(await fetchOrdersInWindow(marketplace, window)));
}
- if (endTime) {
- const isoEnd = new Date(endTime * 1000).toISOString();
- if (filterParts.length && filterParts[0].startsWith('creationdate:')) {
- filterParts[0] = filterParts[0] + `${isoEnd}]`;
- } else {
- filterParts.push(`creationdate:[..${isoEnd}]`);
- }
- } else if (filterParts.length) {
- filterParts[0] = filterParts[0] + ']';
- }
-
- do {
- const params = { limit, offset };
- if (filterParts.length) {
- params.filter = filterParts.join(',');
- }
-
- const data = await makeRequest({
- marketplace,
- path: '/sell/fulfillment/v1/order',
- params,
- });
-
- if (data?.orders?.length) {
- orders.push(...data.orders);
- }
-
- if (!data?.orders?.length || orders.length >= (data.total || 0)) {
- break;
- }
-
- offset += limit;
- } while (true);
-
return orders;
}
@@ -56,38 +224,32 @@ export async function syncOrders(marketplace, { startTime, endTime } = {}) {
return orders;
}
-const ORDER_STATUS_MAP = {
- NOT_STARTED: 'draft',
- IN_PROGRESS: 'confirmed',
- FULFILLED: 'shipped',
- CANCELLED: 'cancelled',
-};
-
-const FULFILLMENT_STATUS_MAP = {
- NOT_STARTED: 'confirmed',
- IN_PROGRESS: 'shipped',
- FULFILLED: 'delivered',
-};
-
export function mapOrderStatus(ebayOrder) {
- if (ebayOrder.cancelStatus?.cancelState === 'CANCELED') {
+ const cancelStatus = ebayOrder.cancelStatus || '';
+ if (CANCELLED_STATUSES.has(ebayOrder.orderStatus) || CANCELLED_STATUSES.has(cancelStatus)) {
return 'cancelled';
}
- const fulfillmentStatus = ebayOrder.fulfillmentStartInstructions?.[0]?.fulfillmentStatus;
- if (fulfillmentStatus && FULFILLMENT_STATUS_MAP[fulfillmentStatus]) {
- return FULFILLMENT_STATUS_MAP[fulfillmentStatus];
+ if (ebayOrder.shippedTime) {
+ return 'shipped';
}
- return ORDER_STATUS_MAP[ebayOrder.orderFulfillmentStatus] || 'draft';
+ if (
+ ebayOrder.orderStatus === 'Completed' ||
+ ebayOrder.checkoutStatus === 'Complete' ||
+ ebayOrder.paidTime
+ ) {
+ return 'confirmed';
+ }
+
+ return 'draft';
}
export function mapOrderToSalesOrder(ebayOrder) {
- const pricingSummary = ebayOrder.pricingSummary || {};
- const totalAmount = parseFloat(pricingSummary.priceSubtotal?.value || 0);
- const shippingAmount = parseFloat(pricingSummary.deliveryCost?.value || 0);
- const totalTax = parseFloat(pricingSummary.tax?.value || 0);
- const grandTotal = parseFloat(pricingSummary.total?.value || 0);
+ const totalAmount = ebayOrder.subtotal || 0;
+ const shippingAmount = ebayOrder.shippingAmount || 0;
+ const totalTax = ebayOrder.salesTaxAmount || 0;
+ const grandTotal = ebayOrder.total || 0;
return {
externalReference: ebayOrder.orderId,
@@ -103,54 +265,47 @@ export function mapOrderToSalesOrder(ebayOrder) {
}
export function mapBuyerToClient(ebayOrder) {
- const buyer = ebayOrder.buyer || {};
- const address = ebayOrder.fulfillmentStartInstructions?.[0]?.shippingStep?.shipTo || {};
-
- const fullName = address.fullName || buyer.username || 'Unknown';
- const contactAddress = address.contactAddress || {};
+ const address = ebayOrder.shippingAddress || {};
+ const buyer = ebayOrder.transactions?.[0] || {};
+ const buyerName = [buyer.firstName, buyer.lastName].filter(Boolean).join(' ');
+ const fullName = address.name || buyerName || ebayOrder.buyerUserId || 'Unknown';
return {
name: fullName,
- email: buyer.buyerRegistrationAddress?.email || '',
- phone: address.primaryPhone?.phoneNumber || '',
- externalReference: buyer.username || '',
+ email: ebayOrder.buyerEmail || '',
+ phone: address.phone || '',
+ externalReference: ebayOrder.buyerUserId || '',
address: {
- addressLine1: contactAddress.addressLine1 || '',
- addressLine2: contactAddress.addressLine2 || '',
- city: contactAddress.city || '',
- state: contactAddress.stateOrProvince || '',
- postcode: contactAddress.postalCode || '',
- country: contactAddress.countryCode || '',
+ addressLine1: address.street1 || '',
+ addressLine2: address.street2 || '',
+ city: address.city || '',
+ state: address.state || '',
+ postcode: address.postalCode || '',
+ country: address.country || '',
},
};
}
export async function getOrder(marketplace, orderId) {
- const order = await makeRequest({
+ const xml = await makeTradingRequest({
marketplace,
- path: `/sell/fulfillment/v1/order/${encodeURIComponent(orderId)}`,
+ callName: 'GetOrders',
+ body: buildGetOrdersRequest({ orderIds: [orderId] }),
});
- const fulfillments = await makeRequest({
- marketplace,
- path: `/sell/fulfillment/v1/order/${encodeURIComponent(orderId)}/shipping_fulfillment`,
- acceptableStatuses: [404],
- });
- return {
- ...order,
- shippingFulfillments: fulfillments?.fulfillments || [],
- };
+ const { orders } = parseGetOrdersResponse(xml);
+ if (!orders.length) {
+ throw new Error(`eBay order ${orderId} was not found`);
+ }
+ return orders[0];
}
export function mapOrderLineItems(ebayOrder) {
- return (ebayOrder.lineItems || []).map((line) => {
- const unit = parseFloat(line.lineItemCost?.value || 0);
- const tax = (line.taxes || []).reduce(
- (sum, taxLine) => sum + parseFloat(taxLine.amount?.value || 0),
- 0
- );
+ return (ebayOrder.transactions || []).map((line) => {
+ const unit = line.transactionPrice || 0;
+ const tax = line.totalTaxAmount || 0;
const quantity = line.quantity || 1;
return {
- externalReference: line.lineItemId,
+ externalReference: line.orderLineItemId || line.transactionId,
sku: line.sku || '',
name: line.title || line.sku || 'Marketplace item',
quantity,
@@ -163,24 +318,26 @@ export function mapOrderLineItems(ebayOrder) {
}
export function mapOrderShipments(ebayOrder) {
- const shippingAmount = parseFloat(ebayOrder.pricingSummary?.deliveryCost?.value || 0);
- const fulfillments = ebayOrder.shippingFulfillments || [];
- if (!fulfillments.length) {
+ const shipments = ebayOrder.shipments || [];
+ if (!shipments.length) {
return [];
}
- const splitAmount = shippingAmount / fulfillments.length;
- return fulfillments.map((fulfillment) => ({
- externalReference: fulfillment.fulfillmentId,
- trackingNumber: fulfillment.shipmentTrackingNumber || '',
+ const shippingAmount = ebayOrder.shippingAmount || 0;
+ const splitAmount = shippingAmount / shipments.length;
+ return shipments.map((shipment) => ({
+ externalReference: shipment.trackingNumber,
+ trackingNumber: shipment.trackingNumber || '',
amount: splitAmount,
amountWithTax: splitAmount,
- state: { type: fulfillment.shippedDate ? 'shipped' : 'planned' },
- shippedAt: fulfillment.shippedDate ? new Date(fulfillment.shippedDate) : undefined,
- lineItemIds: (fulfillment.lineItems || []).map((line) => line.lineItemId).filter(Boolean),
+ state: { type: ebayOrder.shippedTime ? 'shipped' : 'planned' },
+ shippedAt: ebayOrder.shippedTime ? new Date(ebayOrder.shippedTime) : undefined,
}));
}
-export async function createShippingFulfillment(marketplace, { orderId, lineItems, trackingNumber, shippingCarrierCode }) {
+export async function createShippingFulfillment(
+ marketplace,
+ { orderId, lineItems, trackingNumber, shippingCarrierCode }
+) {
if (!orderId) {
throw new Error('orderId is required to create an eBay shipping fulfillment');
}
@@ -204,14 +361,9 @@ export async function createShippingFulfillment(marketplace, { orderId, lineItem
}
export async function handleWebhook(marketplace, event) {
- const topic =
- event?.metadata?.topic ||
- event?.topic ||
- event?.notification?.topic ||
- '';
+ const topic = event?.metadata?.topic || event?.topic || event?.notification?.topic || '';
const data = event?.notification?.data || event?.data || {};
- const notificationId =
- event?.notification?.notificationId || event?.notificationId || '';
+ const notificationId = event?.notification?.notificationId || event?.notificationId || '';
logger.info(`eBay webhook received: ${topic} for marketplace ${marketplace.name}`);
@@ -220,7 +372,10 @@ export async function handleWebhook(marketplace, event) {
const normalized = String(topic).toUpperCase();
- if (normalized.includes('ACCOUNT_DELETION') || normalized.includes('MARKETPLACE_ACCOUNT_DELETION')) {
+ if (
+ normalized.includes('ACCOUNT_DELETION') ||
+ normalized.includes('MARKETPLACE_ACCOUNT_DELETION')
+ ) {
return {
action: 'accountDeletion',
userId: data.userId || data.username,
@@ -233,7 +388,11 @@ export async function handleWebhook(marketplace, event) {
return { action: 'disconnect', notificationId, topic };
}
- if (normalized.includes('ORDER') || normalized === 'ITEM.SOLD' || normalized === 'ORDER.FULFILLMENT') {
+ if (
+ normalized.includes('ORDER') ||
+ normalized === 'ITEM.SOLD' ||
+ normalized === 'ORDER.FULFILLMENT'
+ ) {
return {
action: orderId && topic.toLowerCase().includes('cancel') ? 'orderCancel' : 'orderUpdate',
orderId,
diff --git a/src/integrations/marketplaces/ebay/shippingServices.js b/src/integrations/marketplaces/ebay/shippingServices.js
index a5de3ca..823ba21 100644
--- a/src/integrations/marketplaces/ebay/shippingServices.js
+++ b/src/integrations/marketplaces/ebay/shippingServices.js
@@ -1,43 +1,8 @@
-import { logger, getMarketplaceDebugContext } from './shared.js';
+import { logger } from './shared.js';
+import { makeTradingRequest } from './trading.js';
import { fetchEbayCategoryReferences } from './categoryTree.js';
import { syncAccountPolicies } from './accountPolicySync.js';
-const TRADING_COMPATIBILITY_LEVEL = '1399';
-const TRADING_SANDBOX_URL = 'https://api.sandbox.ebay.com/ws/api.dll';
-const TRADING_PRODUCTION_URL = 'https://api.ebay.com/ws/api.dll';
-
-const MARKETPLACE_SITE_IDS = {
- EBAY_US: '0',
- EBAY_ENCA: '2',
- EBAY_GB: '3',
- EBAY_AU: '15',
- EBAY_AT: '16',
- EBAY_FRBE: '23',
- EBAY_FR: '71',
- EBAY_DE: '77',
- EBAY_IT: '101',
- EBAY_NLBE: '123',
- EBAY_NL: '146',
- EBAY_ES: '186',
- EBAY_CH: '193',
- EBAY_HK: '201',
- EBAY_IN: '203',
- EBAY_IE: '205',
- EBAY_MY: '207',
- EBAY_PH: '211',
- EBAY_PL: '212',
- EBAY_SG: '216',
-};
-
-function getTradingApiUrl(marketplace) {
- return marketplace.config?.sandbox ? TRADING_SANDBOX_URL : TRADING_PRODUCTION_URL;
-}
-
-function getSiteId(marketplace) {
- const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
- return MARKETPLACE_SITE_IDS[marketplaceId] || MARKETPLACE_SITE_IDS.EBAY_GB;
-}
-
export function parseShippingServiceDetails(xml = '') {
const blocks = xml.match(/[\s\S]*?<\/ShippingServiceDetails>/g) || [];
const services = [];
@@ -68,67 +33,13 @@ function buildGeteBayDetailsRequest() {
}
export async function fetchEbayShippingServices(marketplace) {
- const accessToken = marketplace.config?.accessToken;
- if (!accessToken) {
- throw new Error(
- 'eBay marketplace is not authenticated. Complete marketplace authorization first.'
- );
- }
-
- const url = getTradingApiUrl(marketplace);
- const siteId = getSiteId(marketplace);
- const startedAt = Date.now();
- logger.debug('eBay Trading API GeteBayDetails request', {
- ...getMarketplaceDebugContext(marketplace),
- siteId,
- url,
- });
-
- const response = await fetch(url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'text/xml',
- 'X-EBAY-API-CALL-NAME': 'GeteBayDetails',
- 'X-EBAY-API-SITEID': siteId,
- 'X-EBAY-API-COMPATIBILITY-LEVEL': TRADING_COMPATIBILITY_LEVEL,
- 'X-EBAY-API-IAF-TOKEN': accessToken,
- },
+ const xml = await makeTradingRequest({
+ marketplace,
+ callName: 'GeteBayDetails',
body: buildGeteBayDetailsRequest(),
});
- const xml = await response.text();
- const durationMs = Date.now() - startedAt;
- if (!response.ok) {
- logger.error(`GeteBayDetails failed (${response.status})`, {
- durationMs,
- ...getMarketplaceDebugContext(marketplace),
- siteId,
- });
- throw new Error(`eBay GeteBayDetails error (${response.status})`);
- }
-
- const ackMatch = xml.match(/([^<]+)<\/Ack>/);
- const ack = ackMatch?.[1];
- if (ack && ack !== 'Success' && ack !== 'Warning') {
- const message =
- xml.match(/([^<]+)<\/LongMessage>/)?.[1] ||
- xml.match(/([^<]+)<\/ShortMessage>/)?.[1] ||
- ack;
- logger.error(`GeteBayDetails failed: ${message}`, {
- ack,
- durationMs,
- ...getMarketplaceDebugContext(marketplace),
- siteId,
- });
- throw new Error(`eBay GeteBayDetails failed: ${message}`);
- }
-
const services = parseShippingServiceDetails(xml);
- logger.debug(`GeteBayDetails succeeded (${durationMs}ms)`, {
- ...getMarketplaceDebugContext(marketplace),
- siteId,
- serviceCount: services.length,
- });
logger.info(
`Fetched ${services.length} eBay shipping service(s) for marketplace "${marketplace.name}"`
);
diff --git a/src/integrations/marketplaces/ebay/trading.js b/src/integrations/marketplaces/ebay/trading.js
new file mode 100644
index 0000000..afe4191
--- /dev/null
+++ b/src/integrations/marketplaces/ebay/trading.js
@@ -0,0 +1,142 @@
+import { logger, getMarketplaceDebugContext, formatDebugPayload } from './shared.js';
+
+const TRADING_COMPATIBILITY_LEVEL = '1399';
+const TRADING_SANDBOX_URL = 'https://api.sandbox.ebay.com/ws/api.dll';
+const TRADING_PRODUCTION_URL = 'https://api.ebay.com/ws/api.dll';
+
+const MARKETPLACE_SITE_IDS = {
+ EBAY_US: '0',
+ EBAY_ENCA: '2',
+ EBAY_GB: '3',
+ EBAY_AU: '15',
+ EBAY_AT: '16',
+ EBAY_FRBE: '23',
+ EBAY_FR: '71',
+ EBAY_DE: '77',
+ EBAY_IT: '101',
+ EBAY_NLBE: '123',
+ EBAY_NL: '146',
+ EBAY_ES: '186',
+ EBAY_CH: '193',
+ EBAY_HK: '201',
+ EBAY_IN: '203',
+ EBAY_IE: '205',
+ EBAY_MY: '207',
+ EBAY_PH: '211',
+ EBAY_PL: '212',
+ EBAY_SG: '216',
+};
+
+export function getTradingApiUrl(marketplace) {
+ return marketplace.config?.sandbox ? TRADING_SANDBOX_URL : TRADING_PRODUCTION_URL;
+}
+
+export function getTradingSiteId(marketplace) {
+ const marketplaceId = marketplace.config?.marketplaceId || 'EBAY_GB';
+ return MARKETPLACE_SITE_IDS[marketplaceId] || MARKETPLACE_SITE_IDS.EBAY_GB;
+}
+
+export function xmlEscape(value) {
+ return String(value ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+export function xmlUnescape(value) {
+ return String(value ?? '')
+ .replace(/'/g, "'")
+ .replace(/"/g, '"')
+ .replace(/>/g, '>')
+ .replace(/</g, '<')
+ .replace(/&/g, '&');
+}
+
+export function xmlBlocks(xml, tag) {
+ const re = new RegExp(`<${tag}(?:\\s[^>]*)?>[\\s\\S]*?${tag}>`, 'gi');
+ return String(xml || '').match(re) || [];
+}
+
+export function xmlText(xml, tag) {
+ const re = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)${tag}>`, 'i');
+ const match = String(xml || '').match(re);
+ if (!match) return '';
+ return xmlUnescape(match[1].replace(/<[^>]+>/g, '').trim());
+}
+
+export function xmlNumber(xml, tag) {
+ const parsed = parseFloat(xmlText(xml, tag));
+ return Number.isFinite(parsed) ? parsed : 0;
+}
+
+export async function makeTradingRequest({
+ marketplace,
+ callName,
+ body,
+ logResponse = false,
+} = {}) {
+ const accessToken = marketplace.config?.accessToken;
+ if (!accessToken) {
+ throw new Error(
+ 'eBay marketplace is not authenticated. Complete marketplace authorization first.'
+ );
+ }
+
+ const url = getTradingApiUrl(marketplace);
+ const siteId = getTradingSiteId(marketplace);
+ const startedAt = Date.now();
+
+ logger.debug(`eBay Trading API ${callName} request`, {
+ ...getMarketplaceDebugContext(marketplace),
+ siteId,
+ url,
+ });
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/xml',
+ 'X-EBAY-API-CALL-NAME': callName,
+ 'X-EBAY-API-SITEID': siteId,
+ 'X-EBAY-API-COMPATIBILITY-LEVEL': TRADING_COMPATIBILITY_LEVEL,
+ 'X-EBAY-API-IAF-TOKEN': accessToken,
+ },
+ body,
+ });
+
+ const xml = await response.text();
+ const durationMs = Date.now() - startedAt;
+
+ if (!response.ok) {
+ logger.error(`${callName} failed (${response.status})`, {
+ durationMs,
+ ...getMarketplaceDebugContext(marketplace),
+ siteId,
+ });
+ throw new Error(`eBay ${callName} error (${response.status})`);
+ }
+
+ const ack = xmlText(xml, 'Ack');
+ if (ack && ack !== 'Success' && ack !== 'Warning') {
+ const message = xmlText(xml, 'LongMessage') || xmlText(xml, 'ShortMessage') || ack;
+ logger.error(`${callName} failed: ${message}`, {
+ ack,
+ durationMs,
+ ...getMarketplaceDebugContext(marketplace),
+ siteId,
+ response: formatDebugPayload(xml),
+ });
+ throw new Error(`eBay ${callName} failed: ${message}`);
+ }
+
+ logger.debug(`eBay Trading API ${callName} -> ${ack || response.status} (${durationMs}ms)`, {
+ ...getMarketplaceDebugContext(marketplace),
+ siteId,
+ response: logResponse ? formatDebugPayload(xml) : undefined,
+ });
+
+ console.log('xml', xml);
+ return xml;
+}