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:
1 parent
f0ef444992
commit
6264a6262b
26 files changed
+2132
-294
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(): Cart operation should calculate item prices accounting for quantity
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Modules,
|
Modules,
|
||||||
PriceListStatus,
|
PriceListStatus,
|
||||||
PriceListType,
|
PriceListType,
|
||||||
|
ProductStatus,
|
||||||
PromotionRuleOperator,
|
PromotionRuleOperator,
|
||||||
PromotionStatus,
|
PromotionStatus,
|
||||||
PromotionType,
|
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", () => {
|
describe("with sale price lists", () => {
|
||||||
let priceList
|
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 () => {
|
it("should remove promotions when promotion is no longer in active state", async () => {
|
||||||
let responseBeforePromotionUpdate = await api.post(
|
let responseBeforePromotionUpdate = await api.post(
|
||||||
`/store/carts/${cart.id}/line-items`,
|
`/store/carts/${cart.id}/line-items`,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import { Modules, PromotionStatus, PromotionType } from "@medusajs/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 { setupTaxStructure } from "../../../../modules/__tests__/fixtures/tax"
|
||||||
import { medusaTshirtProduct } from "../../../__fixtures__/product"
|
import { medusaTshirtProduct } from "../../../__fixtures__/product"
|
||||||
|
|
||||||
@@ -2469,7 +2473,8 @@ medusaIntegrationTestRunner({
|
|||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "shipping_option_type",
|
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",
|
label: "Shipping Option Type",
|
||||||
required: false,
|
required: false,
|
||||||
field_type: "multiselect",
|
field_type: "multiselect",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
IRegionModuleService,
|
IRegionModuleService,
|
||||||
ISalesChannelModuleService,
|
ISalesChannelModuleService,
|
||||||
IStockLocationService,
|
IStockLocationService,
|
||||||
|
PricingContext,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
@@ -759,11 +760,23 @@ medusaIntegrationTestRunner({
|
|||||||
* Tried jest, but for some reasons it is not able to provide
|
* Tried jest, but for some reasons it is not able to provide
|
||||||
* correct arguments passed to the function
|
* correct arguments passed to the function
|
||||||
*/
|
*/
|
||||||
let pricingContext: any
|
let calculatePricesHasBeenCalled = false
|
||||||
const originalFn = pricingModule.listPriceSets.bind(pricingModule)
|
|
||||||
pricingModule.listPriceSets = function () {
|
const originalFn = pricingModule.calculatePrices.bind(pricingModule)
|
||||||
pricingContext = { ...arguments[0].context }
|
pricingModule.calculatePrices = function (...args) {
|
||||||
return originalFn.bind(pricingModule)(...arguments)
|
calculatePricesHasBeenCalled = true
|
||||||
|
|
||||||
|
const pricingContext = args[1]!.context
|
||||||
|
|
||||||
|
expect(pricingContext).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
unit_price: 100,
|
||||||
|
region_id: region.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return originalFn.bind(pricingModule)(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result } = await createCartWorkflow(appContainer).run({
|
const { result } = await createCartWorkflow(appContainer).run({
|
||||||
@@ -782,15 +795,9 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
setPricingContextHook = undefined
|
setPricingContextHook = undefined
|
||||||
pricingModule.listPriceSets = originalFn
|
pricingModule.calculatePrices = originalFn
|
||||||
|
|
||||||
expect(pricingContext).toEqual(
|
expect(calculatePricesHasBeenCalled).toBe(true)
|
||||||
expect.objectContaining({
|
|
||||||
unit_price: 100,
|
|
||||||
region_id: region.id,
|
|
||||||
currency_code: "usd",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const cart = await cartModuleService.retrieveCart(result.id, {
|
const cart = await cartModuleService.retrieveCart(result.id, {
|
||||||
relations: ["items"],
|
relations: ["items"],
|
||||||
@@ -924,11 +931,25 @@ medusaIntegrationTestRunner({
|
|||||||
* Tried jest, but for some reasons it is not able to provide
|
* Tried jest, but for some reasons it is not able to provide
|
||||||
* correct arguments passed to the function
|
* correct arguments passed to the function
|
||||||
*/
|
*/
|
||||||
let pricingContext: any
|
let calculatePricesHasBeenCalled = false
|
||||||
const originalFn = pricingModule.listPriceSets.bind(pricingModule)
|
|
||||||
pricingModule.listPriceSets = function () {
|
const originalFn = pricingModule.calculatePrices.bind(pricingModule)
|
||||||
pricingContext = { ...arguments[0].context }
|
pricingModule.calculatePrices = function (...args) {
|
||||||
return originalFn.bind(pricingModule)(...arguments)
|
calculatePricesHasBeenCalled = true
|
||||||
|
|
||||||
|
const pricingContext = args[1]!.context
|
||||||
|
|
||||||
|
expect(pricingContext).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
unit_price: 200,
|
||||||
|
region_id: region.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(pricingContext?.customer_id).toBeDefined()
|
||||||
|
expect(pricingContext?.customer_id).not.toEqual("1")
|
||||||
|
|
||||||
|
return originalFn.bind(pricingModule)(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result } = await createCartWorkflow(appContainer).run({
|
const { result } = await createCartWorkflow(appContainer).run({
|
||||||
@@ -947,17 +968,9 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
setPricingContextHook = undefined
|
setPricingContextHook = undefined
|
||||||
pricingModule.listPriceSets = originalFn
|
pricingModule.calculatePrices = originalFn
|
||||||
|
|
||||||
expect(pricingContext).toEqual(
|
expect(calculatePricesHasBeenCalled).toBe(true)
|
||||||
expect.objectContaining({
|
|
||||||
unit_price: 200,
|
|
||||||
region_id: region.id,
|
|
||||||
currency_code: "usd",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
expect(pricingContext.customer_id).toBeDefined()
|
|
||||||
expect(pricingContext.customer_id).not.toEqual("1")
|
|
||||||
|
|
||||||
const cart = await cartModuleService.retrieveCart(result.id, {
|
const cart = await cartModuleService.retrieveCart(result.id, {
|
||||||
relations: ["items"],
|
relations: ["items"],
|
||||||
@@ -1851,7 +1864,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
expect(errors).toEqual([
|
expect(errors).toEqual([
|
||||||
{
|
{
|
||||||
action: "validate-variant-prices",
|
action: "get-variant-price-sets",
|
||||||
handlerType: "invoke",
|
handlerType: "invoke",
|
||||||
error: expect.objectContaining({
|
error: expect.objectContaining({
|
||||||
message: expect.stringContaining(
|
message: expect.stringContaining(
|
||||||
@@ -1960,10 +1973,22 @@ medusaIntegrationTestRunner({
|
|||||||
* correct arguments passed to the function
|
* correct arguments passed to the function
|
||||||
*/
|
*/
|
||||||
let pricingContext: any
|
let pricingContext: any
|
||||||
const originalFn = pricingModule.listPriceSets.bind(pricingModule)
|
let calculatePricessHaveBeenCalled = false
|
||||||
pricingModule.listPriceSets = function () {
|
const originalFn = pricingModule.calculatePrices.bind(pricingModule)
|
||||||
pricingContext = { ...arguments[0].context }
|
pricingModule.calculatePrices = function (...args) {
|
||||||
return originalFn.bind(pricingModule)(...arguments)
|
pricingContext = args[1]!
|
||||||
|
calculatePricessHaveBeenCalled = true
|
||||||
|
|
||||||
|
expect(pricingContext).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
context: expect.objectContaining({
|
||||||
|
unit_price: 100,
|
||||||
|
currency_code: "usd",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return originalFn.bind(pricingModule)(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
await addToCartWorkflow(appContainer).run({
|
await addToCartWorkflow(appContainer).run({
|
||||||
@@ -1979,14 +2004,9 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
setPricingContextHook = undefined
|
setPricingContextHook = undefined
|
||||||
pricingModule.listPriceSets = originalFn
|
pricingModule.calculatePrices = originalFn
|
||||||
|
|
||||||
expect(pricingContext).toEqual(
|
expect(calculatePricessHaveBeenCalled).toBe(true)
|
||||||
expect.objectContaining({
|
|
||||||
unit_price: 100,
|
|
||||||
currency_code: "usd",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
cart = await cartModuleService.retrieveCart(cart.id, {
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
relations: ["items"],
|
relations: ["items"],
|
||||||
@@ -2114,9 +2134,24 @@ medusaIntegrationTestRunner({
|
|||||||
* correct arguments passed to the function
|
* correct arguments passed to the function
|
||||||
*/
|
*/
|
||||||
let pricingContext: any
|
let pricingContext: any
|
||||||
const originalFn = pricingModule.listPriceSets.bind(pricingModule)
|
let calculatePricessHaveBeenCalled = false
|
||||||
pricingModule.listPriceSets = function () {
|
|
||||||
pricingContext = { ...arguments[0].context }
|
const originalFn = pricingModule.calculatePrices.bind(pricingModule)
|
||||||
|
pricingModule.calculatePrices = function (...args) {
|
||||||
|
pricingContext = args[1]!
|
||||||
|
calculatePricessHaveBeenCalled = true
|
||||||
|
|
||||||
|
expect(pricingContext).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
context: expect.objectContaining({
|
||||||
|
unit_price: 200,
|
||||||
|
region_id: cart.region_id,
|
||||||
|
customer_id: cart.customer_id,
|
||||||
|
currency_code: "usd",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
return originalFn.bind(pricingModule)(...arguments)
|
return originalFn.bind(pricingModule)(...arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2133,21 +2168,14 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
setPricingContextHook = undefined
|
setPricingContextHook = undefined
|
||||||
pricingModule.listPriceSets = originalFn
|
pricingModule.calculatePrices = originalFn
|
||||||
|
|
||||||
expect(pricingContext).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
unit_price: 200,
|
|
||||||
region_id: cart.region_id,
|
|
||||||
customer_id: cart.customer_id,
|
|
||||||
currency_code: "usd",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
cart = await cartModuleService.retrieveCart(cart.id, {
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
relations: ["items"],
|
relations: ["items"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
expect(calculatePricessHaveBeenCalled).toBe(true)
|
||||||
|
|
||||||
expect(cart).toEqual(
|
expect(cart).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: cart.id,
|
id: cart.id,
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
IStockLocationServiceNext,
|
IStockLocationServiceNext,
|
||||||
ITaxModuleService,
|
ITaxModuleService,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
Modules,
|
||||||
|
PromotionStatus,
|
||||||
|
PromotionType,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
@@ -25,30 +30,25 @@ medusaIntegrationTestRunner({
|
|||||||
env,
|
env,
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
let appContainer
|
let appContainer
|
||||||
let cartModuleService: ICartModuleService
|
|
||||||
let regionModuleService: IRegionModuleService
|
let regionModuleService: IRegionModuleService
|
||||||
let scModuleService: ISalesChannelModuleService
|
let scModuleService: ISalesChannelModuleService
|
||||||
let productModule: IProductModuleService
|
let productModule: IProductModuleService
|
||||||
let pricingModule: IPricingModuleService
|
let pricingModule: IPricingModuleService
|
||||||
let inventoryModule: IInventoryServiceNext
|
let inventoryModule: IInventoryServiceNext
|
||||||
let stockLocationModule: IStockLocationServiceNext
|
let stockLocationModule: IStockLocationServiceNext
|
||||||
let fulfillmentModule: IFulfillmentModuleService
|
|
||||||
let taxModule: ITaxModuleService
|
let taxModule: ITaxModuleService
|
||||||
let remoteLink, remoteQuery
|
let remoteLink
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
appContainer = getContainer()
|
appContainer = getContainer()
|
||||||
cartModuleService = appContainer.resolve(Modules.CART)
|
|
||||||
regionModuleService = appContainer.resolve(Modules.REGION)
|
regionModuleService = appContainer.resolve(Modules.REGION)
|
||||||
scModuleService = appContainer.resolve(Modules.SALES_CHANNEL)
|
scModuleService = appContainer.resolve(Modules.SALES_CHANNEL)
|
||||||
productModule = appContainer.resolve(Modules.PRODUCT)
|
productModule = appContainer.resolve(Modules.PRODUCT)
|
||||||
pricingModule = appContainer.resolve(Modules.PRICING)
|
pricingModule = appContainer.resolve(Modules.PRICING)
|
||||||
inventoryModule = appContainer.resolve(Modules.INVENTORY)
|
inventoryModule = appContainer.resolve(Modules.INVENTORY)
|
||||||
stockLocationModule = appContainer.resolve(Modules.STOCK_LOCATION)
|
stockLocationModule = appContainer.resolve(Modules.STOCK_LOCATION)
|
||||||
fulfillmentModule = appContainer.resolve(Modules.FULFILLMENT)
|
|
||||||
taxModule = appContainer.resolve(Modules.TAX)
|
taxModule = appContainer.resolve(Modules.TAX)
|
||||||
remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||||
remoteQuery = appContainer.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -199,7 +199,6 @@ medusaIntegrationTestRunner({
|
|||||||
country_code: "US",
|
country_code: "US",
|
||||||
postal_code: "12345",
|
postal_code: "12345",
|
||||||
},
|
},
|
||||||
promo_codes: ["testytest"],
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
variant_id: product.variants[0].id,
|
variant_id: product.variants[0].id,
|
||||||
@@ -386,6 +385,388 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should create a draft order applying the correct promotion on the items", async () => {
|
||||||
|
const region = await regionModuleService.createRegions({
|
||||||
|
name: "US",
|
||||||
|
currency_code: "usd",
|
||||||
|
})
|
||||||
|
|
||||||
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
|
name: "Webshop",
|
||||||
|
})
|
||||||
|
|
||||||
|
const location = await stockLocationModule.createStockLocations({
|
||||||
|
name: "Warehouse",
|
||||||
|
})
|
||||||
|
|
||||||
|
const [product, product_2] = await productModule.createProducts([
|
||||||
|
{
|
||||||
|
title: "Test product",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Another product",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Variant variable",
|
||||||
|
manage_inventory: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryModule.createInventoryItems({
|
||||||
|
sku: "inv-1234",
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryModule.createInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: location.id,
|
||||||
|
stocked_quantity: 2,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const [priceSet, priceSet_2] = await pricingModule.createPriceSets([
|
||||||
|
{
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 1000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a promotion to test with
|
||||||
|
*/
|
||||||
|
const promotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "testytest",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "each",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 100,
|
||||||
|
max_quantity: 100,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "variant_id",
|
||||||
|
operator: "in",
|
||||||
|
values: [product.variants[0].id, product_2.variants[0].id],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/price-preferences",
|
||||||
|
{
|
||||||
|
attribute: "currency_code",
|
||||||
|
value: "usd",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await remoteLink.create([
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
},
|
||||||
|
[Modules.PRICING]: {
|
||||||
|
price_set_id: priceSet.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: product_2.variants[0].id,
|
||||||
|
},
|
||||||
|
[Modules.PRICING]: {
|
||||||
|
price_set_id: priceSet_2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[Modules.SALES_CHANNEL]: {
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
},
|
||||||
|
[Modules.STOCK_LOCATION]: {
|
||||||
|
stock_location_id: location.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
},
|
||||||
|
[Modules.INVENTORY]: {
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: product_2.variants[0].id,
|
||||||
|
},
|
||||||
|
[Modules.INVENTORY]: {
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await setupTaxStructure(taxModule)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
email: "oli@test.dk",
|
||||||
|
region_id: region.id,
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
shipping_address: {
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
phone: "12345",
|
||||||
|
},
|
||||||
|
billing_address: {
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
},
|
||||||
|
promo_codes: ["testytest"],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
is_discountable: true,
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant_id: product_2.variants[0].id,
|
||||||
|
is_discountable: true,
|
||||||
|
unit_price: 200,
|
||||||
|
quantity: 1,
|
||||||
|
metadata: {
|
||||||
|
note: "reduced price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Custom Item",
|
||||||
|
variant_sku: "sku123",
|
||||||
|
variant_barcode: "barcode123",
|
||||||
|
is_discountable: true,
|
||||||
|
unit_price: 2200,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
shipping_methods: [
|
||||||
|
{
|
||||||
|
name: "test-method",
|
||||||
|
shipping_option_id: "test-option",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/draft-orders",
|
||||||
|
payload,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
draft_order: expect.objectContaining({
|
||||||
|
status: "draft",
|
||||||
|
version: 1,
|
||||||
|
summary: expect.objectContaining({
|
||||||
|
// TODO: add summary fields
|
||||||
|
}),
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test product",
|
||||||
|
subtitle: "Test variant",
|
||||||
|
product_title: "Test product",
|
||||||
|
product_description: null,
|
||||||
|
product_subtitle: null,
|
||||||
|
product_type: null,
|
||||||
|
product_type_id: null,
|
||||||
|
product_collection: null,
|
||||||
|
product_handle: "test-product",
|
||||||
|
variant_sku: null,
|
||||||
|
variant_barcode: null,
|
||||||
|
variant_title: "Test variant",
|
||||||
|
variant_option_values: null,
|
||||||
|
requires_shipping: true,
|
||||||
|
is_discountable: true,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
is_custom_price: false,
|
||||||
|
raw_compare_at_unit_price: null,
|
||||||
|
raw_unit_price: expect.objectContaining({
|
||||||
|
value: "3000",
|
||||||
|
}),
|
||||||
|
metadata: {},
|
||||||
|
tax_lines: [
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "US_DEF",
|
||||||
|
provider_id: "system",
|
||||||
|
rate: 2,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
adjustments: [
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 200,
|
||||||
|
code: "testytest",
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
promotion_id: promotion.id,
|
||||||
|
provider_id: null,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
unit_price: 3000,
|
||||||
|
quantity: 2,
|
||||||
|
raw_quantity: expect.objectContaining({
|
||||||
|
value: "2",
|
||||||
|
}),
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
raw_quantity: expect.objectContaining({
|
||||||
|
value: "2",
|
||||||
|
}),
|
||||||
|
raw_fulfilled_quantity: expect.objectContaining({
|
||||||
|
value: "0",
|
||||||
|
}),
|
||||||
|
raw_shipped_quantity: expect.objectContaining({
|
||||||
|
value: "0",
|
||||||
|
}),
|
||||||
|
raw_return_requested_quantity: expect.objectContaining({
|
||||||
|
value: "0",
|
||||||
|
}),
|
||||||
|
raw_return_received_quantity: expect.objectContaining({
|
||||||
|
value: "0",
|
||||||
|
}),
|
||||||
|
raw_return_dismissed_quantity: expect.objectContaining({
|
||||||
|
value: "0",
|
||||||
|
}),
|
||||||
|
raw_written_off_quantity: expect.objectContaining({
|
||||||
|
value: "0",
|
||||||
|
}),
|
||||||
|
quantity: 2,
|
||||||
|
fulfilled_quantity: 0,
|
||||||
|
shipped_quantity: 0,
|
||||||
|
return_requested_quantity: 0,
|
||||||
|
return_received_quantity: 0,
|
||||||
|
return_dismissed_quantity: 0,
|
||||||
|
written_off_quantity: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Another product",
|
||||||
|
subtitle: "Variant variable",
|
||||||
|
raw_unit_price: expect.objectContaining({
|
||||||
|
value: "200",
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
note: "reduced price",
|
||||||
|
},
|
||||||
|
unit_price: 200,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
quantity: 1,
|
||||||
|
raw_quantity: expect.objectContaining({
|
||||||
|
value: "1",
|
||||||
|
}),
|
||||||
|
adjustments: [
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
code: "testytest",
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
promotion_id: promotion.id,
|
||||||
|
provider_id: null,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Custom Item",
|
||||||
|
variant_sku: "sku123",
|
||||||
|
variant_barcode: "barcode123",
|
||||||
|
variant_title: null,
|
||||||
|
is_custom_price: true,
|
||||||
|
raw_unit_price: expect.objectContaining({
|
||||||
|
value: "2200",
|
||||||
|
}),
|
||||||
|
unit_price: 2200,
|
||||||
|
quantity: 1,
|
||||||
|
raw_quantity: expect.objectContaining({
|
||||||
|
value: "1",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
shipping_address: expect.objectContaining({
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
phone: "12345",
|
||||||
|
}),
|
||||||
|
billing_address: expect.objectContaining({
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
}),
|
||||||
|
shipping_methods: [
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "test-method",
|
||||||
|
raw_amount: expect.objectContaining({
|
||||||
|
value: "100",
|
||||||
|
}),
|
||||||
|
is_tax_inclusive: false,
|
||||||
|
shipping_option_id: "test-option",
|
||||||
|
data: null,
|
||||||
|
tax_lines: [
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "US_DEF",
|
||||||
|
provider_id: "system",
|
||||||
|
rate: 2,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
adjustments: [],
|
||||||
|
amount: 100,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
it("should create a draft order and apply tax by product type", async () => {
|
it("should create a draft order and apply tax by product type", async () => {
|
||||||
const productType = await productModule.createProductTypes({
|
const productType = await productModule.createProductTypes({
|
||||||
value: "test_product_type",
|
value: "test_product_type",
|
||||||
@@ -396,11 +777,17 @@ medusaIntegrationTestRunner({
|
|||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
})
|
})
|
||||||
|
|
||||||
const [taxRegion] = await taxModule.createTaxRegions([{
|
const [taxRegion] = await taxModule.createTaxRegions([
|
||||||
|
{
|
||||||
country_code: "US",
|
country_code: "US",
|
||||||
provider_id: "tp_system",
|
provider_id: "tp_system",
|
||||||
default_tax_rate: { name: "US Default Rate", rate: 5, code: "US_DEF" },
|
default_tax_rate: {
|
||||||
}])
|
name: "US Default Rate",
|
||||||
|
rate: 5,
|
||||||
|
code: "US_DEF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const [taxRate] = await taxModule.createTaxRates([
|
const [taxRate] = await taxModule.createTaxRates([
|
||||||
{
|
{
|
||||||
@@ -408,14 +795,15 @@ medusaIntegrationTestRunner({
|
|||||||
name: "US Reduced",
|
name: "US Reduced",
|
||||||
rate: 3,
|
rate: 3,
|
||||||
code: "USREDUCE_PROD_TYPE",
|
code: "USREDUCE_PROD_TYPE",
|
||||||
}])
|
},
|
||||||
|
])
|
||||||
|
|
||||||
await taxModule.createTaxRateRules([
|
await taxModule.createTaxRateRules([
|
||||||
{
|
{
|
||||||
reference: "product_type",
|
reference: "product_type",
|
||||||
reference_id: productType.id,
|
reference_id: productType.id,
|
||||||
tax_rate_id: taxRate.id,
|
tax_rate_id: taxRate.id,
|
||||||
}
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const salesChannel = await scModuleService.createSalesChannels({
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
@@ -426,7 +814,6 @@ medusaIntegrationTestRunner({
|
|||||||
name: "Warehouse",
|
name: "Warehouse",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const [product] = await productModule.createProducts([
|
const [product] = await productModule.createProducts([
|
||||||
{
|
{
|
||||||
title: "Test product",
|
title: "Test product",
|
||||||
@@ -473,6 +860,35 @@ medusaIntegrationTestRunner({
|
|||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a promotion to test with
|
||||||
|
*/
|
||||||
|
const promotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "testytest",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
status: PromotionStatus.ACTIVE,
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "each",
|
||||||
|
currency_code: "usd",
|
||||||
|
value: 100,
|
||||||
|
max_quantity: 100,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "variant_id",
|
||||||
|
operator: "in",
|
||||||
|
values: [product.variants[0].id],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
await remoteLink.create([
|
await remoteLink.create([
|
||||||
{
|
{
|
||||||
@@ -546,7 +962,9 @@ medusaIntegrationTestRunner({
|
|||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.data.draft_order.items[0].tax_lines[0].code).toEqual("USREDUCE_PROD_TYPE")
|
expect(response.data.draft_order.items[0].tax_lines[0].code).toEqual(
|
||||||
|
"USREDUCE_PROD_TYPE"
|
||||||
|
)
|
||||||
expect(response.data.draft_order.items[0].tax_lines[0].rate).toEqual(3)
|
expect(response.data.draft_order.items[0].tax_lines[0].rate).toEqual(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { createOrderChangeWorkflow } from "@medusajs/core-flows"
|
import {
|
||||||
|
createOrderChangeWorkflow,
|
||||||
|
createOrderWorkflow,
|
||||||
|
} from "@medusajs/core-flows"
|
||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import { IOrderModuleService, OrderDTO } from "@medusajs/types"
|
import {
|
||||||
import { Modules } from "@medusajs/utils"
|
CreateOrderLineItemDTO,
|
||||||
|
IOrderModuleService,
|
||||||
|
OrderDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { Modules, ProductStatus } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
@@ -26,6 +33,172 @@ medusaIntegrationTestRunner({
|
|||||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("CreateOrderWorkflow", () => {
|
||||||
|
it("should create an order with items quantity and no unit price and calculate prices based on the correct pricing context including quantity", async () => {
|
||||||
|
const salesChannel = await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{
|
||||||
|
name: "Test Sales Channel",
|
||||||
|
description: "Test Sales Channel Description",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const productData = {
|
||||||
|
title: "Medusa T-Shirt based quantity",
|
||||||
|
handle: "t-shirt-with-quantity-prices",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
sales_channels: [
|
||||||
|
{
|
||||||
|
id: salesChannel.data.sales_channel.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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 salesChannelId = salesChannel.data.sales_channel.id
|
||||||
|
const customer = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/customers",
|
||||||
|
{
|
||||||
|
email: "test1@email.com",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.customer
|
||||||
|
const region = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/regions",
|
||||||
|
{ name: "US", currency_code: "usd", countries: ["us"] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.region
|
||||||
|
|
||||||
|
const { result: created } = await createOrderWorkflow(appContainer).run(
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
email: customer.email,
|
||||||
|
metadata: {
|
||||||
|
foo: "bar",
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Medusa T-Shirt based quantity",
|
||||||
|
variant_id: variantId,
|
||||||
|
quantity: 6,
|
||||||
|
} as CreateOrderLineItemDTO,
|
||||||
|
],
|
||||||
|
sales_channel_id: salesChannelId,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: {
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
phone: "12345",
|
||||||
|
},
|
||||||
|
billing_address: {
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
},
|
||||||
|
shipping_methods: [
|
||||||
|
{
|
||||||
|
name: "Test shipping method",
|
||||||
|
amount: 10,
|
||||||
|
data: {},
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
description: "shipping Tax 1",
|
||||||
|
tax_rate_id: "tax_usa_shipping",
|
||||||
|
code: "code",
|
||||||
|
rate: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
code: "VIP_10",
|
||||||
|
amount: 1,
|
||||||
|
description: "VIP discount",
|
||||||
|
promotion_id: "prom_123",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currency_code: "usd",
|
||||||
|
customer_id: customer.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const order = (
|
||||||
|
await api.get(
|
||||||
|
"/admin/orders/" +
|
||||||
|
created.id +
|
||||||
|
"?fields=+raw_total,+raw_subtotal,+raw_discount_total",
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.order
|
||||||
|
|
||||||
|
expect(order).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
original_item_subtotal: 6000,
|
||||||
|
original_item_tax_total: 0,
|
||||||
|
original_item_total: 6000,
|
||||||
|
original_shipping_subtotal: 10,
|
||||||
|
original_shipping_tax_total: 1,
|
||||||
|
original_shipping_total: 11,
|
||||||
|
original_tax_total: 1,
|
||||||
|
original_total: 6011,
|
||||||
|
item_subtotal: 6000,
|
||||||
|
item_tax_total: 0,
|
||||||
|
item_total: 6000,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Orders - Admin", () => {
|
describe("Orders - Admin", () => {
|
||||||
it("should get an order", async () => {
|
it("should get an order", async () => {
|
||||||
const created = await orderModule.createOrders({
|
const created = await orderModule.createOrders({
|
||||||
@@ -107,7 +280,7 @@ medusaIntegrationTestRunner({
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
status: "pending",
|
status: "pending",
|
||||||
version: 1,
|
version: 1,
|
||||||
display_id: 1,
|
display_id: 2,
|
||||||
payment_collections: [],
|
payment_collections: [],
|
||||||
payment_status: "not_paid",
|
payment_status: "not_paid",
|
||||||
region_id: "test_region_id",
|
region_id: "test_region_id",
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import { Query } from "@medusajs/framework"
|
||||||
import {
|
import {
|
||||||
CalculatedPriceSet,
|
CalculatedPriceSet,
|
||||||
IPricingModuleService,
|
IPricingModuleService,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import { MedusaError, Modules } from "@medusajs/framework/utils"
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
MedusaError,
|
||||||
|
Modules,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,68 +26,52 @@ export interface GetVariantPriceSetsStepInput {
|
|||||||
context?: Record<string, unknown>
|
context?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface GetVariantPriceSetsStepBulkInput {
|
||||||
* The calculated price sets of the variants. The object's keys are the variant IDs.
|
data: {
|
||||||
*/
|
variantId: string
|
||||||
|
context?: Record<string, unknown>
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VariantPriceSetData {
|
||||||
|
id: string
|
||||||
|
price_set?: { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PriceCalculationItem {
|
||||||
|
variantId: string
|
||||||
|
priceSetId: string
|
||||||
|
context?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetVariantPriceSetsStepOutput {
|
export interface GetVariantPriceSetsStepOutput {
|
||||||
[k: string]: CalculatedPriceSet
|
[k: string]: CalculatedPriceSet
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getVariantPriceSetsStepId = "get-variant-price-sets"
|
export const getVariantPriceSetsStepId = "get-variant-price-sets"
|
||||||
/**
|
|
||||||
* This step retrieves the calculated price sets of the specified variants.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* To retrieve a variant's price sets:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* const data = getVariantPriceSetsStep({
|
|
||||||
* variantIds: ["variant_123"],
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* To retrieve the calculated price sets of a variant:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* const data = getVariantPriceSetsStep({
|
|
||||||
* variantIds: ["variant_123"],
|
|
||||||
* context: {
|
|
||||||
* currency_code: "usd"
|
|
||||||
* }
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const getVariantPriceSetsStep = createStep(
|
|
||||||
getVariantPriceSetsStepId,
|
|
||||||
async (data: GetVariantPriceSetsStepInput, { container }) => {
|
|
||||||
if (!data.variantIds.length) {
|
|
||||||
return new StepResponse({})
|
|
||||||
}
|
|
||||||
|
|
||||||
const pricingModuleService = container.resolve<IPricingModuleService>(
|
async function fetchVariantPriceSets(
|
||||||
Modules.PRICING
|
query: Query,
|
||||||
)
|
variantIds: string[]
|
||||||
|
): Promise<VariantPriceSetData[]> {
|
||||||
const remoteQuery = container.resolve("remoteQuery")
|
return (
|
||||||
|
await query.graph({
|
||||||
const variantPriceSets = await remoteQuery({
|
entity: "variant",
|
||||||
entryPoint: "variant",
|
|
||||||
fields: ["id", "price_set.id"],
|
fields: ["id", "price_set.id"],
|
||||||
variables: {
|
filters: { id: variantIds },
|
||||||
id: data.variantIds,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
).data
|
||||||
|
}
|
||||||
|
|
||||||
const notFound: string[] = []
|
/**
|
||||||
const priceSetIds: string[] = []
|
* Validates that all variants have price sets and throws error for missing ones
|
||||||
|
*/
|
||||||
variantPriceSets.forEach((v) => {
|
function validateVariantPriceSets(
|
||||||
if (v.price_set?.id) {
|
variantPriceSets: VariantPriceSetData[]
|
||||||
priceSetIds.push(v.price_set.id)
|
): void {
|
||||||
} else {
|
const notFound = variantPriceSets
|
||||||
notFound.push(v.id)
|
.filter((v) => !v.price_set?.id)
|
||||||
}
|
.map((v) => v.id)
|
||||||
})
|
|
||||||
|
|
||||||
if (notFound.length) {
|
if (notFound.length) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
@@ -90,30 +79,207 @@ export const getVariantPriceSetsStep = createStep(
|
|||||||
`Variants with IDs ${notFound.join(", ")} do not have a price`
|
`Variants with IDs ${notFound.join(", ")} do not have a price`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const calculatedPriceSets = await pricingModuleService.calculatePrices(
|
/**
|
||||||
|
* Unified function to process variants with context grouping optimization
|
||||||
|
* TODO: to be discussed, support batch calculation from the pricing module. Currently
|
||||||
|
* trying to mitigate the impact by grouping items by exact same context.
|
||||||
|
*/
|
||||||
|
async function processVariantPriceSets(
|
||||||
|
pricingService: IPricingModuleService,
|
||||||
|
items: PriceCalculationItem[]
|
||||||
|
): Promise<GetVariantPriceSetsStepOutput> {
|
||||||
|
const result: GetVariantPriceSetsStepOutput = {}
|
||||||
|
|
||||||
|
// Group items by their context to minimize API calls
|
||||||
|
const contextGroups = groupItemsByContext(items)
|
||||||
|
|
||||||
|
for (const [, groupItems] of contextGroups) {
|
||||||
|
const priceSetIds = groupItems.map((item) => item.priceSetId)
|
||||||
|
const context = groupItems[0].context // All items in group have same context
|
||||||
|
|
||||||
|
const calculatedPriceSets = await pricingService.calculatePrices(
|
||||||
{ id: priceSetIds },
|
{ id: priceSetIds },
|
||||||
{ context: data.context as Record<string, string | number> }
|
{ context: context as Record<string, string | number> }
|
||||||
)
|
)
|
||||||
|
|
||||||
const idToPriceSet = new Map<string, Record<string, any>>(
|
// Map calculated prices back to variants
|
||||||
calculatedPriceSets.map((p) => [p.id, p])
|
const priceSetMap = new Map(
|
||||||
|
calculatedPriceSets.map((priceSet) => [priceSet.id, priceSet])
|
||||||
)
|
)
|
||||||
|
|
||||||
const variantToCalculatedPriceSets = variantPriceSets.reduce(
|
for (const item of groupItems) {
|
||||||
(acc, { id, price_set }) => {
|
const calculatedPriceSet = priceSetMap.get(item.priceSetId)
|
||||||
const calculatedPriceSet = idToPriceSet.get(price_set?.id)
|
|
||||||
if (calculatedPriceSet) {
|
if (calculatedPriceSet) {
|
||||||
acc[id] = calculatedPriceSet
|
result[item.variantId] = calculatedPriceSet
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return result
|
||||||
},
|
}
|
||||||
{}
|
|
||||||
|
function createContextKey(context?: Record<string, unknown>): string {
|
||||||
|
if (!context || Object.keys(context).length === 0) {
|
||||||
|
return "no-context"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort keys to ensure consistent grouping regardless of key order
|
||||||
|
const sortedEntries = Object.entries(context)
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
.map(([key, value]) => `${key}:${JSON.stringify(value)}`)
|
||||||
|
|
||||||
|
return sortedEntries.join("|")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups calculation items by their context. It results in less API calls to the pricing module
|
||||||
|
* if we are able to group multiple item with the exact same context
|
||||||
|
*/
|
||||||
|
function groupItemsByContext(
|
||||||
|
items: PriceCalculationItem[]
|
||||||
|
): Map<string, PriceCalculationItem[]> {
|
||||||
|
const groups = new Map<string, PriceCalculationItem[]>()
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const contextKey = createContextKey(item.context)
|
||||||
|
const existingGroup = groups.get(contextKey)
|
||||||
|
|
||||||
|
if (existingGroup) {
|
||||||
|
existingGroup.push(item)
|
||||||
|
} else {
|
||||||
|
groups.set(contextKey, [item])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts shared context input to unified calculation items format
|
||||||
|
*/
|
||||||
|
function createCalculationItemsFromSharedContext(
|
||||||
|
variantPriceSets: VariantPriceSetData[],
|
||||||
|
sharedContext?: Record<string, unknown>
|
||||||
|
): PriceCalculationItem[] {
|
||||||
|
return variantPriceSets
|
||||||
|
.filter((v) => v.price_set?.id)
|
||||||
|
.map((v) => ({
|
||||||
|
variantId: v.id,
|
||||||
|
priceSetId: v.price_set!.id,
|
||||||
|
context: sharedContext,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts individual context input to unified calculation items format
|
||||||
|
*/
|
||||||
|
function createCalculationItemsFromBulkData(
|
||||||
|
bulkData: GetVariantPriceSetsStepBulkInput["data"],
|
||||||
|
variantToPriceSetId: Map<string, string>
|
||||||
|
): PriceCalculationItem[] {
|
||||||
|
const calculationItems: PriceCalculationItem[] = []
|
||||||
|
for (const item of bulkData) {
|
||||||
|
const priceSetId = variantToPriceSetId.get(item.variantId)
|
||||||
|
if (priceSetId) {
|
||||||
|
calculationItems.push({
|
||||||
|
variantId: item.variantId,
|
||||||
|
priceSetId,
|
||||||
|
context: item.context,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calculationItems
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This step retrieves the calculated price sets of the specified variants.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* To retrieve variant price sets with shared context:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const data = getVariantPriceSetsStep({
|
||||||
|
* variantIds: ["variant_123"],
|
||||||
|
* context: { currency_code: "usd" }
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* To retrieve variant price sets with individual contexts:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const data = getVariantPriceSetsStep({
|
||||||
|
* data: [
|
||||||
|
* { variantId: "variant_123", context: { currency_code: "usd" } },
|
||||||
|
* { variantId: "variant_456", context: { currency_code: "usd" } }, // Same context - will be batched
|
||||||
|
* { variantId: "variant_789", context: { currency_code: "eur" } }
|
||||||
|
* ]
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const getVariantPriceSetsStep = createStep(
|
||||||
|
getVariantPriceSetsStepId,
|
||||||
|
async (
|
||||||
|
data: GetVariantPriceSetsStepInput | GetVariantPriceSetsStepBulkInput,
|
||||||
|
{ container }
|
||||||
|
) => {
|
||||||
|
const pricingModuleService = container.resolve<IPricingModuleService>(
|
||||||
|
Modules.PRICING
|
||||||
|
)
|
||||||
|
const query = container.resolve<Query>(ContainerRegistrationKeys.QUERY)
|
||||||
|
|
||||||
|
let calculationItems: PriceCalculationItem[]
|
||||||
|
|
||||||
|
// Handle shared context variants (original input format)
|
||||||
|
if ("variantIds" in data) {
|
||||||
|
if (!data.variantIds.length) {
|
||||||
|
return new StepResponse({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantPriceSets = await fetchVariantPriceSets(
|
||||||
|
query,
|
||||||
|
data.variantIds
|
||||||
)
|
)
|
||||||
|
|
||||||
return new StepResponse(
|
validateVariantPriceSets(variantPriceSets)
|
||||||
variantToCalculatedPriceSets as GetVariantPriceSetsStepOutput
|
|
||||||
|
calculationItems = createCalculationItemsFromSharedContext(
|
||||||
|
variantPriceSets,
|
||||||
|
data.context
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Handle individual context variants (bulk input format)
|
||||||
|
const bulkData = data.data
|
||||||
|
if (!bulkData.length) {
|
||||||
|
return new StepResponse({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantIds = bulkData.map((item) => item.variantId)
|
||||||
|
const variantPriceSets = await fetchVariantPriceSets(query, variantIds)
|
||||||
|
|
||||||
|
validateVariantPriceSets(variantPriceSets)
|
||||||
|
|
||||||
|
// Map variant IDs to price set IDs
|
||||||
|
const variantToPriceSetId = new Map<string, string>()
|
||||||
|
variantPriceSets.forEach((v) => {
|
||||||
|
if (v.price_set?.id) {
|
||||||
|
variantToPriceSetId.set(v.id, v.price_set.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
calculationItems = createCalculationItemsFromBulkData(
|
||||||
|
bulkData,
|
||||||
|
variantToPriceSetId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use unified processing logic for both input types
|
||||||
|
const result = await processVariantPriceSets(
|
||||||
|
pricingModuleService,
|
||||||
|
calculationItems
|
||||||
|
)
|
||||||
|
|
||||||
|
return new StepResponse(result)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -166,7 +166,6 @@ export const productVariantsFields = [
|
|||||||
"product.discountable",
|
"product.discountable",
|
||||||
"product.is_giftcard",
|
"product.is_giftcard",
|
||||||
"product.shipping_profile.id",
|
"product.shipping_profile.id",
|
||||||
"calculated_price.*",
|
|
||||||
"inventory_items.inventory_item_id",
|
"inventory_items.inventory_item_id",
|
||||||
"inventory_items.required_quantity",
|
"inventory_items.required_quantity",
|
||||||
"inventory_items.inventory.requires_shipping",
|
"inventory_items.inventory.requires_shipping",
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import {
|
|||||||
AdditionalData,
|
AdditionalData,
|
||||||
AddToCartWorkflowInputDTO,
|
AddToCartWorkflowInputDTO,
|
||||||
ConfirmVariantInventoryWorkflowInputDTO,
|
ConfirmVariantInventoryWorkflowInputDTO,
|
||||||
|
WithCalculatedPrice,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
CartWorkflowEvents,
|
CartWorkflowEvents,
|
||||||
deduplicate,
|
deduplicate,
|
||||||
|
filterObjectByKeys,
|
||||||
isDefined,
|
isDefined,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
@@ -19,10 +21,10 @@ import {
|
|||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { useQueryGraphStep } from "../../common"
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { emitEventStep } from "../../common/steps/emit-event"
|
import { emitEventStep } from "../../common/steps/emit-event"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
|
||||||
import {
|
import {
|
||||||
createLineItemsStep,
|
createLineItemsStep,
|
||||||
getLineItemActionsStep,
|
getLineItemActionsStep,
|
||||||
|
getVariantPriceSetsStep,
|
||||||
updateLineItemsStep,
|
updateLineItemsStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
import { validateCartStep } from "../steps/validate-cart"
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
@@ -36,6 +38,7 @@ import { requiredVariantFieldsForInventoryConfirmation } from "../utils/prepare-
|
|||||||
import {
|
import {
|
||||||
prepareLineItemData,
|
prepareLineItemData,
|
||||||
PrepareLineItemDataInput,
|
PrepareLineItemDataInput,
|
||||||
|
PrepareVariantLineItemInput,
|
||||||
} from "../utils/prepare-line-item-data"
|
} from "../utils/prepare-line-item-data"
|
||||||
import { pricingContextResult } from "../utils/schemas"
|
import { pricingContextResult } from "../utils/schemas"
|
||||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||||
@@ -148,41 +151,75 @@ export const addToCartWorkflow = createWorkflow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const setPricingContextResult = setPricingContext.getResult()
|
const setPricingContextResult = setPricingContext.getResult()
|
||||||
|
|
||||||
|
const variants = when(
|
||||||
|
"should-calculate-prices",
|
||||||
|
{ variantIds },
|
||||||
|
({ variantIds }) => {
|
||||||
|
return !!variantIds.length
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
const pricingContext = transform(
|
const pricingContext = transform(
|
||||||
{ cart, setPricingContextResult },
|
{ cart, items: input.items, setPricingContextResult },
|
||||||
(data) => {
|
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||||
return {
|
const baseContext = {
|
||||||
...data.cart,
|
...filterObjectByKeys(data.cart, cartFieldsForPricingContext),
|
||||||
...(data.setPricingContextResult ? data.setPricingContextResult : {}),
|
...(data.setPricingContextResult
|
||||||
|
? data.setPricingContextResult
|
||||||
|
: {}),
|
||||||
currency_code: data.cart.currency_code,
|
currency_code: data.cart.currency_code,
|
||||||
region_id: data.cart.region_id,
|
region_id: data.cart.region_id,
|
||||||
region: data.cart.region,
|
region: data.cart.region,
|
||||||
customer_id: data.cart.customer_id,
|
customer_id: data.cart.customer_id,
|
||||||
customer: data.cart.customer,
|
customer: data.cart.customer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data.items
|
||||||
|
.filter((i) => i.variant_id)
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
variantId: item.variant_id!,
|
||||||
|
context: {
|
||||||
|
...baseContext,
|
||||||
|
quantity: item.quantity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
const { data: variantsData } = useQueryGraphStep({
|
||||||
return !!variantIds.length
|
entity: "variants",
|
||||||
}).then(() => {
|
|
||||||
return useRemoteQueryStep({
|
|
||||||
entry_point: "variants",
|
|
||||||
fields: deduplicate([
|
fields: deduplicate([
|
||||||
...productVariantsFields,
|
...productVariantsFields,
|
||||||
...requiredVariantFieldsForInventoryConfirmation,
|
...requiredVariantFieldsForInventoryConfirmation,
|
||||||
]),
|
]),
|
||||||
variables: {
|
filters: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
|
||||||
context: pricingContext,
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||||
|
data: pricingContext,
|
||||||
|
})
|
||||||
|
|
||||||
|
const variants = transform(
|
||||||
|
{ variantsData, calculatedPriceSets },
|
||||||
|
({ variantsData, calculatedPriceSets }) => {
|
||||||
|
return variantsData.map((variant) => {
|
||||||
|
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||||
|
return variant
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
|
return variants as (PrepareVariantLineItemInput &
|
||||||
|
ConfirmVariantInventoryWorkflowInputDTO["variants"][number] &
|
||||||
|
WithCalculatedPrice)[]
|
||||||
|
})
|
||||||
|
|
||||||
const lineItems = transform({ input, variants }, (data) => {
|
const lineItems = transform({ input, variants }, (data) => {
|
||||||
const items = (data.input.items ?? []).map((item) => {
|
const items = (data.input.items ?? []).map((item) => {
|
||||||
const variant = (data.variants ?? []).find(
|
const variant = (data.variants ?? []).find(
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ import {
|
|||||||
WorkflowData,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { emitEventStep } from "../../common/steps/emit-event"
|
import { emitEventStep } from "../../common/steps/emit-event"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
|
||||||
import {
|
import {
|
||||||
createCartsStep,
|
createCartsStep,
|
||||||
findOneOrAnyRegionStep,
|
findOneOrAnyRegionStep,
|
||||||
findOrCreateCustomerStep,
|
findOrCreateCustomerStep,
|
||||||
findSalesChannelStep,
|
findSalesChannelStep,
|
||||||
|
getVariantPriceSetsStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
import { validateLineItemPricesStep } from "../steps/validate-line-item-prices"
|
import { validateLineItemPricesStep } from "../steps/validate-line-item-prices"
|
||||||
import { validateSalesChannelStep } from "../steps/validate-sales-channel"
|
import { validateSalesChannelStep } from "../steps/validate-sales-channel"
|
||||||
@@ -167,30 +168,62 @@ export const createCartWorkflow = createWorkflow(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
const variants = when("has-variants", { variantIds }, ({ variantIds }) => {
|
||||||
return !!variantIds.length
|
return !!variantIds.length
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return useRemoteQueryStep({
|
const { data: variantsData } = useQueryGraphStep({
|
||||||
entry_point: "variants",
|
entity: "variants",
|
||||||
fields: deduplicate([
|
fields: deduplicate([
|
||||||
...productVariantsFields,
|
...productVariantsFields,
|
||||||
...requiredVariantFieldsForInventoryConfirmation,
|
...requiredVariantFieldsForInventoryConfirmation,
|
||||||
]),
|
]),
|
||||||
variables: {
|
filters: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
|
||||||
context: pricingContext,
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const calculatedPriceContext = transform(
|
||||||
|
{ pricingContext, items: input.items },
|
||||||
|
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||||
|
const baseContext = data.pricingContext
|
||||||
|
|
||||||
|
return (data.items ?? [])
|
||||||
|
.filter((i) => i.variant_id)
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
variantId: item.variant_id!,
|
||||||
|
context: {
|
||||||
|
...baseContext,
|
||||||
|
quantity: item.quantity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||||
|
data: calculatedPriceContext,
|
||||||
|
})
|
||||||
|
|
||||||
|
const variants = transform(
|
||||||
|
{ variantsData, calculatedPriceSets },
|
||||||
|
({ variantsData, calculatedPriceSets }) => {
|
||||||
|
return variantsData.map((variant) => {
|
||||||
|
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||||
|
return variant
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
|
return variants
|
||||||
|
})
|
||||||
|
|
||||||
confirmVariantInventoryWorkflow.runAsStep({
|
confirmVariantInventoryWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
sales_channel_id: salesChannel.id,
|
sales_channel_id: salesChannel.id,
|
||||||
variants,
|
variants: variants!,
|
||||||
items: input.items!,
|
items: input.items!,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
AdditionalData,
|
AdditionalData,
|
||||||
ListShippingOptionsForCartWorkflowInput,
|
ListShippingOptionsForCartWorkflowInput,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { isDefined } from "@medusajs/framework/utils"
|
import { filterObjectByKeys, isDefined } from "@medusajs/framework/utils"
|
||||||
import { pricingContextResult } from "../utils/schemas"
|
import { pricingContextResult } from "../utils/schemas"
|
||||||
|
|
||||||
export const listShippingOptionsForCartWorkflowId =
|
export const listShippingOptionsForCartWorkflowId =
|
||||||
@@ -181,7 +181,7 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
|||||||
|
|
||||||
calculated_price: {
|
calculated_price: {
|
||||||
context: {
|
context: {
|
||||||
...cart,
|
...filterObjectByKeys(cart, cartFieldsForPricingContext),
|
||||||
...(setPricingContextResult ? setPricingContextResult : {}),
|
...(setPricingContextResult ? setPricingContextResult : {}),
|
||||||
currency_code: cart.currency_code,
|
currency_code: cart.currency_code,
|
||||||
region_id: cart.region_id,
|
region_id: cart.region_id,
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
WorkflowData,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { AdditionalData, CartDTO } from "@medusajs/types"
|
||||||
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
import { updateLineItemsStep } from "../steps"
|
import { getVariantPriceSetsStep, updateLineItemsStep } from "../steps"
|
||||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||||
import {
|
import {
|
||||||
cartFieldsForPricingContext,
|
cartFieldsForPricingContext,
|
||||||
@@ -23,13 +25,12 @@ import {
|
|||||||
prepareLineItemData,
|
prepareLineItemData,
|
||||||
PrepareLineItemDataInput,
|
PrepareLineItemDataInput,
|
||||||
} from "../utils/prepare-line-item-data"
|
} from "../utils/prepare-line-item-data"
|
||||||
|
import { pricingContextResult } from "../utils/schemas"
|
||||||
import { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-methods"
|
import { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-methods"
|
||||||
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
||||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||||
import { updateTaxLinesWorkflow } from "./update-tax-lines"
|
import { updateTaxLinesWorkflow } from "./update-tax-lines"
|
||||||
import { upsertTaxLinesWorkflow } from "./upsert-tax-lines"
|
import { upsertTaxLinesWorkflow } from "./upsert-tax-lines"
|
||||||
import { AdditionalData } from "@medusajs/types"
|
|
||||||
import { pricingContextResult } from "../utils/schemas"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The details of the cart to refresh.
|
* The details of the cart to refresh.
|
||||||
@@ -142,48 +143,77 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
|||||||
)
|
)
|
||||||
const setPricingContextResult = setPricingContext.getResult()
|
const setPricingContextResult = setPricingContext.getResult()
|
||||||
|
|
||||||
when({ input }, ({ input }) => {
|
when("force-refresh-calculate-prices", { input }, ({ input }) => {
|
||||||
return !!input.force_refresh
|
return !!input.force_refresh
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const cart = useRemoteQueryStep({
|
const { data: cart } = useQueryGraphStep({
|
||||||
entry_point: "cart",
|
entity: "cart",
|
||||||
fields: cartFieldsForRefreshSteps,
|
fields: cartFieldsForRefreshSteps,
|
||||||
variables: { id: input.cart_id },
|
filters: { id: input.cart_id },
|
||||||
list: false,
|
pagination: {
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
isList: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const variantIds = transform({ cart }, (data) => {
|
const variantIds = transform({ cart }, (data: { cart: CartDTO }) => {
|
||||||
return (data.cart.items ?? []).map((i) => i.variant_id).filter(Boolean)
|
return (data.cart.items ?? []).map((i) => i.variant_id).filter(Boolean)
|
||||||
})
|
})
|
||||||
|
|
||||||
const cartPricingContext = transform(
|
const cartPricingContext = transform(
|
||||||
{ cart, setPricingContextResult },
|
{ cart, setPricingContextResult },
|
||||||
(data) => {
|
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||||
return {
|
const cart = data.cart
|
||||||
...filterObjectByKeys(data.cart, cartFieldsForPricingContext),
|
const baseContext = {
|
||||||
|
...filterObjectByKeys(cart, cartFieldsForPricingContext),
|
||||||
...(data.setPricingContextResult
|
...(data.setPricingContextResult
|
||||||
? data.setPricingContextResult
|
? data.setPricingContextResult
|
||||||
: {}),
|
: {}),
|
||||||
currency_code: data.cart.currency_code,
|
currency_code: cart.currency_code,
|
||||||
region_id: data.cart.region_id,
|
region_id: cart.region_id,
|
||||||
region: data.cart.region,
|
region: cart.region,
|
||||||
customer_id: data.cart.customer_id,
|
customer_id: cart.customer_id,
|
||||||
customer: data.cart.customer,
|
customer: cart.customer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cart.items
|
||||||
|
.filter((i) => i.variant_id)
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
variantId: item.variant_id,
|
||||||
|
context: {
|
||||||
|
...baseContext,
|
||||||
|
quantity: item.quantity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const { data: variantsData } = useQueryGraphStep({
|
||||||
entry_point: "variants",
|
entity: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
filters: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
|
||||||
context: cartPricingContext,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}).config({ name: "fetch-variants" })
|
}).config({ name: "fetch-variants" })
|
||||||
|
|
||||||
|
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||||
|
data: cartPricingContext,
|
||||||
|
})
|
||||||
|
|
||||||
|
const variants = transform(
|
||||||
|
{ variantsData, calculatedPriceSets },
|
||||||
|
({ variantsData, calculatedPriceSets }) => {
|
||||||
|
return variantsData.map((variant) => {
|
||||||
|
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||||
|
return variant
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
const lineItems = transform({ cart, variants }, ({ cart, variants }) => {
|
const lineItems = transform({ cart, variants }, ({ cart, variants }) => {
|
||||||
@@ -244,7 +274,7 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
|||||||
input: refreshCartInput,
|
input: refreshCartInput,
|
||||||
})
|
})
|
||||||
|
|
||||||
when({ input }, ({ input }) => {
|
when("force-refresh-update-tax-lines", { input }, ({ input }) => {
|
||||||
return !!input.force_refresh
|
return !!input.force_refresh
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
updateTaxLinesWorkflow.runAsStep({
|
updateTaxLinesWorkflow.runAsStep({
|
||||||
@@ -252,7 +282,7 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
when({ input }, ({ input }) => {
|
when("force-refresh-upsert-tax-lines", { input }, ({ input }) => {
|
||||||
return (
|
return (
|
||||||
!input.force_refresh &&
|
!input.force_refresh &&
|
||||||
(!!input.items?.length || !!input.shipping_methods?.length)
|
(!!input.items?.length || !!input.shipping_methods?.length)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const refreshCartShippingMethodsWorkflowId =
|
|||||||
export const refreshCartShippingMethodsWorkflow = createWorkflow(
|
export const refreshCartShippingMethodsWorkflow = createWorkflow(
|
||||||
refreshCartShippingMethodsWorkflowId,
|
refreshCartShippingMethodsWorkflowId,
|
||||||
(input: WorkflowData<RefreshCartShippingMethodsWorkflowInput>) => {
|
(input: WorkflowData<RefreshCartShippingMethodsWorkflowInput>) => {
|
||||||
const fetchCart = when({ input }, ({ input }) => {
|
const fetchCart = when("fetch-cart", { input }, ({ input }) => {
|
||||||
return !input.cart
|
return !input.cart
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return useRemoteQueryStep({
|
return useRemoteQueryStep({
|
||||||
@@ -94,9 +94,13 @@ export const refreshCartShippingMethodsWorkflow = createWorkflow(
|
|||||||
cart,
|
cart,
|
||||||
})
|
})
|
||||||
|
|
||||||
when({ listShippingOptionsInput }, ({ listShippingOptionsInput }) => {
|
when(
|
||||||
|
"should-prepare-shipping-methods",
|
||||||
|
{ listShippingOptionsInput },
|
||||||
|
({ listShippingOptionsInput }) => {
|
||||||
return !!listShippingOptionsInput?.length
|
return !!listShippingOptionsInput?.length
|
||||||
}).then(() => {
|
}
|
||||||
|
).then(() => {
|
||||||
const shippingOptions =
|
const shippingOptions =
|
||||||
listShippingOptionsForCartWithPricingWorkflow.runAsStep({
|
listShippingOptionsForCartWithPricingWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const refreshPaymentCollectionForCartWorkflowId =
|
|||||||
export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
|
export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
|
||||||
refreshPaymentCollectionForCartWorkflowId,
|
refreshPaymentCollectionForCartWorkflowId,
|
||||||
(input: WorkflowData<RefreshPaymentCollectionForCartWorklowInput>) => {
|
(input: WorkflowData<RefreshPaymentCollectionForCartWorklowInput>) => {
|
||||||
const fetchCart = when({ input }, ({ input }) => {
|
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||||
return !input.cart
|
return !input.cart
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return useRemoteQueryStep({
|
return useRemoteQueryStep({
|
||||||
@@ -88,7 +88,7 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
|
|||||||
cart,
|
cart,
|
||||||
})
|
})
|
||||||
|
|
||||||
when({ cart }, ({ cart }) => {
|
when("should-update-payment-collection", { cart }, ({ cart }) => {
|
||||||
const valueIsEqual = MathBN.eq(
|
const valueIsEqual = MathBN.eq(
|
||||||
cart.payment_collection?.raw_amount ?? -1,
|
cart.payment_collection?.raw_amount ?? -1,
|
||||||
cart.raw_total
|
cart.raw_total
|
||||||
|
|||||||
@@ -93,18 +93,18 @@ export const transferCartCustomerWorkflow = createWorkflow(
|
|||||||
({ cart, customer }) => cart.customer?.id !== customer.id
|
({ cart, customer }) => cart.customer?.id !== customer.id
|
||||||
)
|
)
|
||||||
|
|
||||||
when({ shouldTransfer }, ({ shouldTransfer }) => shouldTransfer).then(
|
when(
|
||||||
() => {
|
"should-transfer-cart",
|
||||||
const cartInput = transform(
|
{ shouldTransfer },
|
||||||
{ cart, customer },
|
({ shouldTransfer }) => shouldTransfer
|
||||||
({ cart, customer }) => [
|
).then(() => {
|
||||||
|
const cartInput = transform({ cart, customer }, ({ cart, customer }) => [
|
||||||
{
|
{
|
||||||
id: cart.id,
|
id: cart.id,
|
||||||
customer_id: customer.id,
|
customer_id: customer.id,
|
||||||
email: customer.email,
|
email: customer.email,
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
)
|
|
||||||
|
|
||||||
updateCartsStep(cartInput)
|
updateCartsStep(cartInput)
|
||||||
|
|
||||||
@@ -119,8 +119,7 @@ export const transferCartCustomerWorkflow = createWorkflow(
|
|||||||
customer_id: customer.customer_id,
|
customer_id: customer.customer_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
return new WorkflowResponse(void 0, {
|
return new WorkflowResponse(void 0, {
|
||||||
hooks: [validate],
|
hooks: [validate],
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const updateCartPromotionsWorkflowId = "update-cart-promotions"
|
|||||||
export const updateCartPromotionsWorkflow = createWorkflow(
|
export const updateCartPromotionsWorkflow = createWorkflow(
|
||||||
updateCartPromotionsWorkflowId,
|
updateCartPromotionsWorkflowId,
|
||||||
(input: WorkflowData<UpdateCartPromotionsWorkflowInput>) => {
|
(input: WorkflowData<UpdateCartPromotionsWorkflowInput>) => {
|
||||||
const fetchCart = when({ input }, ({ input }) => {
|
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||||
return !input.cart
|
return !input.cart
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return useRemoteQueryStep({
|
return useRemoteQueryStep({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AdditionalData,
|
AdditionalData,
|
||||||
|
CartDTO,
|
||||||
UpdateCartWorkflowInputDTO,
|
UpdateCartWorkflowInputDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
@@ -16,11 +17,7 @@ import {
|
|||||||
WorkflowData,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import { emitEventStep, useQueryGraphStep } from "../../common"
|
||||||
emitEventStep,
|
|
||||||
useQueryGraphStep,
|
|
||||||
useRemoteQueryStep,
|
|
||||||
} from "../../common"
|
|
||||||
import { deleteLineItemsStep } from "../../line-item"
|
import { deleteLineItemsStep } from "../../line-item"
|
||||||
import {
|
import {
|
||||||
findOrCreateCustomerStep,
|
findOrCreateCustomerStep,
|
||||||
@@ -83,9 +80,9 @@ export const updateCartWorkflowId = "update-cart"
|
|||||||
export const updateCartWorkflow = createWorkflow(
|
export const updateCartWorkflow = createWorkflow(
|
||||||
updateCartWorkflowId,
|
updateCartWorkflowId,
|
||||||
(input: WorkflowData<UpdateCartWorkflowInput>) => {
|
(input: WorkflowData<UpdateCartWorkflowInput>) => {
|
||||||
const cartToUpdate = useRemoteQueryStep({
|
const { data: cartToUpdate } = useQueryGraphStep({
|
||||||
entry_point: "cart",
|
entity: "cart",
|
||||||
variables: { id: input.id },
|
filters: { id: input.id },
|
||||||
fields: [
|
fields: [
|
||||||
"id",
|
"id",
|
||||||
"email",
|
"email",
|
||||||
@@ -95,18 +92,26 @@ export const updateCartWorkflow = createWorkflow(
|
|||||||
"region.*",
|
"region.*",
|
||||||
"region.countries.*",
|
"region.countries.*",
|
||||||
],
|
],
|
||||||
list: false,
|
pagination: {
|
||||||
throw_if_key_not_found: true,
|
take: 1,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
throwIfKeyNotFound: true,
|
||||||
|
isList: false,
|
||||||
|
},
|
||||||
}).config({ name: "get-cart" })
|
}).config({ name: "get-cart" })
|
||||||
|
|
||||||
const cartDataInput = transform({ input, cartToUpdate }, (data) => {
|
const cartDataInput = transform(
|
||||||
|
{ input, cartToUpdate },
|
||||||
|
(data: { input: UpdateCartWorkflowInput; cartToUpdate: CartDTO }) => {
|
||||||
return {
|
return {
|
||||||
sales_channel_id:
|
sales_channel_id:
|
||||||
data.input.sales_channel_id ?? data.cartToUpdate.sales_channel_id,
|
data.input.sales_channel_id ?? data.cartToUpdate.sales_channel_id,
|
||||||
customer_id: data.cartToUpdate.customer_id,
|
customer_id: data.cartToUpdate.customer_id,
|
||||||
email: data.input.email ?? data.cartToUpdate.email,
|
email: data.input.email ?? data.cartToUpdate.email,
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const [salesChannel, customer] = parallelize(
|
const [salesChannel, customer] = parallelize(
|
||||||
findSalesChannelStep({
|
findSalesChannelStep({
|
||||||
@@ -120,16 +125,23 @@ export const updateCartWorkflow = createWorkflow(
|
|||||||
|
|
||||||
validateSalesChannelStep({ salesChannel })
|
validateSalesChannelStep({ salesChannel })
|
||||||
|
|
||||||
const newRegion = when({ input }, (data) => {
|
const newRegion = when("should-fetch-region", { input }, (data) => {
|
||||||
return !!data.input.region_id
|
return !!data.input.region_id
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return useRemoteQueryStep({
|
const { data: newRegion } = useQueryGraphStep({
|
||||||
entry_point: "region",
|
entity: "region",
|
||||||
variables: { id: input.region_id },
|
filters: { id: input.region_id },
|
||||||
fields: ["id", "countries.*", "currency_code", "name"],
|
fields: ["id", "countries.*", "currency_code", "name"],
|
||||||
list: false,
|
pagination: {
|
||||||
throw_if_key_not_found: true,
|
take: 1,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
throwIfKeyNotFound: true,
|
||||||
|
isList: false,
|
||||||
|
},
|
||||||
}).config({ name: "get-region" })
|
}).config({ name: "get-region" })
|
||||||
|
|
||||||
|
return newRegion
|
||||||
})
|
})
|
||||||
|
|
||||||
const region = transform({ cartToUpdate, newRegion }, (data) => {
|
const region = transform({ cartToUpdate, newRegion }, (data) => {
|
||||||
@@ -239,9 +251,13 @@ export const updateCartWorkflow = createWorkflow(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
when({ regionUpdated }, ({ regionUpdated }) => {
|
when(
|
||||||
|
"should-emit-region-updated",
|
||||||
|
{ regionUpdated },
|
||||||
|
({ regionUpdated }) => {
|
||||||
return !!regionUpdated
|
return !!regionUpdated
|
||||||
}).then(() => {
|
}
|
||||||
|
).then(() => {
|
||||||
emitEventStep({
|
emitEventStep({
|
||||||
eventName: CartWorkflowEvents.REGION_UPDATED,
|
eventName: CartWorkflowEvents.REGION_UPDATED,
|
||||||
data: { id: input.id },
|
data: { id: input.id },
|
||||||
@@ -258,7 +274,7 @@ export const updateCartWorkflow = createWorkflow(
|
|||||||
|
|
||||||
// In case the region is updated, we might have a new currency OR tax inclusivity setting
|
// In case the region is updated, we might have a new currency OR tax inclusivity setting
|
||||||
// Therefore, we need to delete line items with a custom price for good measure
|
// Therefore, we need to delete line items with a custom price for good measure
|
||||||
when({ regionUpdated }, ({ regionUpdated }) => {
|
when("should-delete-line-items", { regionUpdated }, ({ regionUpdated }) => {
|
||||||
return !!regionUpdated
|
return !!regionUpdated
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const lineItems = useQueryGraphStep({
|
const lineItems = useQueryGraphStep({
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
AdditionalData,
|
AdditionalData,
|
||||||
|
CartDTO,
|
||||||
|
CustomerDTO,
|
||||||
|
RegionDTO,
|
||||||
UpdateLineItemInCartWorkflowInputDTO,
|
UpdateLineItemInCartWorkflowInputDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
CartWorkflowEvents,
|
CartWorkflowEvents,
|
||||||
deduplicate,
|
deduplicate,
|
||||||
|
filterObjectByKeys,
|
||||||
isDefined,
|
isDefined,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
|
QueryContext,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
createHook,
|
createHook,
|
||||||
@@ -18,7 +23,6 @@ import {
|
|||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { useQueryGraphStep } from "../../common"
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { emitEventStep } from "../../common/steps/emit-event"
|
import { emitEventStep } from "../../common/steps/emit-event"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
|
||||||
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
|
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
|
||||||
import { validateCartStep } from "../steps/validate-cart"
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||||
@@ -32,6 +36,13 @@ import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
|||||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||||
|
|
||||||
const cartFields = cartFieldsForPricingContext.concat(["items.*"])
|
const cartFields = cartFieldsForPricingContext.concat(["items.*"])
|
||||||
|
const variantFields = productVariantsFields.concat(["calculated_price.*"])
|
||||||
|
|
||||||
|
interface CartQueryDTO extends Omit<CartDTO, "items"> {
|
||||||
|
items: NonNullable<CartDTO["items"]>
|
||||||
|
customer: CustomerDTO
|
||||||
|
region: RegionDTO
|
||||||
|
}
|
||||||
|
|
||||||
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
||||||
/**
|
/**
|
||||||
@@ -97,17 +108,24 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
(
|
(
|
||||||
input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO & AdditionalData>
|
input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO & AdditionalData>
|
||||||
) => {
|
) => {
|
||||||
const cartQuery = useQueryGraphStep({
|
const { data: cart } = useQueryGraphStep({
|
||||||
entity: "cart",
|
entity: "cart",
|
||||||
filters: { id: input.cart_id },
|
filters: { id: input.cart_id },
|
||||||
fields: cartFields,
|
fields: cartFields,
|
||||||
options: { throwIfKeyNotFound: true },
|
options: { throwIfKeyNotFound: true, isList: false },
|
||||||
}).config({ name: "get-cart" })
|
}).config({ name: "get-cart" })
|
||||||
|
|
||||||
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
|
const { item, variantIds } = transform(
|
||||||
const item = transform({ cart, input }, ({ cart, input }) => {
|
{ cart, input },
|
||||||
return cart.items.find((i) => i.id === input.item_id)
|
(data: {
|
||||||
})
|
cart: CartQueryDTO
|
||||||
|
input: UpdateLineItemInCartWorkflowInputDTO & AdditionalData
|
||||||
|
}) => {
|
||||||
|
const item = data.cart.items.find((i) => i.id === data.input.item_id)!
|
||||||
|
const variantIds = [item?.variant_id].filter(Boolean)
|
||||||
|
return { item, variantIds }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
validateCartStep({ cart })
|
validateCartStep({ cart })
|
||||||
|
|
||||||
@@ -116,10 +134,6 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
cart,
|
cart,
|
||||||
})
|
})
|
||||||
|
|
||||||
const variantIds = transform({ item }, ({ item }) => {
|
|
||||||
return [item.variant_id].filter(Boolean)
|
|
||||||
})
|
|
||||||
|
|
||||||
const setPricingContext = createHook(
|
const setPricingContext = createHook(
|
||||||
"setPricingContext",
|
"setPricingContext",
|
||||||
{
|
{
|
||||||
@@ -134,41 +148,56 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const setPricingContextResult = setPricingContext.getResult()
|
const setPricingContextResult = setPricingContext.getResult()
|
||||||
|
|
||||||
const pricingContext = transform(
|
const pricingContext = transform(
|
||||||
{ cart, setPricingContextResult },
|
{ cart, item, update: input.update, setPricingContextResult },
|
||||||
(data) => {
|
(data): Record<string, any> => {
|
||||||
return {
|
return {
|
||||||
...data.cart,
|
...filterObjectByKeys(data.cart, cartFieldsForPricingContext),
|
||||||
...(data.setPricingContextResult ? data.setPricingContextResult : {}),
|
...(data.setPricingContextResult ? data.setPricingContextResult : {}),
|
||||||
|
quantity: data.update.quantity ?? data.item.quantity,
|
||||||
currency_code: data.cart.currency_code,
|
currency_code: data.cart.currency_code,
|
||||||
region_id: data.cart.region_id,
|
region_id: data.cart.region_id!,
|
||||||
region: data.cart.region,
|
region: data.cart.region!,
|
||||||
customer_id: data.cart.customer_id,
|
customer_id: data.cart.customer_id!,
|
||||||
customer: data.cart.customer,
|
customer: data.cart.customer!,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
const variants = when(
|
||||||
|
"should-fetch-variants",
|
||||||
|
{ variantIds },
|
||||||
|
({ variantIds }) => {
|
||||||
return !!variantIds.length
|
return !!variantIds.length
|
||||||
}).then(() => {
|
}
|
||||||
return useRemoteQueryStep({
|
).then(() => {
|
||||||
entry_point: "variants",
|
const calculatedPriceQueryContext = transform(
|
||||||
|
{ pricingContext },
|
||||||
|
({ pricingContext }) => {
|
||||||
|
return QueryContext(pricingContext)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: variants } = useQueryGraphStep({
|
||||||
|
entity: "variants",
|
||||||
fields: deduplicate([
|
fields: deduplicate([
|
||||||
...productVariantsFields,
|
...variantFields,
|
||||||
...requiredVariantFieldsForInventoryConfirmation,
|
...requiredVariantFieldsForInventoryConfirmation,
|
||||||
]),
|
]),
|
||||||
variables: {
|
filters: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
|
||||||
context: pricingContext,
|
|
||||||
},
|
},
|
||||||
|
context: {
|
||||||
|
calculated_price: calculatedPriceQueryContext,
|
||||||
},
|
},
|
||||||
}).config({ name: "fetch-variants" })
|
}).config({ name: "fetch-variants" })
|
||||||
})
|
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
|
return variants
|
||||||
|
})
|
||||||
|
|
||||||
const items = transform({ input, item }, (data) => {
|
const items = transform({ input, item }, (data) => {
|
||||||
return [
|
return [
|
||||||
Object.assign(data.item, { quantity: data.input.update.quantity }),
|
Object.assign(data.item, { quantity: data.input.update.quantity }),
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export const updateTaxLinesWorkflowId = "update-tax-lines"
|
|||||||
export const updateTaxLinesWorkflow = createWorkflow(
|
export const updateTaxLinesWorkflow = createWorkflow(
|
||||||
updateTaxLinesWorkflowId,
|
updateTaxLinesWorkflowId,
|
||||||
(input: WorkflowData<UpdateTaxLinesWorkflowInput>): WorkflowData<void> => {
|
(input: WorkflowData<UpdateTaxLinesWorkflowInput>): WorkflowData<void> => {
|
||||||
const fetchCart = when({ input }, ({ input }) => {
|
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||||
return !input.cart
|
return !input.cart
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return useRemoteQueryStep({
|
return useRemoteQueryStep({
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const upsertTaxLinesWorkflowId = "upsert-tax-lines"
|
|||||||
export const upsertTaxLinesWorkflow = createWorkflow(
|
export const upsertTaxLinesWorkflow = createWorkflow(
|
||||||
upsertTaxLinesWorkflowId,
|
upsertTaxLinesWorkflowId,
|
||||||
(input: WorkflowData<UpsertTaxLinesWorkflowInput>): WorkflowData<void> => {
|
(input: WorkflowData<UpsertTaxLinesWorkflowInput>): WorkflowData<void> => {
|
||||||
const fetchCart = when({ input }, ({ input }) => {
|
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||||
return !input.cart
|
return !input.cart
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return useRemoteQueryStep({
|
return useRemoteQueryStep({
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface SimpleProduct {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FixtureEntryPoints {
|
||||||
|
simple_product: SimpleProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@medusajs/types/dist/modules-sdk/remote-query-entry-points" {
|
||||||
|
export interface RemoteQueryEntryPoints extends FixtureEntryPoints {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { createWorkflow, WorkflowResponse } from "@medusajs/workflows-sdk"
|
||||||
|
import { expectTypeOf } from "expect-type"
|
||||||
|
import { FixtureEntryPoints } from "../__fixtures__/remote-query"
|
||||||
|
import { useQueryGraphStep } from "../use-query-graph"
|
||||||
|
import { MedusaContainer } from "@medusajs/framework"
|
||||||
|
import { asFunction, createContainer } from "awilix"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
describe("useQueryGraphStep", () => {
|
||||||
|
let container!: MedusaContainer
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
container = createContainer() as unknown as MedusaContainer
|
||||||
|
container.register(
|
||||||
|
ContainerRegistrationKeys.QUERY,
|
||||||
|
asFunction(() => {
|
||||||
|
return {
|
||||||
|
graph: () => Promise.resolve({ data: [] }),
|
||||||
|
} as any
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a single data item when is_list is false", async () => {
|
||||||
|
const workflow = createWorkflow("useQueryGraphStepTest1", (_: any) => {
|
||||||
|
const result = useQueryGraphStep({
|
||||||
|
entity: "simple_product",
|
||||||
|
fields: ["*"],
|
||||||
|
filters: {
|
||||||
|
id: "123",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
isList: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new WorkflowResponse(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await workflow(container).run()
|
||||||
|
|
||||||
|
type Result = (typeof result)["result"]
|
||||||
|
|
||||||
|
expectTypeOf<Result["data"]>().toEqualTypeOf<
|
||||||
|
FixtureEntryPoints["simple_product"]
|
||||||
|
>()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a list of data items when is_list is true", async () => {
|
||||||
|
const workflow = createWorkflow("useQueryGraphStepTest1", (_: any) => {
|
||||||
|
const result = useQueryGraphStep({
|
||||||
|
entity: "simple_product",
|
||||||
|
fields: ["*"],
|
||||||
|
filters: {
|
||||||
|
id: "123",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
isList: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new WorkflowResponse(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await workflow(container).run()
|
||||||
|
|
||||||
|
type Result = (typeof result)["result"]
|
||||||
|
|
||||||
|
expectTypeOf<Result["data"]>().toEqualTypeOf<
|
||||||
|
FixtureEntryPoints["simple_product"][]
|
||||||
|
>()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a list of data items when is_list is not specified", async () => {
|
||||||
|
const workflow = createWorkflow("useQueryGraphStepTest1", (_: any) => {
|
||||||
|
const result = useQueryGraphStep({
|
||||||
|
entity: "simple_product",
|
||||||
|
fields: ["*"],
|
||||||
|
filters: {
|
||||||
|
id: "123",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new WorkflowResponse(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await workflow(container).run()
|
||||||
|
|
||||||
|
type Result = (typeof result)["result"]
|
||||||
|
|
||||||
|
expectTypeOf<Result["data"]>().toEqualTypeOf<
|
||||||
|
FixtureEntryPoints["simple_product"][]
|
||||||
|
>()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,10 +7,28 @@ import {
|
|||||||
import { createStep, StepFunction, StepResponse } from "@medusajs/workflows-sdk"
|
import { createStep, StepFunction, StepResponse } from "@medusajs/workflows-sdk"
|
||||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
|
||||||
export type UseQueryGraphStepInput<TEntry extends string> =
|
export type UseQueryGraphStepInput<
|
||||||
RemoteQueryInput<TEntry> & {
|
TEntry extends string,
|
||||||
options?: RemoteJoinerOptions
|
TIsList extends boolean = boolean
|
||||||
|
> = RemoteQueryInput<TEntry> & {
|
||||||
|
options?: RemoteJoinerOptions & {
|
||||||
|
isList?: TIsList
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseQueryGraphStepOutput<
|
||||||
|
TEntry extends string,
|
||||||
|
TIsList extends boolean = boolean
|
||||||
|
> = ReturnType<
|
||||||
|
StepFunction<
|
||||||
|
any,
|
||||||
|
true extends TIsList
|
||||||
|
? GraphResultSet<TEntry>
|
||||||
|
: Omit<GraphResultSet<TEntry>, "data"> & {
|
||||||
|
data: GraphResultSet<TEntry>["data"][number]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
const useQueryGraphStepId = "use-query-graph-step"
|
const useQueryGraphStepId = "use-query-graph-step"
|
||||||
|
|
||||||
@@ -20,9 +38,20 @@ const step = createStep(
|
|||||||
const query = container.resolve<RemoteQueryFunction>(
|
const query = container.resolve<RemoteQueryFunction>(
|
||||||
ContainerRegistrationKeys.QUERY
|
ContainerRegistrationKeys.QUERY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isList = input.options?.isList ?? true
|
||||||
|
delete input.options?.isList
|
||||||
|
|
||||||
const { options, ...queryConfig } = input
|
const { options, ...queryConfig } = input
|
||||||
|
|
||||||
const result = await query.graph(queryConfig as any, options)
|
const result = await query.graph(queryConfig as any, options)
|
||||||
|
|
||||||
|
if (!isList) {
|
||||||
|
const data = result.data?.[0]
|
||||||
|
result.data = data
|
||||||
|
return new StepResponse(result)
|
||||||
|
}
|
||||||
|
|
||||||
return new StepResponse(result)
|
return new StepResponse(result)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -100,9 +129,10 @@ const step = createStep(
|
|||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const useQueryGraphStep = <const TEntry extends string>(
|
export const useQueryGraphStep = <
|
||||||
input: UseQueryGraphStepInput<TEntry>
|
const TEntry extends string,
|
||||||
): ReturnType<StepFunction<any, GraphResultSet<TEntry>>> =>
|
const TIsList extends boolean = boolean
|
||||||
step(input as any) as unknown as ReturnType<
|
>(
|
||||||
StepFunction<any, GraphResultSet<TEntry>>
|
input: UseQueryGraphStepInput<TEntry, TIsList>
|
||||||
>
|
): UseQueryGraphStepOutput<TEntry, TIsList> =>
|
||||||
|
step(input as any) as unknown as UseQueryGraphStepOutput<TEntry, TIsList>
|
||||||
@@ -14,7 +14,6 @@ export const productVariantsFields = [
|
|||||||
"product.type.id",
|
"product.type.id",
|
||||||
"product.collection.title",
|
"product.collection.title",
|
||||||
"product.handle",
|
"product.handle",
|
||||||
"calculated_price.*",
|
|
||||||
"inventory_items.inventory_item_id",
|
"inventory_items.inventory_item_id",
|
||||||
"inventory_items.required_quantity",
|
"inventory_items.required_quantity",
|
||||||
"inventory_items.inventory.requires_shipping",
|
"inventory_items.inventory.requires_shipping",
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ import {
|
|||||||
} from "../../cart/utils/prepare-line-item-data"
|
} from "../../cart/utils/prepare-line-item-data"
|
||||||
import { pricingContextResult } from "../../cart/utils/schemas"
|
import { pricingContextResult } from "../../cart/utils/schemas"
|
||||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||||
import { useRemoteQueryStep } from "../../common"
|
import { useQueryGraphStep, useRemoteQueryStep } from "../../common"
|
||||||
import { createOrderLineItemsStep } from "../steps"
|
import { createOrderLineItemsStep } from "../steps"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
|
import { getVariantPriceSetsStep } from "../../cart"
|
||||||
|
|
||||||
function prepareLineItems(data) {
|
function prepareLineItems(data) {
|
||||||
const items = (data.input.items ?? []).map((item) => {
|
const items = (data.input.items ?? []).map((item) => {
|
||||||
@@ -193,30 +194,66 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
const variants = when(
|
||||||
|
"fetch-variants-with-calculated-price",
|
||||||
|
{ variantIds },
|
||||||
|
({ variantIds }) => {
|
||||||
return !!variantIds.length
|
return !!variantIds.length
|
||||||
}).then(() => {
|
}
|
||||||
return useRemoteQueryStep({
|
).then(() => {
|
||||||
entry_point: "variants",
|
const { data: variantsData } = useQueryGraphStep({
|
||||||
|
entity: "variants",
|
||||||
fields: deduplicate([
|
fields: deduplicate([
|
||||||
...productVariantsFields,
|
...productVariantsFields,
|
||||||
...requiredVariantFieldsForInventoryConfirmation,
|
...requiredVariantFieldsForInventoryConfirmation,
|
||||||
]),
|
]),
|
||||||
variables: {
|
filters: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
|
||||||
context: pricingContext,
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const calculatedPriceContext = transform(
|
||||||
|
{ pricingContext, items: input.items },
|
||||||
|
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||||
|
const baseContext = data.pricingContext
|
||||||
|
|
||||||
|
return (data.items ?? [])
|
||||||
|
.filter((i) => i.variant_id)
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
variantId: item.variant_id!,
|
||||||
|
context: {
|
||||||
|
...baseContext,
|
||||||
|
quantity: item.quantity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||||
|
data: calculatedPriceContext,
|
||||||
|
})
|
||||||
|
|
||||||
|
const variants = transform(
|
||||||
|
{ variantsData, calculatedPriceSets },
|
||||||
|
({ variantsData, calculatedPriceSets }) => {
|
||||||
|
return variantsData.map((variant) => {
|
||||||
|
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||||
|
return variant
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
|
return variants
|
||||||
|
})
|
||||||
|
|
||||||
confirmVariantInventoryWorkflow.runAsStep({
|
confirmVariantInventoryWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
sales_channel_id: salesChannel.id,
|
sales_channel_id: salesChannel.id,
|
||||||
variants,
|
variants: variants!,
|
||||||
items: input.items!,
|
items: input.items!,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AdditionalData, CreateOrderDTO } from "@medusajs/framework/types"
|
import { AdditionalData, CreateOrderDTO } from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
MedusaError,
|
MedusaError,
|
||||||
|
PromotionActions,
|
||||||
deduplicate,
|
deduplicate,
|
||||||
isDefined,
|
isDefined,
|
||||||
isPresent,
|
isPresent,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
transform,
|
transform,
|
||||||
when,
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { getVariantPriceSetsStep } from "../../cart"
|
||||||
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
||||||
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
||||||
import { findSalesChannelStep } from "../../cart/steps/find-sales-channel"
|
import { findSalesChannelStep } from "../../cart/steps/find-sales-channel"
|
||||||
@@ -26,7 +28,8 @@ import {
|
|||||||
} from "../../cart/utils/prepare-line-item-data"
|
} from "../../cart/utils/prepare-line-item-data"
|
||||||
import { pricingContextResult } from "../../cart/utils/schemas"
|
import { pricingContextResult } from "../../cart/utils/schemas"
|
||||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||||
import { useRemoteQueryStep } from "../../common"
|
import { useQueryGraphStep } from "../../common"
|
||||||
|
import { refreshDraftOrderAdjustmentsWorkflow } from "../../draft-order/workflows/refresh-draft-order-adjustments"
|
||||||
import { createOrdersStep } from "../steps"
|
import { createOrdersStep } from "../steps"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
import { updateOrderTaxLinesWorkflow } from "./update-tax-lines"
|
import { updateOrderTaxLinesWorkflow } from "./update-tax-lines"
|
||||||
@@ -205,7 +208,6 @@ export const createOrderWorkflow = createWorkflow(
|
|||||||
)
|
)
|
||||||
const setPricingContextResult = setPricingContext.getResult()
|
const setPricingContextResult = setPricingContext.getResult()
|
||||||
|
|
||||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
|
||||||
const pricingContext = transform(
|
const pricingContext = transform(
|
||||||
{ input, region, customerData, setPricingContextResult },
|
{ input, region, customerData, setPricingContextResult },
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -222,25 +224,133 @@ export const createOrderWorkflow = createWorkflow(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
/**
|
||||||
return !!variantIds.length
|
* Only fetch variants with calculated prices if needed, otherwise only fetch variants without
|
||||||
}).then(() => {
|
* calculated prices.
|
||||||
return useRemoteQueryStep({
|
*
|
||||||
entry_point: "variants",
|
* We need a variant calculated price when the item is either missing a unit price or is not
|
||||||
|
* tax inclusive.
|
||||||
|
*/
|
||||||
|
const { variantIdsForPriceCalculation, variantIdsWithoutCalculatedPrice } =
|
||||||
|
transform({ input }, (data) => {
|
||||||
|
const variantIdsForPriceCalculation: string[] = []
|
||||||
|
const variantIdsWithoutCalculatedPrice: string[] = []
|
||||||
|
|
||||||
|
data.input.items?.forEach((item) => {
|
||||||
|
if (
|
||||||
|
item.variant_id &&
|
||||||
|
(!isDefined(item.unit_price) || !isDefined(item.is_tax_inclusive))
|
||||||
|
) {
|
||||||
|
variantIdsForPriceCalculation.push(item.variant_id!)
|
||||||
|
} else {
|
||||||
|
variantIdsWithoutCalculatedPrice.push(item.variant_id!)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
variantIdsForPriceCalculation,
|
||||||
|
variantIdsWithoutCalculatedPrice,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all variant for which we don't need to calculate the price.
|
||||||
|
*/
|
||||||
|
const { data: variantsWithoutCalculatedPrice } = useQueryGraphStep({
|
||||||
|
entity: "variants",
|
||||||
fields: deduplicate([
|
fields: deduplicate([
|
||||||
...productVariantsFields,
|
...productVariantsFields,
|
||||||
...requiredVariantFieldsForInventoryConfirmation,
|
...requiredVariantFieldsForInventoryConfirmation,
|
||||||
]),
|
]),
|
||||||
variables: {
|
filters: {
|
||||||
id: variantIds,
|
id: variantIdsWithoutCalculatedPrice,
|
||||||
calculated_price: {
|
|
||||||
context: pricingContext,
|
|
||||||
},
|
},
|
||||||
|
}).config({ name: "query-variants-without-calculated-price" })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all variants for which we need to calculate the price.
|
||||||
|
*/
|
||||||
|
const variantsWithCalculatedPrice = when(
|
||||||
|
"fetch-variants-with-calculated-price",
|
||||||
|
{ variantIdsForPriceCalculation },
|
||||||
|
({ variantIdsForPriceCalculation }) => {
|
||||||
|
return !!variantIdsForPriceCalculation.length
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
const calculatePricesContext = transform(
|
||||||
|
{ items: input.items, variantIdsForPriceCalculation, pricingContext },
|
||||||
|
(data) => {
|
||||||
|
const baseContext = data.pricingContext
|
||||||
|
|
||||||
|
return data.variantIdsForPriceCalculation
|
||||||
|
?.map((variant) => {
|
||||||
|
// Since we retrieve the variant ids from the item, it is not possible to not find the item back from the variant id.
|
||||||
|
const item = data.items?.find(
|
||||||
|
(item) => item.variant_id === variant
|
||||||
|
)!
|
||||||
|
|
||||||
|
return {
|
||||||
|
variantId: variant,
|
||||||
|
context: {
|
||||||
|
...baseContext,
|
||||||
|
quantity: item.quantity,
|
||||||
},
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: variants } = useQueryGraphStep({
|
||||||
|
entity: "variants",
|
||||||
|
fields: deduplicate([
|
||||||
|
...productVariantsFields,
|
||||||
|
...requiredVariantFieldsForInventoryConfirmation,
|
||||||
|
]),
|
||||||
|
filters: {
|
||||||
|
id: variantIdsForPriceCalculation,
|
||||||
|
},
|
||||||
|
}).config({ name: "query-variants-to-calculate-prices" })
|
||||||
|
|
||||||
|
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||||
|
data: calculatePricesContext,
|
||||||
})
|
})
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
const reconstructedVariants = transform(
|
||||||
|
{
|
||||||
|
variants,
|
||||||
|
calculatedPriceSets,
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
return data.variants.map((variant) => {
|
||||||
|
variant.calculated_price = data.calculatedPriceSets[variant.id]
|
||||||
|
return variant
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
validateVariantPricesStep({ variants: reconstructedVariants }).config({
|
||||||
|
name: "validate-variants-with-calculated-price",
|
||||||
|
})
|
||||||
|
|
||||||
|
return reconstructedVariants
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate all variants without calculated price and all variants with calculated price.
|
||||||
|
*/
|
||||||
|
const variants = transform(
|
||||||
|
{
|
||||||
|
variantsWithoutCalculatedPrice,
|
||||||
|
variantsWithCalculatedPrice,
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
return [
|
||||||
|
...data.variantsWithoutCalculatedPrice,
|
||||||
|
...(data.variantsWithCalculatedPrice ?? []),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
confirmVariantInventoryWorkflow.runAsStep({
|
confirmVariantInventoryWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
@@ -269,14 +379,63 @@ export const createOrderWorkflow = createWorkflow(
|
|||||||
const orders = createOrdersStep([orderToCreate])
|
const orders = createOrdersStep([orderToCreate])
|
||||||
const order = transform({ orders }, (data) => data.orders?.[0])
|
const order = transform({ orders }, (data) => data.orders?.[0])
|
||||||
|
|
||||||
|
const appliedPromoCodes: string[] = transform(
|
||||||
|
input,
|
||||||
|
(order) => order.promo_codes ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Currently need the refresh because when the order module creates the order, even though
|
||||||
|
* the totals are calculated, the order is being queried and without the totals. There is some
|
||||||
|
* point of discussion for improvements here down the line.
|
||||||
|
*/
|
||||||
|
const { data: freshOrder } = useQueryGraphStep({
|
||||||
|
entity: "orders",
|
||||||
|
fields: [
|
||||||
|
"shipping_address.*",
|
||||||
|
"billing_address.*",
|
||||||
|
"summary.*",
|
||||||
|
"items.*",
|
||||||
|
"credit_lines.*",
|
||||||
|
"items.tax_lines.*",
|
||||||
|
"items.adjustments.*",
|
||||||
|
"shipping_methods.*",
|
||||||
|
"shipping_methods.tax_lines.*",
|
||||||
|
"shipping_methods.adjustments.*",
|
||||||
|
"transactions.*",
|
||||||
|
"currency_code",
|
||||||
|
"items.tax_lines.*",
|
||||||
|
"items.adjustments.*",
|
||||||
|
"shipping_methods.tax_lines.*",
|
||||||
|
"shipping_methods.adjustments.*",
|
||||||
|
"total",
|
||||||
|
"id",
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
id: order.id,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
isList: false,
|
||||||
|
},
|
||||||
|
}).config({ name: "query-fresh-order" })
|
||||||
|
|
||||||
|
parallelize(
|
||||||
updateOrderTaxLinesWorkflow.runAsStep({
|
updateOrderTaxLinesWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
order_id: order.id,
|
order_id: order.id,
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
refreshDraftOrderAdjustmentsWorkflow.runAsStep({
|
||||||
|
input: {
|
||||||
|
order: freshOrder,
|
||||||
|
promo_codes: appliedPromoCodes,
|
||||||
|
action: PromotionActions.REPLACE,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const orderCreated = createHook("orderCreated", {
|
const orderCreated = createHook("orderCreated", {
|
||||||
order,
|
order: freshOrder,
|
||||||
additional_data: input.additional_data,
|
additional_data: input.additional_data,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user