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
@@ -1,13 +1,14 @@
// Always ensure that cartFieldsForPricingContext is present in cartFieldsForRefreshSteps
export const cartFieldsForRefreshSteps = [
"id",
"currency_code",
"quantity",
"subtotal",
"item_total",
"total",
"item_subtotal",
"shipping_subtotal",
"region_id",
"currency_code",
"metadata",
"completed_at",
"sales_channel_id",
@@ -100,6 +101,22 @@ export const completeCartFields = [
"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 = [
"id",
"title",
@@ -4,11 +4,12 @@ import {
} from "@medusajs/framework/types"
import { CartWorkflowEvents } from "@medusajs/framework/utils"
import {
WorkflowData,
createWorkflow,
parallelize,
transform,
WorkflowData,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common"
import { emitEventStep } from "../../common/steps/emit-event"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import {
@@ -18,11 +19,16 @@ import {
} from "../steps"
import { validateCartStep } from "../steps/validate-cart"
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 { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
const cartFields = ["completed_at"].concat(cartFieldsForPricingContext)
export const addToCartWorkflowId = "add-to-cart"
/**
* This workflow adds items to a cart.
@@ -30,30 +36,29 @@ export const addToCartWorkflowId = "add-to-cart"
export const addToCartWorkflow = createWorkflow(
addToCartWorkflowId,
(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) => {
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({
entry_point: "variants",
fields: productVariantsFields,
variables: {
id: variantIds,
calculated_price: {
context: pricingContext,
},
calculated_price: { context: cart },
},
throw_if_key_not_found: true,
})
@@ -73,7 +78,7 @@ export const addToCartWorkflow = createWorkflow(
variant.calculated_price.is_calculated_price_tax_inclusive,
quantity: item.quantity,
metadata: item?.metadata ?? {},
cartId: data.input.cart.id,
cartId: input.cart_id,
}) as CreateLineItemForCartDTO
})
@@ -81,13 +86,13 @@ export const addToCartWorkflow = createWorkflow(
})
const { itemsToCreate = [], itemsToUpdate = [] } = getLineItemActionsStep({
id: input.cart.id,
id: cart.id,
items: lineItems,
})
confirmVariantInventoryWorkflow.runAsStep({
input: {
sales_channel_id: input.cart.sales_channel_id as string,
sales_channel_id: cart.sales_channel_id,
variants,
items: input.items,
itemsToUpdate,
@@ -96,22 +101,22 @@ export const addToCartWorkflow = createWorkflow(
parallelize(
createLineItemsStep({
id: input.cart.id,
id: cart.id,
items: itemsToCreate,
}),
updateLineItemsStep({
id: input.cart.id,
id: cart.id,
items: itemsToUpdate,
})
)
refreshCartItemsWorkflow.runAsStep({
input: { cart_id: input.cart.id },
input: { cart_id: cart.id },
})
emitEventStep({
eventName: CartWorkflowEvents.UPDATED,
data: { id: input.cart.id },
data: { id: cart.id },
})
}
)
@@ -7,6 +7,7 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep, validatePresenceOfStep } from "../../common"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import { cartFieldsForPricingContext } from "../utils/fields"
export const listShippingOptionsForCartWorkflowId =
"list-shipping-options-for-cart"
@@ -26,18 +27,7 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
const cartQuery = useQueryGraphStep({
entity: "cart",
filters: { id: input.cart_id },
fields: [
"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",
],
fields: cartFieldsForPricingContext,
options: { throwIfKeyNotFound: true },
}).config({ name: "get-cart" })
@@ -1,4 +1,8 @@
import { isDefined, PromotionActions } from "@medusajs/framework/utils"
import {
filterObjectByKeys,
isDefined,
PromotionActions,
} from "@medusajs/framework/utils"
import {
createWorkflow,
transform,
@@ -9,6 +13,7 @@ import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import { updateLineItemsStep } from "../steps"
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
import {
cartFieldsForPricingContext,
cartFieldsForRefreshSteps,
productVariantsFields,
} from "../utils/fields"
@@ -41,16 +46,9 @@ export const refreshCartItemsWorkflow = createWorkflow(
return (data.cart.items ?? []).map((i) => i.variant_id)
})
const pricingContext = transform(
{ cart },
({ cart: { currency_code, region_id, customer_id } }) => {
return {
currency_code,
region_id,
customer_id,
}
}
)
const cartPricingContext = transform({ cart }, ({ cart }) => {
return filterObjectByKeys(cart, cartFieldsForPricingContext)
})
const variants = useRemoteQueryStep({
entry_point: "variants",
@@ -58,7 +56,7 @@ export const refreshCartItemsWorkflow = createWorkflow(
variables: {
id: variantIds,
calculated_price: {
context: pricingContext,
context: cartPricingContext,
},
},
throw_if_key_not_found: true,
@@ -6,6 +6,7 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common"
import { updateCartsStep } from "../steps"
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
export const transferCartCustomerWorkflowId = "transfer-cart-customer"
/**
@@ -65,6 +66,10 @@ export const transferCartCustomerWorkflow = createWorkflow(
)
updateCartsStep(cartInput)
refreshCartItemsWorkflow.runAsStep({
input: { cart_id: input.id },
})
}
)
}
@@ -4,14 +4,20 @@ import {
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
import { validateCartStep } from "../steps/validate-cart"
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 { refreshCartItemsWorkflow } from "./refresh-cart-items"
const cartFields = cartFieldsForPricingContext.concat(["items.*"])
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
/**
* This workflow updates a cart's line item.
@@ -19,19 +25,22 @@ export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
export const updateLineItemInCartWorkflow = createWorkflow(
updateLineItemInCartWorkflowId,
(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) => {
return [data.input.item.variant_id]
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
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.
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,
}
validateCartStep({ cart })
const variantIds = transform({ item }, ({ item }) => {
return [item.variant_id]
})
const variants = useRemoteQueryStep({
@@ -40,7 +49,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
variables: {
id: variantIds,
calculated_price: {
context: pricingContext,
context: cart,
},
},
throw_if_key_not_found: true,
@@ -48,13 +57,13 @@ export const updateLineItemInCartWorkflow = createWorkflow(
validateVariantPricesStep({ variants })
const items = transform({ input }, (data) => {
return [data.input.item]
const items = transform({ item }, ({ item }) => {
return [item]
})
confirmVariantInventoryWorkflow.runAsStep({
input: {
sales_channel_id: input.cart.sales_channel_id as string,
sales_channel_id: cart.sales_channel_id,
variants,
items,
},
@@ -62,7 +71,6 @@ export const updateLineItemInCartWorkflow = createWorkflow(
const lineItemUpdate = transform({ input, variants }, (data) => {
const variant = data.variants[0]
const item = data.input.item
return {
data: {
@@ -72,7 +80,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
!!variant.calculated_price.is_calculated_price_tax_inclusive,
},
selector: {
id: item.id,
id: data.input.item_id,
},
}
})
@@ -80,7 +88,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
updateLineItemsStepWithSelector(lineItemUpdate)
refreshCartItemsWorkflow.runAsStep({
input: { cart_id: input.cart.id },
input: { cart_id: input.cart_id },
})
}
)