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
parent 0c49470066
commit 6367bccde8
29 changed files with 1090 additions and 166 deletions

View File

@@ -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,
})
})
})

View File

@@ -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"],
})
})
})

View File

@@ -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)
}

View File

@@ -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
}

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"