fix(medusa-plugin-addon): Fixes admin endpoints, Adds flag to avoid merging add-on line-items
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user