diff --git a/.changeset/funny-radios-juggle.md b/.changeset/funny-radios-juggle.md new file mode 100644 index 0000000000..7c744d78d8 --- /dev/null +++ b/.changeset/funny-radios-juggle.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): quantity prices for line item updates diff --git a/integration-tests/api/__tests__/store/cart/cart.js b/integration-tests/api/__tests__/store/cart/cart.js index 1061bb75e4..11a9cf3cb7 100644 --- a/integration-tests/api/__tests__/store/cart/cart.js +++ b/integration-tests/api/__tests__/store/cart/cart.js @@ -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", () => { diff --git a/integration-tests/factories/simple-price-list-factory.ts b/integration-tests/factories/simple-price-list-factory.ts index 81c2214769..fdf1031a81 100644 --- a/integration-tests/factories/simple-price-list-factory.ts +++ b/integration-tests/factories/simple-price-list-factory.ts @@ -16,6 +16,8 @@ type ProductListPrice = { currency_code: string region_id: string amount: number + min_quantity?: number + max_quantity?: number } export type PriceListFactoryData = { diff --git a/integration-tests/helpers/cart-seeder.js b/integration-tests/helpers/cart-seeder.js index 1d76f44726..63d7256e7c 100644 --- a/integration-tests/helpers/cart-seeder.js +++ b/integration-tests/helpers/cart-seeder.js @@ -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", diff --git a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts index 2d34cc8897..601bff92ed 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts +++ b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts @@ -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 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 56ecc84f06..2d6d975d75 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 @@ -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} diff --git a/packages/medusa/src/services/__mocks__/cart.js b/packages/medusa/src/services/__mocks__/cart.js index f6ed6a1549..fbf729d466 100644 --- a/packages/medusa/src/services/__mocks__/cart.js +++ b/packages/medusa/src/services/__mocks__/cart.js @@ -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"), diff --git a/packages/medusa/src/services/__mocks__/line-item.js b/packages/medusa/src/services/__mocks__/line-item.js index e5886bce4f..d71b950e9c 100644 --- a/packages/medusa/src/services/__mocks__/line-item.js +++ b/packages/medusa/src/services/__mocks__/line-item.js @@ -58,7 +58,7 @@ export const LineItemServiceMock = { variant_id: variantId, unit_price: 100, quantity, - metadata, + ...metadata, }) }), delete: jest.fn().mockImplementation(() => Promise.resolve()), diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 1bbe0d87fb..ba0ef88e0a 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -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") diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index fdbcc5642b..74b7a443f8 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -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 { 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 } } diff --git a/packages/medusa/src/strategies/tax-calculation.ts b/packages/medusa/src/strategies/tax-calculation.ts index cda43e7884..bb7765f939 100644 --- a/packages/medusa/src/strategies/tax-calculation.ts +++ b/packages/medusa/src/strategies/tax-calculation.ts @@ -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(