fix(medusa): quantity prices for line item updates (#5137)

* initial code push

* update metadata and only merge if the existing line item allows merging

* update should_merge check

* undo changes to taxrate service

* update results with unit pricing corresponding to the db values after update

* add should_merge property to line_item creation

* add should_merge property to line_item creation

* fix unit tests

* undo adding "should_merge" to create-line-item

* undo change to "addOrUpdateLineItem"

* :wqh_merge from generate method

* undo changes to unit tests

* revert to adding pricing in updateLineItem method

* update cart service test

* Create funny-radios-juggle.md

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Philip Korsholm
2023-09-28 14:26:58 +02:00
committed by GitHub
parent f88e3865a0
commit 2b91049f58
11 changed files with 202 additions and 20 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): quantity prices for line item updates

View File

@@ -22,6 +22,7 @@ const {
simpleShippingOptionFactory, simpleShippingOptionFactory,
simpleLineItemFactory, simpleLineItemFactory,
simpleSalesChannelFactory, simpleSalesChannelFactory,
simplePriceListFactory,
} = require("../../../../factories") } = require("../../../../factories")
const { const {
simpleDiscountFactory, simpleDiscountFactory,
@@ -650,7 +651,7 @@ describe("/store/carts", () => {
{ {
id: "line-item-2", id: "line-item-2",
cart_id: discountCart.id, cart_id: discountCart.id,
variant_id: "test-variant-quantity", variant_id: "test-variant-quantity-1",
product_id: "test-product", product_id: "test-product",
unit_price: 950, unit_price: 950,
quantity: 1, quantity: 1,
@@ -790,7 +791,7 @@ describe("/store/carts", () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
cart_id: "test-cart-3", cart_id: "test-cart-3",
unit_price: 8000, unit_price: 500,
variant_id: "test-variant-sale-cg", variant_id: "test-variant-sale-cg",
quantity: 3, quantity: 3,
adjustments: [], adjustments: [],
@@ -854,7 +855,7 @@ describe("/store/carts", () => {
allow_discounts: true, allow_discounts: true,
title: "Line Item Disc", title: "Line Item Disc",
thumbnail: "https://test.js/1234", thumbnail: "https://test.js/1234",
unit_price: 1000, unit_price: 800,
quantity: 1, quantity: 1,
variant_id: "test-variant-quantity", variant_id: "test-variant-quantity",
product_id: "test-product", product_id: "test-product",
@@ -876,12 +877,12 @@ describe("/store/carts", () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
cart_id: "test-cart-w-total-percentage-discount", cart_id: "test-cart-w-total-percentage-discount",
unit_price: 1000, unit_price: 800,
variant_id: "test-variant-quantity", variant_id: "test-variant-quantity",
quantity: 10, quantity: 10,
adjustments: [ adjustments: [
expect.objectContaining({ expect.objectContaining({
amount: 1000, amount: 800,
discount_id: "10Percent", discount_id: "10Percent",
description: "discount", description: "discount",
}), }),
@@ -978,6 +979,111 @@ describe("/store/carts", () => {
]) ])
) )
}) })
it("updates line item quantity with unit price reflected", async () => {
const api = useApi()
await simplePriceListFactory(dbConnection, {
id: "pl_current",
prices: [
{
variant_id: "test-variant-sale-cg",
amount: 10,
min_quantity: 5,
currency_code: "usd",
},
],
})
const response = await api
.post(
"/store/carts/test-cart-3/line-items/test-item3/",
{
quantity: 5,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
cart_id: "test-cart-3",
unit_price: 10,
variant_id: "test-variant-sale-cg",
quantity: 5,
}),
])
)
})
it("creates and updates line item quantity with unit price reflected", async () => {
const api = useApi()
await simplePriceListFactory(dbConnection, {
id: "pl_current",
prices: [
{
variant_id: "test-variant-sale-cg",
amount: 10,
min_quantity: 5,
currency_code: "usd",
},
],
})
const createResponse = await api
.post(
"/store/carts/test-cart/line-items/",
{
quantity: 1,
variant_id: "test-variant-sale-cg",
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(createResponse.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
cart_id: "test-cart",
unit_price: 500,
variant_id: "test-variant-sale-cg",
quantity: 1,
}),
])
)
const lineItemId = createResponse.data.cart.items.find(
(i) => i.variant_id === "test-variant-sale-cg"
).id
const response = await api
.post(
`/store/carts/test-cart/line-items/${lineItemId}`,
{
quantity: 5,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
const lineItemIdCount = response.data.cart.items.filter(
(i) => i.variant_id === "test-variant-sale-cg"
)
expect(lineItemIdCount.length).toEqual(1)
expect(response.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
cart_id: "test-cart",
unit_price: 10,
variant_id: "test-variant-sale-cg",
quantity: 5,
}),
])
)
})
}) })
describe("POST /store/carts/:id", () => { describe("POST /store/carts/:id", () => {

View File

@@ -16,6 +16,8 @@ type ProductListPrice = {
currency_code: string currency_code: string
region_id: string region_id: string
amount: number amount: number
min_quantity?: number
max_quantity?: number
} }
export type PriceListFactoryData = { export type PriceListFactoryData = {

View File

@@ -531,6 +531,33 @@ module.exports = async (dataSource, data = {}) => {
variant_id: "test-variant-quantity", variant_id: "test-variant-quantity",
}) })
const quantityVariant1 = manager.create(ProductVariant, {
id: "test-variant-quantity-1",
title: "test variant quantity 1",
product_id: "test-product",
inventory_quantity: 1000,
options: [
{
option_id: "test-option",
value: "Fit",
},
],
})
await manager.save(quantityVariant1)
await manager.insert(MoneyAmount, {
id: "test-price_quantity-1.5",
currency_code: "usd",
amount: 950,
})
await manager.insert(ProductVariantMoneyAmount, {
id: "pvma-quantity-1.5",
money_amount_id: "test-price_quantity-1.5",
variant_id: "test-variant-quantity-1",
})
await manager.insert(MoneyAmount, { await manager.insert(MoneyAmount, {
id: "test-price_quantity-2", id: "test-price_quantity-2",
currency_code: "usd", currency_code: "usd",

View File

@@ -1,4 +1,10 @@
import { IsInt, IsObject, IsOptional, IsString } from "class-validator" import {
IsBoolean,
IsInt,
IsObject,
IsOptional,
IsString,
} from "class-validator"
import { import {
defaultAdminDraftOrdersCartFields, defaultAdminDraftOrdersCartFields,
defaultAdminDraftOrdersCartRelations, defaultAdminDraftOrdersCartRelations,
@@ -114,7 +120,10 @@ export default async (req, res) => {
validated.variant_id, validated.variant_id,
draftOrder.cart.region_id, draftOrder.cart.region_id,
validated.quantity, validated.quantity,
{ metadata: validated.metadata, unit_price: validated.unit_price } {
metadata: validated.metadata,
unit_price: validated.unit_price,
}
) )
await cartService await cartService

View File

@@ -4,6 +4,7 @@ import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "." import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services" import { CartService } from "../../../../services"
import { cleanResponseData } from "../../../../utils/clean-response-data" import { cleanResponseData } from "../../../../utils/clean-response-data"
import { handleAddOrUpdateLineItem } from "./create-line-item/utils/handler-steps"
/** /**
* @oas [post] /store/carts/{id}/line-items/{line_id} * @oas [post] /store/carts/{id}/line-items/{line_id}

View File

@@ -77,6 +77,7 @@ export const carts = {
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
variant_id: IdMap.getId("eur-10-us-12"),
unit_price: 10, unit_price: 10,
variant: { variant: {
id: IdMap.getId("eur-10-us-12"), id: IdMap.getId("eur-10-us-12"),

View File

@@ -58,7 +58,7 @@ export const LineItemServiceMock = {
variant_id: variantId, variant_id: variantId,
unit_price: 100, unit_price: 100,
quantity, quantity,
metadata, ...metadata,
}) })
}), }),
delete: jest.fn().mockImplementation(() => Promise.resolve()), delete: jest.fn().mockImplementation(() => Promise.resolve()),

View File

@@ -21,7 +21,7 @@ import { ShippingOptionServiceMock } from "../__mocks__/shipping-option"
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
import { taxProviderServiceMock } from "../__mocks__/tax-provider" import { taxProviderServiceMock } from "../__mocks__/tax-provider"
import CartService from "../cart" import CartService from "../cart"
import { NewTotalsService, TaxProviderService } from "../index" import { NewTotalsService, PricingService, TaxProviderService } from "../index"
import SystemTaxService from "../system-tax" import SystemTaxService from "../system-tax"
const eventBusService = { const eventBusService = {
@@ -2657,6 +2657,7 @@ describe("CartService", () => {
.register("taxProviderService", asClass(TaxProviderService)) .register("taxProviderService", asClass(TaxProviderService))
.register("newTotalsService", asClass(NewTotalsService)) .register("newTotalsService", asClass(NewTotalsService))
.register("cartService", asClass(CartService)) .register("cartService", asClass(CartService))
.register("pricingService", asClass(PricingService))
const cartService = container.resolve("cartService") const cartService = container.resolve("cartService")

View File

@@ -12,6 +12,7 @@ import {
LineItemService, LineItemService,
NewTotalsService, NewTotalsService,
PaymentProviderService, PaymentProviderService,
PricingService,
ProductService, ProductService,
ProductVariantInventoryService, ProductVariantInventoryService,
ProductVariantService, ProductVariantService,
@@ -62,6 +63,7 @@ import {
import { PaymentSessionInput } from "../types/payment" import { PaymentSessionInput } from "../types/payment"
import { buildQuery, isString, setMetadata } from "../utils" import { buildQuery, isString, setMetadata } from "../utils"
import { validateEmail } from "../utils/is-email" import { validateEmail } from "../utils/is-email"
import { IsNumber } from "class-validator"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
@@ -91,6 +93,7 @@ type InjectedDependencies = {
lineItemAdjustmentService: LineItemAdjustmentService lineItemAdjustmentService: LineItemAdjustmentService
priceSelectionStrategy: IPriceSelectionStrategy priceSelectionStrategy: IPriceSelectionStrategy
productVariantInventoryService: ProductVariantInventoryService productVariantInventoryService: ProductVariantInventoryService
pricingService: PricingService
} }
type TotalsConfig = { type TotalsConfig = {
@@ -134,6 +137,7 @@ class CartService extends TransactionBaseService {
protected readonly featureFlagRouter_: FlagRouter protected readonly featureFlagRouter_: FlagRouter
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
protected readonly productVariantInventoryService_: ProductVariantInventoryService protected readonly productVariantInventoryService_: ProductVariantInventoryService
protected readonly pricingService_: PricingService
constructor({ constructor({
cartRepository, cartRepository,
@@ -162,6 +166,7 @@ class CartService extends TransactionBaseService {
featureFlagRouter, featureFlagRouter,
storeService, storeService,
productVariantInventoryService, productVariantInventoryService,
pricingService,
}: InjectedDependencies) { }: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
super(arguments[0]) super(arguments[0])
@@ -192,6 +197,7 @@ class CartService extends TransactionBaseService {
this.featureFlagRouter_ = featureFlagRouter this.featureFlagRouter_ = featureFlagRouter
this.storeService_ = storeService this.storeService_ = storeService
this.productVariantInventoryService_ = productVariantInventoryService this.productVariantInventoryService_ = productVariantInventoryService
this.pricingService_ = pricingService
} }
/** /**
@@ -975,7 +981,7 @@ 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"] const select: (keyof Cart)[] = ["id", "region_id", "customer_id"]
if ( if (
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
) { ) {
@@ -1014,6 +1020,25 @@ class CartService extends TransactionBaseService {
"Inventory doesn't cover the desired quantity" "Inventory doesn't cover the desired quantity"
) )
} }
const variantsPricing = await this.pricingService_
.withTransaction(transactionManager)
.getProductVariantsPricing(
[
{
variantId: lineItem.variant_id,
quantity: lineItemUpdate.quantity,
},
],
{
region_id: cart.region_id,
customer_id: cart.customer_id,
include_discount_prices: true,
}
)
const { calculated_price } = variantsPricing[lineItem.variant_id]
lineItemUpdate.unit_price = calculated_price ?? undefined
} }
} }

View File

@@ -2,10 +2,10 @@ import { FlagRouter } from "@medusajs/utils"
import { ITaxCalculationStrategy, TaxCalculationContext } from "../interfaces" import { ITaxCalculationStrategy, TaxCalculationContext } from "../interfaces"
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import { import {
LineItem, LineItem,
LineItemTaxLine, LineItemTaxLine,
ShippingMethod, ShippingMethod,
ShippingMethodTaxLine, ShippingMethodTaxLine,
} from "../models" } from "../models"
import { calculatePriceTaxAmount } from "../utils" import { calculatePriceTaxAmount } from "../utils"
@@ -28,13 +28,18 @@ class TaxCalculationStrategy implements ITaxCalculationStrategy {
(tl) => "shipping_method_id" in tl (tl) => "shipping_method_id" in tl
) as ShippingMethodTaxLine[] ) as ShippingMethodTaxLine[]
return Math.round( const lineItemsTax = this.calculateLineItemsTax(
this.calculateLineItemsTax(items, lineItemsTaxLines, calculationContext) + items,
this.calculateShippingMethodsTax( lineItemsTaxLines,
calculationContext.shipping_methods, calculationContext
shippingMethodsTaxLines
)
) )
const shippingMethodsTax = this.calculateShippingMethodsTax(
calculationContext.shipping_methods,
shippingMethodsTaxLines
)
return Math.round(lineItemsTax + shippingMethodsTax)
} }
private calculateLineItemsTax( private calculateLineItemsTax(