From e69c3aba01fc5498c50d7476b854ec6170157ff0 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Mon, 3 Aug 2020 14:05:56 +0200 Subject: [PATCH] creates webhook and cron job to update inventory --- .../loaders/inventory.js | 17 ++++ .../loaders/webhooks.js | 37 +++++++++ .../src/api/index.js | 15 ++++ .../src/loaders/inventory.js | 8 ++ .../src/loaders/webhooks.js | 6 ++ .../src/services/brightpearl.js | 66 +++++++++++++++- .../src/utils/brightpearl.js | 32 ++++++++ .../utils/brightpearl.js | 78 ++++++++++++++----- packages/medusa/src/loaders/plugins.js | 25 +++++- packages/medusa/src/services/event-bus.js | 55 +++++++++++++ 10 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 packages/medusa-plugin-brightpearl/loaders/inventory.js create mode 100644 packages/medusa-plugin-brightpearl/loaders/webhooks.js create mode 100644 packages/medusa-plugin-brightpearl/src/api/index.js create mode 100644 packages/medusa-plugin-brightpearl/src/loaders/inventory.js create mode 100644 packages/medusa-plugin-brightpearl/src/loaders/webhooks.js diff --git a/packages/medusa-plugin-brightpearl/loaders/inventory.js b/packages/medusa-plugin-brightpearl/loaders/inventory.js new file mode 100644 index 0000000000..c534686a7d --- /dev/null +++ b/packages/medusa-plugin-brightpearl/loaders/inventory.js @@ -0,0 +1,17 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var inventorySync = function inventorySync(container) { + var brightpearlService = container.resolve("brightpearlService"); + var eventBus = container.resolve("eventBusService"); + var pattern = "43 4,10,14,20 * * *"; // nice for tests "*/10 * * * * *" + + eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory()); +}; + +var _default = inventorySync; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-plugin-brightpearl/loaders/webhooks.js b/packages/medusa-plugin-brightpearl/loaders/webhooks.js new file mode 100644 index 0000000000..2373540143 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/loaders/webhooks.js @@ -0,0 +1,37 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } + +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } + +var webhookLoader = /*#__PURE__*/function () { + var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(container) { + var brightpearlService; + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + brightpearlService = container.resolve("brightpearlService"); + _context.next = 3; + return brightpearlService.verifyWebhooks(); + + case 3: + case "end": + return _context.stop(); + } + } + }, _callee); + })); + + return function webhookLoader(_x) { + return _ref.apply(this, arguments); + }; +}(); + +var _default = webhookLoader; +exports["default"] = _default; \ No newline at end of file diff --git a/packages/medusa-plugin-brightpearl/src/api/index.js b/packages/medusa-plugin-brightpearl/src/api/index.js new file mode 100644 index 0000000000..c220e73099 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/api/index.js @@ -0,0 +1,15 @@ +import { Router } from "express" +import bodyParser from "body-parser" + +export default (container) => { + const app = Router() + + app.post("/brightpearl/inventory-update", bodyParser.json(), async (req, res) => { + const { id } = req.body + const brightpearlService = req.scope.resolve("brightpearlService") + await brightpearlService.updateInventory(id) + res.sendStatus(200) + }) + + return app +} diff --git a/packages/medusa-plugin-brightpearl/src/loaders/inventory.js b/packages/medusa-plugin-brightpearl/src/loaders/inventory.js new file mode 100644 index 0000000000..00f3751e02 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/loaders/inventory.js @@ -0,0 +1,8 @@ +const inventorySync = container => { + const brightpearlService = container.resolve("brightpearlService") + const eventBus = container.resolve("eventBusService") + const pattern = "43 4,10,14,20 * * *" // nice for tests "*/10 * * * * *" + eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory()) +} + +export default inventorySync diff --git a/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js b/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js new file mode 100644 index 0000000000..ea78833176 --- /dev/null +++ b/packages/medusa-plugin-brightpearl/src/loaders/webhooks.js @@ -0,0 +1,6 @@ +const webhookLoader = async (container) => { + const brightpearlService = container.resolve("brightpearlService") + await brightpearlService.verifyWebhooks() +} + +export default webhookLoader diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index ade9a2b93a..41ef40b996 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -2,10 +2,11 @@ import { BaseService } from "medusa-interfaces" import Brightpearl from "../utils/brightpearl" class BrightpearlService extends BaseService { - constructor({ totalsService, regionService, orderService, discountService }, options) { + constructor({ totalsService, productVariantService, regionService, orderService, discountService }, options) { super() this.options = options + this.productVariantService_ = productVariantService this.regionService_ = regionService this.orderService_ = orderService this.totalsService_ = totalsService @@ -19,6 +20,69 @@ class BrightpearlService extends BaseService { }) } + async verifyWebhooks() { + const hooks = [ + { + subscribeTo: "product.modified.on-hand-modified", + httpMethod: "POST", + uriTemplate: `${this.options.backend_url}/brightpearl/inventory-update`, + bodyTemplate: '{"account": "${account-code}", "lifecycleEvent": "${lifecycle-event}", "resourceType": "${resource-type}", "id": "${resource-id}" }', + contentType: "application/json", + idSetAccepted: false, + } + ] + + const installedHooks = await this.brightpearl_.webhooks.list().catch(() => []) + for (const hook of hooks) { + const isInstalled = installedHooks.find(i => + i.subscribeTo === hook.subscribeTo && + i.httpMethod === hook.httpMethod && + i.uriTemplate === hook.uriTemplate && + i.bodyTemplate === hook.bodyTemplate && + i.contentType === hook.contentType && + i.idSetAccepted === hook.idSetAccepted + ) + + if (!isInstalled) { + await this.brightpearl_.webhooks.create(hook) + } + } + } + + async syncInventory() { + const variants = await this.productVariantService_.list() + return Promise.all(variants.map(async v => { + const brightpearlProduct = await this.retrieveProductBySKU(v.sku) + if (!brightpearlProduct) { + return + } + + const { productId } = brightpearlProduct + const availability = await this.brightpearl_.products.retrieveAvailability(productId) + const onHand = availability[productId].total.onHand + + return this.productVariantService_.update(v._id, { + inventory_quantity: onHand + }) + })) + } + + async updateInventory(productId) { + const brightpearlProduct = await this.brightpearl_.products.retrieve(productId) + const availability = await this.brightpearl_.products.retrieveAvailability(productId) + + const onHand = availability[productId].total.onHand + + const sku = brightpearlProduct.identity.sku + const [ variant ] = await this.productVariantService_.list({ sku }) + + if (variant && variant.manage_inventory) { + await this.productVariantService_.update(variant._id, { + inventory_quantity: onHand + }) + } + } + async createGoodsOutNote(fromOrder, shipment) { const id = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id diff --git a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js index effdcbe728..2a0c87ae61 100644 --- a/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/utils/brightpearl.js @@ -10,6 +10,7 @@ class BrightpearlClient { }, }) + this.webhooks = this.buildWebhookEndpoints() this.payments = this.buildPaymentEndpoints() this.warehouses = this.buildWarehouseEndpoints() this.orders = this.buildOrderEndpoints() @@ -31,6 +32,25 @@ class BrightpearlClient { }) } + buildWebhookEndpoints = () => { + return { + list: () => { + return this.client_.request({ + url: `/integration-service/webhook`, + method: "GET", + }) + .then(({ data }) => data.response) + }, + create: (data) => { + return this.client_.request({ + url: `/integration-service/webhook`, + method: "POST", + data + }) + } + } + } + buildPaymentEndpoints = () => { return { create: (payment) => { @@ -128,6 +148,18 @@ class BrightpearlClient { buildProductEndpoints = () => { return { + retrieveAvailability: productId => { + return this.client_.request({ + url: `/warehouse-service/product-availability/${productId}`, + }) + .then(({ data }) => data.response && data.response) + }, + retrieve: (productId) => { + return this.client_.request({ + url: `/product-service/product/${productId}`, + }) + .then(({ data }) => data.response && data.response[0]) + }, retrieveBySKU: (sku) => { return this.client_.request({ url: `/product-service/product-search?SKU=${sku}`, diff --git a/packages/medusa-plugin-brightpearl/utils/brightpearl.js b/packages/medusa-plugin-brightpearl/utils/brightpearl.js index 279209cf18..8e988a96c0 100644 --- a/packages/medusa-plugin-brightpearl/utils/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/utils/brightpearl.js @@ -25,6 +25,27 @@ var BrightpearlClient = /*#__PURE__*/function () { _classCallCheck(this, BrightpearlClient); + _defineProperty(this, "buildWebhookEndpoints", function () { + return { + list: function list() { + return _this.client_.request({ + url: "/integration-service/webhook", + method: "GET" + }).then(function (_ref) { + var data = _ref.data; + return data.response; + }); + }, + create: function create(data) { + return _this.client_.request({ + url: "/integration-service/webhook", + method: "POST", + data: data + }); + } + }; + }); + _defineProperty(this, "buildPaymentEndpoints", function () { return { create: function create(payment) { @@ -32,8 +53,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/accounting-service/customer-payment", method: "POST", data: payment - }).then(function (_ref) { - var data = _ref.data; + }).then(function (_ref2) { + var data = _ref2.data; return data.response; }); } @@ -46,8 +67,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/warehouse-service/order/".concat(orderId, "/reservation"), method: "GET" - }).then(function (_ref2) { - var data = _ref2.data; + }).then(function (_ref3) { + var data = _ref3.data; return data.response; }); }, @@ -56,8 +77,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/warehouse-service/order/".concat(orderId, "/goods-note/goods-out"), method: "POST", data: data - }).then(function (_ref3) { - var data = _ref3.data; + }).then(function (_ref4) { + var data = _ref4.data; return data.response; }); }, @@ -90,8 +111,8 @@ var BrightpearlClient = /*#__PURE__*/function () { data: { products: data } - }).then(function (_ref4) { - var data = _ref4.data; + }).then(function (_ref5) { + var data = _ref5.data; return data.response; }); } @@ -104,8 +125,8 @@ var BrightpearlClient = /*#__PURE__*/function () { return _this.client_.request({ url: "/order-service/sales-order/".concat(orderId), method: "GET" - }).then(function (_ref5) { - var data = _ref5.data; + }).then(function (_ref6) { + var data = _ref6.data; return data.response.length && data.response[0]; })["catch"](function (err) { return console.log(err); @@ -116,8 +137,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/order-service/sales-order", method: "POST", data: order - }).then(function (_ref6) { - var data = _ref6.data; + }).then(function (_ref7) { + var data = _ref7.data; return data.response; }); } @@ -131,8 +152,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/postal-address", method: "POST", data: address - }).then(function (_ref7) { - var data = _ref7.data; + }).then(function (_ref8) { + var data = _ref8.data; return data.response; }); } @@ -141,11 +162,27 @@ var BrightpearlClient = /*#__PURE__*/function () { _defineProperty(this, "buildProductEndpoints", function () { return { + retrieveAvailability: function retrieveAvailability(productId) { + return _this.client_.request({ + url: "/warehouse-service/product-availability/".concat(productId) + }).then(function (_ref9) { + var data = _ref9.data; + return data.response && data.response; + }); + }, + retrieve: function retrieve(productId) { + return _this.client_.request({ + url: "/product-service/product/".concat(productId) + }).then(function (_ref10) { + var data = _ref10.data; + return data.response && data.response[0]; + }); + }, retrieveBySKU: function retrieveBySKU(sku) { return _this.client_.request({ url: "/product-service/product-search?SKU=".concat(sku) - }).then(function (_ref8) { - var data = _ref8.data; + }).then(function (_ref11) { + var data = _ref11.data; return _this.buildSearchResults_(data.response); }); } @@ -157,8 +194,8 @@ var BrightpearlClient = /*#__PURE__*/function () { retrieveByEmail: function retrieveByEmail(email) { return _this.client_.request({ url: "/contact-service/contact-search?primaryEmail=".concat(email) - }).then(function (_ref9) { - var data = _ref9.data; + }).then(function (_ref12) { + var data = _ref12.data; return _this.buildSearchResults_(data.response); }); }, @@ -167,8 +204,8 @@ var BrightpearlClient = /*#__PURE__*/function () { url: "/contact-service/contact", method: "POST", data: customerData - }).then(function (_ref10) { - var data = _ref10.data; + }).then(function (_ref13) { + var data = _ref13.data; return data.response; }); } @@ -182,6 +219,7 @@ var BrightpearlClient = /*#__PURE__*/function () { 'brightpearl-account-token': options.token } }); + this.webhooks = this.buildWebhookEndpoints(); this.payments = this.buildPaymentEndpoints(); this.warehouses = this.buildWarehouseEndpoints(); this.orders = this.buildOrderEndpoints(); diff --git a/packages/medusa/src/loaders/plugins.js b/packages/medusa/src/loaders/plugins.js index cf6c9a6604..bcc2e92d4e 100644 --- a/packages/medusa/src/loaders/plugins.js +++ b/packages/medusa/src/loaders/plugins.js @@ -16,7 +16,7 @@ import { sync as existsSync } from "fs-exists-cached" /** * Registers all services in the services directory */ -export default ({ rootDirectory, container, app }) => { +export default async ({ rootDirectory, container, app }) => { const { configModule, configFilePath } = getConfigFile( rootDirectory, `medusa-config` @@ -55,6 +55,29 @@ export default ({ rootDirectory, container, app }) => { registerCoreRouters(pluginDetails, container) registerSubscribers(pluginDetails, container) }) + + await Promise.all( + resolved.map(async pluginDetails => runLoaders(pluginDetails, container)) + ) +} + +async function runLoaders(pluginDetails, container) { + const loaderFiles = glob.sync( + `${pluginDetails.resolve}/loaders/[!__]*.js`, + {} + ) + await Promise.all( + loaderFiles.map(async loader => { + try { + const module = require(loader).default + if (typeof module === "function") { + await module(container) + } + } catch (err) { + return Promise.resolve() + } + }) + ) } function registerMedusaApi(pluginDetails, container) { diff --git a/packages/medusa/src/services/event-bus.js b/packages/medusa/src/services/event-bus.js index 29f430746a..52fb3ede4d 100644 --- a/packages/medusa/src/services/event-bus.js +++ b/packages/medusa/src/services/event-bus.js @@ -13,11 +13,20 @@ class EventBusService { /** @private {object} */ this.observers_ = {} + /** @private {object} to handle cron jobs */ + this.cronHandlers_ = {} + + /** @private {BullQueue} used for cron jobs */ + this.cronQueue_ = new Bull(`cron-jobs:queue`, config.redisURI) + /** @private {BullQueue} */ this.queue_ = new Bull(`${this.constructor.name}:queue`, config.redisURI) // Register our worker to handle emit calls this.queue_.process(this.worker_) + + // Register cron worker + this.cronQueue_.process(this.cronWorker_) } /** @@ -38,6 +47,21 @@ class EventBusService { } } + /** + * + */ + registerCronHandler_(event, subscriber) { + if (typeof subscriber !== "function") { + throw new Error("Handler must be a function") + } + + if (this.observers_[event]) { + this.cronHandlers_[event].push(subscriber) + } else { + this.cronHandlers_[event] = [subscriber] + } + } + /** * Calls all subscribers when an event occurs. * @param {string} eventName - the name of the event to be process. @@ -77,6 +101,37 @@ class EventBusService { }) ) } + + cronWorker_ = job => { + const { eventName, data } = job.data + const observers = this.cronHandlers_[eventName] || [] + this.logger_.info(`Processing cron job: ${eventName}`) + + return Promise.all( + observers.map(subscriber => { + return subscriber(data).catch(err => { + this.logger_.warn( + `An error occured while processing ${eventName}: ${err}` + ) + return err + }) + }) + ) + } + + /** + * Registers a cron job. + */ + createCronJob(eventName, data, cron, handler) { + this.registerCronHandler(eventName, handler) + return this.cronQueue_.add( + { + eventName, + data, + }, + { repeat: { cron } } + ) + } } export default EventBusService