diff --git a/.changeset/many-peas-draw.md b/.changeset/many-peas-draw.md new file mode 100644 index 0000000000..e6e37809c5 --- /dev/null +++ b/.changeset/many-peas-draw.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): update unit_price for line-items with quantity pricing when merging on create diff --git a/integration-tests/api/__tests__/admin/draft-order/draft-order.js b/integration-tests/api/__tests__/admin/draft-order/draft-order.js index e6bc6439a2..c83c39ed50 100644 --- a/integration-tests/api/__tests__/admin/draft-order/draft-order.js +++ b/integration-tests/api/__tests__/admin/draft-order/draft-order.js @@ -950,6 +950,41 @@ describe("/admin/draft-orders", () => { expect(updatedDraftOrder.data.draft_order.cart.subtotal).not.toEqual(0) }) + it("updates a line item on the draft order with quantity", async () => { + const api = useApi() + await api.post( + "/admin/draft-orders/test-draft-order/line-items/test-item", + { + unit_price: 1000, + }, + adminReqConfig + ) + + const response = await api.post( + "/admin/draft-orders/test-draft-order/line-items/test-item", + { + quantity: 2, + }, + adminReqConfig + ) + + expect(response.status).toEqual(200) + + const updatedDraftOrder = await api.get( + `/admin/draft-orders/test-draft-order`, + adminReqConfig + ) + + const item = updatedDraftOrder.data.draft_order.cart.items[0] + + expect(item.unit_price).toEqual(1000) + expect(item.quantity).toEqual(2) + expect(updatedDraftOrder.data.draft_order.cart.subtotal).not.toEqual( + undefined + ) + expect(updatedDraftOrder.data.draft_order.cart.subtotal).not.toEqual(0) + }) + it("removes the line item, if quantity is 0", async () => { const api = useApi() diff --git a/integration-tests/api/__tests__/store/cart/cart.js b/integration-tests/api/__tests__/store/cart/cart.js index 11a9cf3cb7..e71863d023 100644 --- a/integration-tests/api/__tests__/store/cart/cart.js +++ b/integration-tests/api/__tests__/store/cart/cart.js @@ -1017,6 +1017,44 @@ describe("/store/carts", () => { ) }) + it("updates line item quantity with unit price reflected when merging line-items", async () => { + const api = useApi() + + await simplePriceListFactory(dbConnection, { + id: "pl_current", + prices: [ + { + variant_id: "test-variant", + amount: 10, + min_quantity: 5, + currency_code: "usd", + }, + ], + }) + + const response = await api + .post( + "/store/carts/test-cart-3/line-items", + { + variant_id: "test-variant", + quantity: 4, + }, + { withCredentials: true } + ) + .catch(console.log) + + expect(response.data.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cart_id: "test-cart-3", + unit_price: 10, + variant_id: "test-variant", + quantity: 5, + }), + ]) + ) + }) + it("creates and updates line item quantity with unit price reflected", async () => { const api = useApi() diff --git a/integration-tests/helpers/cart-seeder.js b/integration-tests/helpers/cart-seeder.js index 63d7256e7c..e72bed645a 100644 --- a/integration-tests/helpers/cart-seeder.js +++ b/integration-tests/helpers/cart-seeder.js @@ -987,6 +987,8 @@ module.exports = async (dataSource, data = {}) => { variant_id: "test-variant", product_id: "test-product", cart_id: "test-cart-3", + should_merge: true, + metadata: {}, }) await manager.save(li2) diff --git a/integration-tests/helpers/draft-order-seeder.js b/integration-tests/helpers/draft-order-seeder.js index 6a0c62732a..6897d80fab 100644 --- a/integration-tests/helpers/draft-order-seeder.js +++ b/integration-tests/helpers/draft-order-seeder.js @@ -92,7 +92,7 @@ module.exports = async (dataSource, data = {}) => { id: "test-variant", title: "test variant", product_id: "test-product", - inventory_quantity: 1, + inventory_quantity: 2, options: [ { option_id: "test-option", diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js b/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js index 2893b7d968..36b87dcc44 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/update-line-item.js @@ -1,6 +1,6 @@ +import { CartServiceMock } from "../../../../../services/__mocks__/cart" import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" -import { CartServiceMock } from "../../../../../services/__mocks__/cart" describe("POST /store/carts/:id/line-items/:line_id", () => { describe("successfully updates a line item", () => { @@ -34,6 +34,7 @@ describe("POST /store/carts/:id/line-items/:line_id", () => { region_id: IdMap.getId("region-france"), quantity: 3, metadata: {}, + should_calculate_prices: true, } ) }) @@ -126,6 +127,7 @@ describe("POST /store/carts/:id/line-items/:line_id", () => { quantity: 3, region_id: expect.any(String), variant_id: expect.any(String), + should_calculate_prices: true, } ) }) @@ -170,6 +172,7 @@ describe("POST /store/carts/:id/line-items/:line_id", () => { quantity: 3, region_id: expect.any(String), variant_id: expect.any(String), + should_calculate_prices: true, } ) }) @@ -215,6 +218,7 @@ describe("POST /store/carts/:id/line-items/:line_id", () => { quantity: 3, region_id: expect.any(String), variant_id: expect.any(String), + should_calculate_prices: true, } ) }) 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 2d6d975d75..40a3c3b956 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 @@ -1,8 +1,9 @@ import { IsInt, IsOptional } from "class-validator" -import { MedusaError } from "medusa-core-utils" -import { EntityManager } from "typeorm" import { defaultStoreCartFields, defaultStoreCartRelations } from "." + import { CartService } from "../../../../services" +import { EntityManager } from "typeorm" +import { MedusaError } from "medusa-core-utils" import { cleanResponseData } from "../../../../utils/clean-response-data" import { handleAddOrUpdateLineItem } from "./create-line-item/utils/handler-steps" @@ -91,6 +92,7 @@ export default async (req, res) => { region_id: cart.region_id, quantity: validated.quantity, metadata: validated.metadata || {}, + should_calculate_prices: true, } await cartService diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 74b7a443f8..ad79166f13 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1,10 +1,34 @@ -import { FlagRouter } from "@medusajs/utils" -import { isEmpty, isEqual } from "lodash" -import { isDefined, MedusaError } from "medusa-core-utils" -import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm" import { - CustomerService, + Address, + Cart, + CustomShippingOption, + Customer, + Discount, + DiscountRule, + DiscountRuleType, + LineItem, + PaymentSession, + PaymentSessionStatus, + SalesChannel, + ShippingMethod, +} from "../models" +import { + AddressPayload, + FindConfig, + TotalField, + WithRequiredProperty, +} from "../types/common" +import { + CartCreateProps, + CartUpdateProps, + FilterableCartProps, + LineItemUpdate, + LineItemValidateData, + isCart, +} from "../types/cart" +import { CustomShippingOptionService, + CustomerService, DiscountService, EventBusService, GiftCardService, @@ -24,46 +48,23 @@ import { TaxProviderService, TotalsService, } from "." +import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm" import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces" -import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" -import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" -import { - Address, - Cart, - Customer, - CustomShippingOption, - Discount, - DiscountRule, - DiscountRuleType, - LineItem, - PaymentSession, - PaymentSessionStatus, - SalesChannel, - ShippingMethod, -} from "../models" +import { MedusaError, isDefined } from "medusa-core-utils" +import { buildQuery, isString, setMetadata } from "../utils" +import { isEmpty, isEqual } from "lodash" + import { AddressRepository } from "../repositories/address" import { CartRepository } from "../repositories/cart" -import { LineItemRepository } from "../repositories/line-item" -import { PaymentSessionRepository } from "../repositories/payment-session" -import { ShippingMethodRepository } from "../repositories/shipping-method" -import { - CartCreateProps, - CartUpdateProps, - FilterableCartProps, - isCart, - LineItemUpdate, - LineItemValidateData, -} from "../types/cart" -import { - AddressPayload, - FindConfig, - TotalField, - WithRequiredProperty, -} from "../types/common" -import { PaymentSessionInput } from "../types/payment" -import { buildQuery, isString, setMetadata } from "../utils" -import { validateEmail } from "../utils/is-email" +import { FlagRouter } from "@medusajs/utils" import { IsNumber } from "class-validator" +import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" +import { LineItemRepository } from "../repositories/line-item" +import { PaymentSessionInput } from "../types/payment" +import { PaymentSessionRepository } from "../repositories/payment-session" +import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" +import { ShippingMethodRepository } from "../repositories/shipping-method" +import { validateEmail } from "../utils/is-email" type InjectedDependencies = { manager: EntityManager @@ -797,7 +798,7 @@ class CartService extends TransactionBaseService { ): Promise { const items: LineItem[] = Array.isArray(lineItems) ? lineItems : [lineItems] - const select: (keyof Cart)[] = ["id"] + const select: (keyof Cart)[] = ["id", "customer_id", "region_id"] if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { select.push("sales_channel_id") @@ -900,10 +901,33 @@ class CartService extends TransactionBaseService { } if (currentItem) { + const variantsPricing = await this.pricingService_ + .withTransaction(transactionManager) + .getProductVariantsPricing( + [ + { + variantId: item.variant_id!, + quantity: item.quantity, + }, + ], + { + region_id: cart.region_id, + customer_id: cart.customer_id, + include_discount_prices: true, + } + ) + + const { calculated_price } = + variantsPricing[currentItem.variant_id!] + lineItemsToUpdate[currentItem.id] = { quantity: item.quantity, has_shipping: false, } + + if (isDefined(calculated_price)) { + lineItemsToUpdate[currentItem.id].unit_price = calculated_price + } } else { // Since the variant is eager loaded, we are removing it before the line item is being created. delete (item as Partial).variant @@ -977,8 +1001,9 @@ class CartService extends TransactionBaseService { async updateLineItem( cartId: string, lineItemId: string, - lineItemUpdate: LineItemUpdate + update: LineItemUpdate ): Promise { + const { should_calculate_prices, ...lineItemUpdate } = update return await this.atomicPhase_( async (transactionManager: EntityManager) => { const select: (keyof Cart)[] = ["id", "region_id", "customer_id"] @@ -1021,24 +1046,26 @@ class CartService extends TransactionBaseService { ) } - const variantsPricing = await this.pricingService_ - .withTransaction(transactionManager) - .getProductVariantsPricing( - [ + if (should_calculate_prices) { + const variantsPricing = await this.pricingService_ + .withTransaction(transactionManager) + .getProductVariantsPricing( + [ + { + variantId: lineItem.variant_id, + quantity: lineItemUpdate.quantity, + }, + ], { - variantId: lineItem.variant_id, - quantity: lineItemUpdate.quantity, - }, - ], - { - region_id: cart.region_id, - customer_id: cart.customer_id, - include_discount_prices: true, - } - ) + 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 + const { calculated_price } = variantsPricing[lineItem.variant_id] + lineItemUpdate.unit_price = calculated_price ?? undefined + } } } diff --git a/packages/medusa/src/types/cart.ts b/packages/medusa/src/types/cart.ts index 4b8a5031b2..f79907a076 100644 --- a/packages/medusa/src/types/cart.ts +++ b/packages/medusa/src/types/cart.ts @@ -1,8 +1,13 @@ -import { ValidateNested } from "class-validator" -import { IsType } from "../utils/validators/is-type" +import { + AddressPayload, + DateComparisonOperator, + StringComparisonOperator, +} from "./common" import { Cart, CartType } from "../models/cart" -import { AddressPayload, DateComparisonOperator, StringComparisonOperator } from "./common" + +import { IsType } from "../utils/validators/is-type" import { Region } from "../models" +import { ValidateNested } from "class-validator" // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isCart(object: any): object is Cart { @@ -29,10 +34,11 @@ export type LineItemUpdate = { metadata?: Record region_id?: string variant_id?: string + should_calculate_prices?: boolean } export type LineItemValidateData = { - variant?: { product_id: string }; + variant?: { product_id: string } variant_id: string }