From 75e59ec6d540e19708929e8a76e97fdb1feef813 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Fri, 15 Oct 2021 16:50:13 +0200 Subject: [PATCH] fix: allow custom shipping options to bypass option requirements --- packages/medusa/src/services/cart.js | 115 +++++++++--------- .../medusa/src/services/shipping-option.js | 36 +++--- 2 files changed, 79 insertions(+), 72 deletions(-) diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index 93c9ecb01b..24dcb514b9 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -160,7 +160,7 @@ class CartService extends BaseService { "total", ] - const totalsToSelect = select.filter(v => totalFields.includes(v)) + const totalsToSelect = select.filter((v) => totalFields.includes(v)) if (totalsToSelect.length > 0) { const relationSet = new Set(relations) relationSet.add("items") @@ -168,14 +168,14 @@ class CartService extends BaseService { relationSet.add("discounts") relationSet.add("discounts.rule") relationSet.add("discounts.rule.valid_for") - //relationSet.add("discounts.parent_discount") - //relationSet.add("discounts.parent_discount.rule") - //relationSet.add("discounts.parent_discount.regions") + // relationSet.add("discounts.parent_discount") + // relationSet.add("discounts.parent_discount.rule") + // relationSet.add("discounts.parent_discount.regions") relationSet.add("shipping_methods") relationSet.add("region") relations = [...relationSet] - select = select.filter(v => !totalFields.includes(v)) + select = select.filter((v) => !totalFields.includes(v)) } return { @@ -238,9 +238,8 @@ class CartService extends BaseService { const cartRepo = this.manager_.getCustomRepository(this.cartRepository_) const validatedId = this.validateId_(cartId) - const { select, relations, totalsToSelect } = this.transformQueryForTotals_( - options - ) + const { select, relations, totalsToSelect } = + this.transformQueryForTotals_(options) const query = { where: { id: validatedId }, @@ -275,7 +274,7 @@ class CartService extends BaseService { * @return {Promise} the result of the create operation */ async create(data) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cartRepo = manager.getCustomRepository(this.cartRepository_) const addressRepo = manager.getCustomRepository(this.addressRepository_) const { region_id } = data @@ -344,7 +343,7 @@ class CartService extends BaseService { * @retur {Promise} the result of the update operation */ async removeLineItem(cartId, lineItemId) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: [ "items", @@ -354,7 +353,7 @@ class CartService extends BaseService { ], }) - const lineItem = cart.items.find(li => li.id === lineItemId) + const lineItem = cart.items.find((li) => li.id === lineItemId) if (!lineItem) { return cart } @@ -423,7 +422,7 @@ class CartService extends BaseService { * @return {Promise} the result of the update operation */ async addLineItem(cartId, lineItem) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: [ "shipping_methods", @@ -436,7 +435,7 @@ class CartService extends BaseService { let currentItem if (lineItem.should_merge) { - currentItem = cart.items.find(line => { + currentItem = cart.items.find((line) => { if (line.should_merge && line.variant_id === lineItem.variant_id) { return _.isEqual(line.metadata, lineItem.metadata) } @@ -502,13 +501,13 @@ class CartService extends BaseService { * @return {Promise} the result of the update operation */ async updateLineItem(cartId, lineItemId, lineItemUpdate) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: ["items", "payment_sessions"], }) // Ensure that the line item exists in the cart - const lineItemExists = cart.items.find(i => i.id === lineItemId) + const lineItemExists = cart.items.find((i) => i.id === lineItemId) if (!lineItemExists) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -555,7 +554,7 @@ class CartService extends BaseService { // if any free shipping discounts, we ensure to update shipping method amount if (shouldAdd) { await Promise.all( - cart.shipping_methods.map(async sm => { + cart.shipping_methods.map(async (sm) => { const smRepo = this.manager_.getCustomRepository( this.shippingMethodRepository_ ) @@ -570,7 +569,7 @@ class CartService extends BaseService { ) } else { await Promise.all( - cart.shipping_methods.map(async sm => { + cart.shipping_methods.map(async (sm) => { const smRepo = this.manager_.getCustomRepository( this.shippingMethodRepository_ ) @@ -586,7 +585,7 @@ class CartService extends BaseService { } async update(cartId, update) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cartRepo = manager.getCustomRepository(this.cartRepository_) const cart = await this.retrieve(cartId, { select: [ @@ -733,9 +732,7 @@ class CartService extends BaseService { * @return {Promise} the resultign customer object */ async createOrFetchUserFromEmail_(email) { - const schema = Validator.string() - .email() - .required() + const schema = Validator.string().email().required() const { value, error } = schema.validate(email.toLowerCase()) if (error) { throw new MedusaError( @@ -886,11 +883,12 @@ class CartService extends BaseService { if (discount.usage_limit) { discount.usage_count = discount.usage_count || 0 - if (discount.usage_limit === discount.usage_count) + if (discount.usage_limit === discount.usage_count) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Discount has been used maximum allowed times" ) + } } const today = new Date() @@ -942,7 +940,7 @@ class CartService extends BaseService { const toParse = [...cart.discounts, discount] let sawNotShipping = false - const newDiscounts = toParse.map(d => { + const newDiscounts = toParse.map((d) => { const drule = d.rule switch (drule.type) { case "free_shipping": @@ -972,7 +970,7 @@ class CartService extends BaseService { * @return {Promise} the resulting cart */ async removeDiscount(cartId, discountCode) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: [ "discounts", @@ -987,7 +985,7 @@ class CartService extends BaseService { await this.adjustFreeShipping_(cart, false) } - cart.discounts = cart.discounts.filter(d => d.code !== discountCode) + cart.discounts = cart.discounts.filter((d) => d.code !== discountCode) const cartRepo = manager.getCustomRepository(this.cartRepository_) @@ -1018,7 +1016,7 @@ class CartService extends BaseService { * Updates the currently selected payment session. */ async updatePaymentSession(cartId, update) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: ["payment_sessions"], }) @@ -1052,7 +1050,7 @@ class CartService extends BaseService { * @return {Promise} the resulting cart */ async authorizePayment(cartId, context = {}) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cartRepository = manager.getCustomRepository(this.cartRepository_) const cart = await this.retrieve(cartId, { @@ -1096,10 +1094,10 @@ class CartService extends BaseService { * Sets a payment method for a cart. * @param {string} cartId - the id of the cart to add payment method to * @param {PaymentMethod} paymentMethod - the method to be set to the cart - * @returns {Promise} result of update operation + * @return {Promise} result of update operation */ async setPaymentSession(cartId, providerId) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const psRepo = manager.getCustomRepository(this.paymentSessionRepository_) const cart = await this.retrieve(cartId, { @@ -1128,13 +1126,13 @@ class CartService extends BaseService { } await Promise.all( - cart.payment_sessions.map(ps => { + cart.payment_sessions.map((ps) => { return psRepo.save({ ...ps, is_selected: null }) }) ) const sess = cart.payment_sessions.find( - ps => ps.provider_id === providerId + (ps) => ps.provider_id === providerId ) sess.is_selected = true @@ -1157,13 +1155,13 @@ class CartService extends BaseService { * amounts, currencies, etc. as well as make sure to filter payment sessions * that are not available for the cart's region. * @param {string} cartId - the id of the cart to set payment session for - * @returns {Promise} the result of the update operation. + * @return {Promise} the result of the update operation. */ async setPaymentSessions(cartOrCartId) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const psRepo = manager.getCustomRepository(this.paymentSessionRepository_) - let cartId = + const cartId = typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id const cart = await this.retrieve(cartId, { select: [ @@ -1192,7 +1190,7 @@ class CartService extends BaseService { const region = cart.region // If there are existing payment sessions ensure that these are up to date - let seen = [] + const seen = [] if (cart.payment_sessions && cart.payment_sessions.length) { for (const session of cart.payment_sessions) { if ( @@ -1242,10 +1240,10 @@ class CartService extends BaseService { * @param {string} cartId - the id of the cart to remove from * @param {string} providerId - the id of the provider whoose payment session * should be removed. - * @returns {Promise} the resulting cart. + * @return {Promise} the resulting cart. */ async deletePaymentSession(cartId, providerId) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: ["payment_sessions"], }) @@ -1283,10 +1281,10 @@ class CartService extends BaseService { * @param {string} cartId - the id of the cart to remove from * @param {string} providerId - the id of the provider whoose payment session * should be removed. - * @returns {Promise} the resulting cart. + * @return {Promise} the resulting cart. */ async refreshPaymentSession(cartId, providerId) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: ["payment_sessions"], }) @@ -1325,7 +1323,7 @@ class CartService extends BaseService { * @return {Promise} the result of the update operation */ async addShippingMethod(cartId, optionId, data) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { select: ["subtotal"], relations: [ @@ -1341,19 +1339,24 @@ class CartService extends BaseService { ], }) - let cartCustomShippingOptions = await this.customShippingOptionService_.list( - { cart_id: cart.id } - ) + const cartCustomShippingOptions = + await this.customShippingOptionService_.list({ cart_id: cart.id }) - let customShippingOption = this.findCustomShippingOption( + const customShippingOption = this.findCustomShippingOption( cartCustomShippingOptions, optionId ) const { shipping_methods } = cart + /** + * If we have a custom shipping option configured we want the price + * override to take effect and do not want `validateCartOption` to check + * if requirements are met, hence we are not passing the entire cart, but + * just the id. + */ const shippingMethodConfig = customShippingOption - ? { cart, price: customShippingOption.price } + ? { cart_id: cart.id, price: customShippingOption.price } : { cart, } @@ -1410,11 +1413,11 @@ class CartService extends BaseService { * throws if custom options is not empty and no shipping option corresponds to optionId * @param {Object} cartCustomShippingOptions - the cart's custom shipping options * @param {string} option - id of the normal or custom shipping option to find in the cartCustomShippingOptions - * @returns {CustomShippingOption | undefined} + * @return {CustomShippingOption | undefined} */ findCustomShippingOption(cartCustomShippingOptions, optionId) { - let customOption = cartCustomShippingOptions?.find( - cso => cso.shipping_option_id === optionId + const customOption = cartCustomShippingOptions?.find( + (cso) => cso.shipping_option_id === optionId ) const hasCustomOptions = cartCustomShippingOptions?.length @@ -1455,7 +1458,7 @@ class CartService extends BaseService { if (cart.items.length) { cart.items = await Promise.all( cart.items - .map(async item => { + .map(async (item) => { const availablePrice = await this.productVariantService_ .getRegionPrice(item.variant_id, regionId) .catch(() => undefined) @@ -1534,20 +1537,20 @@ class CartService extends BaseService { } if (cart.discounts && cart.discounts.length) { - const newDiscounts = cart.discounts.map(d => { + const newDiscounts = cart.discounts.map((d) => { if (d.regions.find(({ id }) => id === regionId)) { return d } }) - cart.discounts = newDiscounts.filter(d => !!d) + cart.discounts = newDiscounts.filter((d) => !!d) } cart.gift_cards = [] if (cart.payment_sessions && cart.payment_sessions.length) { await Promise.all( - cart.payment_sessions.map(ps => + cart.payment_sessions.map((ps) => this.paymentProviderService_ .withTransaction(this.manager_) .deleteSession(ps) @@ -1561,11 +1564,11 @@ class CartService extends BaseService { /** * Deletes a cart from the database. Completed carts cannot be deleted. * @param {string} cartId - the id of the cart to delete - * @returns {Promise} the deleted cart or undefined if the cart was + * @return {Promise} the deleted cart or undefined if the cart was * not found. */ async delete(cartId) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cart = await this.retrieve(cartId, { relations: [ "items", @@ -1605,7 +1608,7 @@ class CartService extends BaseService { * @return {Promise} resolves to the updated result. */ async setMetadata(cartId, key, value) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cartRepo = manager.getCustomRepository(this.cartRepository_) const validatedId = this.validateId_(cartId) @@ -1639,7 +1642,7 @@ class CartService extends BaseService { * @return {Promise} resolves to the updated result. */ async deleteMetadata(cartId, key) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const cartRepo = manager.getCustomRepository(this.cartRepository_) const validatedId = this.validateId_(cartId) diff --git a/packages/medusa/src/services/shipping-option.js b/packages/medusa/src/services/shipping-option.js index ad2cbf8724..70c532dc22 100644 --- a/packages/medusa/src/services/shipping-option.js +++ b/packages/medusa/src/services/shipping-option.js @@ -174,7 +174,7 @@ class ShippingOptionService extends BaseService { * @returns {Promise} the resulting shipping method */ async updateShippingMethod(id, update) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const methodRepo = manager.getCustomRepository(this.methodRepository_) const method = await methodRepo.findOne({ where: { id } }) @@ -203,7 +203,7 @@ class ShippingOptionService extends BaseService { * @param {string} id - the id of the option to use for the method. */ async deleteShippingMethod(sm) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const methodRepo = manager.getCustomRepository(this.methodRepository_) return methodRepo.remove(sm) }) @@ -217,7 +217,7 @@ class ShippingOptionService extends BaseService { * @returns {ShippingMethod} the resulting shipping method. */ async createShippingMethod(optionId, data, config) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const option = await this.retrieve(optionId, { relations: ["requirements"], }) @@ -255,6 +255,10 @@ class ShippingOptionService extends BaseService { toCreate.cart_id = config.cart.id } + if (config.cart_id) { + toCreate.cart_id = config.cart_id + } + if (config.return_id) { toCreate.return_id = config.return_id } @@ -303,7 +307,7 @@ class ShippingOptionService extends BaseService { } const subtotal = cart.subtotal - const requirementResults = option.requirements.map(requirement => { + const requirementResults = option.requirements.map((requirement) => { switch (requirement.type) { case "max_subtotal": return requirement.amount > subtotal @@ -332,7 +336,7 @@ class ShippingOptionService extends BaseService { * @return {Promise} the result of the create operation */ async create(data) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const optionRepo = manager.getCustomRepository(this.optionRepository_) const option = await optionRepo.create(data) @@ -370,7 +374,7 @@ class ShippingOptionService extends BaseService { for (const r of data.requirements) { const validated = await this.validateRequirement_(r) - if (acc.find(raw => raw.type === validated.type)) { + if (acc.find((raw) => raw.type === validated.type)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Only one requirement of each type is allowed" @@ -379,7 +383,7 @@ class ShippingOptionService extends BaseService { if ( acc.find( - raw => + (raw) => (raw.type === "max_subtotal" && validated.amount > raw.amount) || (raw.type === "min_subtotal" && validated.amount < raw.amount) @@ -449,7 +453,7 @@ class ShippingOptionService extends BaseService { * @return {Promise} resolves to the update result. */ async update(optionId, update) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const option = await this.retrieve(optionId, { relations: ["requirements"], }) @@ -477,7 +481,7 @@ class ShippingOptionService extends BaseService { for (const r of update.requirements) { const validated = await this.validateRequirement_(r, optionId) - if (acc.find(raw => raw.type === validated.type)) { + if (acc.find((raw) => raw.type === validated.type)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, "Only one requirement of each type is allowed" @@ -486,7 +490,7 @@ class ShippingOptionService extends BaseService { if ( acc.find( - raw => + (raw) => (raw.type === "max_subtotal" && validated.amount > raw.amount) || (raw.type === "min_subtotal" && validated.amount < raw.amount) @@ -502,12 +506,12 @@ class ShippingOptionService extends BaseService { } if (option.requirements) { - const accReqs = acc.map(a => a.id) + const accReqs = acc.map((a) => a.id) const toRemove = option.requirements.filter( - r => !accReqs.includes(r.id) + (r) => !accReqs.includes(r.id) ) await Promise.all( - toRemove.map(async req => { + toRemove.map(async (req) => { await this.removeRequirement(req.id) }) ) @@ -579,13 +583,13 @@ class ShippingOptionService extends BaseService { * @return {Promise} the result of update */ async addRequirement(optionId, requirement) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { const option = await this.retrieve(optionId, { relations: ["requirements"], }) const validatedReq = await this.validateRequirement_(requirement) - if (option.requirements.find(r => r.type === validatedReq.type)) { + if (option.requirements.find((r) => r.type === validatedReq.type)) { throw new MedusaError( MedusaError.Types.DUPLICATE_ERROR, `A requirement with type: ${validatedReq.type} already exists` @@ -605,7 +609,7 @@ class ShippingOptionService extends BaseService { * @return {Promise} the result of update */ async removeRequirement(requirementId) { - return this.atomicPhase_(async manager => { + return this.atomicPhase_(async (manager) => { try { const reqRepo = manager.getCustomRepository(this.requirementRepository_) const requirement = await reqRepo.findOne({