From 10a53ccaee2d8bed440fbaf7edcb94b9c9cba01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Campam=C3=A1?= Date: Sat, 7 Dec 2024 23:49:56 -0300 Subject: [PATCH] fix(carts): Fixes cart modifications not accounting for certain price lists *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. --- .../cart/store/cart.workflows.spec.ts | 300 ++++++++++++++++++ .../src/cart/workflows/add-to-cart.ts | 15 +- .../workflows/update-line-item-in-cart.ts | 6 +- .../src/common/steps/fetch-customer-groups.ts | 21 ++ 4 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 packages/core/core-flows/src/common/steps/fetch-customer-groups.ts diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index f9a3ebb560e4b..e8b835b703c76 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -29,6 +29,8 @@ import { import { ContainerRegistrationKeys, Modules, + PriceListStatus, + PriceListType, RuleOperator, } from "@medusajs/utils" import { @@ -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_group_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, + }, + }) + + 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", () => { + 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_group_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, + 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, + item, + update: { + metadata: { + foo: "bar", + }, + quantity: 2, + }, + }, + throwOnError: false, + }) + + const updatedItem = await cartModuleService.retrieveLineItem(item.id) + + expect(updatedItem).toEqual( + expect.objectContaining({ + id: item.id, + unit_price: 1500, + quantity: 2, + title: "Test item", + }) + ) + }) + describe("compensation", () => { it("should revert line item update to original state", async () => { expect.assertions(2) diff --git a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts index 7050c005adc00..c8b10c96cd3c6 100644 --- a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts @@ -4,12 +4,13 @@ import { } from "@medusajs/framework/types" import { CartWorkflowEvents } from "@medusajs/framework/utils" import { + WorkflowData, createWorkflow, parallelize, - transform, - WorkflowData, + transform } from "@medusajs/framework/workflows-sdk" import { emitEventStep } from "../../common/steps/emit-event" +import { fetchCustomerGroupsStep } from "../../common/steps/fetch-customer-groups" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { createLineItemsStep, @@ -23,6 +24,7 @@ import { prepareLineItemData } from "../utils/prepare-line-item-data" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" import { refreshCartItemsWorkflow } from "./refresh-cart-items" + export const addToCartWorkflowId = "add-to-cart" /** * This workflow adds items to a cart. @@ -36,14 +38,19 @@ export const addToCartWorkflow = createWorkflow( return (data.input.items ?? []).map((i) => i.variant_id) }) + const { customer_group_ids } = fetchCustomerGroupsStep(input.cart.customer_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 { + const pricingContext = transform({ cart: input.cart, customer_group_ids }, (data) => { + const pricingContext = { currency_code: data.cart.currency_code, region_id: data.cart.region_id, customer_id: data.cart.customer_id, + customer_group_id: data.customer_group_ids, } + console.log(pricingContext) + return pricingContext }) const variants = useRemoteQueryStep({ diff --git a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts index 528c4b01a4376..96537285e6cb5 100644 --- a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts @@ -4,6 +4,7 @@ import { createWorkflow, transform, } from "@medusajs/framework/workflows-sdk" +import { fetchCustomerGroupsStep } from "../../common/steps/fetch-customer-groups" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { updateLineItemsStepWithSelector } from "../../line-item/steps" import { validateCartStep } from "../steps/validate-cart" @@ -25,12 +26,15 @@ export const updateLineItemInCartWorkflow = createWorkflow( return [data.input.item.variant_id] }) + const { customer_group_ids } = fetchCustomerGroupsStep(input.cart.customer_id) + // TODO: This is on par with the context used in v1.*, but we can be more flexible. - const pricingContext = transform({ cart: input.cart }, (data) => { + const pricingContext = transform({ cart: input.cart, customer_group_ids }, (data) => { return { currency_code: data.cart.currency_code, region_id: data.cart.region_id, customer_id: data.cart.customer_id, + customer_group_id: data.customer_group_ids, } }) diff --git a/packages/core/core-flows/src/common/steps/fetch-customer-groups.ts b/packages/core/core-flows/src/common/steps/fetch-customer-groups.ts new file mode 100644 index 0000000000000..32705a0bdc303 --- /dev/null +++ b/packages/core/core-flows/src/common/steps/fetch-customer-groups.ts @@ -0,0 +1,21 @@ +import { refetchEntities } from "@medusajs/framework/http" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" + +export const fetchCustomerGroupsStepId = "fetch-customer-groupsid" +export const fetchCustomerGroupsStep = createStep( + fetchCustomerGroupsStepId, + async (data: string | undefined, {container}) => { + if (!!data) { + const customerGroups = await refetchEntities( + "customer_group", + { customers: { id: data } }, + container, + ["id"] + ) + + return new StepResponse({customer_group_ids: customerGroups.map((cg: any) => cg.id)}) + } else { + return new StepResponse({customer_group_ids: []}) + } + } +)