feat(medusa,pricing): Cart pricing context with customer group (#10579)
* fix(carts): Fixes cart modifications not accounting for certain price lists (#10493) *What* * Fixes #10490 * Expands any available customer_id into its customer_group_ids for cart updates that add line items. *Why* * Cart updates from the storefront were overriding any valid price lists that were correctly being shown in the storefront's product pages. *How* * Adds a new workflow step that expands an optional customer_id into the customer_group_ids it belongs to. * Uses this step in the addToCartWorkflow and updateLineItemInCartWorkflow workflows. *Testing* * Using medusa-dev to test on a local backend. * Adds integration tests for the addToCart and updateLineItemInCart workflows. Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> * chore: update cart workflows to accept new pricing context * chore: add transfer specs * chore: fix specs * chore: modify types + specs * chore: add data migration + dashboard changes * chore: fix update line item workflow * chore: add changeset + unskip spec --------- Co-authored-by: Sergio Campamá <sergiocampama@gmail.com>
This commit is contained in:
1 parent
0c49470066
commit
6367bccde8
29 files changed
+1090
-166
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/pricing": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa, pricing): Cart workflows handle pricing context accurately
|
||||||
@@ -271,7 +271,7 @@ medusaIntegrationTestRunner({
|
|||||||
shipping_address: shippingAddressData,
|
shipping_address: shippingAddressData,
|
||||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||||
},
|
},
|
||||||
storeHeadersWithCustomer
|
storeHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
@@ -414,7 +414,7 @@ medusaIntegrationTestRunner({
|
|||||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||||
promo_codes: [promotion.code],
|
promo_codes: [promotion.code],
|
||||||
},
|
},
|
||||||
storeHeadersWithCustomer
|
storeHeaders
|
||||||
)
|
)
|
||||||
).data.cart
|
).data.cart
|
||||||
})
|
})
|
||||||
@@ -643,28 +643,61 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("with sale price lists", () => {
|
describe("with sale price lists", () => {
|
||||||
let priceList
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
priceList = (
|
await api.post(
|
||||||
|
`/admin/price-lists`,
|
||||||
|
{
|
||||||
|
title: "test price list",
|
||||||
|
description: "test",
|
||||||
|
status: PriceListStatus.ACTIVE,
|
||||||
|
type: PriceListType.SALE,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 350,
|
||||||
|
currency_code: "usd",
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const customerGroup = (
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/price-lists`,
|
"/admin/customer-groups",
|
||||||
{
|
{ name: "VIP" },
|
||||||
title: "test price list",
|
|
||||||
description: "test",
|
|
||||||
status: PriceListStatus.ACTIVE,
|
|
||||||
type: PriceListType.SALE,
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
amount: 350,
|
|
||||||
currency_code: "usd",
|
|
||||||
variant_id: product.variants[0].id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.price_list
|
).data.customer_group
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/customer-groups/${customerGroup.id}/customers`,
|
||||||
|
{
|
||||||
|
add: [customer.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/price-lists`,
|
||||||
|
{
|
||||||
|
title: "test price list",
|
||||||
|
description: "test",
|
||||||
|
status: PriceListStatus.ACTIVE,
|
||||||
|
type: PriceListType.SALE,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 200,
|
||||||
|
currency_code: "usd",
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"customer.groups.id": [customerGroup.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add price from price list and set compare_at_unit_price", async () => {
|
it("should add price from price list and set compare_at_unit_price", async () => {
|
||||||
@@ -709,6 +742,158 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should add price from price list associated to a customer group when customer rules match", async () => {
|
||||||
|
const transferredCart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cart.id}/customer`,
|
||||||
|
{},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
expect(transferredCart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: cart.id,
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
unit_price: 200,
|
||||||
|
compare_at_unit_price: 1500,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
quantity: 1,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
let response = await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: cart.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
unit_price: 200,
|
||||||
|
compare_at_unit_price: 1500,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
quantity: 2,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /store/carts/:id/line-items/:id", () => {
|
||||||
|
let item, customerGroup
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||||
|
},
|
||||||
|
storeHeadersWithCustomer
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
item = cart.items[0]
|
||||||
|
|
||||||
|
customerGroup = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/customer-groups",
|
||||||
|
{ name: "VIP" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.customer_group
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/customer-groups/${customerGroup.id}/customers`,
|
||||||
|
{
|
||||||
|
add: [customer.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update cart's line item", async () => {
|
||||||
|
let response = await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items/${item.id}`,
|
||||||
|
{
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: cart.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
unit_price: 1500,
|
||||||
|
quantity: 2,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/price-lists`,
|
||||||
|
{
|
||||||
|
title: "test price list",
|
||||||
|
description: "test",
|
||||||
|
status: PriceListStatus.ACTIVE,
|
||||||
|
type: PriceListType.SALE,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 200,
|
||||||
|
currency_code: "usd",
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"customer.groups.id": [customerGroup.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await api.post(
|
||||||
|
`/store/carts/${cart.id}/line-items/${item.id}`,
|
||||||
|
{ quantity: 3 },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: cart.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
unit_price: 200,
|
||||||
|
quantity: 3,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import {
|
import {
|
||||||
createAdminUser,
|
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
import {
|
import {
|
||||||
getPricelistFixture,
|
getPricelistFixture,
|
||||||
@@ -102,7 +102,7 @@ medusaIntegrationTestRunner({
|
|||||||
description:
|
description:
|
||||||
"Summer sale for VIP customers. 25% off selected items.",
|
"Summer sale for VIP customers. 25% off selected items.",
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup1.id],
|
"customer.groups.id": [customerGroup1.id],
|
||||||
},
|
},
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
@@ -139,7 +139,7 @@ medusaIntegrationTestRunner({
|
|||||||
starts_at: "2022-07-01T00:00:00.000Z",
|
starts_at: "2022-07-01T00:00:00.000Z",
|
||||||
ends_at: "2022-07-31T00:00:00.000Z",
|
ends_at: "2022-07-31T00:00:00.000Z",
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [expect.stringContaining("cusgroup_")],
|
"customer.groups.id": [expect.stringContaining("cusgroup_")],
|
||||||
},
|
},
|
||||||
prices: [
|
prices: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -335,7 +335,7 @@ medusaIntegrationTestRunner({
|
|||||||
starts_at: "2022-09-01T00:00:00.000Z",
|
starts_at: "2022-09-01T00:00:00.000Z",
|
||||||
ends_at: "2022-12-31T00:00:00.000Z",
|
ends_at: "2022-12-31T00:00:00.000Z",
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup1.id],
|
"customer.groups.id": [customerGroup1.id],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ medusaIntegrationTestRunner({
|
|||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup1.id],
|
"customer.groups.id": [customerGroup1.id],
|
||||||
},
|
},
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
|
|||||||
@@ -1105,7 +1105,7 @@ medusaIntegrationTestRunner({
|
|||||||
variant_id: product.variants[0].id,
|
variant_id: product.variants[0].id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: { customer_group_id: [customerGroup.id] },
|
rules: { "customer.groups.id": [customerGroup.id] },
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -1188,7 +1188,7 @@ medusaIntegrationTestRunner({
|
|||||||
variant_id: product.variants[0].id,
|
variant_id: product.variants[0].id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: { customer_group_id: [customerGroup.id] },
|
rules: { "customer.groups.id": [customerGroup.id] },
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -1783,7 +1783,7 @@ medusaIntegrationTestRunner({
|
|||||||
variant_id: product.variants[0].id,
|
variant_id: product.variants[0].id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: { customer_group_id: [customerGroup.id] },
|
rules: { "customer.groups.id": [customerGroup.id] },
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -1863,7 +1863,7 @@ medusaIntegrationTestRunner({
|
|||||||
variant_id: product.variants[0].id,
|
variant_id: product.variants[0].id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: { customer_group_id: [customerGroup.id] },
|
rules: { "customer.groups.id": [customerGroup.id] },
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
Modules,
|
Modules,
|
||||||
|
PriceListStatus,
|
||||||
|
PriceListType,
|
||||||
RuleOperator,
|
RuleOperator,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
@@ -786,7 +788,7 @@ medusaIntegrationTestRunner({
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cart,
|
cart_id: cart.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -875,7 +877,7 @@ medusaIntegrationTestRunner({
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cart,
|
cart_id: cart.id,
|
||||||
},
|
},
|
||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
})
|
})
|
||||||
@@ -906,7 +908,7 @@ medusaIntegrationTestRunner({
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cart,
|
cart_id: cart.id,
|
||||||
},
|
},
|
||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
})
|
})
|
||||||
@@ -921,9 +923,307 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should add item to cart with price list", async () => {
|
||||||
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
|
name: "Webshop",
|
||||||
|
})
|
||||||
|
|
||||||
|
const customer = await customerModule.createCustomers({
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
})
|
||||||
|
|
||||||
|
const customer_group = await customerModule.createCustomerGroups({
|
||||||
|
name: "Test Group",
|
||||||
|
})
|
||||||
|
|
||||||
|
await customerModule.addCustomerToGroup({
|
||||||
|
customer_id: customer.id,
|
||||||
|
customer_group_id: customer_group.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const location = await stockLocationModule.createStockLocations({
|
||||||
|
name: "Warehouse",
|
||||||
|
})
|
||||||
|
|
||||||
|
let cart = await cartModuleService.createCarts({
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
customer_id: customer.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [product] = await productModule.createProducts([
|
||||||
|
{
|
||||||
|
title: "Test product",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryModule.createInventoryItems({
|
||||||
|
sku: "inv-1234",
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryModule.createInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: location.id,
|
||||||
|
stocked_quantity: 2,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const priceSet = await pricingModule.createPriceSets({
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await pricingModule.createPricePreferences({
|
||||||
|
attribute: "currency_code",
|
||||||
|
value: "usd",
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await pricingModule.createPriceLists([
|
||||||
|
{
|
||||||
|
title: "test price list",
|
||||||
|
description: "test",
|
||||||
|
status: PriceListStatus.ACTIVE,
|
||||||
|
type: PriceListType.OVERRIDE,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 1500,
|
||||||
|
currency_code: "usd",
|
||||||
|
price_set_id: priceSet.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"customer.groups.id": [customer_group.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await remoteLink.create([
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
},
|
||||||
|
[Modules.PRICING]: {
|
||||||
|
price_set_id: priceSet.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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
|
select: ["id", "region_id", "currency_code", "sales_channel_id"],
|
||||||
|
})
|
||||||
|
|
||||||
|
await addToCartWorkflow(appContainer).run({
|
||||||
|
input: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cart_id: cart.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
|
relations: ["items"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cart).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: cart.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
unit_price: 1500,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
quantity: 1,
|
||||||
|
title: "Test variant",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("updateLineItemInCartWorkflow", () => {
|
describe("updateLineItemInCartWorkflow", () => {
|
||||||
|
it("should update item in cart with price list", async () => {
|
||||||
|
const salesChannel = await scModuleService.createSalesChannels({
|
||||||
|
name: "Webshop",
|
||||||
|
})
|
||||||
|
|
||||||
|
const customer = await customerModule.createCustomers({
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
})
|
||||||
|
|
||||||
|
const customer_group = await customerModule.createCustomerGroups({
|
||||||
|
name: "Test Group",
|
||||||
|
})
|
||||||
|
|
||||||
|
await customerModule.addCustomerToGroup({
|
||||||
|
customer_id: customer.id,
|
||||||
|
customer_group_id: customer_group.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const location = await stockLocationModule.createStockLocations({
|
||||||
|
name: "Warehouse",
|
||||||
|
})
|
||||||
|
|
||||||
|
const [product] = await productModule.createProducts([
|
||||||
|
{
|
||||||
|
title: "Test product",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryModule.createInventoryItems({
|
||||||
|
sku: "inv-1234",
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryModule.createInventoryLevels([
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: location.id,
|
||||||
|
stocked_quantity: 2,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const priceSet = await pricingModule.createPriceSets({
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 3000,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await pricingModule.createPriceLists([
|
||||||
|
{
|
||||||
|
title: "test price list",
|
||||||
|
description: "test",
|
||||||
|
status: PriceListStatus.ACTIVE,
|
||||||
|
type: PriceListType.OVERRIDE,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 1500,
|
||||||
|
currency_code: "usd",
|
||||||
|
price_set_id: priceSet.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"customer.groups.id": [customer_group.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await remoteLink.create([
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
},
|
||||||
|
[Modules.PRICING]: {
|
||||||
|
price_set_id: priceSet.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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
let cart = await cartModuleService.createCarts({
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
customer_id: customer.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 5000,
|
||||||
|
title: "Test item",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
cart = await cartModuleService.retrieveCart(cart.id, {
|
||||||
|
select: ["id", "region_id", "currency_code"],
|
||||||
|
relations: ["items", "items.variant_id", "items.metadata"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const item = cart.items?.[0]!
|
||||||
|
|
||||||
|
const { errors } = await updateLineItemInCartWorkflow(
|
||||||
|
appContainer
|
||||||
|
).run({
|
||||||
|
input: {
|
||||||
|
cart_id: cart.id,
|
||||||
|
item_id: item.id,
|
||||||
|
update: {
|
||||||
|
metadata: {
|
||||||
|
foo: "bar",
|
||||||
|
},
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedItem = await cartModuleService.retrieveLineItem(item.id)
|
||||||
|
|
||||||
|
expect(updatedItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: item.id,
|
||||||
|
unit_price: 1500,
|
||||||
|
quantity: 2,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
describe("compensation", () => {
|
describe("compensation", () => {
|
||||||
it("should revert line item update to original state", async () => {
|
it("should revert line item update to original state", async () => {
|
||||||
expect.assertions(2)
|
expect.assertions(2)
|
||||||
@@ -1024,8 +1324,8 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const { errors } = await workflow.run({
|
const { errors } = await workflow.run({
|
||||||
input: {
|
input: {
|
||||||
cart,
|
cart_id: cart.id,
|
||||||
item,
|
item_id: item.id,
|
||||||
update: {
|
update: {
|
||||||
metadata: {
|
metadata: {
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import { IPricingModuleService, IProductModuleService } from "@medusajs/types"
|
import { IPricingModuleService, IProductModuleService } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
Modules,
|
Modules,
|
||||||
remoteQueryObjectFromString,
|
remoteQueryObjectFromString,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ medusaIntegrationTestRunner({
|
|||||||
amount: 5000,
|
amount: 5000,
|
||||||
currency_code: "eur",
|
currency_code: "eur",
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: "vip",
|
"customer.groups.id": "vip",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -75,7 +75,7 @@ medusaIntegrationTestRunner({
|
|||||||
amount: 100,
|
amount: 100,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: "vip",
|
"customer.groups.id": "vip",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -107,7 +107,9 @@ medusaIntegrationTestRunner({
|
|||||||
"variants.calculated_price": {
|
"variants.calculated_price": {
|
||||||
context: {
|
context: {
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
customer_group_id: "vip",
|
customer: {
|
||||||
|
groups: [{ id: "vip" }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import {
|
import {
|
||||||
ICustomerModuleService,
|
ICustomerModuleService,
|
||||||
IPricingModuleService,
|
IPricingModuleService,
|
||||||
@@ -5,7 +6,6 @@ import {
|
|||||||
IRegionModuleService,
|
IRegionModuleService,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { Modules, PriceListStatus, PriceListType } from "@medusajs/utils"
|
import { Modules, PriceListStatus, PriceListType } from "@medusajs/utils"
|
||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
|
||||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||||
import { createVariantPriceSet } from "../../../helpers/create-variant-price-set"
|
import { createVariantPriceSet } from "../../../helpers/create-variant-price-set"
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -116,7 +116,7 @@ medusaIntegrationTestRunner({
|
|||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
@@ -185,7 +185,7 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -206,7 +206,7 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -252,7 +252,7 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -276,7 +276,7 @@ medusaIntegrationTestRunner({
|
|||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
@@ -342,7 +342,7 @@ medusaIntegrationTestRunner({
|
|||||||
status: "active",
|
status: "active",
|
||||||
starts_at: new Date(),
|
starts_at: new Date(),
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
prices: [
|
prices: [
|
||||||
{
|
{
|
||||||
@@ -374,7 +374,7 @@ medusaIntegrationTestRunner({
|
|||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
prices: [
|
prices: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -519,7 +519,7 @@ medusaIntegrationTestRunner({
|
|||||||
title: "new price list name",
|
title: "new price list name",
|
||||||
description: "new price list description",
|
description: "new price list description",
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +536,7 @@ medusaIntegrationTestRunner({
|
|||||||
title: "new price list name",
|
title: "new price list name",
|
||||||
description: "new price list description",
|
description: "new price list description",
|
||||||
rules: {
|
rules: {
|
||||||
customer_group_id: [customerGroup.id],
|
"customer.groups.id": [customerGroup.id],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -95,9 +95,9 @@ export const PriceListConfigurationForm = ({
|
|||||||
const rules = { ...priceList.rules } // preserve other rules set on the PL
|
const rules = { ...priceList.rules } // preserve other rules set on the PL
|
||||||
|
|
||||||
if (groupIds.length) {
|
if (groupIds.length) {
|
||||||
rules["customer_group_id"] = groupIds
|
rules["customer.groups.id"] = groupIds
|
||||||
} else {
|
} else {
|
||||||
delete rules["customer_group_id"]
|
delete rules["customer.groups.id"]
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
|
|||||||
+3
-3
@@ -12,7 +12,7 @@ export const PriceListConfiguration = () => {
|
|||||||
|
|
||||||
const { price_list, isPending, isError, error } = usePriceList(id!)
|
const { price_list, isPending, isError, error } = usePriceList(id!)
|
||||||
|
|
||||||
const customerGroupIds = price_list?.rules.customer_group_id as
|
const customerGroupIds = price_list?.rules?.["customer.groups.id"] as
|
||||||
| string[]
|
| string[]
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ export const PriceListConfiguration = () => {
|
|||||||
const isCustomerGroupsReady = isPending
|
const isCustomerGroupsReady = isPending
|
||||||
? false
|
? false
|
||||||
: !!customerGroupIds?.length && isCustomerGroupsPending
|
: !!customerGroupIds?.length && isCustomerGroupsPending
|
||||||
? false
|
? false
|
||||||
: true
|
: true
|
||||||
|
|
||||||
const ready = !isPending && !!price_list && isCustomerGroupsReady
|
const ready = !isPending && !!price_list && isCustomerGroupsReady
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -82,7 +82,7 @@ export const PriceListCreateForm = ({
|
|||||||
const { rules, products } = data
|
const { rules, products } = data
|
||||||
|
|
||||||
const rulesPayload = rules?.customer_group_id?.length
|
const rulesPayload = rules?.customer_group_id?.length
|
||||||
? { customer_group_id: rules.customer_group_id.map((cg) => cg.id) }
|
? { "customer.groups.id": rules.customer_group_id.map((cg) => cg.id) }
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const prices = exctractPricesFromProducts(products, regions)
|
const prices = exctractPricesFromProducts(products, regions)
|
||||||
@@ -120,10 +120,13 @@ export const PriceListCreateForm = ({
|
|||||||
) => {
|
) => {
|
||||||
form.clearErrors(fields)
|
form.clearErrors(fields)
|
||||||
|
|
||||||
const values = fields.reduce((acc, key) => {
|
const values = fields.reduce(
|
||||||
acc[key] = form.getValues(key)
|
(acc, key) => {
|
||||||
return acc
|
acc[key] = form.getValues(key)
|
||||||
}, {} as Record<string, unknown>)
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, unknown>
|
||||||
|
)
|
||||||
|
|
||||||
const validationResult = schema.safeParse(values)
|
const validationResult = schema.safeParse(values)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
// Always ensure that cartFieldsForPricingContext is present in cartFieldsForRefreshSteps
|
||||||
export const cartFieldsForRefreshSteps = [
|
export const cartFieldsForRefreshSteps = [
|
||||||
"id",
|
"id",
|
||||||
"currency_code",
|
"currency_code",
|
||||||
"quantity",
|
"quantity",
|
||||||
"subtotal",
|
"subtotal",
|
||||||
|
"item_total",
|
||||||
"total",
|
"total",
|
||||||
"item_subtotal",
|
"item_subtotal",
|
||||||
"shipping_subtotal",
|
"shipping_subtotal",
|
||||||
"region_id",
|
"region_id",
|
||||||
"currency_code",
|
|
||||||
"metadata",
|
"metadata",
|
||||||
"completed_at",
|
"completed_at",
|
||||||
"sales_channel_id",
|
"sales_channel_id",
|
||||||
@@ -100,6 +101,22 @@ export const completeCartFields = [
|
|||||||
"items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.name",
|
"items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const cartFieldsForPricingContext = [
|
||||||
|
"id",
|
||||||
|
"sales_channel_id",
|
||||||
|
"currency_code",
|
||||||
|
"region_id",
|
||||||
|
"shipping_address.city",
|
||||||
|
"shipping_address.country_code",
|
||||||
|
"shipping_address.province",
|
||||||
|
"shipping_address.postal_code",
|
||||||
|
"item_total",
|
||||||
|
"total",
|
||||||
|
"customer.id",
|
||||||
|
"email",
|
||||||
|
"customer.groups.id",
|
||||||
|
]
|
||||||
|
|
||||||
export const productVariantsFields = [
|
export const productVariantsFields = [
|
||||||
"id",
|
"id",
|
||||||
"title",
|
"title",
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import { CartWorkflowEvents } from "@medusajs/framework/utils"
|
import { CartWorkflowEvents } from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
|
WorkflowData,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
WorkflowData,
|
|
||||||
} 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 { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
import {
|
import {
|
||||||
@@ -18,11 +19,16 @@ import {
|
|||||||
} from "../steps"
|
} from "../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"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import {
|
||||||
|
cartFieldsForPricingContext,
|
||||||
|
productVariantsFields,
|
||||||
|
} from "../utils/fields"
|
||||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||||
|
|
||||||
|
const cartFields = ["completed_at"].concat(cartFieldsForPricingContext)
|
||||||
|
|
||||||
export const addToCartWorkflowId = "add-to-cart"
|
export const addToCartWorkflowId = "add-to-cart"
|
||||||
/**
|
/**
|
||||||
* This workflow adds items to a cart.
|
* This workflow adds items to a cart.
|
||||||
@@ -30,30 +36,29 @@ export const addToCartWorkflowId = "add-to-cart"
|
|||||||
export const addToCartWorkflow = createWorkflow(
|
export const addToCartWorkflow = createWorkflow(
|
||||||
addToCartWorkflowId,
|
addToCartWorkflowId,
|
||||||
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
|
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
|
||||||
validateCartStep(input)
|
const cartQuery = useQueryGraphStep({
|
||||||
|
entity: "cart",
|
||||||
|
filters: { id: input.cart_id },
|
||||||
|
fields: cartFields,
|
||||||
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "get-cart" })
|
||||||
|
|
||||||
|
const cart = transform({ cartQuery }, ({ cartQuery }) => {
|
||||||
|
return cartQuery.data[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
validateCartStep({ cart })
|
||||||
|
|
||||||
const variantIds = transform({ input }, (data) => {
|
const variantIds = transform({ input }, (data) => {
|
||||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
|
||||||
// TODO: create a common workflow to fetch variants and its prices
|
|
||||||
const pricingContext = transform({ cart: input.cart }, (data) => {
|
|
||||||
return {
|
|
||||||
currency_code: data.cart.currency_code,
|
|
||||||
region_id: data.cart.region_id,
|
|
||||||
customer_id: data.cart.customer_id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
variables: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
calculated_price: { context: cart },
|
||||||
context: pricingContext,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
throw_if_key_not_found: true,
|
||||||
})
|
})
|
||||||
@@ -73,7 +78,7 @@ export const addToCartWorkflow = createWorkflow(
|
|||||||
variant.calculated_price.is_calculated_price_tax_inclusive,
|
variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
metadata: item?.metadata ?? {},
|
metadata: item?.metadata ?? {},
|
||||||
cartId: data.input.cart.id,
|
cartId: input.cart_id,
|
||||||
}) as CreateLineItemForCartDTO
|
}) as CreateLineItemForCartDTO
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -81,13 +86,13 @@ export const addToCartWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { itemsToCreate = [], itemsToUpdate = [] } = getLineItemActionsStep({
|
const { itemsToCreate = [], itemsToUpdate = [] } = getLineItemActionsStep({
|
||||||
id: input.cart.id,
|
id: cart.id,
|
||||||
items: lineItems,
|
items: lineItems,
|
||||||
})
|
})
|
||||||
|
|
||||||
confirmVariantInventoryWorkflow.runAsStep({
|
confirmVariantInventoryWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
sales_channel_id: input.cart.sales_channel_id as string,
|
sales_channel_id: cart.sales_channel_id,
|
||||||
variants,
|
variants,
|
||||||
items: input.items,
|
items: input.items,
|
||||||
itemsToUpdate,
|
itemsToUpdate,
|
||||||
@@ -96,22 +101,22 @@ export const addToCartWorkflow = createWorkflow(
|
|||||||
|
|
||||||
parallelize(
|
parallelize(
|
||||||
createLineItemsStep({
|
createLineItemsStep({
|
||||||
id: input.cart.id,
|
id: cart.id,
|
||||||
items: itemsToCreate,
|
items: itemsToCreate,
|
||||||
}),
|
}),
|
||||||
updateLineItemsStep({
|
updateLineItemsStep({
|
||||||
id: input.cart.id,
|
id: cart.id,
|
||||||
items: itemsToUpdate,
|
items: itemsToUpdate,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
refreshCartItemsWorkflow.runAsStep({
|
refreshCartItemsWorkflow.runAsStep({
|
||||||
input: { cart_id: input.cart.id },
|
input: { cart_id: cart.id },
|
||||||
})
|
})
|
||||||
|
|
||||||
emitEventStep({
|
emitEventStep({
|
||||||
eventName: CartWorkflowEvents.UPDATED,
|
eventName: CartWorkflowEvents.UPDATED,
|
||||||
data: { id: input.cart.id },
|
data: { id: cart.id },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { useQueryGraphStep, validatePresenceOfStep } from "../../common"
|
import { useQueryGraphStep, validatePresenceOfStep } from "../../common"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
|
import { cartFieldsForPricingContext } from "../utils/fields"
|
||||||
|
|
||||||
export const listShippingOptionsForCartWorkflowId =
|
export const listShippingOptionsForCartWorkflowId =
|
||||||
"list-shipping-options-for-cart"
|
"list-shipping-options-for-cart"
|
||||||
@@ -26,18 +27,7 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
|||||||
const cartQuery = useQueryGraphStep({
|
const cartQuery = useQueryGraphStep({
|
||||||
entity: "cart",
|
entity: "cart",
|
||||||
filters: { id: input.cart_id },
|
filters: { id: input.cart_id },
|
||||||
fields: [
|
fields: cartFieldsForPricingContext,
|
||||||
"id",
|
|
||||||
"sales_channel_id",
|
|
||||||
"currency_code",
|
|
||||||
"region_id",
|
|
||||||
"shipping_address.city",
|
|
||||||
"shipping_address.country_code",
|
|
||||||
"shipping_address.province",
|
|
||||||
"shipping_address.postal_code",
|
|
||||||
"item_total",
|
|
||||||
"total",
|
|
||||||
],
|
|
||||||
options: { throwIfKeyNotFound: true },
|
options: { throwIfKeyNotFound: true },
|
||||||
}).config({ name: "get-cart" })
|
}).config({ name: "get-cart" })
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { isDefined, PromotionActions } from "@medusajs/framework/utils"
|
import {
|
||||||
|
filterObjectByKeys,
|
||||||
|
isDefined,
|
||||||
|
PromotionActions,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
@@ -9,6 +13,7 @@ import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
|||||||
import { updateLineItemsStep } from "../steps"
|
import { updateLineItemsStep } from "../steps"
|
||||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||||
import {
|
import {
|
||||||
|
cartFieldsForPricingContext,
|
||||||
cartFieldsForRefreshSteps,
|
cartFieldsForRefreshSteps,
|
||||||
productVariantsFields,
|
productVariantsFields,
|
||||||
} from "../utils/fields"
|
} from "../utils/fields"
|
||||||
@@ -41,16 +46,9 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
|||||||
return (data.cart.items ?? []).map((i) => i.variant_id)
|
return (data.cart.items ?? []).map((i) => i.variant_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const pricingContext = transform(
|
const cartPricingContext = transform({ cart }, ({ cart }) => {
|
||||||
{ cart },
|
return filterObjectByKeys(cart, cartFieldsForPricingContext)
|
||||||
({ cart: { currency_code, region_id, customer_id } }) => {
|
})
|
||||||
return {
|
|
||||||
currency_code,
|
|
||||||
region_id,
|
|
||||||
customer_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
@@ -58,7 +56,7 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
|||||||
variables: {
|
variables: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
calculated_price: {
|
||||||
context: pricingContext,
|
context: cartPricingContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
throw_if_key_not_found: true,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { useQueryGraphStep } from "../../common"
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { updateCartsStep } from "../steps"
|
import { updateCartsStep } from "../steps"
|
||||||
|
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||||
|
|
||||||
export const transferCartCustomerWorkflowId = "transfer-cart-customer"
|
export const transferCartCustomerWorkflowId = "transfer-cart-customer"
|
||||||
/**
|
/**
|
||||||
@@ -65,6 +66,10 @@ export const transferCartCustomerWorkflow = createWorkflow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
updateCartsStep(cartInput)
|
updateCartsStep(cartInput)
|
||||||
|
|
||||||
|
refreshCartItemsWorkflow.runAsStep({
|
||||||
|
input: { cart_id: input.id },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,20 @@ import {
|
|||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
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"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import {
|
||||||
|
cartFieldsForPricingContext,
|
||||||
|
productVariantsFields,
|
||||||
|
} from "../utils/fields"
|
||||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||||
|
|
||||||
|
const cartFields = cartFieldsForPricingContext.concat(["items.*"])
|
||||||
|
|
||||||
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
||||||
/**
|
/**
|
||||||
* This workflow updates a cart's line item.
|
* This workflow updates a cart's line item.
|
||||||
@@ -19,19 +25,22 @@ export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
|||||||
export const updateLineItemInCartWorkflow = createWorkflow(
|
export const updateLineItemInCartWorkflow = createWorkflow(
|
||||||
updateLineItemInCartWorkflowId,
|
updateLineItemInCartWorkflowId,
|
||||||
(input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO>) => {
|
(input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO>) => {
|
||||||
validateCartStep(input)
|
const cartQuery = useQueryGraphStep({
|
||||||
|
entity: "cart",
|
||||||
|
filters: { id: input.cart_id },
|
||||||
|
fields: cartFields,
|
||||||
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "get-cart" })
|
||||||
|
|
||||||
const variantIds = transform({ input }, (data) => {
|
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
|
||||||
return [data.input.item.variant_id]
|
const item = transform({ cart, input }, ({ cart, input }) => {
|
||||||
|
return cart.items.find((i) => i.id === input.item_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
validateCartStep({ cart })
|
||||||
const pricingContext = transform({ cart: input.cart }, (data) => {
|
|
||||||
return {
|
const variantIds = transform({ item }, ({ item }) => {
|
||||||
currency_code: data.cart.currency_code,
|
return [item.variant_id]
|
||||||
region_id: data.cart.region_id,
|
|
||||||
customer_id: data.cart.customer_id,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = useRemoteQueryStep({
|
||||||
@@ -40,7 +49,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
variables: {
|
variables: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: {
|
calculated_price: {
|
||||||
context: pricingContext,
|
context: cart,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
throw_if_key_not_found: true,
|
||||||
@@ -48,13 +57,13 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
const items = transform({ input }, (data) => {
|
const items = transform({ item }, ({ item }) => {
|
||||||
return [data.input.item]
|
return [item]
|
||||||
})
|
})
|
||||||
|
|
||||||
confirmVariantInventoryWorkflow.runAsStep({
|
confirmVariantInventoryWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
sales_channel_id: input.cart.sales_channel_id as string,
|
sales_channel_id: cart.sales_channel_id,
|
||||||
variants,
|
variants,
|
||||||
items,
|
items,
|
||||||
},
|
},
|
||||||
@@ -62,7 +71,6 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
|
|
||||||
const lineItemUpdate = transform({ input, variants }, (data) => {
|
const lineItemUpdate = transform({ input, variants }, (data) => {
|
||||||
const variant = data.variants[0]
|
const variant = data.variants[0]
|
||||||
const item = data.input.item
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -72,7 +80,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
!!variant.calculated_price.is_calculated_price_tax_inclusive,
|
!!variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||||
},
|
},
|
||||||
selector: {
|
selector: {
|
||||||
id: item.id,
|
id: data.input.item_id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -80,7 +88,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
updateLineItemsStepWithSelector(lineItemUpdate)
|
updateLineItemsStepWithSelector(lineItemUpdate)
|
||||||
|
|
||||||
refreshCartItemsWorkflow.runAsStep({
|
refreshCartItemsWorkflow.runAsStep({
|
||||||
input: { cart_id: input.cart.id },
|
input: { cart_id: input.cart_id },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -4,7 +4,7 @@ import { PaymentCollectionDTO } from "../payment"
|
|||||||
import { ProductDTO } from "../product"
|
import { ProductDTO } from "../product"
|
||||||
import { RegionDTO } from "../region"
|
import { RegionDTO } from "../region"
|
||||||
import { BigNumberInput } from "../totals"
|
import { BigNumberInput } from "../totals"
|
||||||
import { CartDTO, CartLineItemDTO } from "./common"
|
import { CartDTO } from "./common"
|
||||||
import {
|
import {
|
||||||
CreateAddressDTO,
|
CreateAddressDTO,
|
||||||
UpdateAddressDTO,
|
UpdateAddressDTO,
|
||||||
@@ -44,8 +44,8 @@ export interface CreateCartCreateLineItemDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateLineItemInCartWorkflowInputDTO {
|
export interface UpdateLineItemInCartWorkflowInputDTO {
|
||||||
cart: CartDTO
|
cart_id: string
|
||||||
item: CartLineItemDTO
|
item_id: string
|
||||||
update: Partial<UpdateLineItemDTO>
|
update: Partial<UpdateLineItemDTO>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +80,8 @@ export interface CreateCartWorkflowInputDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AddToCartWorkflowInputDTO {
|
export interface AddToCartWorkflowInputDTO {
|
||||||
|
cart_id: string
|
||||||
items: CreateCartCreateLineItemDTO[]
|
items: CreateCartCreateLineItemDTO[]
|
||||||
cart: CartWorkflowDTO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateCartWorkflowInputDTO {
|
export interface UpdateCartWorkflowInputDTO {
|
||||||
|
|||||||
@@ -2,5 +2,9 @@ export type MedusaPricingContext = {
|
|||||||
region_id?: string
|
region_id?: string
|
||||||
currency_code?: string
|
currency_code?: string
|
||||||
customer_id?: string
|
customer_id?: string
|
||||||
customer_group_id?: string[]
|
customer?: {
|
||||||
|
groups?: {
|
||||||
|
id: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { filterObjectByKeys } from "../filter-object-by-keys"
|
||||||
|
|
||||||
|
describe("filterObjectByKeys", function () {
|
||||||
|
it("should return an object with only the filtered keys", function () {
|
||||||
|
const cart = {
|
||||||
|
id: "cart_id",
|
||||||
|
customer: {
|
||||||
|
id: "cus_id",
|
||||||
|
groups: [
|
||||||
|
{ id: "group_1", name: "test" },
|
||||||
|
{ id: "group_2", name: "test 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
product_id: "product-1",
|
||||||
|
product: { id: "product-1" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product_id: "product-2",
|
||||||
|
product: { id: "product-2" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
shipping_method: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
let transformedObject = filterObjectByKeys(cart, [
|
||||||
|
"id",
|
||||||
|
"customer.id",
|
||||||
|
"customer.groups.id",
|
||||||
|
"customer.groups.name",
|
||||||
|
"items.product",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(transformedObject).toEqual({
|
||||||
|
id: "cart_id",
|
||||||
|
customer: {
|
||||||
|
id: "cus_id",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: "group_1",
|
||||||
|
name: "test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "group_2",
|
||||||
|
name: "test 2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
product: {
|
||||||
|
id: "product-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product: {
|
||||||
|
id: "product-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
transformedObject = filterObjectByKeys(cart, [
|
||||||
|
"id",
|
||||||
|
"customer.id",
|
||||||
|
"customer.groups.id",
|
||||||
|
"customer.groups.name",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(transformedObject).toEqual({
|
||||||
|
id: "cart_id",
|
||||||
|
customer: {
|
||||||
|
id: "cus_id",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: "group_1",
|
||||||
|
name: "test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "group_2",
|
||||||
|
name: "test 2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
transformedObject = filterObjectByKeys(cart, [
|
||||||
|
"id",
|
||||||
|
"customer.id",
|
||||||
|
"customer.groups.id",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(transformedObject).toEqual({
|
||||||
|
id: "cart_id",
|
||||||
|
customer: {
|
||||||
|
id: "cus_id",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
id: "group_1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "group_2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
transformedObject = filterObjectByKeys(cart, ["id", "customer.id"])
|
||||||
|
|
||||||
|
expect(transformedObject).toEqual({
|
||||||
|
id: "cart_id",
|
||||||
|
customer: {
|
||||||
|
id: "cus_id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
transformedObject = filterObjectByKeys(cart, ["id"])
|
||||||
|
|
||||||
|
expect(transformedObject).toEqual({
|
||||||
|
id: "cart_id",
|
||||||
|
})
|
||||||
|
|
||||||
|
transformedObject = filterObjectByKeys(cart, [])
|
||||||
|
|
||||||
|
expect(transformedObject).toEqual({})
|
||||||
|
|
||||||
|
transformedObject = filterObjectByKeys(cart, [
|
||||||
|
"doesnotexist.doesnotexist",
|
||||||
|
"shipping_method.city",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(transformedObject).toEqual({
|
||||||
|
shipping_method: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { flattenObjectToKeyValuePairs } from "../flatten-object-to-key-value-pairs"
|
||||||
|
|
||||||
|
describe("flattenObjectToKeyValuePairs", function () {
|
||||||
|
it("should return only the properties path of the properties that are set to true", function () {
|
||||||
|
const cart = {
|
||||||
|
id: "cart_id",
|
||||||
|
customer: {
|
||||||
|
id: "cus_id",
|
||||||
|
groups: [
|
||||||
|
{ id: "group_1", name: "test" },
|
||||||
|
{ id: "group_2", name: "test 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
product_id: "product-1",
|
||||||
|
product: { id: "product-1" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product_id: "product-2",
|
||||||
|
product: { id: "product-2" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyValueParis = flattenObjectToKeyValuePairs(cart)
|
||||||
|
expect(keyValueParis).toEqual({
|
||||||
|
id: "cart_id",
|
||||||
|
"customer.id": "cus_id",
|
||||||
|
"customer.groups.id": ["group_1", "group_2"],
|
||||||
|
"customer.groups.name": ["test", "test 2"],
|
||||||
|
"items.product_id": ["product-1", "product-2"],
|
||||||
|
"items.product.id": ["product-1", "product-2"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { isDefined } from "./is-defined"
|
||||||
|
|
||||||
|
export function filterObjectByKeys(obj, paths) {
|
||||||
|
function buildObject(paths) {
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
paths.forEach((path) => {
|
||||||
|
const parts = path.split(".")
|
||||||
|
|
||||||
|
// Handle top-level properties
|
||||||
|
if (parts.length === 1) {
|
||||||
|
const [part] = parts
|
||||||
|
if (obj[part] !== undefined) {
|
||||||
|
result[part] = obj[part]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = result
|
||||||
|
let source = obj
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i]
|
||||||
|
const isLast = i === parts.length - 1
|
||||||
|
|
||||||
|
if (!isDefined(current) || source === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Initialize the current path if it doesn't exist
|
||||||
|
if (!current[part]) {
|
||||||
|
if (Array.isArray(source[part])) {
|
||||||
|
current[part] = source[part].map(() => ({}))
|
||||||
|
} else if (source[part] === null) {
|
||||||
|
current[part] = null
|
||||||
|
} else if (isDefined(source[part])) {
|
||||||
|
current[part] = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(source[part])) {
|
||||||
|
// Get the array path base (e.g., "customer.groups")
|
||||||
|
const arrayPath = parts.slice(0, i + 1).join(".")
|
||||||
|
// Find all paths that start with this array path
|
||||||
|
const relevantPaths = paths
|
||||||
|
.filter((p) => p.startsWith(arrayPath + "."))
|
||||||
|
.map((p) => p.slice(arrayPath.length + 1)) // Remove the array path prefix
|
||||||
|
|
||||||
|
// Update array items with all relevant properties
|
||||||
|
current[part] = source[part].map((item, idx) => {
|
||||||
|
const existingItem = current[part][idx] || {}
|
||||||
|
relevantPaths.forEach((subPath) => {
|
||||||
|
const value = subPath
|
||||||
|
.split(".")
|
||||||
|
.reduce((obj, key) => obj?.[key], item)
|
||||||
|
if (value !== undefined) {
|
||||||
|
let tempObj = existingItem
|
||||||
|
const keys = subPath.split(".")
|
||||||
|
|
||||||
|
keys.slice(0, -1).forEach((key) => {
|
||||||
|
tempObj[key] = tempObj[key] || {}
|
||||||
|
tempObj = tempObj[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
tempObj[keys[keys.length - 1]] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return existingItem
|
||||||
|
})
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
if (isLast) {
|
||||||
|
current[part] = source[part]
|
||||||
|
} else {
|
||||||
|
current = current[part]
|
||||||
|
source = source[part]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildObject(paths)
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
type NestedObject = {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenObjectToKeyValuePairs(obj: NestedObject): NestedObject {
|
||||||
|
const result: NestedObject = {}
|
||||||
|
|
||||||
|
// Find all paths that contain arrays of objects
|
||||||
|
function findArrayPaths(
|
||||||
|
obj: unknown,
|
||||||
|
currentPath: string[] = []
|
||||||
|
): string[][] {
|
||||||
|
const paths: string[][] = []
|
||||||
|
|
||||||
|
if (!obj || typeof obj !== "object") {
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an array of objects, add this path
|
||||||
|
if (Array.isArray(obj) && obj.length > 0 && typeof obj[0] === "object") {
|
||||||
|
paths.push(currentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all properties
|
||||||
|
if (typeof obj === "object") {
|
||||||
|
Object.entries(obj as Record<string, unknown>).forEach(([key, value]) => {
|
||||||
|
const newPath = [...currentPath, key]
|
||||||
|
paths.push(...findArrayPaths(value, newPath))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract array values at a specific path
|
||||||
|
function getArrayValues(obj: unknown, path: string[]): unknown[] {
|
||||||
|
const arrayObj = path.reduce((acc: unknown, key: string) => {
|
||||||
|
if (acc && typeof acc === "object") {
|
||||||
|
return (acc as Record<string, unknown>)[key]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}, obj)
|
||||||
|
|
||||||
|
if (!Array.isArray(arrayObj)) return []
|
||||||
|
|
||||||
|
return arrayObj
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process non-array paths
|
||||||
|
function processRegularPaths(obj: unknown, prefix = ""): void {
|
||||||
|
if (!obj || typeof obj !== "object") {
|
||||||
|
result[prefix] = obj
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) return
|
||||||
|
|
||||||
|
Object.entries(obj as Record<string, unknown>).forEach(([key, value]) => {
|
||||||
|
const newPrefix = prefix ? `${prefix}.${key}` : key
|
||||||
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
processRegularPaths(value, newPrefix)
|
||||||
|
} else if (!Array.isArray(value)) {
|
||||||
|
result[newPrefix] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the object
|
||||||
|
processRegularPaths(obj)
|
||||||
|
|
||||||
|
// Find and process array paths
|
||||||
|
const arrayPaths = findArrayPaths(obj)
|
||||||
|
arrayPaths.forEach((path) => {
|
||||||
|
const pathStr = path.join(".")
|
||||||
|
const arrayObjects = getArrayValues(obj, path)
|
||||||
|
|
||||||
|
if (Array.isArray(arrayObjects) && arrayObjects.length > 0) {
|
||||||
|
// Get all possible keys from the array objects
|
||||||
|
const keys = new Set<string>()
|
||||||
|
arrayObjects.forEach((item) => {
|
||||||
|
if (item && typeof item === "object") {
|
||||||
|
Object.keys(item as object).forEach((k) => keys.add(k))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process each key
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const values = arrayObjects
|
||||||
|
.map((item) => {
|
||||||
|
if (item && typeof item === "object") {
|
||||||
|
return (item as Record<string, unknown>)[key]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
.filter((v) => v !== undefined)
|
||||||
|
|
||||||
|
if (values.length > 0) {
|
||||||
|
const newPath = `${pathStr}.${key}`
|
||||||
|
if (values.every((v) => typeof v === "object" && !Array.isArray(v))) {
|
||||||
|
// If these are all objects, recursively process them
|
||||||
|
const subObj = { [key]: values }
|
||||||
|
const subResult = flattenObjectToKeyValuePairs(subObj)
|
||||||
|
Object.entries(subResult).forEach(([k, v]) => {
|
||||||
|
const finalPath = `${pathStr}.${k}`
|
||||||
|
result[finalPath] = v
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result[newPath] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -18,7 +18,9 @@ export * from "./dynamic-import"
|
|||||||
export * from "./env-editor"
|
export * from "./env-editor"
|
||||||
export * from "./errors"
|
export * from "./errors"
|
||||||
export * from "./file-system"
|
export * from "./file-system"
|
||||||
|
export * from "./filter-object-by-keys"
|
||||||
export * from "./filter-operator-map"
|
export * from "./filter-operator-map"
|
||||||
|
export * from "./flatten-object-to-key-value-pairs"
|
||||||
export * from "./generate-entity-id"
|
export * from "./generate-entity-id"
|
||||||
export * from "./get-caller-file-path"
|
export * from "./get-caller-file-path"
|
||||||
export * from "./get-config-file"
|
export * from "./get-config-file"
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import {
|
|||||||
deleteLineItemsWorkflow,
|
deleteLineItemsWorkflow,
|
||||||
updateLineItemInCartWorkflow,
|
updateLineItemInCartWorkflow,
|
||||||
} from "@medusajs/core-flows"
|
} from "@medusajs/core-flows"
|
||||||
|
import { prepareListQuery } from "@medusajs/framework"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||||
import { HttpTypes } from "@medusajs/framework/types"
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
import { MedusaError } from "@medusajs/framework/utils"
|
import { MedusaError } from "@medusajs/framework/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
|
||||||
import { prepareListQuery } from "@medusajs/framework"
|
|
||||||
import { refetchCart } from "../../../helpers"
|
import { refetchCart } from "../../../helpers"
|
||||||
import { StoreUpdateCartLineItemType } from "../../../validators"
|
import { StoreUpdateCartLineItemType } from "../../../validators"
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export const POST = async (
|
|||||||
req: MedusaRequest<StoreUpdateCartLineItemType>,
|
req: MedusaRequest<StoreUpdateCartLineItemType>,
|
||||||
res: MedusaResponse<HttpTypes.StoreCartResponse>
|
res: MedusaResponse<HttpTypes.StoreCartResponse>
|
||||||
) => {
|
) => {
|
||||||
|
// TODO: Move this to the workflow when the query to line item is fixed
|
||||||
const cart = await refetchCart(
|
const cart = await refetchCart(
|
||||||
req.params.id,
|
req.params.id,
|
||||||
req.scope,
|
req.scope,
|
||||||
@@ -39,14 +40,12 @@ export const POST = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = {
|
|
||||||
cart,
|
|
||||||
item,
|
|
||||||
update: req.validatedBody,
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateLineItemInCartWorkflow(req.scope).run({
|
await updateLineItemInCartWorkflow(req.scope).run({
|
||||||
input,
|
input: {
|
||||||
|
cart_id: req.params.id,
|
||||||
|
item_id: item.id,
|
||||||
|
update: req.validatedBody,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const updatedCart = await refetchCart(
|
const updatedCart = await refetchCart(
|
||||||
|
|||||||
@@ -1,33 +1,25 @@
|
|||||||
import { addToCartWorkflow } from "@medusajs/core-flows"
|
import { addToCartWorkflow } from "@medusajs/core-flows"
|
||||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||||
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
import { refetchCart } from "../../helpers"
|
import { refetchCart } from "../../helpers"
|
||||||
import { StoreAddCartLineItemType } from "../../validators"
|
import { StoreAddCartLineItemType } from "../../validators"
|
||||||
import { HttpTypes } from "@medusajs/framework/types"
|
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: MedusaRequest<StoreAddCartLineItemType>,
|
req: MedusaRequest<StoreAddCartLineItemType>,
|
||||||
res: MedusaResponse<HttpTypes.StoreCartResponse>
|
res: MedusaResponse<HttpTypes.StoreCartResponse>
|
||||||
) => {
|
) => {
|
||||||
|
await addToCartWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
cart_id: req.params.id,
|
||||||
|
items: [req.validatedBody],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const cart = await refetchCart(
|
const cart = await refetchCart(
|
||||||
req.params.id,
|
req.params.id,
|
||||||
req.scope,
|
req.scope,
|
||||||
req.remoteQueryConfig.fields
|
req.remoteQueryConfig.fields
|
||||||
)
|
)
|
||||||
|
|
||||||
const workflowInput = {
|
res.status(200).json({ cart })
|
||||||
items: [req.validatedBody],
|
|
||||||
cart,
|
|
||||||
}
|
|
||||||
|
|
||||||
await addToCartWorkflow(req.scope).run({
|
|
||||||
input: workflowInput,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const updatedCart = await refetchCart(
|
|
||||||
req.params.id,
|
|
||||||
req.scope,
|
|
||||||
req.remoteQueryConfig.fields
|
|
||||||
)
|
|
||||||
|
|
||||||
res.status(200).json({ cart: updatedCart })
|
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,10 @@ export function setPricingContext() {
|
|||||||
["id"]
|
["id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
pricingContext.customer_group_id = customerGroups.map((cg) => cg.id)
|
pricingContext.customer = { groups: [] }
|
||||||
|
customerGroups.map((cg) =>
|
||||||
|
pricingContext.customer?.groups?.push({ id: cg.id })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.pricingContext = pricingContext
|
req.pricingContext = pricingContext
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CartModuleService } from "./services"
|
|
||||||
import { Module, Modules } from "@medusajs/framework/utils"
|
import { Module, Modules } from "@medusajs/framework/utils"
|
||||||
|
import { CartModuleService } from "./services"
|
||||||
|
|
||||||
export default Module(Modules.CART, {
|
export default Module(Modules.CART, {
|
||||||
service: CartModuleService,
|
service: CartModuleService,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20241212190401 extends Migration {
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`UPDATE price_list_rule SET attribute = 'customer.groups.id' WHERE attribute = 'customer_group_id';`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
flattenObjectToKeyValuePairs,
|
||||||
|
isPresent,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
MikroOrmBase,
|
MikroOrmBase,
|
||||||
PriceListStatus,
|
PriceListStatus,
|
||||||
@@ -58,6 +60,14 @@ export class PricingRepository
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flattenedContext = Object.entries(
|
||||||
|
flattenObjectToKeyValuePairs(context)
|
||||||
|
).filter(
|
||||||
|
([key, value]) =>
|
||||||
|
(isPresent(value) && !Array.isArray(value)) ||
|
||||||
|
(Array.isArray(value) && value.flat(1).length)
|
||||||
|
)
|
||||||
|
|
||||||
// Gets all the prices where rules match for each of the contexts
|
// Gets all the prices where rules match for each of the contexts
|
||||||
// that the price set is configured for
|
// that the price set is configured for
|
||||||
const priceSubQueryKnex = knex({
|
const priceSubQueryKnex = knex({
|
||||||
@@ -135,19 +145,20 @@ export class PricingRepository
|
|||||||
priceBuilder
|
priceBuilder
|
||||||
.whereNull("price.price_list_id")
|
.whereNull("price.price_list_id")
|
||||||
.andWhere((withoutPriceListBuilder) => {
|
.andWhere((withoutPriceListBuilder) => {
|
||||||
for (const [key, value] of Object.entries(context)) {
|
for (const [key, value] of flattenedContext) {
|
||||||
withoutPriceListBuilder.orWhere((orBuilder) => {
|
withoutPriceListBuilder.orWhere((orBuilder) => {
|
||||||
orBuilder.where("pr.attribute", key)
|
orBuilder.where("pr.attribute", key)
|
||||||
|
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
orBuilder.where((operatorGroupBuilder) => {
|
buildOperatorQueries(orBuilder, value)
|
||||||
buildOperatorQueries(operatorGroupBuilder, value)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
orBuilder.where({ "pr.value": value })
|
const normalizeValue = Array.isArray(value) ? value : [value]
|
||||||
|
|
||||||
|
orBuilder.whereIn("pr.value", normalizeValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
withoutPriceListBuilder.orWhere("price.rules_count", "=", 0)
|
withoutPriceListBuilder.orWhere("price.rules_count", "=", 0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -171,7 +182,7 @@ export class PricingRepository
|
|||||||
})
|
})
|
||||||
.andWhere(function () {
|
.andWhere(function () {
|
||||||
this.andWhere(function () {
|
this.andWhere(function () {
|
||||||
for (const [key, value] of Object.entries(context)) {
|
for (const [key, value] of flattenedContext) {
|
||||||
this.orWhere({ "plr.attribute": key })
|
this.orWhere({ "plr.attribute": key })
|
||||||
this.where(
|
this.where(
|
||||||
"plr.value",
|
"plr.value",
|
||||||
@@ -185,14 +196,18 @@ export class PricingRepository
|
|||||||
|
|
||||||
this.andWhere(function () {
|
this.andWhere(function () {
|
||||||
this.andWhere((contextBuilder) => {
|
this.andWhere((contextBuilder) => {
|
||||||
for (const [key, value] of Object.entries(context)) {
|
for (const [key, value] of flattenedContext) {
|
||||||
contextBuilder.orWhere((orBuilder) => {
|
contextBuilder.orWhere((orBuilder) => {
|
||||||
orBuilder.where("pr.attribute", key)
|
orBuilder.where("pr.attribute", key)
|
||||||
|
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
buildOperatorQueries(orBuilder, value)
|
buildOperatorQueries(orBuilder, value)
|
||||||
} else {
|
} else {
|
||||||
orBuilder.where({ "pr.value": value })
|
const normalizeValue = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: [value]
|
||||||
|
|
||||||
|
orBuilder.whereIn("pr.value", normalizeValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user