Adds integration to send sales orders to brightpearl
This commit is contained in:
@@ -162,6 +162,12 @@ class StripeProviderService extends PaymentService {
|
|||||||
try {
|
try {
|
||||||
const { id } = data
|
const { id } = data
|
||||||
return this.stripe_.paymentIntents.cancel(id)
|
return this.stripe_.paymentIntents.cancel(id)
|
||||||
|
.catch(err => {
|
||||||
|
if (err.statusCode === 400) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw 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) {
|
async buildOrder(order) {
|
||||||
console.log("build", order)
|
|
||||||
const subtotal = await this.totalsService_.getSubtotal(order)
|
const subtotal = await this.totalsService_.getSubtotal(order)
|
||||||
const total = await this.totalsService_.getTotal(order)
|
const total = await this.totalsService_.getTotal(order)
|
||||||
const tax = await this.totalsService_.getTaxTotal(order)
|
const tax = await this.totalsService_.getTaxTotal(order)
|
||||||
|
|||||||
@@ -7,6 +7,39 @@ import errorHandler from "./middlewares/error-handler"
|
|||||||
export default (container, config) => {
|
export default (container, config) => {
|
||||||
const app = Router()
|
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)
|
admin(app, container, config)
|
||||||
store(app, container, config)
|
store(app, container, config)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import PaymentMethodSchema from "./schemas/payment-method"
|
|||||||
import ShippingMethodSchema from "./schemas/shipping-method"
|
import ShippingMethodSchema from "./schemas/shipping-method"
|
||||||
import AddressSchema from "./schemas/address"
|
import AddressSchema from "./schemas/address"
|
||||||
import DiscountSchema from "./schemas/discount"
|
import DiscountSchema from "./schemas/discount"
|
||||||
|
import ShipmentSchema from "./schemas/shipment"
|
||||||
|
|
||||||
class OrderModel extends BaseModel {
|
class OrderModel extends BaseModel {
|
||||||
static modelName = "Order"
|
static modelName = "Order"
|
||||||
@@ -23,6 +24,8 @@ class OrderModel extends BaseModel {
|
|||||||
shipping_address: { type: AddressSchema, required: true },
|
shipping_address: { type: AddressSchema, required: true },
|
||||||
items: { type: [LineItemSchema], required: true },
|
items: { type: [LineItemSchema], required: true },
|
||||||
currency_code: { type: String, required: true },
|
currency_code: { type: String, required: true },
|
||||||
|
tax_rate: { type: Number, required: true },
|
||||||
|
shipments: { type: [ShipmentSchema], default: [] },
|
||||||
region_id: { type: String, required: true },
|
region_id: { type: String, required: true },
|
||||||
discounts: { type: [DiscountSchema], default: [] },
|
discounts: { type: [DiscountSchema], default: [] },
|
||||||
customer_id: { type: String },
|
customer_id: { type: String },
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class RegionModel extends BaseModel {
|
|||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
currency_code: { type: String, required: true },
|
currency_code: { type: String, required: true },
|
||||||
tax_rate: { type: Number, required: true, default: 0 },
|
tax_rate: { type: Number, required: true, default: 0 },
|
||||||
|
tax_code: { type: String },
|
||||||
countries: { type: [String], default: [] },
|
countries: { type: [String], default: [] },
|
||||||
payment_providers: { type: [String], default: [] },
|
payment_providers: { type: [String], default: [] },
|
||||||
fulfillment_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 {
|
class OrderService extends BaseService {
|
||||||
static Events = {
|
static Events = {
|
||||||
GIFT_CARD_CREATED: "order.gift_card_created",
|
GIFT_CARD_CREATED: "order.gift_card_created",
|
||||||
|
PAYMENT_CAPTURED: "order.payment_captured",
|
||||||
|
SHIPMENT_CREATED: "order.shipment_created",
|
||||||
PLACED: "order.placed",
|
PLACED: "order.placed",
|
||||||
UPDATED: "order.updated",
|
UPDATED: "order.updated",
|
||||||
CANCELLED: "order.cancelled",
|
CANCELLED: "order.cancelled",
|
||||||
@@ -352,6 +354,7 @@ class OrderService extends BaseService {
|
|||||||
email: cart.email,
|
email: cart.email,
|
||||||
customer_id: cart.customer_id,
|
customer_id: cart.customer_id,
|
||||||
cart_id: cart._id,
|
cart_id: cart._id,
|
||||||
|
tax_rate: region.tax_rate,
|
||||||
currency_code: region.currency_code,
|
currency_code: region.currency_code,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +369,58 @@ class OrderService extends BaseService {
|
|||||||
.then(() => this.orderModel_.findOne({ cart_id: cart._id }))
|
.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
|
* Creates an order
|
||||||
* @param {object} order - the order to create
|
* @param {object} order - the order to create
|
||||||
@@ -535,14 +590,19 @@ class OrderService extends BaseService {
|
|||||||
|
|
||||||
await paymentProvider.capturePayment(data)
|
await paymentProvider.capturePayment(data)
|
||||||
|
|
||||||
return this.orderModel_.updateOne(
|
return this.orderModel_
|
||||||
{
|
.updateOne(
|
||||||
_id: orderId,
|
{
|
||||||
},
|
_id: orderId,
|
||||||
{
|
},
|
||||||
$set: updateFields,
|
{
|
||||||
}
|
$set: updateFields,
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
.then(result => {
|
||||||
|
this.eventBus_.emit(OrderService.Events.PAYMENT_CAPTURED, result)
|
||||||
|
return result
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -252,6 +252,45 @@ class TotalsService extends BaseService {
|
|||||||
return discounts
|
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
|
* Calculates the total discount amount for each of the different supported
|
||||||
* discount types. If discounts aren't present or invalid returns 0.
|
* discount types. If discounts aren't present or invalid returns 0.
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ class OrderSubscriber {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.eventBus_.subscribe("order.placed", async order => {
|
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)
|
this.eventBus_.subscribe("order.placed", this.handleDiscounts)
|
||||||
|
|||||||
Reference in New Issue
Block a user