diff --git a/.changeset/real-items-rhyme.md b/.changeset/real-items-rhyme.md new file mode 100644 index 0000000000..62eccb8b26 --- /dev/null +++ b/.changeset/real-items-rhyme.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): Remove shipping on updates to `cart.items` diff --git a/integration-tests/api/__tests__/store/cart/cart.js b/integration-tests/api/__tests__/store/cart/cart.js index 6555ee1ad8..75e90262be 100644 --- a/integration-tests/api/__tests__/store/cart/cart.js +++ b/integration-tests/api/__tests__/store/cart/cart.js @@ -1623,7 +1623,8 @@ describe("/store/carts", () => { const cart = response.data.cart - const shippingAmount = 1000 + // shipping is 0 because of line item update + const shippingAmount = 0 const expectedTotal = quantity * variant1Price + quantity * variant2Price + @@ -1635,7 +1636,7 @@ describe("/store/carts", () => { expect(cart.total).toBe(expectedTotal) expect(cart.subtotal).toBe(expectedSubtotal) expect(cart.discount_total).toBe(discountAmount) - expect(cart.shipping_total).toBe(1000) + expect(cart.shipping_total).toBe(shippingAmount) }) it("updates cart customer id", async () => { @@ -1648,6 +1649,87 @@ describe("/store/carts", () => { expect(response.status).toEqual(200) }) + it("should remove shipping on line item remove", async () => { + const api = useApi() + + const cartId = "test-cart-2" + const lineId = "test-item" + const optionId = "test-option" + + await api.post( + `/store/carts/${cartId}/shipping-methods`, + { + option_id: optionId, + }, + { withCredentials: true } + ) + + const cart = await api.get(`/store/carts/${cartId}`) + + expect(cart.data.cart.shipping_total).toEqual(1000) + + const response = await api.delete( + `/store/carts/${cartId}/line-items/${lineId}` + ) + + expect(response.data.cart.shipping_total).toEqual(0) + }) + + it("should remove shipping on line item update", async () => { + const api = useApi() + + const cartId = "test-cart-2" + const lineId = "test-item" + const optionId = "test-option" + + await api.post( + `/store/carts/${cartId}/shipping-methods`, + { + option_id: optionId, + }, + { withCredentials: true } + ) + + const cart = await api.get(`/store/carts/${cartId}`) + + expect(cart.data.cart.shipping_total).toEqual(1000) + + const response = await api.post( + `/store/carts/${cartId}/line-items/${lineId}`, + { + quantity: 2, + } + ) + + expect(response.data.cart.shipping_total).toEqual(0) + }) + + it("should remove shipping on line item add", async () => { + const api = useApi() + + const cartId = "test-cart-2" + const optionId = "test-option" + + await api.post( + `/store/carts/${cartId}/shipping-methods`, + { + option_id: optionId, + }, + { withCredentials: true } + ) + + const cart = await api.get(`/store/carts/${cartId}`) + + expect(cart.data.cart.shipping_total).toEqual(1000) + + const response = await api.post(`/store/carts/${cartId}/line-items`, { + variant_id: "test-variant-sale-customer", + quantity: 1, + }) + + expect(response.data.cart.shipping_total).toEqual(0) + }) + it("updates prices when cart customer id is updated", async () => { const api = useApi() @@ -1958,8 +2040,9 @@ describe("/store/carts", () => { expect(getRes.status).toEqual(200) expect(getRes.data.type).toEqual("order") + // inventory pre-purchase was 10 const variantRes = await api.get("/store/variants/test-variant") - expect(variantRes.data.variant.inventory_quantity).toEqual(0) + expect(variantRes.data.variant.inventory_quantity).toEqual(9) }) it("calculates correct payment totals on cart completion taking into account line item adjustments", async () => { @@ -2332,7 +2415,8 @@ describe("/store/carts", () => { }), ]) ) - expect(cartWithGiftcard.data.cart.total).toBe(2900) // 1000 (giftcard) + 900 (standard item with 10% discount) + 1000 Shipping + expect(cartWithGiftcard.data.cart.total).toBe(1900) // 1000 (giftcard) + 900 (standard item with 10% discount) - 1000 Shipping (because of line item update) + expect(cartWithGiftcard.data.cart.shipping_total).toBe(0) // 0 because of line item update expect(cartWithGiftcard.data.cart.discount_total).toBe(100) expect(cartWithGiftcard.status).toEqual(200) }) diff --git a/integration-tests/helpers/cart-seeder.js b/integration-tests/helpers/cart-seeder.js index e6dfc0813c..1476c219eb 100644 --- a/integration-tests/helpers/cart-seeder.js +++ b/integration-tests/helpers/cart-seeder.js @@ -520,7 +520,7 @@ module.exports = async (dataSource, data = {}) => { id: "test-variant", title: "test variant", product_id: "test-product", - inventory_quantity: 1, + inventory_quantity: 10, options: [ { option_id: "test-option", diff --git a/packages/medusa/src/api/routes/store/carts/update-line-item.ts b/packages/medusa/src/api/routes/store/carts/update-line-item.ts index 89e5c8b64f..b7dd54b09a 100644 --- a/packages/medusa/src/api/routes/store/carts/update-line-item.ts +++ b/packages/medusa/src/api/routes/store/carts/update-line-item.ts @@ -73,9 +73,9 @@ export default async (req, res) => { if (validated.quantity === 0) { await cartService.withTransaction(m).removeLineItem(id, line_id) } else { - const cart = await cartService - .withTransaction(m) - .retrieve(id, { relations: ["items", "items.variant"] }) + const cart = await cartService.withTransaction(m).retrieve(id, { + relations: ["items", "items.variant", "shipping_methods"], + }) const existing = cart.items.find((i) => i.id === line_id) if (!existing) { diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index fda650d251..90e798b55b 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -3,40 +3,40 @@ import { isEmpty, isEqual } from "lodash" import { MedusaError, isDefined } from "medusa-core-utils" import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm" import { - CustomShippingOptionService, - CustomerService, - DiscountService, - EventBusService, - GiftCardService, - LineItemAdjustmentService, - LineItemService, - NewTotalsService, - PaymentProviderService, - ProductService, - ProductVariantInventoryService, - ProductVariantService, - RegionService, - SalesChannelService, - ShippingOptionService, - StoreService, - TaxProviderService, - TotalsService, + CustomShippingOptionService, + CustomerService, + DiscountService, + EventBusService, + GiftCardService, + LineItemAdjustmentService, + LineItemService, + NewTotalsService, + PaymentProviderService, + ProductService, + ProductVariantInventoryService, + ProductVariantService, + RegionService, + SalesChannelService, + ShippingOptionService, + StoreService, + TaxProviderService, + TotalsService, } from "." import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { - Address, - Cart, - CustomShippingOption, - Customer, - Discount, - DiscountRule, - DiscountRuleType, - LineItem, - PaymentSession, - PaymentSessionStatus, - SalesChannel, - ShippingMethod, + Address, + Cart, + CustomShippingOption, + Customer, + Discount, + DiscountRule, + DiscountRuleType, + LineItem, + PaymentSession, + PaymentSessionStatus, + SalesChannel, + ShippingMethod, } from "../models" import { AddressRepository } from "../repositories/address" import { CartRepository } from "../repositories/cart" @@ -44,18 +44,18 @@ import { LineItemRepository } from "../repositories/line-item" import { PaymentSessionRepository } from "../repositories/payment-session" import { ShippingMethodRepository } from "../repositories/shipping-method" import { - CartCreateProps, - CartUpdateProps, - FilterableCartProps, - LineItemUpdate, - LineItemValidateData, - isCart, + CartCreateProps, + CartUpdateProps, + FilterableCartProps, + LineItemUpdate, + LineItemValidateData, + isCart, } from "../types/cart" import { - AddressPayload, - FindConfig, - TotalField, - WithRequiredProperty, + AddressPayload, + FindConfig, + TotalField, + WithRequiredProperty, } from "../types/common" import { PaymentSessionInput } from "../types/payment" import { buildQuery, isString, setMetadata } from "../utils" @@ -480,7 +480,11 @@ class CartService extends TransactionBaseService { return await this.atomicPhase_( async (transactionManager: EntityManager) => { const cart = await this.retrieve(cartId, { - relations: ["items.variant.product.profiles", "payment_sessions"], + relations: [ + "items.variant.product.profiles", + "payment_sessions", + "shipping_methods", + ], }) const lineItem = cart.items.find((item) => item.id === lineItemId) @@ -488,7 +492,6 @@ class CartService extends TransactionBaseService { return cart } - // Remove shipping methods if they are not needed if (cart.shipping_methods?.length) { await this.shippingOptionService_ .withTransaction(transactionManager) @@ -620,7 +623,10 @@ class CartService extends TransactionBaseService { return await this.atomicPhase_( async (transactionManager: EntityManager) => { - let cart = await this.retrieve(cartId, { select }) + let cart = await this.retrieve(cartId, { + select, + relations: ["shipping_methods"], + }) if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { if (config.validateSalesChannels) { @@ -710,6 +716,12 @@ class CartService extends TransactionBaseService { throw err }) + if (cart.shipping_methods?.length) { + await this.shippingOptionService_ + .withTransaction(transactionManager) + .deleteShippingMethods(cart.shipping_methods) + } + cart = await this.retrieve(cart.id, { relations: [ "items.variant.product.profiles", @@ -920,6 +932,18 @@ class CartService extends TransactionBaseService { ): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { + const select: (keyof Cart)[] = ["id"] + if ( + this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) + ) { + select.push("sales_channel_id") + } + + const cart = await this.retrieve(cartId, { + select: select, + relations: ["shipping_methods"], + }) + const lineItem = await this.lineItemService_.retrieve(lineItemId, { select: ["id", "quantity", "variant_id", "cart_id"], }) @@ -934,17 +958,6 @@ class CartService extends TransactionBaseService { if (lineItemUpdate.quantity) { if (lineItem.variant_id) { - const select: (keyof Cart)[] = ["id"] - if ( - this.featureFlagRouter_.isFeatureEnabled( - SalesChannelFeatureFlag.key - ) - ) { - select.push("sales_channel_id") - } - - const cart = await this.retrieve(cartId, { select: select }) - const hasInventory = await this.productVariantInventoryService_.confirmInventory( lineItem.variant_id, @@ -961,6 +974,12 @@ class CartService extends TransactionBaseService { } } + if (cart.shipping_methods?.length) { + await this.shippingOptionService_ + .withTransaction(transactionManager) + .deleteShippingMethods(cart.shipping_methods) + } + await this.lineItemService_ .withTransaction(transactionManager) .update(lineItemId, lineItemUpdate) diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index 79c14047be..f1094b96ce 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -485,10 +485,6 @@ class LineItemService extends TransactionBaseService { async deleteWithTaxLines(id: string): Promise { return await this.atomicPhase_( async (transactionManager: EntityManager) => { - const lineItemRepository = transactionManager.withRepository( - this.lineItemRepository_ - ) - await this.taxProviderService_ .withTransaction(transactionManager) .clearLineItemsTaxLines([id])