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 ThamirandSergio Campamá authored and GitHub committed 2024-12-17 11:10:30 +01:00
1 parent 0c49470066
commit 6367bccde8
29 files changed
+1090 -166

No files matched your search

@@ -95,9 +95,9 @@ export const PriceListConfigurationForm = ({
const rules = { ...priceList.rules } // preserve other rules set on the PL
if (groupIds.length) {
rules["customer_group_id"] = groupIds
rules["customer.groups.id"] = groupIds
} else {
delete rules["customer_group_id"]
delete rules["customer.groups.id"]
}
await mutateAsync(
@@ -12,7 +12,7 @@ export const PriceListConfiguration = () => {
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[]
| undefined
@@ -37,8 +37,8 @@ export const PriceListConfiguration = () => {
const isCustomerGroupsReady = isPending
? false
: !!customerGroupIds?.length && isCustomerGroupsPending
? false
: true
? false
: true
const ready = !isPending && !!price_list && isCustomerGroupsReady
@@ -82,7 +82,7 @@ export const PriceListCreateForm = ({
const { rules, products } = data
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
const prices = exctractPricesFromProducts(products, regions)
@@ -120,10 +120,13 @@ export const PriceListCreateForm = ({
) => {
form.clearErrors(fields)
const values = fields.reduce((acc, key) => {
acc[key] = form.getValues(key)
return acc
}, {} as Record<string, unknown>)
const values = fields.reduce(
(acc, key) => {
acc[key] = form.getValues(key)
return acc
},
{} as Record<string, unknown>
)
const validationResult = schema.safeParse(values)
@@ -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 },
})
}
)
+4 -4
View File
@@ -4,7 +4,7 @@ import { PaymentCollectionDTO } from "../payment"
import { ProductDTO } from "../product"
import { RegionDTO } from "../region"
import { BigNumberInput } from "../totals"
import { CartDTO, CartLineItemDTO } from "./common"
import { CartDTO } from "./common"
import {
CreateAddressDTO,
UpdateAddressDTO,
@@ -44,8 +44,8 @@ export interface CreateCartCreateLineItemDTO {
}
export interface UpdateLineItemInCartWorkflowInputDTO {
cart: CartDTO
item: CartLineItemDTO
cart_id: string
item_id: string
update: Partial<UpdateLineItemDTO>
}
@@ -80,8 +80,8 @@ export interface CreateCartWorkflowInputDTO {
}
export interface AddToCartWorkflowInputDTO {
cart_id: string
items: CreateCartCreateLineItemDTO[]
cart: CartWorkflowDTO
}
export interface UpdateCartWorkflowInputDTO {
@@ -2,5 +2,9 @@ export type MedusaPricingContext = {
region_id?: string
currency_code?: 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
}
+2
View File
@@ -18,7 +18,9 @@ export * from "./dynamic-import"
export * from "./env-editor"
export * from "./errors"
export * from "./file-system"
export * from "./filter-object-by-keys"
export * from "./filter-operator-map"
export * from "./flatten-object-to-key-value-pairs"
export * from "./generate-entity-id"
export * from "./get-caller-file-path"
export * from "./get-config-file"
@@ -2,10 +2,10 @@ import {
deleteLineItemsWorkflow,
updateLineItemInCartWorkflow,
} from "@medusajs/core-flows"
import { prepareListQuery } from "@medusajs/framework"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { prepareListQuery } from "@medusajs/framework"
import { refetchCart } from "../../../helpers"
import { StoreUpdateCartLineItemType } from "../../../validators"
@@ -13,6 +13,7 @@ export const POST = async (
req: MedusaRequest<StoreUpdateCartLineItemType>,
res: MedusaResponse<HttpTypes.StoreCartResponse>
) => {
// TODO: Move this to the workflow when the query to line item is fixed
const cart = await refetchCart(
req.params.id,
req.scope,
@@ -39,14 +40,12 @@ export const POST = async (
)
}
const input = {
cart,
item,
update: req.validatedBody,
}
await updateLineItemInCartWorkflow(req.scope).run({
input,
input: {
cart_id: req.params.id,
item_id: item.id,
update: req.validatedBody,
},
})
const updatedCart = await refetchCart(
@@ -1,33 +1,25 @@
import { addToCartWorkflow } from "@medusajs/core-flows"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
import { refetchCart } from "../../helpers"
import { StoreAddCartLineItemType } from "../../validators"
import { HttpTypes } from "@medusajs/framework/types"
export const POST = async (
req: MedusaRequest<StoreAddCartLineItemType>,
res: MedusaResponse<HttpTypes.StoreCartResponse>
) => {
await addToCartWorkflow(req.scope).run({
input: {
cart_id: req.params.id,
items: [req.validatedBody],
},
})
const cart = await refetchCart(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
const workflowInput = {
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 })
res.status(200).json({ cart })
}
@@ -49,7 +49,10 @@ export function setPricingContext() {
["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
+1 -1
View File
@@ -1,5 +1,5 @@
import { CartModuleService } from "./services"
import { Module, Modules } from "@medusajs/framework/utils"
import { CartModuleService } from "./services"
export default Module(Modules.CART, {
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 {
flattenObjectToKeyValuePairs,
isPresent,
MedusaError,
MikroOrmBase,
PriceListStatus,
@@ -58,6 +60,14 @@ export class PricingRepository
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
// that the price set is configured for
const priceSubQueryKnex = knex({
@@ -135,19 +145,20 @@ export class PricingRepository
priceBuilder
.whereNull("price.price_list_id")
.andWhere((withoutPriceListBuilder) => {
for (const [key, value] of Object.entries(context)) {
for (const [key, value] of flattenedContext) {
withoutPriceListBuilder.orWhere((orBuilder) => {
orBuilder.where("pr.attribute", key)
if (typeof value === "number") {
orBuilder.where((operatorGroupBuilder) => {
buildOperatorQueries(operatorGroupBuilder, value)
})
buildOperatorQueries(orBuilder, value)
} else {
orBuilder.where({ "pr.value": value })
const normalizeValue = Array.isArray(value) ? value : [value]
orBuilder.whereIn("pr.value", normalizeValue)
}
})
}
withoutPriceListBuilder.orWhere("price.rules_count", "=", 0)
})
})
@@ -171,7 +182,7 @@ export class PricingRepository
})
.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.where(
"plr.value",
@@ -185,14 +196,18 @@ export class PricingRepository
this.andWhere(function () {
this.andWhere((contextBuilder) => {
for (const [key, value] of Object.entries(context)) {
for (const [key, value] of flattenedContext) {
contextBuilder.orWhere((orBuilder) => {
orBuilder.where("pr.attribute", key)
if (typeof value === "number") {
buildOperatorQueries(orBuilder, value)
} else {
orBuilder.where({ "pr.value": value })
const normalizeValue = Array.isArray(value)
? value
: [value]
orBuilder.whereIn("pr.value", normalizeValue)
}
})
}