fix(medusa): Remove shipping on updates to cart.items (#4715)

* rm shipping on line item updates

* Add tests

* remove verbose flag

* Create real-items-rhyme.md
This commit is contained in:
Oli Juhl
2023-08-08 14:53:47 +02:00
committed by GitHub
parent 0cba4d647d
commit a2d7540e40
6 changed files with 170 additions and 66 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): Remove shipping on updates to `cart.items`

View File

@@ -1623,7 +1623,8 @@ describe("/store/carts", () => {
const cart = response.data.cart const cart = response.data.cart
const shippingAmount = 1000 // shipping is 0 because of line item update
const shippingAmount = 0
const expectedTotal = const expectedTotal =
quantity * variant1Price + quantity * variant1Price +
quantity * variant2Price + quantity * variant2Price +
@@ -1635,7 +1636,7 @@ describe("/store/carts", () => {
expect(cart.total).toBe(expectedTotal) expect(cart.total).toBe(expectedTotal)
expect(cart.subtotal).toBe(expectedSubtotal) expect(cart.subtotal).toBe(expectedSubtotal)
expect(cart.discount_total).toBe(discountAmount) expect(cart.discount_total).toBe(discountAmount)
expect(cart.shipping_total).toBe(1000) expect(cart.shipping_total).toBe(shippingAmount)
}) })
it("updates cart customer id", async () => { it("updates cart customer id", async () => {
@@ -1648,6 +1649,87 @@ describe("/store/carts", () => {
expect(response.status).toEqual(200) 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 () => { it("updates prices when cart customer id is updated", async () => {
const api = useApi() const api = useApi()
@@ -1958,8 +2040,9 @@ describe("/store/carts", () => {
expect(getRes.status).toEqual(200) expect(getRes.status).toEqual(200)
expect(getRes.data.type).toEqual("order") expect(getRes.data.type).toEqual("order")
// inventory pre-purchase was 10
const variantRes = await api.get("/store/variants/test-variant") 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 () => { 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.data.cart.discount_total).toBe(100)
expect(cartWithGiftcard.status).toEqual(200) expect(cartWithGiftcard.status).toEqual(200)
}) })

View File

@@ -520,7 +520,7 @@ module.exports = async (dataSource, data = {}) => {
id: "test-variant", id: "test-variant",
title: "test variant", title: "test variant",
product_id: "test-product", product_id: "test-product",
inventory_quantity: 1, inventory_quantity: 10,
options: [ options: [
{ {
option_id: "test-option", option_id: "test-option",

View File

@@ -73,9 +73,9 @@ export default async (req, res) => {
if (validated.quantity === 0) { if (validated.quantity === 0) {
await cartService.withTransaction(m).removeLineItem(id, line_id) await cartService.withTransaction(m).removeLineItem(id, line_id)
} else { } else {
const cart = await cartService const cart = await cartService.withTransaction(m).retrieve(id, {
.withTransaction(m) relations: ["items", "items.variant", "shipping_methods"],
.retrieve(id, { relations: ["items", "items.variant"] }) })
const existing = cart.items.find((i) => i.id === line_id) const existing = cart.items.find((i) => i.id === line_id)
if (!existing) { if (!existing) {

View File

@@ -3,40 +3,40 @@ import { isEmpty, isEqual } from "lodash"
import { MedusaError, isDefined } from "medusa-core-utils" import { MedusaError, isDefined } from "medusa-core-utils"
import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm" import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm"
import { import {
CustomShippingOptionService, CustomShippingOptionService,
CustomerService, CustomerService,
DiscountService, DiscountService,
EventBusService, EventBusService,
GiftCardService, GiftCardService,
LineItemAdjustmentService, LineItemAdjustmentService,
LineItemService, LineItemService,
NewTotalsService, NewTotalsService,
PaymentProviderService, PaymentProviderService,
ProductService, ProductService,
ProductVariantInventoryService, ProductVariantInventoryService,
ProductVariantService, ProductVariantService,
RegionService, RegionService,
SalesChannelService, SalesChannelService,
ShippingOptionService, ShippingOptionService,
StoreService, StoreService,
TaxProviderService, TaxProviderService,
TotalsService, TotalsService,
} from "." } from "."
import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces" import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
import { import {
Address, Address,
Cart, Cart,
CustomShippingOption, CustomShippingOption,
Customer, Customer,
Discount, Discount,
DiscountRule, DiscountRule,
DiscountRuleType, DiscountRuleType,
LineItem, LineItem,
PaymentSession, PaymentSession,
PaymentSessionStatus, PaymentSessionStatus,
SalesChannel, SalesChannel,
ShippingMethod, ShippingMethod,
} from "../models" } from "../models"
import { AddressRepository } from "../repositories/address" import { AddressRepository } from "../repositories/address"
import { CartRepository } from "../repositories/cart" import { CartRepository } from "../repositories/cart"
@@ -44,18 +44,18 @@ import { LineItemRepository } from "../repositories/line-item"
import { PaymentSessionRepository } from "../repositories/payment-session" import { PaymentSessionRepository } from "../repositories/payment-session"
import { ShippingMethodRepository } from "../repositories/shipping-method" import { ShippingMethodRepository } from "../repositories/shipping-method"
import { import {
CartCreateProps, CartCreateProps,
CartUpdateProps, CartUpdateProps,
FilterableCartProps, FilterableCartProps,
LineItemUpdate, LineItemUpdate,
LineItemValidateData, LineItemValidateData,
isCart, isCart,
} from "../types/cart" } from "../types/cart"
import { import {
AddressPayload, AddressPayload,
FindConfig, FindConfig,
TotalField, TotalField,
WithRequiredProperty, WithRequiredProperty,
} from "../types/common" } from "../types/common"
import { PaymentSessionInput } from "../types/payment" import { PaymentSessionInput } from "../types/payment"
import { buildQuery, isString, setMetadata } from "../utils" import { buildQuery, isString, setMetadata } from "../utils"
@@ -480,7 +480,11 @@ class CartService extends TransactionBaseService {
return await this.atomicPhase_( return await this.atomicPhase_(
async (transactionManager: EntityManager) => { async (transactionManager: EntityManager) => {
const cart = await this.retrieve(cartId, { 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) const lineItem = cart.items.find((item) => item.id === lineItemId)
@@ -488,7 +492,6 @@ class CartService extends TransactionBaseService {
return cart return cart
} }
// Remove shipping methods if they are not needed
if (cart.shipping_methods?.length) { if (cart.shipping_methods?.length) {
await this.shippingOptionService_ await this.shippingOptionService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
@@ -620,7 +623,10 @@ class CartService extends TransactionBaseService {
return await this.atomicPhase_( return await this.atomicPhase_(
async (transactionManager: EntityManager) => { 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 (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
if (config.validateSalesChannels) { if (config.validateSalesChannels) {
@@ -710,6 +716,12 @@ class CartService extends TransactionBaseService {
throw err throw err
}) })
if (cart.shipping_methods?.length) {
await this.shippingOptionService_
.withTransaction(transactionManager)
.deleteShippingMethods(cart.shipping_methods)
}
cart = await this.retrieve(cart.id, { cart = await this.retrieve(cart.id, {
relations: [ relations: [
"items.variant.product.profiles", "items.variant.product.profiles",
@@ -920,6 +932,18 @@ class CartService extends TransactionBaseService {
): Promise<Cart> { ): Promise<Cart> {
return await this.atomicPhase_( return await this.atomicPhase_(
async (transactionManager: EntityManager) => { 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, { const lineItem = await this.lineItemService_.retrieve(lineItemId, {
select: ["id", "quantity", "variant_id", "cart_id"], select: ["id", "quantity", "variant_id", "cart_id"],
}) })
@@ -934,17 +958,6 @@ class CartService extends TransactionBaseService {
if (lineItemUpdate.quantity) { if (lineItemUpdate.quantity) {
if (lineItem.variant_id) { 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 = const hasInventory =
await this.productVariantInventoryService_.confirmInventory( await this.productVariantInventoryService_.confirmInventory(
lineItem.variant_id, 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_ await this.lineItemService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
.update(lineItemId, lineItemUpdate) .update(lineItemId, lineItemUpdate)

View File

@@ -485,10 +485,6 @@ class LineItemService extends TransactionBaseService {
async deleteWithTaxLines(id: string): Promise<LineItem | undefined | null> { async deleteWithTaxLines(id: string): Promise<LineItem | undefined | null> {
return await this.atomicPhase_( return await this.atomicPhase_(
async (transactionManager: EntityManager) => { async (transactionManager: EntityManager) => {
const lineItemRepository = transactionManager.withRepository(
this.lineItemRepository_
)
await this.taxProviderService_ await this.taxProviderService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
.clearLineItemsTaxLines([id]) .clearLineItemsTaxLines([id])