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:
Riqwan Thamir
2024-12-17 11:10:30 +01:00
committed by GitHub
co-authored by Sergio Campamá
parent 0c49470066
commit 6367bccde8
29 changed files with 1090 additions and 166 deletions
@@ -29,6 +29,8 @@ import {
import {
ContainerRegistrationKeys,
Modules,
PriceListStatus,
PriceListType,
RuleOperator,
} from "@medusajs/utils"
import {
@@ -786,7 +788,7 @@ medusaIntegrationTestRunner({
quantity: 1,
},
],
cart,
cart_id: cart.id,
},
})
@@ -875,7 +877,7 @@ medusaIntegrationTestRunner({
quantity: 1,
},
],
cart,
cart_id: cart.id,
},
throwOnError: false,
})
@@ -906,7 +908,7 @@ medusaIntegrationTestRunner({
quantity: 1,
},
],
cart,
cart_id: cart.id,
},
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", () => {
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", () => {
it("should revert line item update to original state", async () => {
expect.assertions(2)
@@ -1024,8 +1324,8 @@ medusaIntegrationTestRunner({
const { errors } = await workflow.run({
input: {
cart,
item,
cart_id: cart.id,
item_id: item.id,
update: {
metadata: {
foo: "bar",
@@ -1,10 +1,10 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { IPricingModuleService, IProductModuleService } from "@medusajs/types"
import {
ContainerRegistrationKeys,
Modules,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
jest.setTimeout(50000)
@@ -56,7 +56,7 @@ medusaIntegrationTestRunner({
amount: 5000,
currency_code: "eur",
rules: {
customer_group_id: "vip",
"customer.groups.id": "vip",
},
},
],
@@ -75,7 +75,7 @@ medusaIntegrationTestRunner({
amount: 100,
currency_code: "usd",
rules: {
customer_group_id: "vip",
"customer.groups.id": "vip",
},
},
],
@@ -107,7 +107,9 @@ medusaIntegrationTestRunner({
"variants.calculated_price": {
context: {
currency_code: "usd",
customer_group_id: "vip",
customer: {
groups: [{ id: "vip" }],
},
},
},
},
@@ -1,3 +1,4 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
ICustomerModuleService,
IPricingModuleService,
@@ -5,7 +6,6 @@ import {
IRegionModuleService,
} from "@medusajs/types"
import { Modules, PriceListStatus, PriceListType } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import { createVariantPriceSet } from "../../../helpers/create-variant-price-set"
@@ -94,7 +94,7 @@ medusaIntegrationTestRunner({
},
],
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
},
])
@@ -116,7 +116,7 @@ medusaIntegrationTestRunner({
updated_at: expect.any(String),
deleted_at: null,
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
prices: [
{
@@ -185,7 +185,7 @@ medusaIntegrationTestRunner({
},
],
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
},
{
@@ -206,7 +206,7 @@ medusaIntegrationTestRunner({
},
],
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
},
])
@@ -252,7 +252,7 @@ medusaIntegrationTestRunner({
},
],
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
},
])
@@ -276,7 +276,7 @@ medusaIntegrationTestRunner({
updated_at: expect.any(String),
deleted_at: null,
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
prices: [
{
@@ -342,7 +342,7 @@ medusaIntegrationTestRunner({
status: "active",
starts_at: new Date(),
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
prices: [
{
@@ -374,7 +374,7 @@ medusaIntegrationTestRunner({
updated_at: expect.any(String),
deleted_at: null,
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
prices: [
expect.objectContaining({
@@ -519,7 +519,7 @@ medusaIntegrationTestRunner({
title: "new price list name",
description: "new price list description",
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
}
@@ -536,7 +536,7 @@ medusaIntegrationTestRunner({
title: "new price list name",
description: "new price list description",
rules: {
customer_group_id: [customerGroup.id],
"customer.groups.id": [customerGroup.id],
},
})
)