Adds integration to send sales orders to brightpearl
This commit is contained in:
@@ -162,6 +162,12 @@ class StripeProviderService extends PaymentService {
|
||||
try {
|
||||
const { id } = data
|
||||
return this.stripe_.paymentIntents.cancel(id)
|
||||
.catch(err => {
|
||||
if (err.statusCode === 400) {
|
||||
return
|
||||
}
|
||||
throw err
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// noop
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "medusa-plugin-brightpearl",
|
||||
"version": "1.0.0",
|
||||
"description": "Brightpearl plugin for Medusa Commerce",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-plugin-brightpearl"
|
||||
},
|
||||
"author": "Sebastian Rindom",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d dist",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^0.3.0",
|
||||
"medusa-interfaces": "^0.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import Brightpearl from "../utils/brightpearl"
|
||||
|
||||
class BrightpearlService extends BaseService {
|
||||
constructor({ totalsService, regionService, orderService, discountService }, options) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
this.regionService_ = regionService
|
||||
this.orderService_ = orderService
|
||||
this.totalsService_ = totalsService
|
||||
this.discountService_ = discountService
|
||||
|
||||
this.brightpearl_ = new Brightpearl({
|
||||
account: options.account,
|
||||
datacenter: options.datacenter,
|
||||
app_ref: options.app_ref,
|
||||
token: options.token
|
||||
})
|
||||
}
|
||||
|
||||
async createGoodsOutNote(fromOrder, shipment) {
|
||||
const id = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id
|
||||
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
const order = await this.brightpearl_.orders.retrieve(id)
|
||||
const productRows = shipment.item_ids.map(id => {
|
||||
const row = order.rows.find(({ externalRef }) => externalRef === id)
|
||||
return {
|
||||
productId: row.productId,
|
||||
salesOrderRowId: row.id,
|
||||
quantity: row.quantity
|
||||
}
|
||||
})
|
||||
|
||||
const goodsOut = {
|
||||
warehouses: [
|
||||
{
|
||||
releaseDate: new Date(),
|
||||
warehouseId: this.options.warehouse,
|
||||
transfer: false,
|
||||
products: productRows,
|
||||
}
|
||||
],
|
||||
priority: false,
|
||||
}
|
||||
|
||||
return this.brightpearl_.warehouses.createGoodsOutNote(id, goodsOut)
|
||||
}
|
||||
|
||||
registerGoodsOutShipped(noteId, shipment) {
|
||||
return this.brightpearl_.warehouses.registerGoodsOutEvent(noteId, {
|
||||
events: [
|
||||
{
|
||||
eventCode: "SHW",
|
||||
occured: new Date(),
|
||||
eventOwnerId: this.options.event_owner,
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
registerGoodsOutTrackingNumber(noteId, shipment) {
|
||||
return this.brightpearl_.warehouses.updateGoodsOutNote(noteId, {
|
||||
priority: false,
|
||||
shipping: {
|
||||
reference: shipment.tracking_number,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async createSalesOrder(fromOrder) {
|
||||
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
|
||||
|
||||
// All sales orders must have a customer
|
||||
if (!customer) {
|
||||
customer = await this.createCustomer(fromOrder)
|
||||
}
|
||||
|
||||
const { shipping_address } = fromOrder
|
||||
const order = {
|
||||
currency: {
|
||||
code: fromOrder.currency_code
|
||||
},
|
||||
externalRef: fromOrder._id,
|
||||
customer: {
|
||||
id: customer.contactId,
|
||||
address: {
|
||||
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||
addressLine1: shipping_address.address_1,
|
||||
addressLine2: shipping_address.address_2,
|
||||
postalCode: shipping_address.postal_code,
|
||||
countryIsoCode: shipping_address.country_code,
|
||||
telephone: shipping_address.phone,
|
||||
email: fromOrder.email,
|
||||
}
|
||||
},
|
||||
delivery: {
|
||||
shippingMethodId: 0,
|
||||
address: {
|
||||
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||
addressLine1: shipping_address.address_1,
|
||||
addressLine2: shipping_address.address_2,
|
||||
postalCode: shipping_address.postal_code,
|
||||
countryIsoCode: shipping_address.country_code,
|
||||
telephone: shipping_address.phone,
|
||||
email: fromOrder.email,
|
||||
}
|
||||
},
|
||||
rows: await this.getBrightpearlRows(fromOrder)
|
||||
}
|
||||
|
||||
return this.brightpearl_.orders.create(order)
|
||||
.then(async salesOrderId => {
|
||||
const order = await this.brightpearl_.orders.retrieve(salesOrderId)
|
||||
const resResult = await this.brightpearl_.warehouses.createReservation(order, this.options.warehouse)
|
||||
return salesOrderId
|
||||
})
|
||||
.then(async salesOrderId => {
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "AUTH"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: "1220",
|
||||
orderId: salesOrderId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
// Only if authorization type
|
||||
if (paymentType === "AUTH") {
|
||||
const today = new Date()
|
||||
const authExpire = today.setDate(today.getDate() + 7)
|
||||
payment.amountAuthorized = await this.totalsService_.getTotal(fromOrder)
|
||||
payment.authorizationExpiry = new Date(authExpire)
|
||||
} else {
|
||||
// For captured
|
||||
}
|
||||
|
||||
await this.brightpearl_.payments.create(payment)
|
||||
|
||||
return salesOrderId
|
||||
})
|
||||
.then((salesOrderId) => {
|
||||
return this.orderService_.setMetadata(fromOrder._id, "brightpearl_sales_order_id", salesOrderId)
|
||||
})
|
||||
}
|
||||
|
||||
async createCapturedPayment(fromOrder) {
|
||||
const soId = fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id
|
||||
if (!soId) {
|
||||
return
|
||||
}
|
||||
|
||||
const paymentType = "CAPTURE"
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: "1220",
|
||||
orderId: soId,
|
||||
paymentDate: new Date(),
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
amountPaid: await this.totalsService_.getTotal(fromOrder),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
await this.brightpearl_.payments.create(payment)
|
||||
}
|
||||
|
||||
async getBrightpearlRows(fromOrder) {
|
||||
const region = await this.regionService_.retrieve(fromOrder.region_id)
|
||||
const discount = fromOrder.discounts.find(({ discount_rule }) => discount_rule.type !== "free_shipping")
|
||||
let lineDiscounts = []
|
||||
if (discount) {
|
||||
lineDiscounts = this.discountService_.getLineDiscounts(fromOrder, discount)
|
||||
}
|
||||
|
||||
const lines = await Promise.all(fromOrder.items.map(async item => {
|
||||
const bpProduct = await this.retrieveProductBySKU(item.content.variant.sku)
|
||||
|
||||
const discount = lineDiscounts.find(l => l.item._id.equals(item._id)) || { amount: 0 }
|
||||
|
||||
const row = {}
|
||||
if (bpProduct) {
|
||||
row.productId = bpProduct.productId
|
||||
} else {
|
||||
row.name = item.title
|
||||
}
|
||||
row.net = item.content.unit_price * item.quantity - discount.amount
|
||||
row.tax = row.net * fromOrder.tax_rate
|
||||
row.quantity = item.quantity
|
||||
row.taxCode = region.tax_code
|
||||
row.externalRef = item._id
|
||||
row.nominalCode = this.options.sales_account_code || "4000"
|
||||
|
||||
return row
|
||||
}))
|
||||
|
||||
const shippingTotal = this.totalsService_.getShippingTotal(fromOrder)
|
||||
const shippingMethods = fromOrder.shipping_methods
|
||||
if (shippingMethods.length > 0) {
|
||||
lines.push({
|
||||
name: `Shipping: ${shippingMethods.map(m => m.name).join(" + ")}`,
|
||||
quantity: 1,
|
||||
net: shippingTotal,
|
||||
tax: shippingTotal * fromOrder.tax_rate,
|
||||
taxCode: region.tax_code,
|
||||
nominalCode: this.options.shipping_account_code || "4040",
|
||||
})
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
retrieveCustomerByEmail(email) {
|
||||
return this.brightpearl_.customers.retrieveByEmail(email).then(customers => {
|
||||
if (!customers.length) {
|
||||
return null
|
||||
}
|
||||
return customers.find(c => c.primaryEmail === email)
|
||||
})
|
||||
}
|
||||
|
||||
retrieveProductBySKU(sku) {
|
||||
return this.brightpearl_.products.retrieveBySKU(sku).then(products => {
|
||||
if (!products.length) {
|
||||
return null
|
||||
}
|
||||
return products[0]
|
||||
})
|
||||
}
|
||||
|
||||
async createCustomer(fromOrder) {
|
||||
const address = await this.brightpearl_.addresses.create({
|
||||
addressLine1: fromOrder.shipping_address.address_1,
|
||||
addressLine2: fromOrder.shipping_address.address_2,
|
||||
postalCode: fromOrder.shipping_address.postal_code,
|
||||
countryIsoCode: fromOrder.shipping_address.country_code,
|
||||
})
|
||||
|
||||
const customer = await this.brightpearl_.customers.create({
|
||||
firstName: fromOrder.shipping_address.first_name,
|
||||
lastName: fromOrder.shipping_address.last_name,
|
||||
postAddressIds: {
|
||||
DEF: address,
|
||||
BIL: address,
|
||||
DEL: address,
|
||||
}
|
||||
})
|
||||
|
||||
return { contactId: customer }
|
||||
}
|
||||
}
|
||||
|
||||
export default BrightpearlService
|
||||
@@ -0,0 +1,31 @@
|
||||
class OrderSubscriber {
|
||||
constructor({ eventBusService, orderService, brightpearlService }) {
|
||||
this.orderService_ = orderService
|
||||
this.brightpearlService_ = brightpearlService
|
||||
|
||||
eventBusService.subscribe("order.placed", this.sendToBrightpearl)
|
||||
eventBusService.subscribe("order.payment_captured", this.registerCapturedPayment)
|
||||
eventBusService.subscribe("order.shipment_created", this.registerShipment)
|
||||
}
|
||||
|
||||
sendToBrightpearl = order => {
|
||||
return this.brightpearlService_.createSalesOrder(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)
|
||||
await this.brightpearlService_.registerGoodsOutShipped(noteId, shipment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderSubscriber
|
||||
@@ -0,0 +1,166 @@
|
||||
import axios from "axios"
|
||||
|
||||
class BrightpearlClient {
|
||||
constructor(options) {
|
||||
this.client_ = axios.create({
|
||||
baseURL: `https://${options.datacenter}.brightpearl.com/public-api/${options.account}`,
|
||||
headers: {
|
||||
'brightpearl-app-ref': options.app_ref,
|
||||
'brightpearl-account-token': options.token
|
||||
},
|
||||
})
|
||||
|
||||
this.payments = this.buildPaymentEndpoints()
|
||||
this.warehouses = this.buildWarehouseEndpoints()
|
||||
this.orders = this.buildOrderEndpoints()
|
||||
this.addresses = this.buildAddressEndpoints()
|
||||
this.customers = this.buildCustomerEndpoints()
|
||||
this.products = this.buildProductEndpoints()
|
||||
}
|
||||
|
||||
buildSearchResults_(response) {
|
||||
const { results, metaData } = response
|
||||
// Map the column names to the columns
|
||||
return results.map(resColumns => {
|
||||
const object = {}
|
||||
for (let i = 0; i < resColumns.length; i++) {
|
||||
const fieldName = metaData.columns[i].name
|
||||
object[fieldName] = resColumns[i]
|
||||
}
|
||||
return object
|
||||
})
|
||||
}
|
||||
|
||||
buildPaymentEndpoints = () => {
|
||||
return {
|
||||
create: (payment) => {
|
||||
return this.client_.request({
|
||||
url: `/accounting-service/customer-payment`,
|
||||
method: "POST",
|
||||
data: payment
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildWarehouseEndpoints = () => {
|
||||
return {
|
||||
retrieveReservation: (orderId) => {
|
||||
return this.client_.request({
|
||||
url: `/warehouse-service/order/${orderId}/reservation`,
|
||||
method: "GET",
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
createGoodsOutNote: (orderId, data) => {
|
||||
return this.client_.request({
|
||||
url: `/warehouse-service/order/${orderId}/goods-note/goods-out`,
|
||||
method: "POST",
|
||||
data,
|
||||
}).then(({ data }) => data.response)
|
||||
},
|
||||
updateGoodsOutNote: (noteId, update) => {
|
||||
return this.client_.request({
|
||||
url: `/warehouse-service/goods-note/goods-out/${noteId}`,
|
||||
method: "PUT",
|
||||
data: update
|
||||
})
|
||||
},
|
||||
registerGoodsOutEvent: (noteId, data) => {
|
||||
return this.client_.request({
|
||||
url: `/warehouse-service/goods-note/goods-out/${noteId}/event`,
|
||||
method: "POST",
|
||||
data
|
||||
})
|
||||
},
|
||||
createReservation: (order, warehouse) => {
|
||||
const id = order.id
|
||||
const data = order.rows.map(r => ({
|
||||
productId: r.productId,
|
||||
salesOrderRowId: r.id,
|
||||
quantity: r.quantity
|
||||
}))
|
||||
return this.client_.request({
|
||||
url: `/warehouse-service/order/${id}/reservation/warehouse/${warehouse}`,
|
||||
method: "POST",
|
||||
data: {
|
||||
products: data
|
||||
}
|
||||
}).then(({ data }) => data.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildOrderEndpoints = () => {
|
||||
return {
|
||||
retrieve: (orderId) => {
|
||||
return this.client_.request({
|
||||
url: `/order-service/sales-order/${orderId}`,
|
||||
method: "GET",
|
||||
})
|
||||
.then(({ data }) => data.response.length && data.response[0])
|
||||
.catch(err => console.log(err))
|
||||
},
|
||||
create: (order) => {
|
||||
return this.client_.request({
|
||||
url: `/order-service/sales-order`,
|
||||
method: "POST",
|
||||
data: order
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildAddressEndpoints = () => {
|
||||
return {
|
||||
create: (address) => {
|
||||
return this.client_.request({
|
||||
url: `/contact-service/postal-address`,
|
||||
method: "POST",
|
||||
data: address
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildProductEndpoints = () => {
|
||||
return {
|
||||
retrieveBySKU: (sku) => {
|
||||
return this.client_.request({
|
||||
url: `/product-service/product-search?SKU=${sku}`,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
return this.buildSearchResults_(data.response)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildCustomerEndpoints = () => {
|
||||
return {
|
||||
retrieveByEmail: (email) => {
|
||||
return this.client_.request({
|
||||
url: `/contact-service/contact-search?primaryEmail=${email}`,
|
||||
})
|
||||
.then(({data }) => {
|
||||
return this.buildSearchResults_(data.response)
|
||||
})
|
||||
},
|
||||
|
||||
create: (customerData) => {
|
||||
return this.client_.request({
|
||||
url: `/contact-service/contact`,
|
||||
method: "POST",
|
||||
data: customerData
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BrightpearlClient
|
||||
@@ -0,0 +1,216 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports["default"] = void 0;
|
||||
|
||||
var _axios = _interopRequireDefault(require("axios"));
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
|
||||
|
||||
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
|
||||
|
||||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
||||
|
||||
var BrightpearlClient = /*#__PURE__*/function () {
|
||||
function BrightpearlClient(options) {
|
||||
var _this = this;
|
||||
|
||||
_classCallCheck(this, BrightpearlClient);
|
||||
|
||||
_defineProperty(this, "buildPaymentEndpoints", function () {
|
||||
return {
|
||||
create: function create(payment) {
|
||||
return _this.client_.request({
|
||||
url: "/accounting-service/customer-payment",
|
||||
method: "POST",
|
||||
data: payment
|
||||
}).then(function (_ref) {
|
||||
var data = _ref.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildWarehouseEndpoints", function () {
|
||||
return {
|
||||
retrieveReservation: function retrieveReservation(orderId) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/".concat(orderId, "/reservation"),
|
||||
method: "GET"
|
||||
}).then(function (_ref2) {
|
||||
var data = _ref2.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
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 (_ref3) {
|
||||
var data = _ref3.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
updateGoodsOutNote: function updateGoodsOutNote(noteId, update) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/goods-note/goods-out/".concat(noteId),
|
||||
method: "PUT",
|
||||
data: update
|
||||
});
|
||||
},
|
||||
registerGoodsOutEvent: function registerGoodsOutEvent(noteId, data) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/goods-note/goods-out/".concat(noteId, "/event"),
|
||||
method: "POST",
|
||||
data: data
|
||||
});
|
||||
},
|
||||
createReservation: function createReservation(order, warehouse) {
|
||||
var id = order.id;
|
||||
var data = order.rows.map(function (r) {
|
||||
return {
|
||||
productId: r.productId,
|
||||
salesOrderRowId: r.id,
|
||||
quantity: r.quantity
|
||||
};
|
||||
});
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/".concat(id, "/reservation/warehouse/").concat(warehouse),
|
||||
method: "POST",
|
||||
data: {
|
||||
products: data
|
||||
}
|
||||
}).then(function (_ref4) {
|
||||
var data = _ref4.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildOrderEndpoints", function () {
|
||||
return {
|
||||
retrieve: function retrieve(orderId) {
|
||||
return _this.client_.request({
|
||||
url: "/order-service/sales-order/".concat(orderId),
|
||||
method: "GET"
|
||||
}).then(function (_ref5) {
|
||||
var data = _ref5.data;
|
||||
return data.response.length && data.response[0];
|
||||
})["catch"](function (err) {
|
||||
return console.log(err);
|
||||
});
|
||||
},
|
||||
create: function create(order) {
|
||||
return _this.client_.request({
|
||||
url: "/order-service/sales-order",
|
||||
method: "POST",
|
||||
data: order
|
||||
}).then(function (_ref6) {
|
||||
var data = _ref6.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildAddressEndpoints", function () {
|
||||
return {
|
||||
create: function create(address) {
|
||||
return _this.client_.request({
|
||||
url: "/contact-service/postal-address",
|
||||
method: "POST",
|
||||
data: address
|
||||
}).then(function (_ref7) {
|
||||
var data = _ref7.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildProductEndpoints", function () {
|
||||
return {
|
||||
retrieveBySKU: function retrieveBySKU(sku) {
|
||||
return _this.client_.request({
|
||||
url: "/product-service/product-search?SKU=".concat(sku)
|
||||
}).then(function (_ref8) {
|
||||
var data = _ref8.data;
|
||||
return _this.buildSearchResults_(data.response);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildCustomerEndpoints", function () {
|
||||
return {
|
||||
retrieveByEmail: function retrieveByEmail(email) {
|
||||
return _this.client_.request({
|
||||
url: "/contact-service/contact-search?primaryEmail=".concat(email)
|
||||
}).then(function (_ref9) {
|
||||
var data = _ref9.data;
|
||||
return _this.buildSearchResults_(data.response);
|
||||
});
|
||||
},
|
||||
create: function create(customerData) {
|
||||
return _this.client_.request({
|
||||
url: "/contact-service/contact",
|
||||
method: "POST",
|
||||
data: customerData
|
||||
}).then(function (_ref10) {
|
||||
var data = _ref10.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.client_ = _axios["default"].create({
|
||||
baseURL: "https://".concat(options.datacenter, ".brightpearl.com/public-api/").concat(options.account),
|
||||
headers: {
|
||||
'brightpearl-app-ref': options.app_ref,
|
||||
'brightpearl-account-token': options.token
|
||||
}
|
||||
});
|
||||
this.payments = this.buildPaymentEndpoints();
|
||||
this.warehouses = this.buildWarehouseEndpoints();
|
||||
this.orders = this.buildOrderEndpoints();
|
||||
this.addresses = this.buildAddressEndpoints();
|
||||
this.customers = this.buildCustomerEndpoints();
|
||||
this.products = this.buildProductEndpoints();
|
||||
}
|
||||
|
||||
_createClass(BrightpearlClient, [{
|
||||
key: "buildSearchResults_",
|
||||
value: function buildSearchResults_(response) {
|
||||
var results = response.results,
|
||||
metaData = response.metaData; // Map the column names to the columns
|
||||
|
||||
return results.map(function (resColumns) {
|
||||
var object = {};
|
||||
|
||||
for (var i = 0; i < resColumns.length; i++) {
|
||||
var fieldName = metaData.columns[i].name;
|
||||
object[fieldName] = resColumns[i];
|
||||
}
|
||||
|
||||
return object;
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
return BrightpearlClient;
|
||||
}();
|
||||
|
||||
var _default = BrightpearlClient;
|
||||
exports["default"] = _default;
|
||||
@@ -50,7 +50,6 @@ class SegmentService extends BaseService {
|
||||
|
||||
|
||||
async buildOrder(order) {
|
||||
console.log("build", order)
|
||||
const subtotal = await this.totalsService_.getSubtotal(order)
|
||||
const total = await this.totalsService_.getTotal(order)
|
||||
const tax = await this.totalsService_.getTaxTotal(order)
|
||||
|
||||
@@ -7,6 +7,39 @@ import errorHandler from "./middlewares/error-handler"
|
||||
export default (container, config) => {
|
||||
const app = Router()
|
||||
|
||||
app.post("/create-shipment/:order_id", async (req, res) => {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const order = await orderService.retrieve(req.params.order_id)
|
||||
|
||||
await orderService.createShipment(order._id, {
|
||||
item_ids: order.items.map(({ _id }) => `${_id}`),
|
||||
tracking_number: "1234",
|
||||
})
|
||||
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app.post("/run-hook/:order_id/capture", async (req, res) => {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const order = await orderService.retrieve(req.params.order_id)
|
||||
|
||||
eventBus.emit("order.payment_captured", order)
|
||||
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app.post("/run-hook/:order_id", async (req, res) => {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
const order = await orderService.retrieve(req.params.order_id)
|
||||
|
||||
eventBus.emit("order.placed", order)
|
||||
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
admin(app, container, config)
|
||||
store(app, container, config)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import PaymentMethodSchema from "./schemas/payment-method"
|
||||
import ShippingMethodSchema from "./schemas/shipping-method"
|
||||
import AddressSchema from "./schemas/address"
|
||||
import DiscountSchema from "./schemas/discount"
|
||||
import ShipmentSchema from "./schemas/shipment"
|
||||
|
||||
class OrderModel extends BaseModel {
|
||||
static modelName = "Order"
|
||||
@@ -23,6 +24,8 @@ class OrderModel extends BaseModel {
|
||||
shipping_address: { type: AddressSchema, required: true },
|
||||
items: { type: [LineItemSchema], required: true },
|
||||
currency_code: { type: String, required: true },
|
||||
tax_rate: { type: Number, required: true },
|
||||
shipments: { type: [ShipmentSchema], default: [] },
|
||||
region_id: { type: String, required: true },
|
||||
discounts: { type: [DiscountSchema], default: [] },
|
||||
customer_id: { type: String },
|
||||
|
||||
@@ -7,6 +7,7 @@ class RegionModel extends BaseModel {
|
||||
name: { type: String, required: true },
|
||||
currency_code: { type: String, required: true },
|
||||
tax_rate: { type: Number, required: true, default: 0 },
|
||||
tax_code: { type: String },
|
||||
countries: { type: [String], default: [] },
|
||||
payment_providers: { type: [String], default: [] },
|
||||
fulfillment_providers: { type: [String], default: [] },
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
item_ids: { type: [String], required: true },
|
||||
tracking_number: { type: String, default: "" },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
})
|
||||
@@ -5,6 +5,8 @@ import { BaseService } from "medusa-interfaces"
|
||||
class OrderService extends BaseService {
|
||||
static Events = {
|
||||
GIFT_CARD_CREATED: "order.gift_card_created",
|
||||
PAYMENT_CAPTURED: "order.payment_captured",
|
||||
SHIPMENT_CREATED: "order.shipment_created",
|
||||
PLACED: "order.placed",
|
||||
UPDATED: "order.updated",
|
||||
CANCELLED: "order.cancelled",
|
||||
@@ -352,6 +354,7 @@ class OrderService extends BaseService {
|
||||
email: cart.email,
|
||||
customer_id: cart.customer_id,
|
||||
cart_id: cart._id,
|
||||
tax_rate: region.tax_rate,
|
||||
currency_code: region.currency_code,
|
||||
}
|
||||
|
||||
@@ -366,6 +369,58 @@ class OrderService extends BaseService {
|
||||
.then(() => this.orderModel_.findOne({ cart_id: cart._id }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a shipment to the order to indicate that an order has left the warehouse
|
||||
*/
|
||||
async createShipment(orderId, shipment) {
|
||||
const order = await this.retrieve(orderId)
|
||||
|
||||
console.log(order)
|
||||
if (order.fulfillment_status === "shipped") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Order has already been shipped"
|
||||
)
|
||||
}
|
||||
|
||||
const shipmentSchema = Validator.object({
|
||||
item_ids: Validator.array()
|
||||
.items(Validator.string())
|
||||
.required(),
|
||||
tracking_number: Validator.string().required(),
|
||||
})
|
||||
|
||||
const { value, error } = shipmentSchema.validate(shipment)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipment not valid: ${error}`
|
||||
)
|
||||
}
|
||||
|
||||
const existing = order.shipments || []
|
||||
const shipments = [...existing, value]
|
||||
const allCovered = order.items.every(
|
||||
i => !!shipments.find(s => s.item_ids.includes(`${i._id}`))
|
||||
)
|
||||
|
||||
const update = {
|
||||
$push: { shipments: value },
|
||||
$set: {
|
||||
fulfillment_status: allCovered ? "shipped" : "partially_shipped",
|
||||
},
|
||||
}
|
||||
|
||||
// Add the shipment to the order
|
||||
return this.orderModel_.updateOne({ _id: orderId }, update).then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.SHIPMENT_CREATED, {
|
||||
order_id: orderId,
|
||||
shipment,
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an order
|
||||
* @param {object} order - the order to create
|
||||
@@ -535,14 +590,19 @@ class OrderService extends BaseService {
|
||||
|
||||
await paymentProvider.capturePayment(data)
|
||||
|
||||
return this.orderModel_.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: updateFields,
|
||||
}
|
||||
)
|
||||
return this.orderModel_
|
||||
.updateOne(
|
||||
{
|
||||
_id: orderId,
|
||||
},
|
||||
{
|
||||
$set: updateFields,
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
this.eventBus_.emit(OrderService.Events.PAYMENT_CAPTURED, result)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -252,6 +252,45 @@ class TotalsService extends BaseService {
|
||||
return discounts
|
||||
}
|
||||
|
||||
async getLineDiscounts(cart, discount) {
|
||||
const subtotal = this.getSubtotal(cart)
|
||||
const { type, allocation, value } = discount.discount_rule
|
||||
if (allocation === "total") {
|
||||
let percentage = 0
|
||||
if (type === "percentage") {
|
||||
percentage = value / 100
|
||||
} else if (type === "fixed") {
|
||||
percentage = value / subtotal
|
||||
}
|
||||
|
||||
return cart.items.map(item => {
|
||||
const lineTotal = item.content.unit_price * item.quantity
|
||||
|
||||
return {
|
||||
item,
|
||||
amount: lineTotal * percentage,
|
||||
}
|
||||
})
|
||||
} else if (allocation === "item") {
|
||||
const allocationDiscounts = this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
cart,
|
||||
type
|
||||
)
|
||||
return cart.items.map(item => {
|
||||
const discounted = allocationDiscounts.find(a =>
|
||||
a.lineItem._id.equals(item._id)
|
||||
)
|
||||
return {
|
||||
item,
|
||||
amount: !!discounted ? discounted.amount : 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return cart.items.map(i => ({ item: i, amount: 0 }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total discount amount for each of the different supported
|
||||
* discount types. If discounts aren't present or invalid returns 0.
|
||||
|
||||
@@ -39,7 +39,11 @@ class OrderSubscriber {
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("order.placed", async order => {
|
||||
await this.cartService_.delete(order.cart_id)
|
||||
await this.cartService_.delete(order.cart_id).catch(err => {
|
||||
if (err.type !== "not_found") {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("order.placed", this.handleDiscounts)
|
||||
|
||||
Reference in New Issue
Block a user