fix: Cart operation should calculate item prices accounting for quantity (#13251)

* fix(): Cart operation should calculate item prices accounting for quantity

* fix(): Cart operation should calculate item prices accounting for quantity

* fix(): Cart operation should calculate item prices accounting for quantity

* fix when call warning

* fix tests and remove unnecessary object copy

* Create warm-dancers-allow.md

* fix update line item in cart workflow

* fix changeset

* update order flows

* fix cart spec integration tests

* improve create order workflow

* fixes and tests adjustments/improvements

* configurable useQueryGraphStep return type

* revert nullable take

* cleanup useQueryGraphStep
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2025-08-25 09:38:58 +02:00
1 parent f0ef444992
commit 6264a6262b
27 files changed
+2173 -335

No files matched your search

@@ -4,6 +4,7 @@ import {
Modules,
PriceListStatus,
PriceListType,
ProductStatus,
PromotionRuleOperator,
PromotionStatus,
PromotionType,
@@ -190,6 +191,100 @@ medusaIntegrationTestRunner({
)
})
it("should successfully create a cart with a line item with quantity and calculate prices based on the correct quantity", async () => {
const productData = {
title: "Medusa T-Shirt based quantity",
handle: "t-shirt-with-quantity-prices",
status: ProductStatus.PUBLISHED,
options: [
{
title: "Size",
values: ["S"],
},
],
variants: [
{
title: "S",
sku: "SHIRT-S-BLACK-w-quantity-prices",
options: {
Size: "S",
},
manage_inventory: false,
prices: [
{
amount: 1500,
currency_code: "usd",
min_quantity: 1,
max_quantity: 4,
},
{
amount: 1000,
currency_code: "usd",
min_quantity: 5,
max_quantity: 10,
},
],
},
],
}
const newProduct = await api.post(
`/admin/products`,
productData,
adminHeaders
)
const variantId = newProduct.data.product.variants[0].id
const newCart = (
await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
shipping_address: shippingAddressData,
items: [{ variant_id: variantId, quantity: 6 }],
},
storeHeaders
)
).data.cart
expect(newCart).toEqual(
expect.objectContaining({
item_subtotal: 5714.285714285715,
item_tax_total: 285.7142857142857,
item_total: 6000,
items: [
expect.objectContaining({
quantity: 6,
title: "Medusa T-Shirt based quantity",
unit_price: 1000,
updated_at: expect.any(String),
variant_barcode: null,
variant_id: expect.any(String),
variant_sku: "SHIRT-S-BLACK-w-quantity-prices",
variant_title: "S",
}),
],
original_item_subtotal: 5714.285714285715,
original_item_tax_total: 285.7142857142857,
original_item_total: 6000,
original_shipping_subtotal: 0,
original_shipping_tax_total: 0,
original_shipping_total: 0,
original_tax_total: 285.7142857142857,
original_total: 6000,
shipping_subtotal: 0,
shipping_tax_total: 0,
shipping_total: 0,
subtotal: 5714.285714285715,
tax_total: 285.7142857142857,
total: 6000,
})
)
})
describe("with sale price lists", () => {
let priceList
@@ -442,6 +537,473 @@ medusaIntegrationTestRunner({
)
})
it("should add item to cart and calculate prices based on item quantity", async () => {
const productData = {
title: "Medusa T-Shirt based quantity",
handle: "t-shirt-with-quantity-prices",
status: ProductStatus.PUBLISHED,
options: [
{
title: "Size",
values: ["S"],
},
],
variants: [
{
title: "S",
sku: "SHIRT-S-BLACK-w-quantity-prices",
options: {
Size: "S",
},
manage_inventory: false,
prices: [
{
amount: 1500,
currency_code: "usd",
min_quantity: 1,
max_quantity: 4,
},
{
amount: 1000,
currency_code: "usd",
min_quantity: 5,
max_quantity: 10,
},
],
},
],
}
const newProduct = await api.post(
`/admin/products`,
productData,
adminHeaders
)
const variantId = newProduct.data.product.variants[0].id
const newCart = (
await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
shipping_address: shippingAddressData,
},
storeHeaders
)
).data.cart
/**
* Add item to cart with quantity 1
* in order to have the price calculated based on the price rule
* with min_quantity 1 and max_quantity 4
*/
let response = await api.post(
`/store/carts/${newCart.id}/line-items`,
{
variant_id: variantId,
quantity: 1,
},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
billing_address: null,
completed_at: null,
created_at: expect.any(String),
credit_line_subtotal: 0,
credit_line_tax_total: 0,
credit_line_total: 0,
credit_lines: [],
currency_code: "usd",
customer_id: null,
discount_subtotal: 0,
discount_tax_total: 0,
discount_total: 0,
email: null,
id: newCart.id,
item_subtotal: 1428.5714285714287,
item_tax_total: 71.42857142857143,
item_total: 1500,
items: [
expect.objectContaining({
compare_at_unit_price: null,
created_at: expect.any(String),
id: expect.any(String),
is_tax_inclusive: true,
metadata: {},
product: expect.objectContaining({
categories: [],
collection_id: null,
id: expect.any(String),
tags: [],
type_id: null,
}),
product_collection: null,
product_description: null,
product_handle: "t-shirt-with-quantity-prices",
product_id: expect.any(String),
product_subtitle: null,
product_title: "Medusa T-Shirt based quantity",
product_type: null,
product_type_id: null,
quantity: 1,
requires_shipping: false,
tax_lines: [
{
code: "CADEFAULT",
description: "CA Default Rate",
id: expect.any(String),
provider_id: "system",
rate: 5,
},
],
thumbnail: null,
title: "Medusa T-Shirt based quantity",
unit_price: 1500,
updated_at: expect.any(String),
variant_barcode: null,
variant_id: expect.any(String),
variant_sku: "SHIRT-S-BLACK-w-quantity-prices",
variant_title: "S",
}),
],
metadata: null,
original_item_subtotal: 1428.5714285714287,
original_item_tax_total: 71.42857142857143,
original_item_total: 1500,
original_shipping_subtotal: 0,
original_shipping_tax_total: 0,
original_shipping_total: 0,
original_tax_total: 71.42857142857143,
original_total: 1500,
region: expect.objectContaining({
automatic_taxes: true,
countries: expect.any(Array),
currency_code: "usd",
id: expect.any(String),
name: "US",
}),
region_id: expect.any(String),
sales_channel_id: expect.any(String),
shipping_address: expect.objectContaining({
address_1: "test address 1",
address_2: "test address 2",
city: "SF",
company: null,
country_code: "US",
first_name: null,
id: expect.any(String),
last_name: null,
phone: null,
postal_code: "94016",
province: "CA",
}),
shipping_address_id: expect.any(String),
shipping_methods: [],
shipping_subtotal: 0,
shipping_tax_total: 0,
shipping_total: 0,
subtotal: 1428.5714285714287,
tax_total: 71.42857142857143,
total: 1500,
updated_at: expect.any(String),
})
)
/**
* Add item to cart with quantity 5
* in order to have the price calculated based on the price rule
* with min_quantity 5 and max_quantity 10
*/
response = await api.post(
`/store/carts/${newCart.id}/line-items`,
{
variant_id: variantId,
quantity: 5,
},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
billing_address: null,
completed_at: null,
created_at: expect.any(String),
credit_line_subtotal: 0,
credit_line_tax_total: 0,
credit_line_total: 0,
credit_lines: [],
currency_code: "usd",
customer_id: null,
discount_subtotal: 0,
discount_tax_total: 0,
discount_total: 0,
email: null,
id: newCart.id,
item_subtotal: 5714.285714285715,
item_tax_total: 285.7142857142857,
item_total: 6000,
items: [
expect.objectContaining({
adjustments: [],
compare_at_unit_price: null,
created_at: expect.any(String),
id: expect.any(String),
is_tax_inclusive: true,
metadata: {},
product: {
categories: [],
collection_id: null,
id: expect.any(String),
tags: [],
type_id: null,
},
product_collection: null,
product_description: null,
product_handle: "t-shirt-with-quantity-prices",
product_id: expect.any(String),
product_subtitle: null,
product_title: "Medusa T-Shirt based quantity",
product_type: null,
product_type_id: null,
quantity: 6,
requires_shipping: false,
tax_lines: [
{
code: "CADEFAULT",
description: "CA Default Rate",
id: expect.any(String),
provider_id: "system",
rate: 5,
},
],
thumbnail: null,
title: "Medusa T-Shirt based quantity",
unit_price: 1000,
updated_at: expect.any(String),
variant_barcode: null,
variant_id: expect.any(String),
variant_sku: "SHIRT-S-BLACK-w-quantity-prices",
variant_title: "S",
}),
],
metadata: null,
original_item_subtotal: 5714.285714285715,
original_item_tax_total: 285.7142857142857,
original_item_total: 6000,
original_shipping_subtotal: 0,
original_shipping_tax_total: 0,
original_shipping_total: 0,
original_tax_total: 285.7142857142857,
original_total: 6000,
promotions: [],
region: {
automatic_taxes: true,
countries: expect.any(Array),
currency_code: "usd",
id: expect.any(String),
name: "US",
},
region_id: expect.any(String),
sales_channel_id: expect.any(String),
shipping_address: {
address_1: "test address 1",
address_2: "test address 2",
city: "SF",
company: null,
country_code: "US",
first_name: null,
id: expect.any(String),
last_name: null,
phone: null,
postal_code: "94016",
province: "CA",
},
shipping_address_id: expect.any(String),
shipping_methods: [],
shipping_subtotal: 0,
shipping_tax_total: 0,
shipping_total: 0,
subtotal: 5714.285714285715,
tax_total: 285.7142857142857,
total: 6000,
updated_at: expect.any(String),
})
)
})
it("should update a cart line item quantity and calculate prices based the new item quantity", async () => {
const productData = {
title: "Medusa T-Shirt based quantity",
handle: "t-shirt-with-quantity-prices",
status: ProductStatus.PUBLISHED,
options: [
{
title: "Size",
values: ["S"],
},
],
variants: [
{
title: "S",
sku: "SHIRT-S-BLACK-w-quantity-prices",
options: {
Size: "S",
},
manage_inventory: false,
prices: [
{
amount: 1500,
currency_code: "usd",
min_quantity: 1,
max_quantity: 4,
},
{
amount: 1000,
currency_code: "usd",
min_quantity: 5,
max_quantity: 10,
},
],
},
],
}
const newProduct = await api.post(
`/admin/products`,
productData,
adminHeaders
)
const variantId = newProduct.data.product.variants[0].id
const newCart = (
await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
shipping_address: shippingAddressData,
},
storeHeaders
)
).data.cart
/**
* Add item to cart with quantity 1
* in order to have the price calculated based on the price rule
* with min_quantity 1 and max_quantity 4
*/
let response = await api.post(
`/store/carts/${newCart.id}/line-items`,
{
variant_id: variantId,
quantity: 1,
},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
item_subtotal: 1428.5714285714287,
item_tax_total: 71.42857142857143,
item_total: 1500,
items: [
expect.objectContaining({
quantity: 1,
title: "Medusa T-Shirt based quantity",
unit_price: 1500,
updated_at: expect.any(String),
variant_barcode: null,
variant_id: expect.any(String),
variant_sku: "SHIRT-S-BLACK-w-quantity-prices",
variant_title: "S",
}),
],
original_item_subtotal: 1428.5714285714287,
original_item_tax_total: 71.42857142857143,
original_item_total: 1500,
original_shipping_subtotal: 0,
original_shipping_tax_total: 0,
original_shipping_total: 0,
original_tax_total: 71.42857142857143,
original_total: 1500,
shipping_subtotal: 0,
shipping_tax_total: 0,
shipping_total: 0,
subtotal: 1428.5714285714287,
tax_total: 71.42857142857143,
total: 1500,
})
)
/**
* update item quantity to 5
* in order to have the price calculated based on the price rule
* with min_quantity 5 and max_quantity 10
*/
const itemId = response.data.cart.items[0].id
response = await api
.post(
`/store/carts/${newCart.id}/line-items/${itemId}`,
{
quantity: 6,
},
storeHeaders
)
.catch((e) => {
console.log(e.response.data)
throw e
})
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
item_subtotal: 5714.285714285715,
item_tax_total: 285.7142857142857,
item_total: 6000,
items: [
expect.objectContaining({
quantity: 6,
title: "Medusa T-Shirt based quantity",
unit_price: 1000,
updated_at: expect.any(String),
variant_barcode: null,
variant_id: expect.any(String),
variant_sku: "SHIRT-S-BLACK-w-quantity-prices",
variant_title: "S",
}),
],
original_item_subtotal: 5714.285714285715,
original_item_tax_total: 285.7142857142857,
original_item_total: 6000,
original_shipping_subtotal: 0,
original_shipping_tax_total: 0,
original_shipping_total: 0,
original_tax_total: 285.7142857142857,
original_total: 6000,
shipping_subtotal: 0,
shipping_tax_total: 0,
shipping_total: 0,
subtotal: 5714.285714285715,
tax_total: 285.7142857142857,
total: 6000,
})
)
})
it("should remove promotions when promotion is no longer in active state", async () => {
let responseBeforePromotionUpdate = await api.post(
`/store/carts/${cart.id}/line-items`,
@@ -1,6 +1,10 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { Modules, PromotionStatus, PromotionType } from "@medusajs/utils"
import { createAdminUser, generatePublishableKey, generateStoreHeaders, } from "../../../../helpers/create-admin-user"
import {
createAdminUser,
generatePublishableKey,
generateStoreHeaders,
} from "../../../../helpers/create-admin-user"
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures/tax"
import { medusaTshirtProduct } from "../../../__fixtures__/product"
@@ -2469,7 +2473,8 @@ medusaIntegrationTestRunner({
expect.arrayContaining([
expect.objectContaining({
id: "shipping_option_type",
value: "shipping_methods.shipping_option.shipping_option_type_id",
value:
"shipping_methods.shipping_option.shipping_option_type_id",
label: "Shipping Option Type",
required: false,
field_type: "multiselect",