fix(medusa-plugin-addon): Fixes admin endpoints, Adds flag to avoid merging add-on line-items

This commit is contained in:
Oliver Windall Juhl
2020-09-23 09:51:47 +02:00
committed by GitHub
parent 34eb53f72e
commit d8483cd135
14 changed files with 169 additions and 88 deletions
@@ -6,7 +6,7 @@ export default (rootDirectory) => {
const app = Router()
store(app, rootDirectory)
admin(app)
admin(app, rootDirectory)
return app
}
@@ -5,11 +5,11 @@ export default async (req, res) => {
name: Validator.string().required(),
prices: Validator.array()
.items({
currency_code: Validator.string().required(),
amount: Validator.number().required(),
currency_code: Validator.string(),
amount: Validator.number(),
})
.required(),
valid_for: Validator.array().items(Validator.string()).required(),
valid_for: Validator.array().items(),
metadata: Validator.object().optional(),
})
@@ -3,7 +3,13 @@ export default async (req, res) => {
try {
const addOnService = req.scope.resolve("addOnService")
const addOn = await addOnService.retrieve(id)
let addOn = await addOnService.retrieve(id)
addOn = await addOnService.decorate(
addOn,
["name", "valid_for", "prices"],
["valid_for"]
)
res.json({ add_on: addOn })
} catch (err) {
throw err
@@ -1,10 +1,24 @@
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) => {
export default (app, rootDirectory) => {
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
const config = (configModule && configModule.projectConfig) || {}
const adminCors = config.admin_cors || ""
route.use(
cors({
origin: adminCors.split(","),
credentials: true,
})
)
app.use("/admin", route)
route.post(
@@ -19,5 +33,23 @@ export default (app) => {
middlewares.wrap(require("./update-add-on").default)
)
route.get(
"/add-ons",
bodyParser.json(),
middlewares.wrap(require("./list-add-ons").default)
)
route.get(
"/add-ons/:id",
bodyParser.json(),
middlewares.wrap(require("./get-add-on").default)
)
route.delete(
"/add-ons/:id",
bodyParser.json(),
middlewares.wrap(require("./delete-add-on").default)
)
return app
}
@@ -1,10 +1,20 @@
export default async (req, res) => {
try {
const addOnService = req.scope.resolve("addOnService")
const addOns = await addOnService.list({})
let addOns = await addOnService.list({})
addOns = await Promise.all(
addOns.map((ao) =>
addOnService.decorate(
ao,
["name", "valid_for", "prices"],
["valid_for"]
)
)
)
res.status(200).json({ add_ons: addOns })
} catch (err) {
console.log(err)
throw err
}
}
@@ -1,7 +1,7 @@
import { Validator, MedusaError } from "medusa-core-utils";
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params;
const { id } = req.params
const schema = Validator.object().keys({
name: Validator.string().optional(),
@@ -11,21 +11,29 @@ export default async (req, res) => {
amount: Validator.number().required(),
})
.optional(),
valid_for: Validator.array().items(Validator.string()).optional(),
valid_for: Validator.array().optional(),
metadata: Validator.object().optional(),
});
})
const { value, error } = schema.validate(req.body);
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details);
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const addOnService = req.scope.resolve("addOnService");
const addOnService = req.scope.resolve("addOnService")
const addOn = await addOnService.update(id, value);
if (value.metadata) {
Object.entries(value.metadata).map(([key, value]) => {
addOnService.setMetadata(id, key, value)
})
res.status(200).json({ addOn });
delete value.metadata
}
const addOn = await addOnService.update(id, value)
res.status(200).json({ addOn })
} catch (err) {
throw err;
throw err
}
};
}
@@ -29,6 +29,16 @@ export default async (req, res) => {
cart = await cartService.addLineItem(cart._id, lineItem)
cart = await cartService.decorate(cart, [], ["region"])
cart.items = await Promise.all(
cart.items.map((item) =>
lineItemService.decorate(
item,
["title", "quantity", "thumbnail", "content", "should_merge"],
["add_ons"]
)
)
)
res.status(200).json({ cart })
} catch (err) {
@@ -13,7 +13,13 @@ export default async (req, res) => {
try {
const addOnService = req.scope.resolve("addOnService")
const addOn = await addOnService.retrieveByProduct(value.product_id)
let addOn = await addOnService.retrieveByProduct(value.product_id)
addOn = await addOnService.decorate(
addOn,
["name", "valid_for", "prices"],
["valid_for"]
)
res.json({ add_on: addOn })
} catch (err) {
throw err
@@ -42,6 +42,16 @@ export default async (req, res) => {
}
cart = await cartService.decorate(cart, [], ["region"])
cart.items = await Promise.all(
cart.items.map((item) =>
lineItemService.decorate(
item,
["title", "quantity", "thumbnail", "content", "should_merge"],
["add_ons"]
)
)
)
res.status(200).json({ cart })
} catch (err) {
@@ -33,56 +33,6 @@ class AddOnLineItemService extends BaseService {
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
@@ -90,7 +40,7 @@ class AddOnLineItemService extends BaseService {
* @param {*} quantity - number of items
* @param {[string]} addOnIds - id of add-ons
*/
async generate(variantId, regionId, quantity, addOnIds) {
async generate(variantId, regionId, quantity, addOnIds, metadata = {}) {
const variant = await this.productVariantService_.retrieve(variantId)
const region = await this.regionService_.retrieve(regionId)
@@ -132,13 +82,16 @@ class AddOnLineItemService extends BaseService {
title: product.title,
quantity,
thumbnail: product.thumbnail,
should_merge: false,
content: {
unit_price: unitPrice * quantity,
variant,
product,
quantity: 1,
},
should_merge: false,
metadata: {
...metadata,
add_ons: addOnIds,
},
}
@@ -146,26 +99,17 @@ class AddOnLineItemService extends BaseService {
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
async decorate(lineItem, fields, expandFields = []) {
const requiredFields = ["_id", "metadata"]
const decorated = _.pick(lineItem, fields.concat(requiredFields))
if (expandFields.includes("add_ons") && decorated.metadata.add_ons) {
decorated.metadata.add_ons = await Promise.all(
decorated.metadata.add_ons.map(
async (ao) => await this.addOnService_.retrieve(ao)
)
}
} else if (!Array.isArray(match.content)) {
return (
line.content.variant._id.equals(match.content.variant._id) &&
line.content.quantity === match.content.quantity
)
}
return false
return decorated
}
}
@@ -197,6 +197,57 @@ class AddOnService extends BaseService {
`A price for region: ${region.name} could not be found`
)
}
/**
* Decorates a add-on with add-on variants.
* @param {AddOn} addOn - the add-on to decorate.
* @param {string[]} fields - the fields to include.
* @param {string[]} expandFields - fields to expand.
* @return {AddOn} return the decorated add-on.
*/
async decorate(addOn, fields, expandFields = []) {
const requiredFields = ["_id", "metadata"]
const decorated = _.pick(addOn, fields.concat(requiredFields))
if (expandFields.includes("valid_for")) {
decorated.valid_for = await Promise.all(
decorated.valid_for.map(
async (p) => await this.productService_.retrieve(p)
)
)
}
return decorated
}
/**
* Dedicated method to set metadata for an add-on.
* To ensure that plugins does not overwrite each
* others metadata fields, setMetadata is provided.
* @param {string} addOnId - the add-on to decorate.
* @param {string} key - key for metadata field
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
*/
async setMetadata(addOnId, key, value) {
const validatedId = this.validateId_(addOnId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.addOnModel_
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
.then((result) => {
this.eventBus_.emit(AddOnService.Events.UPDATED, result)
return result
})
.catch((err) => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
}
export default AddOnService
@@ -8,6 +8,7 @@ export default new mongoose.Schema({
description: { type: String },
thumbnail: { type: String },
is_giftcard: { type: Boolean, default: false },
should_merge: { type: Boolean, default: true },
has_shipping: { type: Boolean, default: false },
// mongoose doesn't allow multi-type validation but this field allows both
+1 -1
View File
@@ -375,7 +375,7 @@ class CartService extends BaseService {
// If content matches one of the line items currently in the cart we can
// simply update the quantity of the existing line item
if (currentItem) {
if (currentItem && validatedLineItem.should_merge) {
const newQuantity = currentItem.quantity + validatedLineItem.quantity
// Confirm inventory
@@ -38,6 +38,7 @@ class LineItemService extends BaseService {
const lineItemSchema = Validator.object({
title: Validator.string().required(),
is_giftcard: Validator.bool().optional(),
should_merge: Validator.bool().optional(),
description: Validator.string()
.allow("")
.optional(),
@@ -110,6 +111,7 @@ class LineItemService extends BaseService {
title: product.title,
description: variant.title,
quantity,
should_merge: true,
thumbnail: product.thumbnail,
content: {
unit_price,
@@ -117,6 +119,7 @@ class LineItemService extends BaseService {
product,
quantity: 1,
},
metadata,
}
if (product.is_giftcard) {