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
2023-10-03 11:20:49 +02:00
committed by GitHub
parent c5703a4765
commit 90e24c593f
9 changed files with 186 additions and 67 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): update unit_price for line-items with quantity pricing when merging on create

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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",

View File

@@ -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,
}
)
})

View File

@@ -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

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 {
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<void> {
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<LineItem>).variant
@@ -977,8 +1001,9 @@ class CartService extends TransactionBaseService {
async updateLineItem(
cartId: string,
lineItemId: string,
lineItemUpdate: LineItemUpdate
update: LineItemUpdate
): Promise<Cart> {
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
}
}
}

View File

@@ -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<string, unknown>
region_id?: string
variant_id?: string
should_calculate_prices?: boolean
}
export type LineItemValidateData = {
variant?: { product_id: string };
variant?: { product_id: string }
variant_id: string
}