From d8483cd1352ecc587112723786b7c31882f9416e Mon Sep 17 00:00:00 2001 From: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Wed, 23 Sep 2020 09:51:47 +0200 Subject: [PATCH] fix(medusa-plugin-addon): Fixes admin endpoints, Adds flag to avoid merging add-on line-items --- .../medusa-plugin-add-ons/src/api/index.js | 2 +- .../src/api/routes/admin/create-add-on.js | 6 +- .../src/api/routes/admin/get-add-on.js | 8 +- .../src/api/routes/admin/index.js | 34 +++++++- .../src/api/routes/admin/list-add-ons.js | 12 ++- .../src/api/routes/admin/update-add-on.js | 30 ++++--- .../src/api/routes/store/create-line-item.js | 10 +++ .../src/api/routes/store/get-by-product.js | 8 +- .../src/api/routes/store/update-line-item.js | 10 +++ .../src/services/add-on-line-item.js | 80 +++---------------- .../src/services/add-on.js | 51 ++++++++++++ .../medusa/src/models/schemas/line-item.js | 1 + packages/medusa/src/services/cart.js | 2 +- packages/medusa/src/services/line-item.js | 3 + 14 files changed, 169 insertions(+), 88 deletions(-) diff --git a/packages/medusa-plugin-add-ons/src/api/index.js b/packages/medusa-plugin-add-ons/src/api/index.js index 829aa1f001..2ed442a00a 100644 --- a/packages/medusa-plugin-add-ons/src/api/index.js +++ b/packages/medusa-plugin-add-ons/src/api/index.js @@ -6,7 +6,7 @@ export default (rootDirectory) => { const app = Router() store(app, rootDirectory) - admin(app) + admin(app, rootDirectory) return app } diff --git a/packages/medusa-plugin-add-ons/src/api/routes/admin/create-add-on.js b/packages/medusa-plugin-add-ons/src/api/routes/admin/create-add-on.js index 0a88bcf9dc..08343344b9 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/admin/create-add-on.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/admin/create-add-on.js @@ -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(), }) diff --git a/packages/medusa-plugin-add-ons/src/api/routes/admin/get-add-on.js b/packages/medusa-plugin-add-ons/src/api/routes/admin/get-add-on.js index 129c3bbfde..a4ef0bfbf6 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/admin/get-add-on.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/admin/get-add-on.js @@ -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 diff --git a/packages/medusa-plugin-add-ons/src/api/routes/admin/index.js b/packages/medusa-plugin-add-ons/src/api/routes/admin/index.js index 751fb89dfa..5defbd8475 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/admin/index.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/admin/index.js @@ -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 } diff --git a/packages/medusa-plugin-add-ons/src/api/routes/admin/list-add-ons.js b/packages/medusa-plugin-add-ons/src/api/routes/admin/list-add-ons.js index ae3ea4cb27..fc61f78f36 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/admin/list-add-ons.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/admin/list-add-ons.js @@ -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 } } diff --git a/packages/medusa-plugin-add-ons/src/api/routes/admin/update-add-on.js b/packages/medusa-plugin-add-ons/src/api/routes/admin/update-add-on.js index 551f4479bb..d5cff10c64 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/admin/update-add-on.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/admin/update-add-on.js @@ -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 } -}; +} diff --git a/packages/medusa-plugin-add-ons/src/api/routes/store/create-line-item.js b/packages/medusa-plugin-add-ons/src/api/routes/store/create-line-item.js index 8f08465594..c96ddf1fcf 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/store/create-line-item.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/store/create-line-item.js @@ -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) { diff --git a/packages/medusa-plugin-add-ons/src/api/routes/store/get-by-product.js b/packages/medusa-plugin-add-ons/src/api/routes/store/get-by-product.js index 9dc796d4e1..d6746cde9d 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/store/get-by-product.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/store/get-by-product.js @@ -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 diff --git a/packages/medusa-plugin-add-ons/src/api/routes/store/update-line-item.js b/packages/medusa-plugin-add-ons/src/api/routes/store/update-line-item.js index 2cadf9fe2c..19cc235a5d 100644 --- a/packages/medusa-plugin-add-ons/src/api/routes/store/update-line-item.js +++ b/packages/medusa-plugin-add-ons/src/api/routes/store/update-line-item.js @@ -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) { diff --git a/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js b/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js index eae5433a2f..71b80a301d 100644 --- a/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js +++ b/packages/medusa-plugin-add-ons/src/services/add-on-line-item.js @@ -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 } } diff --git a/packages/medusa-plugin-add-ons/src/services/add-on.js b/packages/medusa-plugin-add-ons/src/services/add-on.js index 9ddaf33d57..7524de0ac9 100644 --- a/packages/medusa-plugin-add-ons/src/services/add-on.js +++ b/packages/medusa-plugin-add-ons/src/services/add-on.js @@ -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 diff --git a/packages/medusa/src/models/schemas/line-item.js b/packages/medusa/src/models/schemas/line-item.js index 230e21325c..85ed586c54 100644 --- a/packages/medusa/src/models/schemas/line-item.js +++ b/packages/medusa/src/models/schemas/line-item.js @@ -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 diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index 7914e3e296..24d5939078 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -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 diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js index 823f9b59cb..01af78a3f0 100644 --- a/packages/medusa/src/services/line-item.js +++ b/packages/medusa/src/services/line-item.js @@ -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) {