Fix/pricing update fixes (#5275)

* fix for merging line items and quantity prices

* add changeset

* fix draft order unit_price calculation

* update test to reflect only quantity change

* fix unit tests

* update conditional
This commit is contained in:
Philip Korsholm authored and GitHub committed 2023-10-03 11:20:49 +02:00
1 parent c5703a4765
commit 90e24c593f
9 files changed
+186 -67

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): update unit_price for line-items with quantity pricing when merging on create
@@ -950,6 +950,41 @@ describe("/admin/draft-orders", () => {
expect(updatedDraftOrder.data.draft_order.cart.subtotal).not.toEqual(0) 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 () => { it("removes the line item, if quantity is 0", async () => {
const api = useApi() const api = useApi()
@@ -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 () => { it("creates and updates line item quantity with unit price reflected", async () => {
const api = useApi() const api = useApi()
+2
View File
@@ -987,6 +987,8 @@ module.exports = async (dataSource, data = {}) => {
variant_id: "test-variant", variant_id: "test-variant",
product_id: "test-product", product_id: "test-product",
cart_id: "test-cart-3", cart_id: "test-cart-3",
should_merge: true,
metadata: {},
}) })
await manager.save(li2) await manager.save(li2)
@@ -92,7 +92,7 @@ module.exports = async (dataSource, data = {}) => {
id: "test-variant", id: "test-variant",
title: "test variant", title: "test variant",
product_id: "test-product", product_id: "test-product",
inventory_quantity: 1, inventory_quantity: 2,
options: [ options: [
{ {
option_id: "test-option", option_id: "test-option",
@@ -1,6 +1,6 @@
import { CartServiceMock } from "../../../../../services/__mocks__/cart"
import { IdMap } from "medusa-test-utils" import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request" import { request } from "../../../../../helpers/test-request"
import { CartServiceMock } from "../../../../../services/__mocks__/cart"
describe("POST /store/carts/:id/line-items/:line_id", () => { describe("POST /store/carts/:id/line-items/:line_id", () => {
describe("successfully updates a line item", () => { 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"), region_id: IdMap.getId("region-france"),
quantity: 3, quantity: 3,
metadata: {}, metadata: {},
should_calculate_prices: true,
} }
) )
}) })
@@ -126,6 +127,7 @@ describe("POST /store/carts/:id/line-items/:line_id", () => {
quantity: 3, quantity: 3,
region_id: expect.any(String), region_id: expect.any(String),
variant_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, quantity: 3,
region_id: expect.any(String), region_id: expect.any(String),
variant_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, quantity: 3,
region_id: expect.any(String), region_id: expect.any(String),
variant_id: expect.any(String), variant_id: expect.any(String),
should_calculate_prices: true,
} }
) )
}) })
@@ -1,8 +1,9 @@
import { IsInt, IsOptional } from "class-validator" import { IsInt, IsOptional } from "class-validator"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "." import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services" import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { cleanResponseData } from "../../../../utils/clean-response-data" import { cleanResponseData } from "../../../../utils/clean-response-data"
import { handleAddOrUpdateLineItem } from "./create-line-item/utils/handler-steps" import { handleAddOrUpdateLineItem } from "./create-line-item/utils/handler-steps"
@@ -91,6 +92,7 @@ export default async (req, res) => {
region_id: cart.region_id, region_id: cart.region_id,
quantity: validated.quantity, quantity: validated.quantity,
metadata: validated.metadata || {}, metadata: validated.metadata || {},
should_calculate_prices: true,
} }
await cartService await cartService
+86 -59
View File
@@ -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 { 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, CustomShippingOptionService,
CustomerService,
DiscountService, DiscountService,
EventBusService, EventBusService,
GiftCardService, GiftCardService,
@@ -24,46 +48,23 @@ import {
TaxProviderService, TaxProviderService,
TotalsService, TotalsService,
} from "." } from "."
import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm"
import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces" import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces"
import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" import { MedusaError, isDefined } from "medusa-core-utils"
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { buildQuery, isString, setMetadata } from "../utils"
import { import { isEmpty, isEqual } from "lodash"
Address,
Cart,
Customer,
CustomShippingOption,
Discount,
DiscountRule,
DiscountRuleType,
LineItem,
PaymentSession,
PaymentSessionStatus,
SalesChannel,
ShippingMethod,
} from "../models"
import { AddressRepository } from "../repositories/address" import { AddressRepository } from "../repositories/address"
import { CartRepository } from "../repositories/cart" import { CartRepository } from "../repositories/cart"
import { LineItemRepository } from "../repositories/line-item" import { FlagRouter } from "@medusajs/utils"
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 { IsNumber } from "class-validator" 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 = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
@@ -797,7 +798,7 @@ class CartService extends TransactionBaseService {
): Promise<void> { ): Promise<void> {
const items: LineItem[] = Array.isArray(lineItems) ? lineItems : [lineItems] 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")) { if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
select.push("sales_channel_id") select.push("sales_channel_id")
@@ -900,10 +901,33 @@ class CartService extends TransactionBaseService {
} }
if (currentItem) { 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] = { lineItemsToUpdate[currentItem.id] = {
quantity: item.quantity, quantity: item.quantity,
has_shipping: false, has_shipping: false,
} }
if (isDefined(calculated_price)) {
lineItemsToUpdate[currentItem.id].unit_price = calculated_price
}
} else { } else {
// Since the variant is eager loaded, we are removing it before the line item is being created. // Since the variant is eager loaded, we are removing it before the line item is being created.
delete (item as Partial<LineItem>).variant delete (item as Partial<LineItem>).variant
@@ -977,8 +1001,9 @@ class CartService extends TransactionBaseService {
async updateLineItem( async updateLineItem(
cartId: string, cartId: string,
lineItemId: string, lineItemId: string,
lineItemUpdate: LineItemUpdate update: LineItemUpdate
): Promise<Cart> { ): Promise<Cart> {
const { should_calculate_prices, ...lineItemUpdate } = update
return await this.atomicPhase_( return await this.atomicPhase_(
async (transactionManager: EntityManager) => { async (transactionManager: EntityManager) => {
const select: (keyof Cart)[] = ["id", "region_id", "customer_id"] const select: (keyof Cart)[] = ["id", "region_id", "customer_id"]
@@ -1021,24 +1046,26 @@ class CartService extends TransactionBaseService {
) )
} }
const variantsPricing = await this.pricingService_ if (should_calculate_prices) {
.withTransaction(transactionManager) const variantsPricing = await this.pricingService_
.getProductVariantsPricing( .withTransaction(transactionManager)
[ .getProductVariantsPricing(
[
{
variantId: lineItem.variant_id,
quantity: lineItemUpdate.quantity,
},
],
{ {
variantId: lineItem.variant_id, region_id: cart.region_id,
quantity: lineItemUpdate.quantity, 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] const { calculated_price } = variantsPricing[lineItem.variant_id]
lineItemUpdate.unit_price = calculated_price ?? undefined lineItemUpdate.unit_price = calculated_price ?? undefined
}
} }
} }
+10 -4
View File
@@ -1,8 +1,13 @@
import { ValidateNested } from "class-validator" import {
import { IsType } from "../utils/validators/is-type" AddressPayload,
DateComparisonOperator,
StringComparisonOperator,
} from "./common"
import { Cart, CartType } from "../models/cart" import { Cart, CartType } from "../models/cart"
import { AddressPayload, DateComparisonOperator, StringComparisonOperator } from "./common"
import { IsType } from "../utils/validators/is-type"
import { Region } from "../models" import { Region } from "../models"
import { ValidateNested } from "class-validator"
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isCart(object: any): object is Cart { export function isCart(object: any): object is Cart {
@@ -29,10 +34,11 @@ export type LineItemUpdate = {
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
region_id?: string region_id?: string
variant_id?: string variant_id?: string
should_calculate_prices?: boolean
} }
export type LineItemValidateData = { export type LineItemValidateData = {
variant?: { product_id: string }; variant?: { product_id: string }
variant_id: string variant_id: string
} }