feat(plugins): Adds add-on plugin
Adds an add-on plugin, that supports adding add-ons to line items in the cart
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
|
||||||
|
|
||||||
|
/dist
|
||||||
|
|
||||||
|
/api
|
||||||
|
/services
|
||||||
|
/models
|
||||||
|
/subscribers
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/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 @@
|
|||||||
|
// noop
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "medusa-plugin-add-ons",
|
||||||
|
"version": "1.0.0-alpha.30",
|
||||||
|
"description": "Add-on plugin for Medusa Commerce",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/medusajs/medusa",
|
||||||
|
"directory": "packages/medusa-plugin-add-ons"
|
||||||
|
},
|
||||||
|
"author": "Oliver Juhl",
|
||||||
|
"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": "^1.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "babel src -d .",
|
||||||
|
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||||
|
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"medusa-core-utils": "^1.0.10",
|
||||||
|
"redis": "^3.0.2"
|
||||||
|
},
|
||||||
|
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import admin from "./routes/admin"
|
||||||
|
import store from "./routes/store"
|
||||||
|
|
||||||
|
export default (rootDirectory) => {
|
||||||
|
const app = Router()
|
||||||
|
|
||||||
|
store(app, rootDirectory)
|
||||||
|
admin(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { default as wrap } from "./await-middleware"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
wrap,
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const schema = Validator.object().keys({
|
||||||
|
name: Validator.string().required(),
|
||||||
|
prices: Validator.array()
|
||||||
|
.items({
|
||||||
|
currency_code: Validator.string().required(),
|
||||||
|
amount: Validator.number().required(),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
valid_for: Validator.array().items(Validator.string()).required(),
|
||||||
|
metadata: Validator.object().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(req.body)
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const addOnService = req.scope.resolve("addOnService")
|
||||||
|
|
||||||
|
const addOn = await addOnService.create(value)
|
||||||
|
|
||||||
|
res.status(200).json({ addOn })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const addOnService = req.scope.resolve("addOnService")
|
||||||
|
try {
|
||||||
|
await addOnService.delete(id)
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
id,
|
||||||
|
object: "addOn",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
const addOnService = req.scope.resolve("addOnService")
|
||||||
|
const addOn = await addOnService.retrieve(id)
|
||||||
|
res.json({ add_on: addOn })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import bodyParser from "body-parser"
|
||||||
|
import middlewares from "../../middlewares"
|
||||||
|
|
||||||
|
const route = Router()
|
||||||
|
|
||||||
|
export default (app) => {
|
||||||
|
app.use("/admin", route)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/add-ons",
|
||||||
|
bodyParser.json(),
|
||||||
|
middlewares.wrap(require("./create-add-on").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/add-ons/:id",
|
||||||
|
bodyParser.json(),
|
||||||
|
middlewares.wrap(require("./update-add-on").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default async (req, res) => {
|
||||||
|
try {
|
||||||
|
const addOnService = req.scope.resolve("addOnService")
|
||||||
|
const addOns = await addOnService.list({})
|
||||||
|
|
||||||
|
res.status(200).json({ add_ons: addOns })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Validator, MedusaError } from "medusa-core-utils";
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const schema = Validator.object().keys({
|
||||||
|
name: Validator.string().optional(),
|
||||||
|
prices: Validator.array()
|
||||||
|
.items({
|
||||||
|
currency_code: Validator.string().required(),
|
||||||
|
amount: Validator.number().required(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
valid_for: Validator.array().items(Validator.string()).optional(),
|
||||||
|
metadata: Validator.object().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(req.body);
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const addOnService = req.scope.resolve("addOnService");
|
||||||
|
|
||||||
|
const addOn = await addOnService.update(id, value);
|
||||||
|
|
||||||
|
res.status(200).json({ addOn });
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const schema = Validator.object().keys({
|
||||||
|
variant_id: Validator.string().required(),
|
||||||
|
quantity: Validator.number().required(),
|
||||||
|
add_ons: Validator.array().items(Validator.string()).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(req.body)
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lineItemService = req.scope.resolve("addOnLineItemService")
|
||||||
|
const cartService = req.scope.resolve("cartService")
|
||||||
|
|
||||||
|
let cart = await cartService.retrieve(id)
|
||||||
|
|
||||||
|
const lineItem = await lineItemService.generate(
|
||||||
|
value.variant_id,
|
||||||
|
cart.region_id,
|
||||||
|
value.quantity,
|
||||||
|
value.add_ons
|
||||||
|
)
|
||||||
|
|
||||||
|
cart = await cartService.addLineItem(cart._id, lineItem)
|
||||||
|
cart = await cartService.decorate(cart, [], ["region"])
|
||||||
|
|
||||||
|
res.status(200).json({ cart })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Validator } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const schema = Validator.object({
|
||||||
|
product_id: Validator.string().required(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(region_id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const addOnService = req.scope.resolve("addOnService")
|
||||||
|
const addOn = await addOnService.retrieveByProduct(value.product_id)
|
||||||
|
res.json({ add_on: addOn })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import bodyParser from "body-parser"
|
||||||
|
import cors from "cors"
|
||||||
|
import middlewares from "../../middlewares"
|
||||||
|
import { getConfigFile } from "medusa-core-utils"
|
||||||
|
|
||||||
|
const route = Router()
|
||||||
|
|
||||||
|
export default (app, rootDirectory) => {
|
||||||
|
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
||||||
|
const config = (configModule && configModule.projectConfig) || {}
|
||||||
|
|
||||||
|
const storeCors = config.store_cors || ""
|
||||||
|
|
||||||
|
route.use(
|
||||||
|
cors({
|
||||||
|
origin: storeCors.split(","),
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
app.use("/store", route)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/carts/:id/line-items/add-on",
|
||||||
|
bodyParser.json(),
|
||||||
|
middlewares.wrap(require("./create-line-item").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/carts/:id/line-items/:line_id/add-on",
|
||||||
|
bodyParser.json(),
|
||||||
|
middlewares.wrap(require("./update-line-item").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id, line_id } = req.params
|
||||||
|
|
||||||
|
const schema = Validator.object().keys({
|
||||||
|
add_ons: Validator.array().items(Validator.string()).optional(),
|
||||||
|
quantity: Validator.number().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(req.body)
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lineItemService = req.scope.resolve("addOnLineItemService")
|
||||||
|
const cartService = req.scope.resolve("cartService")
|
||||||
|
|
||||||
|
let cart
|
||||||
|
if (value.quantity === 0) {
|
||||||
|
cart = await cartService.removeLineItem(id, line_id)
|
||||||
|
} else {
|
||||||
|
cart = await cartService.retrieve(id)
|
||||||
|
|
||||||
|
const existing = cart.items.find((i) => i._id.equals(line_id))
|
||||||
|
if (!existing) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Could not find the line item"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineItem = await lineItemService.generate(
|
||||||
|
existing.content.variant._id,
|
||||||
|
cart.region_id,
|
||||||
|
value.quantity,
|
||||||
|
value.add_ons
|
||||||
|
)
|
||||||
|
|
||||||
|
cart = await cartService.updateLineItem(cart._id, line_id, lineItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
cart = await cartService.decorate(cart, [], ["region"])
|
||||||
|
|
||||||
|
res.status(200).json({ cart })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const addOns = {
|
||||||
|
testAddOn: {
|
||||||
|
_id: IdMap.getId("test-add-on"),
|
||||||
|
name: "Chili",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "DKK",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
valid_for: [IdMap.getId("test-product")],
|
||||||
|
},
|
||||||
|
testAddOn2: {
|
||||||
|
_id: IdMap.getId("test-add-on-2"),
|
||||||
|
name: "Chili",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "DKK",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
valid_for: [IdMap.getId("test-product")],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddOnModelMock = {
|
||||||
|
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
find: jest.fn().mockImplementation((query) => {
|
||||||
|
return Promise.resolve([addOns.testAddOn, addOns.testAddOn2])
|
||||||
|
}),
|
||||||
|
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
findOne: jest.fn().mockImplementation((query) => {
|
||||||
|
if (query._id === IdMap.getId("test-add-on")) {
|
||||||
|
return Promise.resolve(addOns.testAddOn)
|
||||||
|
}
|
||||||
|
if (query._id === IdMap.getId("test-add-on-2")) {
|
||||||
|
return Promise.resolve(addOns.testAddOn2)
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import mongoose from "mongoose"
|
||||||
|
import { BaseModel } from "medusa-interfaces"
|
||||||
|
|
||||||
|
class AddOnModel extends BaseModel {
|
||||||
|
static modelName = "AddOn"
|
||||||
|
static schema = {
|
||||||
|
name: { type: String, required: true },
|
||||||
|
prices: { type: [], required: true },
|
||||||
|
// Valid products
|
||||||
|
valid_for: { type: [String], required: true },
|
||||||
|
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddOnModel
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const addOns = {
|
||||||
|
testAddOn: {
|
||||||
|
_id: IdMap.getId("test-add-on"),
|
||||||
|
name: "Chili",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "DKK",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
valid_for: [IdMap.getId("test-product")],
|
||||||
|
},
|
||||||
|
testAddOn2: {
|
||||||
|
_id: IdMap.getId("test-add-on-2"),
|
||||||
|
name: "Chili",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "DKK",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
valid_for: [IdMap.getId("test-product")],
|
||||||
|
},
|
||||||
|
testAddOn3: {
|
||||||
|
_id: IdMap.getId("test-add-on-3"),
|
||||||
|
name: "Herbs",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "DKK",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
valid_for: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddOnServiceMock = {
|
||||||
|
retrieve: jest.fn().mockImplementation((addOnId) => {
|
||||||
|
if (addOnId === IdMap.getId("test-add-on")) {
|
||||||
|
return Promise.resolve(addOns.testAddOn)
|
||||||
|
}
|
||||||
|
if (addOnId === IdMap.getId("test-add-on-2")) {
|
||||||
|
return Promise.resolve(addOns.testAddOn2)
|
||||||
|
}
|
||||||
|
if (addOnId === IdMap.getId("test-add-on-3")) {
|
||||||
|
return Promise.resolve(addOns.testAddOn3)
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}),
|
||||||
|
getRegionPrice: jest.fn().mockImplementation((addOnId, regionId) => {
|
||||||
|
if (addOnId === IdMap.getId("test-add-on")) {
|
||||||
|
return Promise.resolve(20)
|
||||||
|
}
|
||||||
|
if (addOnId === IdMap.getId("test-add-on-2")) {
|
||||||
|
return Promise.resolve(20)
|
||||||
|
}
|
||||||
|
if (addOnId === IdMap.getId("test-add-on-3")) {
|
||||||
|
return Promise.resolve(20)
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return AddOnServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const EventBusServiceMock = {
|
||||||
|
emit: jest.fn(),
|
||||||
|
subscribe: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return EventBusServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
const variant1 = {
|
||||||
|
_id: IdMap.getId("test-variant-1"),
|
||||||
|
title: "variant1",
|
||||||
|
options: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const variant2 = {
|
||||||
|
_id: IdMap.getId("test-variant-2"),
|
||||||
|
title: "variant2",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: IdMap.getId("color_id"),
|
||||||
|
value: "black",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
option_id: IdMap.getId("size_id"),
|
||||||
|
value: "160",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const variant3 = {
|
||||||
|
_id: IdMap.getId("test-variant-3"),
|
||||||
|
title: "variant3",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: IdMap.getId("color_id"),
|
||||||
|
value: "blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
option_id: IdMap.getId("size_id"),
|
||||||
|
value: "150",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const variant4 = {
|
||||||
|
_id: IdMap.getId("test-variant-4"),
|
||||||
|
title: "variant4",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: IdMap.getId("color_id"),
|
||||||
|
value: "blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
option_id: IdMap.getId("size_id"),
|
||||||
|
value: "50",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const variants = {
|
||||||
|
one: variant1,
|
||||||
|
two: variant2,
|
||||||
|
three: variant3,
|
||||||
|
four: variant4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductVariantServiceMock = {
|
||||||
|
retrieve: jest.fn().mockImplementation((variantId) => {
|
||||||
|
if (variantId === IdMap.getId("test-variant-1")) {
|
||||||
|
return Promise.resolve(variant1)
|
||||||
|
}
|
||||||
|
if (variantId === IdMap.getId("test-variant-2")) {
|
||||||
|
return Promise.resolve(variant2)
|
||||||
|
}
|
||||||
|
if (variantId === IdMap.getId("test-variant-3")) {
|
||||||
|
return Promise.resolve(variant3)
|
||||||
|
}
|
||||||
|
if (variantId === IdMap.getId("test-variant-4")) {
|
||||||
|
return Promise.resolve(variant4)
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}),
|
||||||
|
getRegionPrice: jest.fn().mockImplementation((variantId, regionId) => {
|
||||||
|
if (variantId === IdMap.getId("test-variant-1")) {
|
||||||
|
if (regionId === IdMap.getId("world")) {
|
||||||
|
return Promise.resolve(10)
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error("Not found"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return ProductVariantServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const products = {
|
||||||
|
product1: {
|
||||||
|
_id: IdMap.getId("test-product"),
|
||||||
|
description: "Test description",
|
||||||
|
title: "Product 1",
|
||||||
|
variants: [IdMap.getId("test-variant-1")],
|
||||||
|
// metadata: {
|
||||||
|
// add_ons: [IdMap.getId("test-add-on"), IdMap.getId("test-add-on-2")],
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
product2: {
|
||||||
|
_id: IdMap.getId("test-product-2"),
|
||||||
|
title: "Product 2",
|
||||||
|
metadata: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductServiceMock = {
|
||||||
|
retrieve: jest.fn().mockImplementation((productId) => {
|
||||||
|
if (productId === IdMap.getId("test-product")) {
|
||||||
|
return Promise.resolve(products.product1)
|
||||||
|
}
|
||||||
|
if (productId === IdMap.getId("test-product-2")) {
|
||||||
|
return Promise.resolve(products.product2)
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}),
|
||||||
|
list: jest.fn().mockImplementation((query) => {
|
||||||
|
return Promise.resolve([products.product1])
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return ProductServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const regions = {
|
||||||
|
testRegion: {
|
||||||
|
_id: IdMap.getId("world"),
|
||||||
|
name: "Test Region",
|
||||||
|
countries: ["DK", "US", "DE"],
|
||||||
|
tax_rate: 0.25,
|
||||||
|
payment_providers: ["default_provider", "unregistered"],
|
||||||
|
fulfillment_providers: ["test_shipper"],
|
||||||
|
currency_code: "DKK",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RegionServiceMock = {
|
||||||
|
retrieve: jest.fn().mockImplementation((regionId) => {
|
||||||
|
if (regionId === IdMap.getId("world")) {
|
||||||
|
return Promise.resolve(regions.testRegion)
|
||||||
|
}
|
||||||
|
throw Error(regionId + "not found")
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return RegionServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import AddOnLineItemService from "../add-on-line-item"
|
||||||
|
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||||
|
import { ProductServiceMock } from "../__mocks__/product"
|
||||||
|
import { RegionServiceMock } from "../__mocks__/region"
|
||||||
|
import { AddOnServiceMock } from "../__mocks__/add-on"
|
||||||
|
|
||||||
|
describe("LineItemService", () => {
|
||||||
|
describe("generate", () => {
|
||||||
|
let result
|
||||||
|
|
||||||
|
const lineItemService = new AddOnLineItemService({
|
||||||
|
addOnService: AddOnServiceMock,
|
||||||
|
productVariantService: ProductVariantServiceMock,
|
||||||
|
productService: ProductServiceMock,
|
||||||
|
regionService: RegionServiceMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates line item and successfully calculates full unit_price", async () => {
|
||||||
|
result = await lineItemService.generate(
|
||||||
|
IdMap.getId("test-variant-1"),
|
||||||
|
IdMap.getId("world"),
|
||||||
|
1,
|
||||||
|
[IdMap.getId("test-add-on"), IdMap.getId("test-add-on-2")]
|
||||||
|
)
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: "Product 1",
|
||||||
|
thumbnail: undefined,
|
||||||
|
content: {
|
||||||
|
unit_price: 50,
|
||||||
|
variant: {
|
||||||
|
_id: IdMap.getId("test-variant-1"),
|
||||||
|
title: "variant1",
|
||||||
|
options: [],
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
_id: IdMap.getId("test-product"),
|
||||||
|
description: "Test description",
|
||||||
|
title: "Product 1",
|
||||||
|
variants: [IdMap.getId("test-variant-1")],
|
||||||
|
},
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
add_ons: [IdMap.getId("test-add-on"), IdMap.getId("test-add-on-2")],
|
||||||
|
},
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates line item and successfully calculates full unit_price for large quantity", async () => {
|
||||||
|
result = await lineItemService.generate(
|
||||||
|
IdMap.getId("test-variant-1"),
|
||||||
|
IdMap.getId("world"),
|
||||||
|
3,
|
||||||
|
[IdMap.getId("test-add-on"), IdMap.getId("test-add-on-2")]
|
||||||
|
)
|
||||||
|
expect(result).toEqual({
|
||||||
|
title: "Product 1",
|
||||||
|
thumbnail: undefined,
|
||||||
|
content: {
|
||||||
|
unit_price: 150,
|
||||||
|
variant: {
|
||||||
|
_id: IdMap.getId("test-variant-1"),
|
||||||
|
title: "variant1",
|
||||||
|
options: [],
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
_id: IdMap.getId("test-product"),
|
||||||
|
description: "Test description",
|
||||||
|
title: "Product 1",
|
||||||
|
variants: [IdMap.getId("test-variant-1")],
|
||||||
|
},
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
add_ons: [IdMap.getId("test-add-on"), IdMap.getId("test-add-on-2")],
|
||||||
|
},
|
||||||
|
quantity: 3,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails if variant has no associated product", async () => {
|
||||||
|
try {
|
||||||
|
await lineItemService.generate(
|
||||||
|
IdMap.getId("test-variant-1"),
|
||||||
|
IdMap.getId("world"),
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
IdMap.getId("test-add-on"),
|
||||||
|
IdMap.getId("test-add-on-2"),
|
||||||
|
IdMap.getId("test-add-on-3"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toBe(`Herbs can not be added to Product 1`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { AddOnModelMock, addOns } from "../../models/__mocks__/add-on"
|
||||||
|
import AddOnService from "../add-on"
|
||||||
|
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||||
|
import { ProductServiceMock } from "../__mocks__/product"
|
||||||
|
|
||||||
|
describe("AddOnService", () => {
|
||||||
|
describe("create", () => {
|
||||||
|
const addOnService = new AddOnService({
|
||||||
|
addOnModel: AddOnModelMock,
|
||||||
|
productService: ProductServiceMock,
|
||||||
|
eventBusService: EventBusServiceMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls model layer create", async () => {
|
||||||
|
await addOnService.create({
|
||||||
|
name: "Chili",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "DKK",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
valid_for: [IdMap.getId("test-product")],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(AddOnModelMock.create).toBeCalledTimes(1)
|
||||||
|
expect(AddOnModelMock.create).toBeCalledWith({
|
||||||
|
name: "Chili",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "DKK",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
valid_for: [IdMap.getId("test-product")],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("retrieve", () => {
|
||||||
|
let result
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const addOnService = new AddOnService({
|
||||||
|
addOnModel: AddOnModelMock,
|
||||||
|
})
|
||||||
|
result = await addOnService.retrieve(IdMap.getId("test-add-on"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls model layer retrieve", async () => {
|
||||||
|
expect(AddOnModelMock.findOne).toBeCalledTimes(1)
|
||||||
|
expect(AddOnModelMock.findOne).toBeCalledWith({
|
||||||
|
_id: IdMap.getId("test-add-on"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns the add-on", () => {
|
||||||
|
expect(result).toEqual(addOns.testAddOn)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
const addOnService = new AddOnService({
|
||||||
|
addOnModel: AddOnModelMock,
|
||||||
|
productService: ProductServiceMock,
|
||||||
|
eventBusService: EventBusServiceMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls model layer create", async () => {
|
||||||
|
await addOnService.update(IdMap.getId("test-add-on"), {
|
||||||
|
name: "Chili Spice",
|
||||||
|
valid_for: [IdMap.getId("test-product"), IdMap.getId("test-product-2")],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(AddOnModelMock.updateOne).toBeCalledTimes(1)
|
||||||
|
expect(AddOnModelMock.updateOne).toBeCalledWith(
|
||||||
|
{ _id: IdMap.getId("test-add-on") },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
name: "Chili Spice",
|
||||||
|
valid_for: [
|
||||||
|
IdMap.getId("test-product"),
|
||||||
|
IdMap.getId("test-product-2"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ runValidators: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("retrieveByProduct", () => {
|
||||||
|
describe("successful retrieval", () => {
|
||||||
|
let result
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const addOnService = new AddOnService({
|
||||||
|
addOnModel: AddOnModelMock,
|
||||||
|
productService: ProductServiceMock,
|
||||||
|
})
|
||||||
|
result = await addOnService.retrieveByProduct(
|
||||||
|
IdMap.getId("test-product")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls ProductService retrieve", async () => {
|
||||||
|
expect(ProductServiceMock.retrieve).toBeCalledTimes(1)
|
||||||
|
expect(ProductServiceMock.retrieve).toBeCalledWith(
|
||||||
|
IdMap.getId("test-product")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls model layer", () => {
|
||||||
|
expect(AddOnModelMock.find).toBeCalledTimes(1)
|
||||||
|
expect(AddOnModelMock.find).toBeCalledWith({
|
||||||
|
valid_for: IdMap.getId("test-product"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns the add-ons", () => {
|
||||||
|
expect(result).toEqual([addOns.testAddOn, addOns.testAddOn2])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import _ from "lodash"
|
||||||
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
class AddOnLineItemService extends BaseService {
|
||||||
|
static Events = {
|
||||||
|
UPDATED: "add_on.updated",
|
||||||
|
CREATED: "add_on.created",
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
addOnService,
|
||||||
|
productService,
|
||||||
|
productVariantService,
|
||||||
|
regionService,
|
||||||
|
eventBusService,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.addOnService_ = addOnService
|
||||||
|
|
||||||
|
this.productService_ = productService
|
||||||
|
|
||||||
|
this.productVariantService_ = productVariantService
|
||||||
|
|
||||||
|
this.regionService_ = regionService
|
||||||
|
|
||||||
|
this.eventBus_ = eventBusService
|
||||||
|
|
||||||
|
this.options_ = options
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to validate line items.
|
||||||
|
* @param {object} rawLineItem - the raw line item to validate.
|
||||||
|
* @return {object} the validated id
|
||||||
|
*/
|
||||||
|
validate(rawLineItem) {
|
||||||
|
const content = Validator.object({
|
||||||
|
unit_price: Validator.number().required(),
|
||||||
|
variant: Validator.object().required(),
|
||||||
|
product: Validator.object().required(),
|
||||||
|
quantity: Validator.number().integer().min(1).default(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const lineItemSchema = Validator.object({
|
||||||
|
title: Validator.string().required(),
|
||||||
|
is_giftcard: Validator.bool().optional(),
|
||||||
|
description: Validator.string().allow("").optional(),
|
||||||
|
thumbnail: Validator.string().allow("").optional(),
|
||||||
|
content: Validator.alternatives()
|
||||||
|
.try(content, Validator.array().items(content))
|
||||||
|
.required(),
|
||||||
|
quantity: Validator.number().integer().min(1).required(),
|
||||||
|
metadata: Validator.object().default({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = lineItemSchema.validate(rawLineItem)
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
error.details[0].message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contents of a line item
|
||||||
|
* @typedef {(object | array)} LineItemContent
|
||||||
|
* @property {number} unit_price - the price of the content
|
||||||
|
* @property {object} variant - the product variant of the content
|
||||||
|
* @property {object} product - the product of the content
|
||||||
|
* @property {number} quantity - the quantity of the content
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of contents grouped in the same line item
|
||||||
|
* @typedef {LineItemContent[]} LineItemContentArray
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a line item.
|
||||||
|
* @param {string} variantId - id of the line item variant
|
||||||
|
* @param {*} regionId - id of the cart region
|
||||||
|
* @param {*} quantity - number of items
|
||||||
|
* @param {[string]} addOnIds - id of add-ons
|
||||||
|
*/
|
||||||
|
async generate(variantId, regionId, quantity, addOnIds) {
|
||||||
|
const variant = await this.productVariantService_.retrieve(variantId)
|
||||||
|
const region = await this.regionService_.retrieve(regionId)
|
||||||
|
|
||||||
|
const products = await this.productService_.list({ variants: variantId })
|
||||||
|
// this should never fail, since a variant must have a product associated
|
||||||
|
// with it to exists, but better safe than sorry
|
||||||
|
if (!products.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Could not find product for variant with id: ${variantId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = products[0]
|
||||||
|
|
||||||
|
let unitPrice = await this.productVariantService_.getRegionPrice(
|
||||||
|
variant._id,
|
||||||
|
region._id
|
||||||
|
)
|
||||||
|
|
||||||
|
const addOnPrices = await Promise.all(
|
||||||
|
addOnIds.map(async (id) => {
|
||||||
|
const addOn = await this.addOnService_.retrieve(id)
|
||||||
|
// Check if any of the add-ons can't be added to the product
|
||||||
|
if (!addOn.valid_for.includes(`${product._id}`)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`${addOn.name} can not be added to ${product.title}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return await this.addOnService_.getRegionPrice(id, region._id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unitPrice += _.sum(addOnPrices)
|
||||||
|
|
||||||
|
const line = {
|
||||||
|
title: product.title,
|
||||||
|
quantity,
|
||||||
|
thumbnail: product.thumbnail,
|
||||||
|
content: {
|
||||||
|
unit_price: unitPrice * quantity,
|
||||||
|
variant,
|
||||||
|
product,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
add_ons: addOnIds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
isEqual(line, match) {
|
||||||
|
if (Array.isArray(line.content)) {
|
||||||
|
if (
|
||||||
|
Array.isArray(match.content) &&
|
||||||
|
match.content.length === line.content.length
|
||||||
|
) {
|
||||||
|
return line.content.every(
|
||||||
|
(c, index) =>
|
||||||
|
c.variant._id.equals(match[index].variant._id) &&
|
||||||
|
c.quantity === match[index].quantity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!Array.isArray(match.content)) {
|
||||||
|
return (
|
||||||
|
line.content.variant._id.equals(match.content.variant._id) &&
|
||||||
|
line.content.quantity === match.content.quantity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddOnLineItemService
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import _ from "lodash"
|
||||||
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
class AddOnService extends BaseService {
|
||||||
|
static Events = {
|
||||||
|
UPDATED: "add_on.updated",
|
||||||
|
CREATED: "add_on.created",
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
addOnModel,
|
||||||
|
productService,
|
||||||
|
productVariantService,
|
||||||
|
regionService,
|
||||||
|
eventBusService,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.addOnModel_ = addOnModel
|
||||||
|
|
||||||
|
this.productService_ = productService
|
||||||
|
|
||||||
|
this.productVariantService_ = productVariantService
|
||||||
|
|
||||||
|
this.regionService_ = regionService
|
||||||
|
|
||||||
|
this.eventBus_ = eventBusService
|
||||||
|
|
||||||
|
this.options_ = options
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to validate add-on ids. Throws an error if the cast fails
|
||||||
|
* @param {string} rawId - the raw add-on id to validate.
|
||||||
|
* @return {string} the validated id
|
||||||
|
*/
|
||||||
|
validateId_(rawId) {
|
||||||
|
const schema = Validator.objectId()
|
||||||
|
const { value, error } = schema.validate(rawId.toString())
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
"The addOnId could not be casted to an ObjectId"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} selector - the query object for find
|
||||||
|
* @return {Promise} the result of the find operation
|
||||||
|
*/
|
||||||
|
list(selector, offset, limit) {
|
||||||
|
return this.addOnModel_.find(selector, {}, offset, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an add-on by id.
|
||||||
|
* @param {string} addOnId - the id of the add-on to get.
|
||||||
|
* @return {Promise<AddOn>} the add-on document.
|
||||||
|
*/
|
||||||
|
async retrieve(addOnId) {
|
||||||
|
const validatedId = this.validateId_(addOnId)
|
||||||
|
const addOn = await this.addOnModel_
|
||||||
|
.findOne({ _id: validatedId })
|
||||||
|
.catch((err) => {
|
||||||
|
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!addOn) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Add-on with ${addOnId} was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return addOn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an add-on.
|
||||||
|
* @param {object} addOn - the add-on to create
|
||||||
|
* @return {Promise} resolves to the creation result.
|
||||||
|
*/
|
||||||
|
async create(addOn) {
|
||||||
|
await Promise.all(
|
||||||
|
addOn.valid_for.map((prodId) => {
|
||||||
|
this.productService_.retrieve(prodId)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.addOnModel_
|
||||||
|
.create(addOn)
|
||||||
|
.then((result) => {
|
||||||
|
this.eventBus_.emit(AddOnService.Events.CREATED, result)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an add-on.
|
||||||
|
* @param {object} addOnId - the add-on to delete
|
||||||
|
* @return {Promise} resolves to the deletion result.
|
||||||
|
*/
|
||||||
|
async delete(addOnId) {
|
||||||
|
const addOn = await this.retrieve(addOnId)
|
||||||
|
return this.addOnModel_.deleteOne({ _id: addOn._id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all valid add-ons for a given product.
|
||||||
|
* @param {object} productId - the product id to find add-ons for
|
||||||
|
* @return {Promise} returns a promise containing all add-ons for the product
|
||||||
|
*/
|
||||||
|
async retrieveByProduct(productId) {
|
||||||
|
const product = await this.productService_.retrieve(productId)
|
||||||
|
return this.addOnModel_.find({ valid_for: product._id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an add-on. Metadata updates should use dedicated methods, e.g.
|
||||||
|
* `setMetadata`, etc. The function will throw errors if metadata updates
|
||||||
|
* are attempted.
|
||||||
|
* @param {string} addOnId - the id of the add-on. Must be a string that
|
||||||
|
* can be casted to an ObjectId
|
||||||
|
* @param {object} update - an object with the update values.
|
||||||
|
* @return {Promise} resolves to the update result.
|
||||||
|
*/
|
||||||
|
async update(addOnId, update) {
|
||||||
|
const validatedId = this.validateId_(addOnId)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
update.valid_for.map((prodId) => {
|
||||||
|
this.productService_.retrieve(prodId)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (update.metadata) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Use setMetadata to update metadata fields"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addOnModel_
|
||||||
|
.updateOne(
|
||||||
|
{ _id: validatedId },
|
||||||
|
{ $set: update },
|
||||||
|
{ runValidators: true }
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the price specific to a region. If no region specific money amount
|
||||||
|
* exists the function will try to use a currency price. If no default
|
||||||
|
* currency price exists the function will throw an error.
|
||||||
|
* @param {string} addOnId - the id of the add-on to get price from
|
||||||
|
* @param {string} regionId - the id of the region to get price for
|
||||||
|
* @return {number} the price specific to the region
|
||||||
|
*/
|
||||||
|
async getRegionPrice(addOnId, regionId) {
|
||||||
|
const addOn = await this.retrieve(addOnId)
|
||||||
|
const region = await this.regionService_.retrieve(regionId)
|
||||||
|
|
||||||
|
let price
|
||||||
|
addOn.prices.forEach(({ amount, currency_code }) => {
|
||||||
|
if (!price && currency_code === region.currency_code) {
|
||||||
|
// If we haven't yet found a price and the current money amount is
|
||||||
|
// the default money amount for the currency of the region we have found
|
||||||
|
// a possible price match
|
||||||
|
price = amount
|
||||||
|
} else if (region_id === region._id) {
|
||||||
|
// If the region matches directly with the money amount this is the best
|
||||||
|
// price
|
||||||
|
price = amount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return the price if we found a suitable match
|
||||||
|
if (price) {
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got this far no price could be found for the region
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`A price for region: ${region.name} could not be found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddOnService
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,12 @@ export default (app, container) => {
|
|||||||
|
|
||||||
app.use("/carts", route)
|
app.use("/carts", route)
|
||||||
|
|
||||||
|
// Inject plugin routes
|
||||||
|
const routers = middlewareService.getRouters("store/carts")
|
||||||
|
for (const router of routers) {
|
||||||
|
route.use("/", router)
|
||||||
|
}
|
||||||
|
|
||||||
route.get("/:id", middlewares.wrap(require("./get-cart").default))
|
route.get("/:id", middlewares.wrap(require("./get-cart").default))
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
|
|||||||
Reference in New Issue
Block a user