Brightpearl integration sales flow
This commit is contained in:
@@ -4,12 +4,27 @@ 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
|
||||
app.post("/brightpearl/goods-out", bodyParser.json(), async (req, res) => {
|
||||
const { id, lifecycle_event } = req.body
|
||||
const brightpearlService = req.scope.resolve("brightpearlService")
|
||||
await brightpearlService.updateInventory(id)
|
||||
|
||||
if (lifecycle_event === "created") {
|
||||
await brightpearlService.createFulfillmentFromGoodsOut(id)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -41,24 +41,52 @@ class BrightpearlService extends BaseService {
|
||||
}
|
||||
|
||||
const client = new Brightpearl({
|
||||
account: this.options.account,
|
||||
url: data.api_domain,
|
||||
auth_type: data.token_type,
|
||||
access_token: data.access_token,
|
||||
})
|
||||
|
||||
this.authData_ = data
|
||||
this.brightpearlClient_ = client
|
||||
return client
|
||||
}
|
||||
|
||||
async getAuthData() {
|
||||
if (this.authData_) {
|
||||
return this.authData_
|
||||
}
|
||||
|
||||
const { data } = await this.oauthService_.retrieveByName("brightpearl")
|
||||
if (!data || !data.access_token) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"You must authenticate the Brightpearl app in settings before continuing"
|
||||
)
|
||||
}
|
||||
|
||||
this.authData_ = data
|
||||
return data
|
||||
}
|
||||
|
||||
async verifyWebhooks() {
|
||||
const brightpearl = await this.getClient()
|
||||
const hooks = [
|
||||
{
|
||||
subscribeTo: "goods-out-note.created",
|
||||
httpMethod: "POST",
|
||||
uriTemplate: `${this.options.backend_url}/brightpearl/goods-out`,
|
||||
bodyTemplate:
|
||||
'{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }',
|
||||
contentType: "application/json",
|
||||
idSetAccepted: false,
|
||||
},
|
||||
{
|
||||
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}" }',
|
||||
'{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }',
|
||||
contentType: "application/json",
|
||||
idSetAccepted: false,
|
||||
},
|
||||
@@ -107,18 +135,22 @@ class BrightpearlService extends BaseService {
|
||||
|
||||
async updateInventory(productId) {
|
||||
const client = await this.getClient()
|
||||
const brightpearlProduct = await client.products.retrieve(productId)
|
||||
const availability = await client.products.retrieveAvailability(productId)
|
||||
const availability = await client.products
|
||||
.retrieveAvailability(productId)
|
||||
.catch(() => null)
|
||||
|
||||
const onHand = availability[productId].total.onHand
|
||||
if (availability) {
|
||||
const brightpearlProduct = await client.products.retrieve(productId)
|
||||
const onHand = availability[productId].total.onHand
|
||||
|
||||
const sku = brightpearlProduct.identity.sku
|
||||
const [variant] = await this.productVariantService_.list({ sku })
|
||||
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,
|
||||
})
|
||||
if (variant && variant.manage_inventory) {
|
||||
await this.productVariantService_.update(variant._id, {
|
||||
inventory_quantity: onHand,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,11 +206,158 @@ class BrightpearlService extends BaseService {
|
||||
return client.warehouses.updateGoodsOutNote(noteId, {
|
||||
priority: false,
|
||||
shipping: {
|
||||
reference: shipment.tracking_number,
|
||||
reference: shipment.tracking_numbers.join(", "),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async createRefundCredit(fromOrder, fromRefund) {
|
||||
const region = await this.regionService_.retrieve(fromOrder.region_id)
|
||||
const client = await this.getClient()
|
||||
const authData = await this.getAuthData()
|
||||
const orderId = fromOrder.metadata.brightpearl_sales_order_id
|
||||
if (orderId) {
|
||||
let accountingCode = "4000"
|
||||
if (
|
||||
fromRefund.reason === "discount" &&
|
||||
this.options.discount_account_code
|
||||
) {
|
||||
accountingCode = this.options.discount_account_code
|
||||
}
|
||||
|
||||
const parentSo = await client.orders.retrieve(orderId)
|
||||
const order = {
|
||||
currency: parentSo.currency,
|
||||
ref: parentSo.ref,
|
||||
externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
customer: parentSo.customer,
|
||||
delivery: parentSo.delivery,
|
||||
parentId: orderId,
|
||||
rows: [
|
||||
{
|
||||
name: `${fromRefund.reason}: ${fromRefund.note}`,
|
||||
quantity: 1,
|
||||
taxCode: region.tax_code,
|
||||
net: fromRefund.amount / (1 + fromOrder.tax_rate),
|
||||
tax:
|
||||
fromRefund.amount - fromRefund.amount / (1 + fromOrder.tax_rate),
|
||||
nominalCode: accountingCode,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return client.orders
|
||||
.createCredit(order)
|
||||
.then(async (creditId) => {
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "PAYMENT"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`,
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: creditId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
amountPaid: fromRefund.amount,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
const existing = fromOrder.metadata.brightpearl_credit_ids || []
|
||||
const newIds = [...existing, creditId]
|
||||
|
||||
await client.payments.create(payment)
|
||||
|
||||
return this.orderService_.setMetadata(
|
||||
fromOrder._id,
|
||||
"brightpearl_credit_ids",
|
||||
newIds
|
||||
)
|
||||
})
|
||||
.catch((err) => console.log(err.response.data.errors))
|
||||
}
|
||||
}
|
||||
|
||||
async createSalesCredit(fromOrder, fromReturn) {
|
||||
const region = await this.regionService_.retrieve(fromOrder.region_id)
|
||||
const client = await this.getClient()
|
||||
const authData = await this.getAuthData()
|
||||
const orderId = fromOrder.metadata.brightpearl_sales_order_id
|
||||
if (orderId) {
|
||||
const parentSo = await client.orders.retrieve(orderId)
|
||||
const order = {
|
||||
currency: parentSo.currency,
|
||||
ref: parentSo.ref,
|
||||
externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
customer: parentSo.customer,
|
||||
delivery: parentSo.delivery,
|
||||
parentId: orderId,
|
||||
rows: fromReturn.items.map((i) => {
|
||||
const parentRow = parentSo.rows.find((row) => {
|
||||
return row.externalRef === i.item_id
|
||||
})
|
||||
return {
|
||||
net: (parentRow.net / parentRow.quantity) * i.quantity,
|
||||
tax: (parentRow.tax / parentRow.quantity) * i.quantity,
|
||||
productId: parentRow.productId,
|
||||
taxCode: parentRow.taxCode,
|
||||
externalRef: parentRow.externalRef,
|
||||
nominalCode: parentRow.nominalCode,
|
||||
quantity: i.quantity,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const total = order.rows.reduce((acc, next) => {
|
||||
return acc + next.net + next.tax
|
||||
}, 0)
|
||||
|
||||
const difference = fromReturn.refund_amount - total
|
||||
if (difference) {
|
||||
order.rows.push({
|
||||
name: "Difference",
|
||||
quantity: 1,
|
||||
taxCode: region.tax_code,
|
||||
net: difference / (1 + fromOrder.tax_rate),
|
||||
tax: difference - difference / (1 + fromOrder.tax_rate),
|
||||
nominalCode: this.options.sales_account_code || "4000",
|
||||
})
|
||||
}
|
||||
|
||||
return client.orders
|
||||
.createCredit(order)
|
||||
.then(async (creditId) => {
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "PAYMENT"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`,
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: creditId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
amountPaid: fromReturn.refund_amount,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
const existing = fromOrder.metadata.brightpearl_credit_ids || []
|
||||
const newIds = [...existing, creditId]
|
||||
|
||||
await client.payments.create(payment)
|
||||
|
||||
return this.orderService_.setMetadata(
|
||||
fromOrder._id,
|
||||
"brightpearl_credit_ids",
|
||||
newIds
|
||||
)
|
||||
})
|
||||
.catch((err) => console.log(err.response.data.errors))
|
||||
}
|
||||
}
|
||||
|
||||
async createSalesOrder(fromOrder) {
|
||||
const client = await this.getClient()
|
||||
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
|
||||
@@ -188,12 +367,17 @@ class BrightpearlService extends BaseService {
|
||||
customer = await this.createCustomer(fromOrder)
|
||||
}
|
||||
|
||||
const authData = await this.getAuthData()
|
||||
|
||||
const { shipping_address } = fromOrder
|
||||
const order = {
|
||||
currency: {
|
||||
code: fromOrder.currency_code,
|
||||
},
|
||||
ref: fromOrder._id,
|
||||
externalRef: fromOrder._id,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
customer: {
|
||||
id: customer.contactId,
|
||||
address: {
|
||||
@@ -237,7 +421,7 @@ class BrightpearlService extends BaseService {
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: "1220",
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: salesOrderId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
paymentDate: new Date(),
|
||||
@@ -368,6 +552,35 @@ class BrightpearlService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
async createFulfillmentFromGoodsOut(id) {
|
||||
const client = await this.getClient()
|
||||
|
||||
// Get goods out and associated order
|
||||
const goodsOut = await client.warehouses.retrieveGoodsOutNote(id)
|
||||
const order = await client.orders.retrieve(goodsOut.orderId)
|
||||
|
||||
console.log(order)
|
||||
|
||||
// Combine the line items that we are going to create a fulfillment for
|
||||
const lines = Object.keys(goodsOut.orderRows)
|
||||
.map((key) => {
|
||||
const row = order.rows.find((r) => r.id == key)
|
||||
if (row) {
|
||||
return {
|
||||
item_id: row.externalRef,
|
||||
quantity: goodsOut.orderRows[key][0].quantity,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter((i) => !!i)
|
||||
|
||||
return this.orderService_.createFulfillment(order.ref, lines, {
|
||||
goods_out_note: id,
|
||||
})
|
||||
}
|
||||
|
||||
async createCustomer(fromOrder) {
|
||||
const client = await this.getClient()
|
||||
const address = await client.addresses.create({
|
||||
|
||||
@@ -3,29 +3,53 @@ class OrderSubscriber {
|
||||
this.orderService_ = orderService
|
||||
this.brightpearlService_ = brightpearlService
|
||||
|
||||
eventBusService.subscribe("order.refund_created", this.registerRefund)
|
||||
|
||||
eventBusService.subscribe("order.items_returned", this.registerReturn)
|
||||
|
||||
eventBusService.subscribe("order.placed", this.sendToBrightpearl)
|
||||
eventBusService.subscribe("order.payment_captured", this.registerCapturedPayment)
|
||||
|
||||
eventBusService.subscribe(
|
||||
"order.payment_captured",
|
||||
this.registerCapturedPayment
|
||||
)
|
||||
|
||||
eventBusService.subscribe("order.shipment_created", this.registerShipment)
|
||||
}
|
||||
|
||||
sendToBrightpearl = order => {
|
||||
sendToBrightpearl = (order) => {
|
||||
return this.brightpearlService_.createSalesOrder(order)
|
||||
}
|
||||
|
||||
registerCapturedPayment = order => {
|
||||
registerCapturedPayment = (order) => {
|
||||
return this.brightpearlService_.createCapturedPayment(order)
|
||||
}
|
||||
|
||||
registerShipment = async (data) => {
|
||||
const { order_id, shipment } = data
|
||||
const order = await this.orderService_.retrieve(order_id)
|
||||
const notes = await this.brightpearlService_.createGoodsOutNote(order, shipment)
|
||||
if (notes.length) {
|
||||
const noteId = notes[0]
|
||||
await this.brightpearlService_.registerGoodsOutTrackingNumber(noteId, shipment)
|
||||
const noteId = shipment.metadata.goods_out_note
|
||||
if (noteId) {
|
||||
await this.brightpearlService_.registerGoodsOutTrackingNumber(
|
||||
noteId,
|
||||
shipment
|
||||
)
|
||||
await this.brightpearlService_.registerGoodsOutShipped(noteId, shipment)
|
||||
}
|
||||
}
|
||||
|
||||
registerReturn = (data) => {
|
||||
const { order, return: fromReturn } = data
|
||||
return this.brightpearlService_
|
||||
.createSalesCredit(order, fromReturn)
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
|
||||
registerRefund = (data) => {
|
||||
const { order, refund } = data
|
||||
return this.brightpearlService_
|
||||
.createRefundCredit(order, refund)
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderSubscriber
|
||||
|
||||
@@ -23,7 +23,7 @@ class BrightpearlClient {
|
||||
|
||||
constructor(options) {
|
||||
this.client_ = axios.create({
|
||||
baseURL: `${options.url}/public-api/${options.account}`,
|
||||
baseURL: `https://${options.url}/public-api/${options.account}`,
|
||||
headers: {
|
||||
"brightpearl-app-ref": "medusa-dev",
|
||||
"brightpearl-dev-ref": "sebrindom",
|
||||
@@ -97,6 +97,14 @@ class BrightpearlClient {
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
retrieveGoodsOutNote: (id) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/warehouse-service/order/*/goods-note/goods-out/${id}`,
|
||||
method: "GET",
|
||||
})
|
||||
.then(({ data }) => data.response && data.response[id])
|
||||
},
|
||||
createGoodsOutNote: (orderId, data) => {
|
||||
return this.client_
|
||||
.request({
|
||||
@@ -160,6 +168,15 @@ class BrightpearlClient {
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
createCredit: (salesCredit) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/order-service/sales-credit`,
|
||||
method: "POST",
|
||||
data: salesCredit,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,13 +98,22 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
retrieveGoodsOutNote: function retrieveGoodsOutNote(id) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/*/goods-note/goods-out/".concat(id),
|
||||
method: "GET"
|
||||
}).then(function (_ref5) {
|
||||
var data = _ref5.data;
|
||||
return data.response && data.response[id];
|
||||
});
|
||||
},
|
||||
createGoodsOutNote: function createGoodsOutNote(orderId, data) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/".concat(orderId, "/goods-note/goods-out"),
|
||||
method: "POST",
|
||||
data: data
|
||||
}).then(function (_ref5) {
|
||||
var data = _ref5.data;
|
||||
}).then(function (_ref6) {
|
||||
var data = _ref6.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
@@ -137,8 +146,8 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
data: {
|
||||
products: data
|
||||
}
|
||||
}).then(function (_ref6) {
|
||||
var data = _ref6.data;
|
||||
}).then(function (_ref7) {
|
||||
var data = _ref7.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
@@ -151,8 +160,8 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
return _this.client_.request({
|
||||
url: "/order-service/sales-order/".concat(orderId),
|
||||
method: "GET"
|
||||
}).then(function (_ref7) {
|
||||
var data = _ref7.data;
|
||||
}).then(function (_ref8) {
|
||||
var data = _ref8.data;
|
||||
return data.response.length && data.response[0];
|
||||
})["catch"](function (err) {
|
||||
return console.log(err);
|
||||
@@ -163,8 +172,18 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
url: "/order-service/sales-order",
|
||||
method: "POST",
|
||||
data: order
|
||||
}).then(function (_ref8) {
|
||||
var data = _ref8.data;
|
||||
}).then(function (_ref9) {
|
||||
var data = _ref9.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
createCredit: function createCredit(salesCredit) {
|
||||
return _this.client_.request({
|
||||
url: "/order-service/sales-credit",
|
||||
method: "POST",
|
||||
data: salesCredit
|
||||
}).then(function (_ref10) {
|
||||
var data = _ref10.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
@@ -178,8 +197,8 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
url: "/contact-service/postal-address",
|
||||
method: "POST",
|
||||
data: address
|
||||
}).then(function (_ref9) {
|
||||
var data = _ref9.data;
|
||||
}).then(function (_ref11) {
|
||||
var data = _ref11.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
@@ -191,24 +210,24 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
retrieveAvailability: function retrieveAvailability(productId) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/product-availability/".concat(productId)
|
||||
}).then(function (_ref10) {
|
||||
var data = _ref10.data;
|
||||
}).then(function (_ref12) {
|
||||
var data = _ref12.data;
|
||||
return data.response && data.response;
|
||||
});
|
||||
},
|
||||
retrieve: function retrieve(productId) {
|
||||
return _this.client_.request({
|
||||
url: "/product-service/product/".concat(productId)
|
||||
}).then(function (_ref11) {
|
||||
var data = _ref11.data;
|
||||
}).then(function (_ref13) {
|
||||
var data = _ref13.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 (_ref12) {
|
||||
var data = _ref12.data;
|
||||
}).then(function (_ref14) {
|
||||
var data = _ref14.data;
|
||||
return _this.buildSearchResults_(data.response);
|
||||
});
|
||||
}
|
||||
@@ -220,8 +239,8 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
retrieveByEmail: function retrieveByEmail(email) {
|
||||
return _this.client_.request({
|
||||
url: "/contact-service/contact-search?primaryEmail=".concat(email)
|
||||
}).then(function (_ref13) {
|
||||
var data = _ref13.data;
|
||||
}).then(function (_ref15) {
|
||||
var data = _ref15.data;
|
||||
return _this.buildSearchResults_(data.response);
|
||||
});
|
||||
},
|
||||
@@ -230,8 +249,8 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
url: "/contact-service/contact",
|
||||
method: "POST",
|
||||
data: customerData
|
||||
}).then(function (_ref14) {
|
||||
var data = _ref14.data;
|
||||
}).then(function (_ref16) {
|
||||
var data = _ref16.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
@@ -239,7 +258,7 @@ var BrightpearlClient = /*#__PURE__*/function () {
|
||||
});
|
||||
|
||||
this.client_ = _axios["default"].create({
|
||||
baseURL: "".concat(options.url, "/public-api/").concat(options.account),
|
||||
baseURL: "https://".concat(options.url, "/public-api/").concat(options.account),
|
||||
headers: {
|
||||
"brightpearl-app-ref": "medusa-dev",
|
||||
"brightpearl-dev-ref": "sebrindom",
|
||||
|
||||
Reference in New Issue
Block a user