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,
|
||||
simpleLineItemFactory,
|
||||
simpleSalesChannelFactory,
|
||||
simplePriceListFactory,
|
||||
} = require("../../../../factories")
|
||||
const {
|
||||
simpleDiscountFactory,
|
||||
@@ -650,7 +651,7 @@ describe("/store/carts", () => {
|
||||
{
|
||||
id: "line-item-2",
|
||||
cart_id: discountCart.id,
|
||||
variant_id: "test-variant-quantity",
|
||||
variant_id: "test-variant-quantity-1",
|
||||
product_id: "test-product",
|
||||
unit_price: 950,
|
||||
quantity: 1,
|
||||
@@ -790,7 +791,7 @@ describe("/store/carts", () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
cart_id: "test-cart-3",
|
||||
unit_price: 8000,
|
||||
unit_price: 500,
|
||||
variant_id: "test-variant-sale-cg",
|
||||
quantity: 3,
|
||||
adjustments: [],
|
||||
@@ -854,7 +855,7 @@ describe("/store/carts", () => {
|
||||
allow_discounts: true,
|
||||
title: "Line Item Disc",
|
||||
thumbnail: "https://test.js/1234",
|
||||
unit_price: 1000,
|
||||
unit_price: 800,
|
||||
quantity: 1,
|
||||
variant_id: "test-variant-quantity",
|
||||
product_id: "test-product",
|
||||
@@ -876,12 +877,12 @@ describe("/store/carts", () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
cart_id: "test-cart-w-total-percentage-discount",
|
||||
unit_price: 1000,
|
||||
unit_price: 800,
|
||||
variant_id: "test-variant-quantity",
|
||||
quantity: 10,
|
||||
adjustments: [
|
||||
expect.objectContaining({
|
||||
amount: 1000,
|
||||
amount: 800,
|
||||
discount_id: "10Percent",
|
||||
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", () => {
|
||||
|
||||
@@ -16,6 +16,8 @@ type ProductListPrice = {
|
||||
currency_code: string
|
||||
region_id: string
|
||||
amount: number
|
||||
min_quantity?: number
|
||||
max_quantity?: number
|
||||
}
|
||||
|
||||
export type PriceListFactoryData = {
|
||||
|
||||
@@ -531,6 +531,33 @@ module.exports = async (dataSource, data = {}) => {
|
||||
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, {
|
||||
id: "test-price_quantity-2",
|
||||
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 {
|
||||
defaultAdminDraftOrdersCartFields,
|
||||
defaultAdminDraftOrdersCartRelations,
|
||||
@@ -114,7 +120,10 @@ export default async (req, res) => {
|
||||
validated.variant_id,
|
||||
draftOrder.cart.region_id,
|
||||
validated.quantity,
|
||||
{ metadata: validated.metadata, unit_price: validated.unit_price }
|
||||
{
|
||||
metadata: validated.metadata,
|
||||
unit_price: validated.unit_price,
|
||||
}
|
||||
)
|
||||
|
||||
await cartService
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EntityManager } from "typeorm"
|
||||
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
||||
import { CartService } from "../../../../services"
|
||||
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}
|
||||
|
||||
@@ -77,6 +77,7 @@ export const carts = {
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
variant_id: IdMap.getId("eur-10-us-12"),
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
id: IdMap.getId("eur-10-us-12"),
|
||||
|
||||
@@ -58,7 +58,7 @@ export const LineItemServiceMock = {
|
||||
variant_id: variantId,
|
||||
unit_price: 100,
|
||||
quantity,
|
||||
metadata,
|
||||
...metadata,
|
||||
})
|
||||
}),
|
||||
delete: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ShippingOptionServiceMock } from "../__mocks__/shipping-option"
|
||||
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||
import CartService from "../cart"
|
||||
import { NewTotalsService, TaxProviderService } from "../index"
|
||||
import { NewTotalsService, PricingService, TaxProviderService } from "../index"
|
||||
import SystemTaxService from "../system-tax"
|
||||
|
||||
const eventBusService = {
|
||||
@@ -2657,6 +2657,7 @@ describe("CartService", () => {
|
||||
.register("taxProviderService", asClass(TaxProviderService))
|
||||
.register("newTotalsService", asClass(NewTotalsService))
|
||||
.register("cartService", asClass(CartService))
|
||||
.register("pricingService", asClass(PricingService))
|
||||
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
LineItemService,
|
||||
NewTotalsService,
|
||||
PaymentProviderService,
|
||||
PricingService,
|
||||
ProductService,
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
@@ -62,6 +63,7 @@ import {
|
||||
import { PaymentSessionInput } from "../types/payment"
|
||||
import { buildQuery, isString, setMetadata } from "../utils"
|
||||
import { validateEmail } from "../utils/is-email"
|
||||
import { IsNumber } from "class-validator"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -91,6 +93,7 @@ type InjectedDependencies = {
|
||||
lineItemAdjustmentService: LineItemAdjustmentService
|
||||
priceSelectionStrategy: IPriceSelectionStrategy
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
pricingService: PricingService
|
||||
}
|
||||
|
||||
type TotalsConfig = {
|
||||
@@ -134,6 +137,7 @@ class CartService extends TransactionBaseService {
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
protected readonly pricingService_: PricingService
|
||||
|
||||
constructor({
|
||||
cartRepository,
|
||||
@@ -162,6 +166,7 @@ class CartService extends TransactionBaseService {
|
||||
featureFlagRouter,
|
||||
storeService,
|
||||
productVariantInventoryService,
|
||||
pricingService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -192,6 +197,7 @@ class CartService extends TransactionBaseService {
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
this.storeService_ = storeService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
this.pricingService_ = pricingService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -975,7 +981,7 @@ class CartService extends TransactionBaseService {
|
||||
): Promise<Cart> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const select: (keyof Cart)[] = ["id"]
|
||||
const select: (keyof Cart)[] = ["id", "region_id", "customer_id"]
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
|
||||
) {
|
||||
@@ -1014,6 +1020,25 @@ class CartService extends TransactionBaseService {
|
||||
"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 TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import {
|
||||
LineItem,
|
||||
LineItemTaxLine,
|
||||
ShippingMethod,
|
||||
ShippingMethodTaxLine,
|
||||
LineItem,
|
||||
LineItemTaxLine,
|
||||
ShippingMethod,
|
||||
ShippingMethodTaxLine,
|
||||
} from "../models"
|
||||
import { calculatePriceTaxAmount } from "../utils"
|
||||
|
||||
@@ -28,13 +28,18 @@ class TaxCalculationStrategy implements ITaxCalculationStrategy {
|
||||
(tl) => "shipping_method_id" in tl
|
||||
) as ShippingMethodTaxLine[]
|
||||
|
||||
return Math.round(
|
||||
this.calculateLineItemsTax(items, lineItemsTaxLines, calculationContext) +
|
||||
this.calculateShippingMethodsTax(
|
||||
calculationContext.shipping_methods,
|
||||
shippingMethodsTaxLines
|
||||
)
|
||||
const lineItemsTax = this.calculateLineItemsTax(
|
||||
items,
|
||||
lineItemsTaxLines,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
const shippingMethodsTax = this.calculateShippingMethodsTax(
|
||||
calculationContext.shipping_methods,
|
||||
shippingMethodsTaxLines
|
||||
)
|
||||
|
||||
return Math.round(lineItemsTax + shippingMethodsTax)
|
||||
}
|
||||
|
||||
private calculateLineItemsTax(
|
||||
|
||||
Reference in New Issue
Block a user