Implement syncWebhooks functionality and refactor ensureWebhookSubscriptions
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces the `syncWebhooks` function to streamline the process of synchronizing webhook configurations for marketplaces. It enhances the existing `ensureWebhookSubscriptions` function to utilize `syncWebhooks`, ensuring that webhook URLs and verification tokens are correctly set and updated. Additionally, a new module for managing webhook configurations is added, which includes utility functions for building webhook URLs and generating verification tokens. Tests are also included to validate the new functionality and its integration with existing systems, improving the overall reliability of webhook management.
This commit is contained in:
parent
61d29a8f7e
commit
1cef3a8d1d
@ -7,7 +7,7 @@ const listingFindById = jest.fn();
|
|||||||
const listingVarientFind = jest.fn();
|
const listingVarientFind = jest.fn();
|
||||||
|
|
||||||
jest.unstable_mockModule('../../config.js', () => ({
|
jest.unstable_mockModule('../../config.js', () => ({
|
||||||
default: { server: { logLevel: 'error' } },
|
default: { server: { logLevel: 'error' }, app: { urlApi: 'https://api.example.com' } },
|
||||||
}));
|
}));
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
jest.unstable_mockModule('log4js', () => ({
|
||||||
default: {
|
default: {
|
||||||
@ -54,10 +54,13 @@ jest.unstable_mockModule('../marketplaces/ebay/accountPolicies.js', () => ({
|
|||||||
persistMarketplaceMapping: jest.fn(),
|
persistMarketplaceMapping: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.unstable_mockModule('../marketplaces/tiktokShop.js', () => ({}));
|
jest.unstable_mockModule('../marketplaces/tiktokShop.js', () => ({}));
|
||||||
|
const ensureWebhookSubscriptions = jest.fn();
|
||||||
|
|
||||||
jest.unstable_mockModule('../marketplaces/ebay/index.js', () => ({
|
jest.unstable_mockModule('../marketplaces/ebay/index.js', () => ({
|
||||||
updateItem,
|
updateItem,
|
||||||
createItem: jest.fn(),
|
createItem: jest.fn(),
|
||||||
ensureAuthenticatedMarketplace: jest.fn(async (marketplace) => marketplace),
|
ensureAuthenticatedMarketplace: jest.fn(async (marketplace) => marketplace),
|
||||||
|
ensureWebhookSubscriptions,
|
||||||
publishOfferForSku: jest.fn(),
|
publishOfferForSku: jest.fn(),
|
||||||
syncListingImages: jest.fn(),
|
syncListingImages: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@ -70,7 +73,7 @@ jest.unstable_mockModule('../marketplaceSync.js', () => ({
|
|||||||
pushShipmentFulfillment: jest.fn(),
|
pushShipmentFulfillment: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { updateListing } = await import('../marketplaceworker.js');
|
const { updateListing, syncWebhooks } = await import('../marketplaceworker.js');
|
||||||
|
|
||||||
function leanDoc(value) {
|
function leanDoc(value) {
|
||||||
return {
|
return {
|
||||||
@ -133,3 +136,62 @@ describe('updateListing fingerprint skip', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('syncWebhooks', () => {
|
||||||
|
const user = { _id: 'user-1' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
editObject.mockImplementation(async ({ updateData }) => ({
|
||||||
|
_id: 'mp-1',
|
||||||
|
name: 'eBay UK',
|
||||||
|
provider: 'ebay',
|
||||||
|
...updateData,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists webhookUrl and verificationToken then subscribes', async () => {
|
||||||
|
ensureWebhookSubscriptions.mockResolvedValue({ destinationId: 'dest-1', created: ['ORDER'] });
|
||||||
|
|
||||||
|
const result = await syncWebhooks(
|
||||||
|
{ _id: 'mp-1', name: 'eBay UK', provider: 'ebay', config: {} },
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(editObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
updateData: expect.objectContaining({
|
||||||
|
config: expect.objectContaining({
|
||||||
|
webhookUrl: 'https://api.example.com/marketplaces/mp-1/hook',
|
||||||
|
verificationToken: expect.stringMatching(/^[a-f0-9]{64}$/),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(ensureWebhookSubscriptions).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
destinationId: 'dest-1',
|
||||||
|
created: ['ORDER'],
|
||||||
|
webhookUrl: 'https://api.example.com/marketplaces/mp-1/hook',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not persist when webhook config is already present', async () => {
|
||||||
|
ensureWebhookSubscriptions.mockResolvedValue({ destinationId: 'dest-1', created: [] });
|
||||||
|
const marketplace = {
|
||||||
|
_id: 'mp-1',
|
||||||
|
name: 'eBay UK',
|
||||||
|
provider: 'ebay',
|
||||||
|
config: {
|
||||||
|
webhookUrl: 'https://custom.example/hook',
|
||||||
|
verificationToken: 'existing-token-32-characters-long!',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await syncWebhooks(marketplace, user);
|
||||||
|
|
||||||
|
expect(editObject).not.toHaveBeenCalled();
|
||||||
|
expect(ensureWebhookSubscriptions).toHaveBeenCalledWith(marketplace);
|
||||||
|
expect(result.webhookUrl).toBe('https://custom.example/hook');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -381,6 +381,17 @@ export function ensureWebhookSubscriptions(marketplace, user, { wait = false } =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function syncWebhooks(marketplace, user, { wait = true } = {}) {
|
||||||
|
return sendJob(
|
||||||
|
'syncWebhooks',
|
||||||
|
{
|
||||||
|
...marketplacePayload(marketplace),
|
||||||
|
...userPayload(user),
|
||||||
|
},
|
||||||
|
{ wait }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function pushMarketplaceShipmentFulfillment(marketplace, user, shipment) {
|
export function pushMarketplaceShipmentFulfillment(marketplace, user, shipment) {
|
||||||
return sendJob('pushShipmentFulfillment', {
|
return sendJob('pushShipmentFulfillment', {
|
||||||
...marketplacePayload(marketplace),
|
...marketplacePayload(marketplace),
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it, jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../config.js', () => ({
|
||||||
|
default: { app: { urlApi: 'https://api.example.com/' }, server: { logLevel: 'error' } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { buildMarketplaceWebhookUrl, webhookConfigUpdates, generateWebhookVerificationToken } =
|
||||||
|
await import('../webhookConfig.js');
|
||||||
|
|
||||||
|
describe('webhookConfig', () => {
|
||||||
|
it('builds the public hook URL from urlApi and marketplace id', () => {
|
||||||
|
expect(buildMarketplaceWebhookUrl({ _id: 'mkt-1' })).toBe(
|
||||||
|
'https://api.example.com/marketplaces/mkt-1/hook'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty URL when the marketplace id is missing', () => {
|
||||||
|
expect(buildMarketplaceWebhookUrl({})).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates a 64-character verification token', () => {
|
||||||
|
expect(generateWebhookVerificationToken()).toMatch(/^[a-f0-9]{64}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fills missing webhookUrl and verificationToken', () => {
|
||||||
|
const updates = webhookConfigUpdates({ _id: 'mkt-1', config: {} });
|
||||||
|
expect(updates.webhookUrl).toBe('https://api.example.com/marketplaces/mkt-1/hook');
|
||||||
|
expect(updates.verificationToken).toMatch(/^[a-f0-9]{64}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not overwrite existing webhook fields', () => {
|
||||||
|
const updates = webhookConfigUpdates({
|
||||||
|
_id: 'mkt-1',
|
||||||
|
config: {
|
||||||
|
webhookUrl: 'https://custom.example/hook',
|
||||||
|
verificationToken: 'existing-token-32-characters-long!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(updates).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,13 @@
|
|||||||
import { describe, expect, it, jest } from '@jest/globals';
|
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../../config.js', () => ({
|
||||||
|
default: {
|
||||||
|
app: { urlApi: 'https://api.example.com' },
|
||||||
|
server: { logLevel: 'error' },
|
||||||
|
smtp: { from: 'FarmControl <noreply@farmcontrol.app>' },
|
||||||
|
},
|
||||||
|
}));
|
||||||
jest.unstable_mockModule('../shared.js', () => ({
|
jest.unstable_mockModule('../shared.js', () => ({
|
||||||
makeRequest: jest.fn(),
|
makeRequest: jest.fn(),
|
||||||
getApiBaseUrl: () => 'https://api.sandbox.ebay.com',
|
getApiBaseUrl: () => 'https://api.sandbox.ebay.com',
|
||||||
@ -11,9 +18,12 @@ jest.unstable_mockModule('../shared.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { makeRequest } = await import('../shared.js');
|
const { makeRequest } = await import('../shared.js');
|
||||||
const { buildWebhookChallengeResponse, verifyNotificationSignature } = await import(
|
const {
|
||||||
'../notifications.js'
|
buildWebhookChallengeResponse,
|
||||||
);
|
verifyNotificationSignature,
|
||||||
|
ensureWebhookSubscriptions,
|
||||||
|
resolveNotificationAlertEmail,
|
||||||
|
} = await import('../notifications.js');
|
||||||
|
|
||||||
describe('eBay notification challenge', () => {
|
describe('eBay notification challenge', () => {
|
||||||
it('hashes challengeCode + verificationToken + endpoint', () => {
|
it('hashes challengeCode + verificationToken + endpoint', () => {
|
||||||
@ -68,3 +78,155 @@ describe('eBay notification signature', () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('eBay webhook subscriptions', () => {
|
||||||
|
const marketplace = {
|
||||||
|
name: 'eBay',
|
||||||
|
config: {
|
||||||
|
verificationToken: 'verify-token-32-characters-long!!',
|
||||||
|
webhookUrl: 'https://example.com/marketplaces/1/hook',
|
||||||
|
accessToken: 'user-token',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
makeRequest.mockReset();
|
||||||
|
globalThis.fetch = jest.fn(async (url, options) => {
|
||||||
|
if (String(url).includes('/identity/v1/oauth2/token')) {
|
||||||
|
const body = String(options?.body || '');
|
||||||
|
expect(body).toContain('grant_type=client_credentials');
|
||||||
|
expect(decodeURIComponent(body)).toContain('https://api.ebay.com/oauth/api_scope');
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ access_token: 'app-token', expires_in: 7200 }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected fetch ${url}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when webhook URL and verification token are missing', async () => {
|
||||||
|
await expect(ensureWebhookSubscriptions({ name: 'eBay', config: {} })).rejects.toThrow(
|
||||||
|
/webhook URL and verification token/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the alert email from SMTP from', () => {
|
||||||
|
expect(resolveNotificationAlertEmail({})).toBe('noreply@farmcontrol.app');
|
||||||
|
expect(
|
||||||
|
resolveNotificationAlertEmail({ config: { notificationAlertEmail: 'ops@example.com' } })
|
||||||
|
).toBe('ops@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates notification config before creating a destination', async () => {
|
||||||
|
makeRequest.mockImplementation(async ({ method = 'GET', path }) => {
|
||||||
|
if (path.endsWith('/config') && method === 'GET') return null;
|
||||||
|
if (path.endsWith('/config') && method === 'PUT') return null;
|
||||||
|
if (path.endsWith('/destination') && method === 'GET') return { destinations: [] };
|
||||||
|
if (path.endsWith('/destination') && method === 'POST') return { destinationId: 'dest-1' };
|
||||||
|
if (path.endsWith('/topic')) {
|
||||||
|
return {
|
||||||
|
topics: [
|
||||||
|
{
|
||||||
|
topicId: 'ORDER',
|
||||||
|
status: 'ENABLED',
|
||||||
|
supportedPayloads: [{ schemaVersion: '1.0' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (path.endsWith('/subscription') && method === 'GET') return { subscriptions: [] };
|
||||||
|
if (path.endsWith('/subscription') && method === 'POST') return {};
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ensureWebhookSubscriptions(marketplace);
|
||||||
|
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'PUT',
|
||||||
|
path: '/commerce/notification/v1/config',
|
||||||
|
body: { alertEmail: 'noreply@farmcontrol.app' },
|
||||||
|
marketplaceHeaders: false,
|
||||||
|
marketplace: expect.objectContaining({
|
||||||
|
name: 'eBay',
|
||||||
|
config: expect.objectContaining({ accessToken: 'app-token' }),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(makeRequest).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/commerce/notification/v1/destination',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({ destinationId: 'dest-1', created: ['ORDER'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('continues destination setup when eBay /config PUT returns 500', async () => {
|
||||||
|
makeRequest.mockImplementation(async ({ method = 'GET', path }) => {
|
||||||
|
if (path.endsWith('/config') && method === 'GET') return null;
|
||||||
|
if (path.endsWith('/config') && method === 'PUT') {
|
||||||
|
throw new Error('eBay API error (500): There was a problem with an eBay internal system');
|
||||||
|
}
|
||||||
|
if (path.endsWith('/destination') && method === 'GET') return { destinations: [] };
|
||||||
|
if (path.endsWith('/destination') && method === 'POST') return { destinationId: 'dest-1' };
|
||||||
|
if (path.endsWith('/topic')) return { topics: [] };
|
||||||
|
if (path.endsWith('/subscription')) return { subscriptions: [] };
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ensureWebhookSubscriptions(marketplace);
|
||||||
|
expect(result).toMatchObject({ destinationId: 'dest-1', created: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('explains how to set /config when destination create is rejected', async () => {
|
||||||
|
makeRequest.mockImplementation(async ({ method = 'GET', path }) => {
|
||||||
|
if (path.endsWith('/config') && method === 'GET') return null;
|
||||||
|
if (path.endsWith('/config') && method === 'PUT') {
|
||||||
|
throw new Error('eBay API error (500): internal');
|
||||||
|
}
|
||||||
|
if (path.endsWith('/destination') && method === 'GET') return { destinations: [] };
|
||||||
|
if (path.endsWith('/destination') && method === 'POST') {
|
||||||
|
throw new Error('eBay API error (409): Please provide configurations required for notifications');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(ensureWebhookSubscriptions(marketplace)).rejects.toThrow(/Developer Portal/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not replace an existing alert email', async () => {
|
||||||
|
makeRequest.mockImplementation(async ({ method = 'GET', path }) => {
|
||||||
|
if (path.endsWith('/config')) return { alertEmail: 'existing@example.com' };
|
||||||
|
if (path.endsWith('/destination') && method === 'GET') {
|
||||||
|
return {
|
||||||
|
destinations: [
|
||||||
|
{
|
||||||
|
destinationId: 'dest-1',
|
||||||
|
deliveryConfig: { endpoint: marketplace.config.webhookUrl },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (path.endsWith('/topic')) return { topics: [] };
|
||||||
|
if (path.endsWith('/subscription')) return { subscriptions: [] };
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureWebhookSubscriptions(marketplace);
|
||||||
|
|
||||||
|
expect(makeRequest).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'PUT',
|
||||||
|
path: '/commerce/notification/v1/config',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import config from '../../../config.js';
|
||||||
import { getApiBaseUrl, getBasicAuthHeader, makeRequest, logger, formatDebugPayload, getMarketplaceDebugContext } from './shared.js';
|
import { getApiBaseUrl, getBasicAuthHeader, makeRequest, logger, formatDebugPayload, getMarketplaceDebugContext } from './shared.js';
|
||||||
|
import { buildMarketplaceWebhookUrl } from '../webhookConfig.js';
|
||||||
|
|
||||||
const NOTIFICATION_BASE = '/commerce/notification/v1';
|
const NOTIFICATION_BASE = '/commerce/notification/v1';
|
||||||
|
const DEFAULT_APP_SCOPE = 'https://api.ebay.com/oauth/api_scope';
|
||||||
const APPLICATION_SCOPE =
|
const APPLICATION_SCOPE =
|
||||||
'https://api.ebay.com/oauth/api_scope/commerce.notification.subscription';
|
'https://api.ebay.com/oauth/api_scope/commerce.notification.subscription';
|
||||||
|
const applicationTokenCache = new Map();
|
||||||
|
|
||||||
const HANDLED_TOPIC_MATCHERS = [
|
const HANDLED_TOPIC_MATCHERS = [
|
||||||
'MARKETPLACE_ACCOUNT_DELETION',
|
'MARKETPLACE_ACCOUNT_DELETION',
|
||||||
@ -22,7 +26,7 @@ function getVerificationToken(marketplace) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWebhookEndpoint(marketplace) {
|
function getWebhookEndpoint(marketplace) {
|
||||||
return marketplace.config?.webhookUrl || '';
|
return marketplace.config?.webhookUrl || buildMarketplaceWebhookUrl(marketplace) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWebhookChallengeResponse(marketplace, { challengeCode, endpoint } = {}) {
|
export function buildWebhookChallengeResponse(marketplace, { challengeCode, endpoint } = {}) {
|
||||||
@ -42,8 +46,36 @@ export function buildWebhookChallengeResponse(marketplace, { challengeCode, endp
|
|||||||
return { challengeResponse };
|
return { challengeResponse };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getApplicationToken(marketplace) {
|
function toPlainMarketplace(marketplace) {
|
||||||
logger.debug('eBay application token request', getMarketplaceDebugContext(marketplace));
|
if (!marketplace) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (typeof marketplace.toObject === 'function') {
|
||||||
|
return marketplace.toObject();
|
||||||
|
}
|
||||||
|
if (typeof marketplace.toJSON === 'function') {
|
||||||
|
return marketplace.toJSON();
|
||||||
|
}
|
||||||
|
return { ...marketplace };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applicationTokenCacheKey(marketplace, scope) {
|
||||||
|
const clientId = marketplace?.config?.clientId || '';
|
||||||
|
const sandbox = marketplace?.config?.sandbox ? 'sandbox' : 'prod';
|
||||||
|
return `${sandbox}:${clientId}:${scope}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getApplicationToken(marketplace, scope = APPLICATION_SCOPE) {
|
||||||
|
const cacheKey = applicationTokenCacheKey(marketplace, scope);
|
||||||
|
const cached = applicationTokenCache.get(cacheKey);
|
||||||
|
if (cached && cached.expiresAt > Date.now() + 30_000) {
|
||||||
|
return cached.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('eBay application token request', {
|
||||||
|
...getMarketplaceDebugContext(marketplace),
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const response = await fetch(`${getApiBaseUrl(marketplace)}/identity/v1/oauth2/token`, {
|
const response = await fetch(`${getApiBaseUrl(marketplace)}/identity/v1/oauth2/token`, {
|
||||||
@ -54,7 +86,7 @@ async function getApplicationToken(marketplace) {
|
|||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
grant_type: 'client_credentials',
|
grant_type: 'client_credentials',
|
||||||
scope: APPLICATION_SCOPE,
|
scope,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@ -64,6 +96,7 @@ async function getApplicationToken(marketplace) {
|
|||||||
status: response.status,
|
status: response.status,
|
||||||
durationMs,
|
durationMs,
|
||||||
...getMarketplaceDebugContext(marketplace),
|
...getMarketplaceDebugContext(marketplace),
|
||||||
|
scope,
|
||||||
response: formatDebugPayload(data),
|
response: formatDebugPayload(data),
|
||||||
});
|
});
|
||||||
throw new Error(data.error_description || data.error || 'Failed to mint eBay application token');
|
throw new Error(data.error_description || data.error || 'Failed to mint eBay application token');
|
||||||
@ -71,60 +104,43 @@ async function getApplicationToken(marketplace) {
|
|||||||
|
|
||||||
logger.debug(`eBay application token request succeeded (${durationMs}ms)`, {
|
logger.debug(`eBay application token request succeeded (${durationMs}ms)`, {
|
||||||
...getMarketplaceDebugContext(marketplace),
|
...getMarketplaceDebugContext(marketplace),
|
||||||
|
scope,
|
||||||
expiresIn: data.expires_in,
|
expiresIn: data.expires_in,
|
||||||
tokenType: data.token_type,
|
tokenType: data.token_type,
|
||||||
});
|
});
|
||||||
|
applicationTokenCache.set(cacheKey, {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
expiresAt: Date.now() + Number(data.expires_in || 7200) * 1000,
|
||||||
|
});
|
||||||
return data.access_token;
|
return data.access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function notificationRequest(marketplace, { method = 'GET', path, body, useApplicationToken = false }) {
|
async function notificationRequest(
|
||||||
if (!useApplicationToken) {
|
marketplace,
|
||||||
return makeRequest({ marketplace, method, path, body, acceptableStatuses: [404] });
|
{ method = 'GET', path, body, useApplicationToken = false, applicationScope } = {}
|
||||||
}
|
) {
|
||||||
|
const plain = toPlainMarketplace(marketplace);
|
||||||
|
const requestMarketplace = {
|
||||||
|
...plain,
|
||||||
|
name: marketplace?.name || plain.name,
|
||||||
|
config: { ...(plain.config || {}) },
|
||||||
|
};
|
||||||
|
|
||||||
const token = await getApplicationToken(marketplace);
|
if (useApplicationToken) {
|
||||||
const url = `${getApiBaseUrl(marketplace)}${path}`;
|
requestMarketplace.config.accessToken = await getApplicationToken(
|
||||||
const startedAt = Date.now();
|
requestMarketplace,
|
||||||
logger.debug(`eBay notification API ${method} ${path}`, {
|
applicationScope || APPLICATION_SCOPE
|
||||||
...getMarketplaceDebugContext(marketplace),
|
|
||||||
useApplicationToken: true,
|
|
||||||
body: body ? formatDebugPayload(body) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
Accept: 'application/json',
|
|
||||||
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
||||||
},
|
|
||||||
...(body && method !== 'GET' ? { body: JSON.stringify(body) } : {}),
|
|
||||||
});
|
|
||||||
const durationMs = Date.now() - startedAt;
|
|
||||||
if (response.status === 204 || response.status === 404) {
|
|
||||||
logger.debug(
|
|
||||||
`eBay notification API ${method} ${path} -> ${response.status} (${durationMs}ms)`,
|
|
||||||
getMarketplaceDebugContext(marketplace)
|
|
||||||
);
|
);
|
||||||
return response.status === 404 ? null : null;
|
|
||||||
}
|
|
||||||
const data = await response.json().catch(() => null);
|
|
||||||
if (!response.ok) {
|
|
||||||
const message = data?.errors?.[0]?.message || data?.error_description || response.statusText;
|
|
||||||
logger.error(`eBay notification API error (${response.status}): ${message}`, {
|
|
||||||
path,
|
|
||||||
durationMs,
|
|
||||||
...getMarketplaceDebugContext(marketplace),
|
|
||||||
response: data ? formatDebugPayload(data) : undefined,
|
|
||||||
});
|
|
||||||
throw new Error(`eBay notification API error (${response.status}): ${message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`eBay notification API ${method} ${path} -> ${response.status} (${durationMs}ms)`, {
|
return makeRequest({
|
||||||
...getMarketplaceDebugContext(marketplace),
|
marketplace: requestMarketplace,
|
||||||
response: data ? formatDebugPayload(data) : undefined,
|
method,
|
||||||
|
path,
|
||||||
|
body,
|
||||||
|
acceptableStatuses: [404],
|
||||||
|
marketplaceHeaders: false,
|
||||||
});
|
});
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function topicIsHandled(topicId = '') {
|
function topicIsHandled(topicId = '') {
|
||||||
@ -132,36 +148,127 @@ function topicIsHandled(topicId = '') {
|
|||||||
return HANDLED_TOPIC_MATCHERS.some((matcher) => id.includes(matcher));
|
return HANDLED_TOPIC_MATCHERS.some((matcher) => id.includes(matcher));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseEmailAddress(value) {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const bracket = text.match(/<([^>]+)>/);
|
||||||
|
const email = (bracket?.[1] || text).trim();
|
||||||
|
return email.includes('@') ? email : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveNotificationAlertEmail(marketplace) {
|
||||||
|
return (
|
||||||
|
parseEmailAddress(marketplace?.config?.notificationAlertEmail) ||
|
||||||
|
parseEmailAddress(config.smtp?.from) ||
|
||||||
|
'noreply@farmcontrol.app'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_PORTAL_HINT =
|
||||||
|
'Set the alert email in the eBay Developer Portal (Application Keys → Alerts & Notifications), then retry Sync Webhooks.';
|
||||||
|
|
||||||
|
function isMissingNotificationConfigError(err) {
|
||||||
|
return /195003|configurations required for notifications/i.test(err?.message || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function putNotificationConfig(marketplace, alertEmail) {
|
||||||
|
try {
|
||||||
|
await notificationRequest(marketplace, {
|
||||||
|
method: 'PUT',
|
||||||
|
path: `${NOTIFICATION_BASE}/config`,
|
||||||
|
useApplicationToken: true,
|
||||||
|
applicationScope: DEFAULT_APP_SCOPE,
|
||||||
|
body: { alertEmail },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`eBay notification config PUT with application token failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await notificationRequest(marketplace, {
|
||||||
|
method: 'PUT',
|
||||||
|
path: `${NOTIFICATION_BASE}/config`,
|
||||||
|
body: { alertEmail },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`eBay notification config PUT with user token failed: ${err.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureNotificationConfig(marketplace) {
|
||||||
|
const existing = await notificationRequest(marketplace, {
|
||||||
|
path: `${NOTIFICATION_BASE}/config`,
|
||||||
|
useApplicationToken: true,
|
||||||
|
applicationScope: DEFAULT_APP_SCOPE,
|
||||||
|
});
|
||||||
|
if (existing?.alertEmail) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertEmail = resolveNotificationAlertEmail(marketplace);
|
||||||
|
const updated = await putNotificationConfig(marketplace, alertEmail);
|
||||||
|
if (updated) {
|
||||||
|
logger.info(`eBay notification alert email set for "${marketplace.name}"`);
|
||||||
|
return { alertEmail };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`Continuing webhook sync for "${marketplace.name}" without a stored eBay /config. ${CONFIG_PORTAL_HINT}`
|
||||||
|
);
|
||||||
|
return { alertEmail, skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDestinationByEndpoint(destinations, endpoint) {
|
||||||
|
return (destinations?.destinations || []).find((item) => item.deliveryConfig?.endpoint === endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureWebhookSubscriptions(marketplace) {
|
export async function ensureWebhookSubscriptions(marketplace) {
|
||||||
const endpoint = getWebhookEndpoint(marketplace);
|
const endpoint = getWebhookEndpoint(marketplace);
|
||||||
const verificationToken = getVerificationToken(marketplace);
|
const verificationToken = getVerificationToken(marketplace);
|
||||||
if (!endpoint || !verificationToken) {
|
if (!endpoint || !verificationToken) {
|
||||||
logger.warn(
|
throw new Error(
|
||||||
`Skipping eBay webhook subscription for "${marketplace.name}" (missing webhookUrl or verificationToken)`
|
'Marketplace webhook URL and verification token are required to subscribe to eBay notifications'
|
||||||
);
|
);
|
||||||
return { skipped: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ensureNotificationConfig(marketplace);
|
||||||
|
|
||||||
const destinations = await notificationRequest(marketplace, {
|
const destinations = await notificationRequest(marketplace, {
|
||||||
path: `${NOTIFICATION_BASE}/destination`,
|
path: `${NOTIFICATION_BASE}/destination`,
|
||||||
});
|
});
|
||||||
let destination = (destinations?.destinations || []).find(
|
let destination = findDestinationByEndpoint(destinations, endpoint);
|
||||||
(item) => item.deliveryConfig?.endpoint === endpoint
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!destination) {
|
if (!destination) {
|
||||||
destination = await notificationRequest(marketplace, {
|
try {
|
||||||
method: 'POST',
|
destination = await notificationRequest(marketplace, {
|
||||||
path: `${NOTIFICATION_BASE}/destination`,
|
method: 'POST',
|
||||||
body: {
|
path: `${NOTIFICATION_BASE}/destination`,
|
||||||
name: `FarmControl ${marketplace.name}`.slice(0, 200),
|
body: {
|
||||||
status: 'ENABLED',
|
name: `FarmControl ${marketplace.name || ''}`.replace(/\s+/g, ' ').trim().slice(0, 64),
|
||||||
deliveryConfig: {
|
status: 'ENABLED',
|
||||||
endpoint,
|
deliveryConfig: {
|
||||||
verificationToken,
|
endpoint,
|
||||||
|
verificationToken,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
} catch (err) {
|
||||||
|
if (isMissingNotificationConfigError(err)) {
|
||||||
|
throw new Error(`${err.message}. ${CONFIG_PORTAL_HINT}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!destination?.destinationId && !destination?.id) {
|
||||||
|
const refreshed = await notificationRequest(marketplace, {
|
||||||
|
path: `${NOTIFICATION_BASE}/destination`,
|
||||||
|
});
|
||||||
|
destination = findDestinationByEndpoint(refreshed, endpoint) || destination;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationId = destination?.destinationId || destination?.id;
|
const destinationId = destination?.destinationId || destination?.id;
|
||||||
|
|||||||
@ -153,6 +153,7 @@ export async function makeRequest({
|
|||||||
rawBody = false,
|
rawBody = false,
|
||||||
contentType,
|
contentType,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
marketplaceHeaders = true,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { accessToken } = marketplace.config || {};
|
const { accessToken } = marketplace.config || {};
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
@ -171,11 +172,13 @@ export async function makeRequest({
|
|||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Accept-Language': getAcceptLanguage(marketplace),
|
|
||||||
...extraHeaders,
|
...extraHeaders,
|
||||||
};
|
};
|
||||||
|
|
||||||
headers['X-EBAY-C-MARKETPLACE-ID'] = getEbayMarketplaceId(marketplace);
|
if (marketplaceHeaders) {
|
||||||
|
headers['Accept-Language'] = getAcceptLanguage(marketplace);
|
||||||
|
headers['X-EBAY-C-MARKETPLACE-ID'] = getEbayMarketplaceId(marketplace);
|
||||||
|
}
|
||||||
|
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
method,
|
method,
|
||||||
@ -191,7 +194,9 @@ export async function makeRequest({
|
|||||||
fetchOptions.body = body;
|
fetchOptions.body = body;
|
||||||
} else {
|
} else {
|
||||||
fetchOptions.headers['Content-Type'] = 'application/json';
|
fetchOptions.headers['Content-Type'] = 'application/json';
|
||||||
fetchOptions.headers['Content-Language'] = getAcceptLanguage(marketplace);
|
if (marketplaceHeaders) {
|
||||||
|
fetchOptions.headers['Content-Language'] = getAcceptLanguage(marketplace);
|
||||||
|
}
|
||||||
fetchOptions.body = JSON.stringify(body);
|
fetchOptions.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/integrations/marketplaces/webhookConfig.js
Normal file
32
src/integrations/marketplaces/webhookConfig.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
import config from '../../config.js';
|
||||||
|
|
||||||
|
export function buildMarketplaceWebhookUrl(marketplace) {
|
||||||
|
const base = String(config.app?.urlApi || '').replace(/\/$/, '');
|
||||||
|
if (!base || !marketplace?._id) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `${base}/marketplaces/${marketplace._id}/hook`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateWebhookVerificationToken() {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function webhookConfigUpdates(marketplace) {
|
||||||
|
const existing = marketplace?.config || {};
|
||||||
|
const updates = {};
|
||||||
|
|
||||||
|
if (!existing.webhookUrl) {
|
||||||
|
const webhookUrl = buildMarketplaceWebhookUrl(marketplace);
|
||||||
|
if (webhookUrl) {
|
||||||
|
updates.webhookUrl = webhookUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.verificationToken) {
|
||||||
|
updates.verificationToken = generateWebhookVerificationToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
@ -30,6 +30,7 @@ import {
|
|||||||
listingSyncHash,
|
listingSyncHash,
|
||||||
listingSyncUnchanged,
|
listingSyncUnchanged,
|
||||||
} from './marketplaces/syncFingerprint.js';
|
} from './marketplaces/syncFingerprint.js';
|
||||||
|
import { webhookConfigUpdates } from './marketplaces/webhookConfig.js';
|
||||||
|
|
||||||
const logger = log4js.getLogger('Marketplace Worker');
|
const logger = log4js.getLogger('Marketplace Worker');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -401,13 +402,27 @@ export async function syncTaxRateOutbound(marketplace, user, policyId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureWebhookSubscriptions(marketplace, user) {
|
export async function syncWebhooks(marketplace, user) {
|
||||||
const authenticatedMarketplace = await ensureMarketplaceAuth(marketplace, user);
|
const configUpdates = webhookConfigUpdates(marketplace);
|
||||||
const provider = getProvider(authenticatedMarketplace);
|
let readyMarketplace = marketplace;
|
||||||
if (typeof provider.ensureWebhookSubscriptions !== 'function') {
|
if (Object.keys(configUpdates).length > 0) {
|
||||||
return { skipped: true };
|
readyMarketplace = await persistMarketplaceUpdate(marketplace, configUpdates, {}, user);
|
||||||
}
|
}
|
||||||
return provider.ensureWebhookSubscriptions(authenticatedMarketplace);
|
|
||||||
|
const authenticatedMarketplace = await ensureMarketplaceAuth(readyMarketplace, user);
|
||||||
|
const provider = getProvider(authenticatedMarketplace);
|
||||||
|
const webhookUrl = authenticatedMarketplace.config?.webhookUrl;
|
||||||
|
if (typeof provider.ensureWebhookSubscriptions !== 'function') {
|
||||||
|
return { skipped: true, webhookUrl };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
webhookUrl,
|
||||||
|
...(await provider.ensureWebhookSubscriptions(authenticatedMarketplace)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureWebhookSubscriptions(marketplace, user) {
|
||||||
|
return syncWebhooks(marketplace, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function debugMarketplaceGet(marketplace, user, path, params = {}) {
|
export async function debugMarketplaceGet(marketplace, user, path, params = {}) {
|
||||||
@ -1173,9 +1188,10 @@ export async function runJob(action, payload = {}) {
|
|||||||
const marketplace = await loadMarketplace(payload.marketplaceId);
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
return syncTaxRateOutbound(marketplace, user, payload.policyId);
|
return syncTaxRateOutbound(marketplace, user, payload.policyId);
|
||||||
}
|
}
|
||||||
case 'ensureWebhookSubscriptions': {
|
case 'ensureWebhookSubscriptions':
|
||||||
|
case 'syncWebhooks': {
|
||||||
const marketplace = await loadMarketplace(payload.marketplaceId);
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
return ensureWebhookSubscriptions(marketplace, user);
|
return syncWebhooks(marketplace, user);
|
||||||
}
|
}
|
||||||
case 'pushShipmentFulfillment': {
|
case 'pushShipmentFulfillment': {
|
||||||
const marketplace = await loadMarketplace(payload.marketplaceId);
|
const marketplace = await loadMarketplace(payload.marketplaceId);
|
||||||
|
|||||||
@ -54,6 +54,7 @@ import {
|
|||||||
syncMarketplacePaymentPoliciesRouteHandler,
|
syncMarketplacePaymentPoliciesRouteHandler,
|
||||||
syncMarketplaceReturnPoliciesRouteHandler,
|
syncMarketplaceReturnPoliciesRouteHandler,
|
||||||
syncMarketplaceTaxRatesRouteHandler,
|
syncMarketplaceTaxRatesRouteHandler,
|
||||||
|
syncMarketplaceWebhooksRouteHandler,
|
||||||
marketplaceWebhookRouteHandler,
|
marketplaceWebhookRouteHandler,
|
||||||
marketplaceWebhookChallengeRouteHandler,
|
marketplaceWebhookChallengeRouteHandler,
|
||||||
subscribeMarketplaceWebhooksRouteHandler,
|
subscribeMarketplaceWebhooksRouteHandler,
|
||||||
@ -222,6 +223,15 @@ router.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:id/sync/webhooks',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('marketplace', 'sync'),
|
||||||
|
async (req, res) => {
|
||||||
|
syncMarketplaceWebhooksRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/:id/hook', async (req, res) => {
|
router.get('/:id/hook', async (req, res) => {
|
||||||
marketplaceWebhookChallengeRouteHandler(req, res);
|
marketplaceWebhookChallengeRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import config from '../../config.js';
|
import config from '../../config.js';
|
||||||
import { marketplaceModel } from '../../database/schemas/sales/marketplace.schema.js';
|
import { marketplaceModel } from '../../database/schemas/sales/marketplace.schema.js';
|
||||||
|
import { buildMarketplaceWebhookUrl } from '../../integrations/marketplaces/webhookConfig.js';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import {
|
import {
|
||||||
@ -450,6 +451,38 @@ export const syncMarketplaceOrdersRouteHandler = async (req, res) => {
|
|||||||
res.send({ success: true, message: 'Order sync started' });
|
res.send({ success: true, message: 'Order sync started' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const syncMarketplaceWebhooksRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
const marketplace = await getObject({ model: marketplaceModel, id });
|
||||||
|
if (marketplace?.error) {
|
||||||
|
logger.warn('Marketplace not found for webhook sync.');
|
||||||
|
return res.status(marketplace.code).send(marketplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!marketplace.active) {
|
||||||
|
return res.status(400).send({ error: 'Marketplace is not active.', code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!marketplaceIntegration.hasIntegration(marketplace.provider)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
error: `No integration available for provider: ${marketplace.provider}`,
|
||||||
|
code: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await marketplaceIntegration.syncWebhooks(marketplace, req.user, {
|
||||||
|
wait: true,
|
||||||
|
});
|
||||||
|
logger.info(`Webhook sync completed for marketplace ${marketplace.name}`);
|
||||||
|
res.send({ success: true, message: 'Webhook sync completed', ...result });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Error syncing webhooks for marketplace ${marketplace.name}:`, err.message);
|
||||||
|
res.status(400).send({ error: err.message, code: 400 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const marketplaceWebhookRouteHandler = async (req, res) => {
|
export const marketplaceWebhookRouteHandler = async (req, res) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
|
|
||||||
@ -496,6 +529,7 @@ export const marketplaceWebhookChallengeRouteHandler = async (req, res) => {
|
|||||||
const challengeCode = req.query.challenge_code || req.query.challengeCode;
|
const challengeCode = req.query.challenge_code || req.query.challengeCode;
|
||||||
const endpoint =
|
const endpoint =
|
||||||
marketplace.config?.webhookUrl ||
|
marketplace.config?.webhookUrl ||
|
||||||
|
buildMarketplaceWebhookUrl(marketplace) ||
|
||||||
`${req.protocol}://${req.get('host')}/marketplaces/${id}/hook`;
|
`${req.protocol}://${req.get('host')}/marketplaces/${id}/hook`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -522,7 +556,7 @@ export const subscribeMarketplaceWebhooksRouteHandler = async (req, res) => {
|
|||||||
return res.status(marketplace.code).send(marketplace);
|
return res.status(marketplace.code).send(marketplace);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await marketplaceIntegration.ensureWebhookSubscriptions(marketplace, req.user, {
|
const result = await marketplaceIntegration.syncWebhooks(marketplace, req.user, {
|
||||||
wait: true,
|
wait: true,
|
||||||
});
|
});
|
||||||
res.send({ success: true, ...result });
|
res.send({ success: true, ...result });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user