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:
5
.changeset/funny-radios-juggle.md
Normal file
5
.changeset/funny-radios-juggle.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): quantity prices for line item updates
|
||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user