creates webhook and cron job to update inventory

This commit is contained in:
Sebastian Rindom
2020-08-03 14:05:56 +02:00
parent c18677d5f2
commit e69c3aba01
10 changed files with 317 additions and 22 deletions
@@ -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;
@@ -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;
@@ -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
}
@@ -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
@@ -0,0 +1,6 @@
const webhookLoader = async (container) => {
const brightpearlService = container.resolve("brightpearlService")
await brightpearlService.verifyWebhooks()
}
export default webhookLoader
@@ -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
@@ -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}`,
@@ -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();
+24 -1
View File
@@ -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) {
+55
View File
@@ -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