feat: webshipper (#118)
* register fulfillment providers as singletons * fix: adds subscribers/api endpoints and webhook handlers * Adds readme * Allow document attachments to orders: * chore: gitignore utils * chore: rm compiled files
This commit is contained in:
@@ -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,14 @@
|
|||||||
|
/lib
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
/*.js
|
||||||
|
!index.js
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
/api
|
||||||
|
/services
|
||||||
|
/models
|
||||||
|
/subscribers
|
||||||
|
/utils
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/lib
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
/*.js
|
||||||
|
!index.js
|
||||||
|
yarn.lock
|
||||||
|
/src
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# medusa-fulfillment-webshipper
|
||||||
|
|
||||||
|
Adds Webshipper as a fulfilment provider in Medusa Commerce.
|
||||||
|
|
||||||
|
On each new fulfillment an order is created in Webshipper. The plugin listens for shipment events and updated the shipment accordingly.
|
||||||
|
A webhook listener is exposed at `/webshipper/shipments` to listen for shipment creations. You must create this webhook in Webshipper to have Medusa listen for shipment events.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
account: [your webshipper account] (required)
|
||||||
|
api_token: [a webshipper api token] (required)
|
||||||
|
order_channel_id: [the channel id to register orders on] (required)
|
||||||
|
webhook_secret: [the webhook secret used to listen for shipments] (required)
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// noop
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "medusa-fulfillment-webshipper",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Webshipper Fulfillment provider for Medusa",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/medusajs/medusa",
|
||||||
|
"directory": "packages/medusa-fulfillment-webshipper"
|
||||||
|
},
|
||||||
|
"author": "Sebastian Rindom",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.7.5",
|
||||||
|
"@babel/core": "^7.7.5",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||||
|
"@babel/preset-env": "^7.7.5",
|
||||||
|
"@babel/runtime": "^7.9.6",
|
||||||
|
"client-sessions": "^0.8.0",
|
||||||
|
"cross-env": "^5.2.1",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"jest": "^25.5.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "babel src -d .",
|
||||||
|
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||||
|
"watch": "babel -w src --out-dir . --ignore **/__tests__"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"medusa-interfaces": "1.x"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.20.0",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"medusa-core-utils": "^1.0.10"
|
||||||
|
},
|
||||||
|
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408"
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import bodyParser from "body-parser"
|
||||||
|
import crypto from "crypto"
|
||||||
|
import cors from "cors"
|
||||||
|
import { getConfigFile } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default (rootDirectory) => {
|
||||||
|
const app = Router()
|
||||||
|
|
||||||
|
const { configModule } = getConfigFile(rootDirectory, "medusa-config")
|
||||||
|
const { projectConfig } = configModule
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin: projectConfig.store_cors.split(","),
|
||||||
|
credentials: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
app.options("/webshipper/drop-points/:rate_id", cors(corsOptions))
|
||||||
|
app.get(
|
||||||
|
"/webshipper/drop-points/:rate_id",
|
||||||
|
cors(corsOptions),
|
||||||
|
async (req, res) => {
|
||||||
|
const { rate_id } = req.params
|
||||||
|
const { address_1, postal_code, country_code } = req.query
|
||||||
|
|
||||||
|
try {
|
||||||
|
const webshipperService = req.scope.resolve(
|
||||||
|
"webshipperFulfillmentService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dropPoints = await webshipperService.retrieveDropPoints(
|
||||||
|
rate_id,
|
||||||
|
postal_code,
|
||||||
|
country_code,
|
||||||
|
address_1
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
drop_points: dropPoints,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
res.json({ drop_points: [] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/webshipper/shipments",
|
||||||
|
bodyParser.raw({ type: "application/vnd.api+json" }),
|
||||||
|
async (req, res) => {
|
||||||
|
const eventBus = req.scope.resolve("eventBusService")
|
||||||
|
const logger = req.scope.resolve("logger")
|
||||||
|
|
||||||
|
const secret = `da791d87513eb091640f9fb6c4b94384`
|
||||||
|
const hmac = crypto.createHmac("sha256", secret)
|
||||||
|
const digest = hmac.update(req.body).digest("base64")
|
||||||
|
const hash = req.header("x-webshipper-hmac-sha256")
|
||||||
|
|
||||||
|
if (hash === digest) {
|
||||||
|
eventBus.emit("webshipper.shipment", {
|
||||||
|
headers: req.headers,
|
||||||
|
body: JSON.parse(req.body),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.warn("Webshipper webhook could not be authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { FulfillmentService } from "medusa-interfaces"
|
||||||
|
import Webshipper from "../utils/webshipper"
|
||||||
|
|
||||||
|
class WebshipperFulfillmentService extends FulfillmentService {
|
||||||
|
static identifier = "webshipper"
|
||||||
|
|
||||||
|
constructor({ logger, orderService }, options) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.logger_ = logger
|
||||||
|
this.orderService_ = orderService
|
||||||
|
this.options_ = options
|
||||||
|
this.client_ = new Webshipper({
|
||||||
|
account: this.options_.account,
|
||||||
|
token: this.options_.api_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerInvoiceGenerator(service) {
|
||||||
|
if (typeof service.createInvoice === "function") {
|
||||||
|
this.invoiceGenerator_ = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFulfillmentOptions() {
|
||||||
|
const rates = await this.client_.shippingRates.list()
|
||||||
|
|
||||||
|
return rates.data.map((r) => ({
|
||||||
|
id: r.attributes.name,
|
||||||
|
webshipper_id: r.id,
|
||||||
|
name: r.attributes.name,
|
||||||
|
require_drop_point: r.attributes.require_drop_point,
|
||||||
|
carrier_id: r.attributes.carrier_id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateFulfillmentData(data, _) {
|
||||||
|
if (data.require_drop_point) {
|
||||||
|
if (!data.drop_point_id) {
|
||||||
|
throw new Error("Must have drop point id")
|
||||||
|
} else {
|
||||||
|
// TODO: validate that the drop point exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateOption(data) {
|
||||||
|
const rate = await this.client_.shippingRates
|
||||||
|
.retrieve(data.webshipper_id)
|
||||||
|
.catch(() => undefined)
|
||||||
|
return !!rate
|
||||||
|
}
|
||||||
|
|
||||||
|
canCalculate() {
|
||||||
|
// Return whether or not we are able to calculate dynamically
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatePrice() {
|
||||||
|
// Calculate prices
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrder(methodData, fulfillmentItems, fromOrder) {
|
||||||
|
const existing =
|
||||||
|
fromOrder.metadata && fromOrder.metadata.webshipper_order_id
|
||||||
|
|
||||||
|
let webshipperOrder
|
||||||
|
if (existing) {
|
||||||
|
webshipperOrder = await this.client_.orders.retrieve(existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!webshipperOrder) {
|
||||||
|
let invoice
|
||||||
|
if (this.invoiceGenerator_) {
|
||||||
|
const base64Invoice = await this.invoiceGenerator_.createInvoice(
|
||||||
|
fromOrder,
|
||||||
|
fulfillmentItems
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice = await this.client_.documents
|
||||||
|
.create({
|
||||||
|
type: "documents",
|
||||||
|
attributes: {
|
||||||
|
document_size: this.options_.document_size || "A4",
|
||||||
|
document_format: "PDF",
|
||||||
|
base64: base64Invoice,
|
||||||
|
document_type: "invoice",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { shipping_address } = fromOrder
|
||||||
|
const newOrder = {
|
||||||
|
type: "orders",
|
||||||
|
attributes: {
|
||||||
|
status: "pending",
|
||||||
|
ext_ref: `${fromOrder._id}.${fromOrder.fulfillments.length}`,
|
||||||
|
visible_ref: `${fromOrder.display_id}-${
|
||||||
|
fromOrder.fulfillments.length + 1
|
||||||
|
}`,
|
||||||
|
order_lines: fulfillmentItems.map((item) => {
|
||||||
|
return {
|
||||||
|
ext_ref: item._id,
|
||||||
|
sku: item.content.variant.sku,
|
||||||
|
description: item.title,
|
||||||
|
quantity: item.quantity,
|
||||||
|
country_of_origin:
|
||||||
|
item.content.variant.metadata &&
|
||||||
|
item.content.variant.metadata.origin_country,
|
||||||
|
tarif_number:
|
||||||
|
item.content.variant.metadata &&
|
||||||
|
item.content.variant.metadata.hs_code,
|
||||||
|
unit_price: item.content.unit_price,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
delivery_address: {
|
||||||
|
att_contact: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||||
|
address_1: shipping_address.address_1,
|
||||||
|
address_2: shipping_address.address_2,
|
||||||
|
zip: shipping_address.postal_code,
|
||||||
|
city: shipping_address.city,
|
||||||
|
country_code: shipping_address.country_code,
|
||||||
|
state: shipping_address.province,
|
||||||
|
phone: shipping_address.phone,
|
||||||
|
email: fromOrder.email,
|
||||||
|
},
|
||||||
|
currency: fromOrder.currency_code,
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
order_channel: {
|
||||||
|
data: {
|
||||||
|
id: this.options_.order_channel_id,
|
||||||
|
type: "order_channels",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shipping_rate: {
|
||||||
|
data: {
|
||||||
|
id: methodData.webshipper_id,
|
||||||
|
type: "shipping_rates",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (methodData.require_drop_point) {
|
||||||
|
newOrder.attributes.drop_point = {
|
||||||
|
drop_point_id: methodData.drop_point_id,
|
||||||
|
name: methodData.drop_point_name,
|
||||||
|
zip: methodData.drop_point_zip,
|
||||||
|
address_1: methodData.drop_point_address_1,
|
||||||
|
city: methodData.drop_point_city,
|
||||||
|
country_code: methodData.drop_point_country_code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (invoice) {
|
||||||
|
newOrder.relationships.documents = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: invoice.data.id,
|
||||||
|
type: invoice.data.type,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.client_.orders
|
||||||
|
.create(newOrder)
|
||||||
|
.then((result) => {
|
||||||
|
return result.data
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger_.warn(err.response)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleWebhook(_, body) {
|
||||||
|
const wsOrder = await this.retrieveRelationship(
|
||||||
|
body.data.relationships.order
|
||||||
|
)
|
||||||
|
if (wsOrder.data.attributes.ext_ref) {
|
||||||
|
const trackingNumbers = body.data.attributes.tracking_links.map(
|
||||||
|
(l) => l.number
|
||||||
|
)
|
||||||
|
const [orderId, fulfillmentIndex] = wsOrder.data.attributes.ext_ref.split(
|
||||||
|
"."
|
||||||
|
)
|
||||||
|
|
||||||
|
const order = await this.orderService_.retrieve(orderId)
|
||||||
|
const fulfillment = order.fulfillments[fulfillmentIndex]
|
||||||
|
if (fulfillment) {
|
||||||
|
await this.orderService_.createShipment(
|
||||||
|
order._id,
|
||||||
|
fulfillment._id,
|
||||||
|
trackingNumbers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveDropPoints(id, zip, countryCode, address1) {
|
||||||
|
const points = await this.client_
|
||||||
|
.request({
|
||||||
|
method: "POST",
|
||||||
|
url: `/v2/drop_point_locators`,
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
type: "drop_point_locators",
|
||||||
|
attributes: {
|
||||||
|
shipping_rate_id: id,
|
||||||
|
delivery_address: {
|
||||||
|
zip,
|
||||||
|
country_code: countryCode,
|
||||||
|
address_1: address1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ data }) => data)
|
||||||
|
|
||||||
|
return points.attributes.drop_points
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieveRelationship(relation) {
|
||||||
|
const link = relation.links.related
|
||||||
|
return this.client_.request({
|
||||||
|
method: "GET",
|
||||||
|
url: link,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebshipperFulfillmentService
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
class WebshipperSubscriber {
|
||||||
|
constructor({ eventBusService, logger, webshipperFulfillmentService }) {
|
||||||
|
this.webshipperService_ = webshipperFulfillmentService
|
||||||
|
|
||||||
|
eventBusService.subscribe("webshipper.shipment", this.handleShipment)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShipment = async ({ headers, body }) => {
|
||||||
|
return this.webshipperService_.handleWebhook(headers, body).catch((err) => {
|
||||||
|
logger.warn(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebshipperSubscriber
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
class Webshipper {
|
||||||
|
constructor({ account, token }) {
|
||||||
|
this.account_ = account
|
||||||
|
this.token_ = token
|
||||||
|
this.client_ = axios.create({
|
||||||
|
baseURL: `https://${account}.api.webshipper.io`,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/vnd.api+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.documents = this.buildDocumentEndpoints_()
|
||||||
|
this.shippingRates = this.buildShippingRateEndpoints_()
|
||||||
|
this.orders = this.buildOrderEndpoints_()
|
||||||
|
this.shipments = this.buildShipmentEndpoints_()
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(data) {
|
||||||
|
return this.client_(data).then(({ data }) => data)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDocumentEndpoints_ = () => {
|
||||||
|
return {
|
||||||
|
create: async (data) => {
|
||||||
|
const path = `/v2/documents`
|
||||||
|
return this.client_({
|
||||||
|
method: "POST",
|
||||||
|
url: path,
|
||||||
|
data: {
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildShippingRateEndpoints_ = () => {
|
||||||
|
return {
|
||||||
|
retrieve: async (id) => {
|
||||||
|
const path = `/v2/shipping_rates/${id}`
|
||||||
|
return this.client_({
|
||||||
|
method: "GET",
|
||||||
|
url: path,
|
||||||
|
}).then(({ data }) => data)
|
||||||
|
},
|
||||||
|
list: async () => {
|
||||||
|
const path = `/v2/shipping_rates`
|
||||||
|
return this.client_({
|
||||||
|
method: "GET",
|
||||||
|
url: path,
|
||||||
|
}).then(({ data }) => data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOrderEndpoints_ = () => {
|
||||||
|
return {
|
||||||
|
retrieve: async (id) => {
|
||||||
|
const path = `/v2/orders/${id}`
|
||||||
|
return this.client_({
|
||||||
|
method: "GET",
|
||||||
|
url: path,
|
||||||
|
}).then(({ data }) => data)
|
||||||
|
},
|
||||||
|
create: async (data) => {
|
||||||
|
const path = `/v2/orders`
|
||||||
|
return this.client_({
|
||||||
|
method: "POST",
|
||||||
|
url: path,
|
||||||
|
data: {
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildShipmentEndpoints_ = () => {
|
||||||
|
return {
|
||||||
|
create: async (data) => {
|
||||||
|
const path = `/v2/shipments`
|
||||||
|
return this.client_({
|
||||||
|
method: "POST",
|
||||||
|
url: path,
|
||||||
|
data: {
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
}).then(({ data }) => data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Webshipper
|
||||||
@@ -15,12 +15,36 @@ class BaseFulfillmentService extends BaseService {
|
|||||||
return this.constructor.identifier
|
return this.constructor.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before a shipping option is created in Admin. The method should
|
||||||
|
* return all of the options that the fulfillment provider can be used with,
|
||||||
|
* and it is here the distinction between different shipping options are
|
||||||
|
* enforced. For example, a fulfillment provider may offer Standard Shipping
|
||||||
|
* and Express Shipping as fulfillment options, it is up to the store operator
|
||||||
|
* to create shipping options in Medusa that can be chosen between by the
|
||||||
|
* customer.
|
||||||
|
*/
|
||||||
getFulfillmentOptions() {}
|
getFulfillmentOptions() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before a shipping method is set on a cart to ensure that the data
|
||||||
|
* sent with the shipping method is valid. The data object may contain extra
|
||||||
|
* data about the shipment such as an id of a drop point. It is up to the
|
||||||
|
* fulfillment provider to enforce that the correct data is being sent
|
||||||
|
* through.
|
||||||
|
* @param {object} data - the data to validate
|
||||||
|
* @param {object} cart - the cart to which the shipping method will be applied
|
||||||
|
* @return {object} the data to populate `cart.shipping_methods.$.data` this
|
||||||
|
* is usually important for future actions like generating shipping labels
|
||||||
|
*/
|
||||||
validateFulfillmentData(data, cart) {
|
validateFulfillmentData(data, cart) {
|
||||||
throw Error("validateFulfillmentData must be overridden by the child class")
|
throw Error("validateFulfillmentData must be overridden by the child class")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before a shipping option is created in Admin. Use this to ensure
|
||||||
|
* that a fulfillment option does in fact exist.
|
||||||
|
*/
|
||||||
validateOption(data) {
|
validateOption(data) {
|
||||||
throw Error("validateOption must be overridden by the child class")
|
throw Error("validateOption must be overridden by the child class")
|
||||||
}
|
}
|
||||||
@@ -29,7 +53,10 @@ class BaseFulfillmentService extends BaseService {
|
|||||||
throw Error("canCalculate must be overridden by the child class")
|
throw Error("canCalculate must be overridden by the child class")
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatePrice(data) {
|
/**
|
||||||
|
* Used to calculate a price for a given shipping option.
|
||||||
|
*/
|
||||||
|
calculatePrice(data, cart) {
|
||||||
throw Error("calculatePrice must be overridden by the child class")
|
throw Error("calculatePrice must be overridden by the child class")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ import { sync as existsSync } from "fs-exists-cached"
|
|||||||
* Registers all services in the services directory
|
* Registers all services in the services directory
|
||||||
*/
|
*/
|
||||||
export default async ({ rootDirectory, container, app }) => {
|
export default async ({ rootDirectory, container, app }) => {
|
||||||
const { configModule, configFilePath } = getConfigFile(
|
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
||||||
rootDirectory,
|
|
||||||
`medusa-config`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!configModule) {
|
if (!configModule) {
|
||||||
return
|
return
|
||||||
@@ -53,7 +50,7 @@ export default async ({ rootDirectory, container, app }) => {
|
|||||||
registerModels(pluginDetails, container)
|
registerModels(pluginDetails, container)
|
||||||
await registerServices(pluginDetails, container)
|
await registerServices(pluginDetails, container)
|
||||||
registerMedusaApi(pluginDetails, container)
|
registerMedusaApi(pluginDetails, container)
|
||||||
registerApi(pluginDetails, app, rootDirectory)
|
registerApi(pluginDetails, app, rootDirectory, container)
|
||||||
registerCoreRouters(pluginDetails, container)
|
registerCoreRouters(pluginDetails, container)
|
||||||
registerSubscribers(pluginDetails, container)
|
registerSubscribers(pluginDetails, container)
|
||||||
})
|
})
|
||||||
@@ -143,12 +140,22 @@ function registerCoreRouters(pluginDetails, container) {
|
|||||||
/**
|
/**
|
||||||
* Registers the plugin's api routes.
|
* Registers the plugin's api routes.
|
||||||
*/
|
*/
|
||||||
function registerApi(pluginDetails, app, rootDirectory = "") {
|
function registerApi(pluginDetails, app, rootDirectory = "", container) {
|
||||||
|
const logger = container.resolve("logger")
|
||||||
|
logger.info(`Registering custom endpoints for ${pluginDetails.name}`)
|
||||||
try {
|
try {
|
||||||
const routes = require(`${pluginDetails.resolve}/api`).default
|
const routes = require(`${pluginDetails.resolve}/api`).default
|
||||||
app.use("/", routes(rootDirectory))
|
if (routes) {
|
||||||
|
app.use("/", routes(rootDirectory))
|
||||||
|
}
|
||||||
return app
|
return app
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err.message !== `Cannot find module '${pluginDetails.resolve}/api'`) {
|
||||||
|
logger.warn(
|
||||||
|
`An error occured while registering customer endpoints for ${pluginDetails.name}`
|
||||||
|
)
|
||||||
|
logger.error(err.stack)
|
||||||
|
}
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +225,7 @@ async function registerServices(pluginDetails, container) {
|
|||||||
container.register({
|
container.register({
|
||||||
[name]: asFunction(
|
[name]: asFunction(
|
||||||
cradle => new loaded(cradle, pluginDetails.options)
|
cradle => new loaded(cradle, pluginDetails.options)
|
||||||
),
|
).singleton(),
|
||||||
[`fp_${loaded.identifier}`]: aliasTo(name),
|
[`fp_${loaded.identifier}`]: aliasTo(name),
|
||||||
})
|
})
|
||||||
} else if (loaded.prototype instanceof FileService) {
|
} else if (loaded.prototype instanceof FileService) {
|
||||||
|
|||||||
Reference in New Issue
Block a user