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:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user