feat(fulfillment): List shipping options filtered by context anmd rules (#6507)

**What**
Should be able to list the shipping options with or without a context, when a context is provided all the rules of the shipping options must be valid for the shipping options to be returned.

FIXES CORE-1765
This commit is contained in:
Adrien de Peretti
2024-02-26 14:59:55 +00:00
committed by GitHub
parent b13c669528
commit ac829fc67f
7 changed files with 457 additions and 320 deletions
@@ -1,11 +1,15 @@
import {
Context,
DAL,
FilterableShippingOptionProps,
FilterQuery,
FindConfig,
FulfillmentTypes,
IFulfillmentModuleService,
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
ShippingOptionDTO,
UpdateFulfillmentSetDTO,
} from "@medusajs/types"
import {
@@ -29,7 +33,7 @@ import {
ShippingOptionType,
ShippingProfile,
} from "@models"
import { validateRules } from "@utils"
import { isContextValid, validateRules } from "@utils"
const generateMethodForModels = [
ServiceZone,
@@ -113,6 +117,100 @@ export default class FulfillmentModuleService<
return joinerConfig
}
protected static normalizeShippingOptionsListParams(
filters: FilterableShippingOptionProps = {},
config: FindConfig<ShippingOptionDTO> = {}
) {
let { fulfillment_set_id, fulfillment_set_type, context, ...where } =
filters
const normalizedConfig = { ...config }
normalizedConfig.relations = [
"rules",
"type",
"shipping_profile",
"service_provider",
...(normalizedConfig.relations ?? []),
]
// The assumption is that there won't be an infinite amount of shipping options. So if a context filtering needs to be applied we can retrieve them all.
normalizedConfig.take =
normalizedConfig.take ?? (context ? null : undefined)
let normalizedFilters = { ...where } as FilterQuery
if (fulfillment_set_id || fulfillment_set_type) {
const fulfillmentSetConstraints = {}
if (fulfillment_set_id) {
fulfillmentSetConstraints["id"] = fulfillment_set_id
}
if (fulfillment_set_type) {
fulfillmentSetConstraints["type"] = fulfillment_set_type
}
normalizedFilters = {
...normalizedFilters,
service_zone: {
fulfillment_set: fulfillmentSetConstraints,
},
}
normalizedConfig.relations.push("service_zone.fulfillment_set")
}
normalizedConfig.relations = Array.from(new Set(normalizedConfig.relations))
return {
filters: normalizedFilters,
config: normalizedConfig,
context,
}
}
@InjectManager("baseRepository_")
// @ts-ignore
async listShippingOptions(
filters: FilterableShippingOptionProps = {},
config: FindConfig<ShippingOptionDTO> = {},
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO[]> {
const {
filters: normalizedFilters,
config: normalizedConfig,
context,
} = FulfillmentModuleService.normalizeShippingOptionsListParams(
filters,
config
)
let shippingOptions = await this.shippingOptionService_.list(
normalizedFilters,
normalizedConfig,
sharedContext
)
// Apply rules context filtering
if (context) {
shippingOptions = shippingOptions.filter((shippingOption) => {
if (!shippingOption.rules?.length) {
return true
}
return isContextValid(
context,
shippingOption.rules.map((r) => r)
)
})
}
return await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO[]
>(shippingOptions, {
populate: true,
})
}
create(
data: FulfillmentTypes.CreateFulfillmentSetDTO[],
sharedContext?: Context
@@ -1,4 +1,4 @@
import { isContextValidForRules, RuleOperator } from "../utils"
import { isContextValid, RuleOperator } from "../utils"
describe("isContextValidForRules", () => {
const context = {
@@ -19,37 +19,37 @@ describe("isContextValidForRules", () => {
value: "wrongValue",
}
it("returns true when all rules are valid and atLeastOneValidRule is false", () => {
it("returns true when all rules are valid", () => {
const rules = [validRule, validRule]
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns true when all rules are valid and atLeastOneValidRule is true", () => {
it("returns true when some rules are valid", () => {
const rules = [validRule, validRule]
const options = { atLeastOneValidRule: true }
expect(isContextValidForRules(context, rules, options)).toBe(true)
const options = { someAreValid: true }
expect(isContextValid(context, rules, options)).toBe(true)
})
it("returns true when some rules are valid and atLeastOneValidRule is true", () => {
it("returns true when some rules are valid and someAreValid is true", () => {
const rules = [validRule, invalidRule]
const options = { atLeastOneValidRule: true }
expect(isContextValidForRules(context, rules, options)).toBe(true)
const options = { someAreValid: true }
expect(isContextValid(context, rules, options)).toBe(true)
})
it("returns false when some rules are valid and atLeastOneValidRule is false", () => {
it("returns false when some rules are valid", () => {
const rules = [validRule, invalidRule]
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns false when no rules are valid and atLeastOneValidRule is true", () => {
it("returns false when no rules are valid and someAreValid is true", () => {
const rules = [invalidRule, invalidRule]
const options = { atLeastOneValidRule: true }
expect(isContextValidForRules(context, rules, options)).toBe(false)
const options = { someAreValid: true }
expect(isContextValid(context, rules, options)).toBe(false)
})
it("returns false when no rules are valid and atLeastOneValidRule is false", () => {
it("returns false when no rules are valid", () => {
const rules = [invalidRule, invalidRule]
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'gt' operator is valid", () => {
@@ -61,7 +61,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'gt' operator is invalid", () => {
@@ -73,7 +73,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "0" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'gte' operator is valid", () => {
@@ -85,7 +85,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'gte' operator is invalid", () => {
@@ -97,7 +97,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'lt' operator is valid", () => {
@@ -109,7 +109,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'lt' operator is invalid", () => {
@@ -121,7 +121,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'lte' operator is valid", () => {
@@ -133,7 +133,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
// ... existing tests ...
@@ -147,7 +147,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'in' operator is valid", () => {
@@ -159,7 +159,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'in' operator is invalid", () => {
@@ -171,7 +171,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'nin' operator is valid", () => {
@@ -183,7 +183,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'nin' operator is invalid", () => {
@@ -195,7 +195,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'ne' operator is valid", () => {
@@ -207,7 +207,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'ne' operator is invalid", () => {
@@ -219,7 +219,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
it("returns true when the 'eq' operator is valid", () => {
@@ -231,7 +231,7 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(true)
expect(isContextValid(context, rules)).toBe(true)
})
it("returns false when the 'eq' operator is invalid", () => {
@@ -243,6 +243,6 @@ describe("isContextValidForRules", () => {
},
]
const context = { attribute1: "2" }
expect(isContextValidForRules(context, rules)).toBe(false)
expect(isContextValid(context, rules)).toBe(false)
})
})
+14 -7
View File
@@ -10,8 +10,8 @@ import { isString, MedusaError, pickValueFromObject } from "@medusajs/utils"
export type Rule = {
attribute: string
operator: RuleOperator
value: string | string[]
operator: Lowercase<keyof typeof RuleOperator>
value: string | string[] | null
}
export enum RuleOperator {
@@ -71,18 +71,18 @@ const operatorsPredicate = {
* @param rules
* @param options
*/
export function isContextValidForRules(
export function isContextValid(
context: Record<string, any>,
rules: Rule[],
options: {
atLeastOneValidRule: boolean
someAreValid: boolean
} = {
atLeastOneValidRule: false,
someAreValid: false,
}
) {
const { atLeastOneValidRule } = options
const { someAreValid } = options
const loopComparator = atLeastOneValidRule ? rules.some : rules.every
const loopComparator = someAreValid ? rules.some : rules.every
const predicate = (rule) => {
const { attribute, operator, value } = rule
const contextValue = pickValueFromObject(attribute, context)
@@ -137,6 +137,13 @@ export function validateRule(rule: Record<string, unknown>): boolean {
"Rule value must be an array for in/nin operators"
)
}
} else {
if (!isString(rule.value)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Rule value must be a string for the selected operator ${rule.operator}`
)
}
}
return true