feat(fulfillment): Separate list and context rules validation (#6674)
**What** - Add method to validate fulfillment option from the provider - Separate list/list and count from context rules validation and add listShippingOptionsForContext FIXES CORE-1861
This commit is contained in:
1 parent
1956dce80a
commit
3188e703b3
16 files changed
+1011
-326
No files matched your search
@@ -1,5 +0,0 @@
|
||||
describe("noop", function () {
|
||||
it("should run", function () {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
Context,
|
||||
DAL,
|
||||
FilterableFulfillmentSetProps,
|
||||
FilterableShippingOptionProps,
|
||||
FilterQuery,
|
||||
FindConfig,
|
||||
FulfillmentDTO,
|
||||
FulfillmentTypes,
|
||||
@@ -131,69 +129,17 @@ 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",
|
||||
"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 = {},
|
||||
async listShippingOptionsForContext(
|
||||
filters: FulfillmentTypes.FilterableShippingOptionForContextProps,
|
||||
config: FindConfig<ShippingOptionDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<FulfillmentTypes.ShippingOptionDTO[]> {
|
||||
const {
|
||||
filters: normalizedFilters,
|
||||
config: normalizedConfig,
|
||||
context,
|
||||
} = FulfillmentModuleService.normalizeShippingOptionsListParams(
|
||||
config: normalizedConfig,
|
||||
filters: normalizedFilters,
|
||||
} = FulfillmentModuleService.normalizeListShippingOptionsForContextParams(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
@@ -204,7 +150,6 @@ export default class FulfillmentModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
// Apply rules context filtering
|
||||
if (context) {
|
||||
shippingOptions = shippingOptions.filter((shippingOption) => {
|
||||
if (!shippingOption.rules?.length) {
|
||||
@@ -245,14 +190,6 @@ export default class FulfillmentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
async retrieveFulfillmentOptions(
|
||||
providerId: string
|
||||
): Promise<Record<string, any>[]> {
|
||||
return await this.fulfillmentProviderService_.getFulfillmentOptions(
|
||||
providerId
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async listFulfillments(
|
||||
filters: FulfillmentTypes.FilterableFulfillmentProps = {},
|
||||
@@ -331,6 +268,20 @@ export default class FulfillmentModuleService<
|
||||
): Promise<TEntity | TEntity[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
if (!data_.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
for (const fulfillmentSet of data_) {
|
||||
if (fulfillmentSet.service_zones?.length) {
|
||||
for (const serviceZone of fulfillmentSet.service_zones) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
FulfillmentModuleService.validateGeoZones(serviceZone.geo_zones)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createdFulfillmentSets = await this.fulfillmentSetService_.create(
|
||||
data_,
|
||||
sharedContext
|
||||
@@ -384,6 +335,14 @@ export default class FulfillmentModuleService<
|
||||
return []
|
||||
}
|
||||
|
||||
for (const serviceZone of data_) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
FulfillmentModuleService.validateGeoZones(serviceZone.geo_zones)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createdServiceZones = await this.serviceZoneService_.create(
|
||||
data_,
|
||||
sharedContext
|
||||
@@ -520,13 +479,17 @@ export default class FulfillmentModuleService<
|
||||
| FulfillmentTypes.CreateGeoZoneDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
FulfillmentModuleService.validateGeoZones(data_)
|
||||
|
||||
const createdGeoZones = await this.geoZoneService_.create(
|
||||
data,
|
||||
data_,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize<FulfillmentTypes.GeoZoneDTO[]>(
|
||||
createdGeoZones,
|
||||
Array.isArray(data) ? createdGeoZones : createdGeoZones[0],
|
||||
{
|
||||
populate: true,
|
||||
}
|
||||
@@ -777,6 +740,11 @@ export default class FulfillmentModuleService<
|
||||
fulfillmentSet.service_zones = fulfillmentSet.service_zones.map(
|
||||
(serviceZone) => {
|
||||
if (!("id" in serviceZone)) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
FulfillmentModuleService.validateGeoZones(
|
||||
serviceZone.geo_zones
|
||||
)
|
||||
}
|
||||
return serviceZone
|
||||
}
|
||||
return serviceZonesMap.get(serviceZone.id)!
|
||||
@@ -940,6 +908,7 @@ export default class FulfillmentModuleService<
|
||||
|
||||
serviceZone.geo_zones = serviceZone.geo_zones.map((geoZone) => {
|
||||
if (!("id" in geoZone)) {
|
||||
FulfillmentModuleService.validateGeoZones([geoZone])
|
||||
return geoZone
|
||||
}
|
||||
return geoZonesMap.get(geoZone.id)!
|
||||
@@ -1138,6 +1107,14 @@ export default class FulfillmentModuleService<
|
||||
| FulfillmentTypes.UpdateGeoZoneDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
if (!data_.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
FulfillmentModuleService.validateGeoZones(data_)
|
||||
|
||||
const updatedGeoZones = await this.geoZoneService_.update(
|
||||
data,
|
||||
sharedContext
|
||||
@@ -1271,6 +1248,41 @@ export default class FulfillmentModuleService<
|
||||
return Array.isArray(result) ? result[0] : result
|
||||
}
|
||||
|
||||
async retrieveFulfillmentOptions(
|
||||
providerId: string
|
||||
): Promise<Record<string, any>[]> {
|
||||
return await this.fulfillmentProviderService_.getFulfillmentOptions(
|
||||
providerId
|
||||
)
|
||||
}
|
||||
|
||||
async validateFulfillmentOption(
|
||||
providerId: string,
|
||||
data: Record<string, unknown>
|
||||
): Promise<boolean> {
|
||||
return await this.fulfillmentProviderService_.validateOption(
|
||||
providerId,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async validateShippingOption(
|
||||
shippingOptionId: string,
|
||||
context: Record<string, unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const shippingOptions = await this.listShippingOptionsForContext(
|
||||
{ id: shippingOptionId, context },
|
||||
{
|
||||
relations: ["rules"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return !!shippingOptions.length
|
||||
}
|
||||
|
||||
protected static canCancelFulfillmentOrThrow(fulfillment: Fulfillment) {
|
||||
if (fulfillment.shipped_at) {
|
||||
throw new MedusaError(
|
||||
@@ -1336,4 +1348,213 @@ export default class FulfillmentModuleService<
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected static validateGeoZones(
|
||||
geoZones: (
|
||||
| (Partial<FulfillmentTypes.CreateGeoZoneDTO> & { type: string })
|
||||
| (Partial<FulfillmentTypes.UpdateGeoZoneDTO> & { type: string })
|
||||
)[]
|
||||
) {
|
||||
const requirePropForType = {
|
||||
country: ["country_code"],
|
||||
province: ["country_code", "province_code"],
|
||||
city: ["country_code", "province_code", "city"],
|
||||
zip: ["country_code", "province_code", "city", "postal_expression"],
|
||||
}
|
||||
|
||||
for (const geoZone of geoZones) {
|
||||
if (!requirePropForType[geoZone.type]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Invalid geo zone type: ${geoZone.type}`
|
||||
)
|
||||
}
|
||||
|
||||
for (const prop of requirePropForType[geoZone.type]) {
|
||||
if (!geoZone[prop]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Missing required property ${prop} for geo zone type ${geoZone.type}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static normalizeListShippingOptionsForContextParams(
|
||||
filters: FulfillmentTypes.FilterableShippingOptionForContextProps,
|
||||
config: FindConfig<ShippingOptionDTO> = {}
|
||||
) {
|
||||
let {
|
||||
fulfillment_set_id,
|
||||
fulfillment_set_type,
|
||||
address,
|
||||
context,
|
||||
...where
|
||||
} = filters
|
||||
|
||||
const normalizedConfig = { ...config }
|
||||
normalizedConfig.relations = [
|
||||
"rules",
|
||||
"type",
|
||||
"shipping_profile",
|
||||
"provider",
|
||||
...(normalizedConfig.relations ?? []),
|
||||
]
|
||||
|
||||
normalizedConfig.take =
|
||||
normalizedConfig.take ?? (context ? null : undefined)
|
||||
|
||||
let normalizedFilters = { ...where }
|
||||
|
||||
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: {
|
||||
...(normalizedFilters.service_zone ?? {}),
|
||||
fulfillment_set: {
|
||||
...(normalizedFilters.service_zone?.fulfillment_set ?? {}),
|
||||
...fulfillmentSetConstraints,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
normalizedConfig.relations.push("service_zone.fulfillment_set")
|
||||
}
|
||||
|
||||
if (address) {
|
||||
const geoZoneConstraints =
|
||||
FulfillmentModuleService.buildGeoZoneConstraintsFromAddress(address)
|
||||
|
||||
normalizedFilters = {
|
||||
...normalizedFilters,
|
||||
service_zone: {
|
||||
...(normalizedFilters.service_zone ?? {}),
|
||||
geo_zones: {
|
||||
$or: geoZoneConstraints.map((geoZoneConstraint) => ({
|
||||
// Apply eventually provided constraints on the geo zone along side the address constraints
|
||||
...(normalizedFilters.service_zone?.geo_zones ?? {}),
|
||||
...geoZoneConstraint,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
normalizedConfig.relations.push("service_zone.geo_zones")
|
||||
}
|
||||
|
||||
normalizedConfig.relations = Array.from(new Set(normalizedConfig.relations))
|
||||
|
||||
return {
|
||||
filters: normalizedFilters,
|
||||
config: normalizedConfig,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the constraints for the geo zones based on the address properties
|
||||
* available and the hierarchy of required properties.
|
||||
* We build a OR constraint from the narrowest to the broadest
|
||||
* e.g. if we have a postal expression we build a constraint for the postal expression require props of type zip
|
||||
* and a constraint for the city required props of type city
|
||||
* and a constraint for the province code required props of type province
|
||||
* and a constraint for the country code required props of type country
|
||||
* example:
|
||||
* {
|
||||
* $or: [
|
||||
* {
|
||||
* type: "zip",
|
||||
* country_code: "SE",
|
||||
* province_code: "AB",
|
||||
* city: "Stockholm",
|
||||
* postal_expression: "12345"
|
||||
* },
|
||||
* {
|
||||
* type: "city",
|
||||
* country_code: "SE",
|
||||
* province_code: "AB",
|
||||
* city: "Stockholm"
|
||||
* },
|
||||
* {
|
||||
* type: "province",
|
||||
* country_code: "SE",
|
||||
* province_code: "AB"
|
||||
* },
|
||||
* {
|
||||
* type: "country",
|
||||
* country_code: "SE"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
private static buildGeoZoneConstraintsFromAddress(
|
||||
address: FulfillmentTypes.FilterableShippingOptionForContextProps["address"]
|
||||
) {
|
||||
/**
|
||||
* Define the hierarchy of required properties for the geo zones.
|
||||
*/
|
||||
const geoZoneRequirePropertyHierarchy = {
|
||||
postal_expression: [
|
||||
"country_code",
|
||||
"province_code",
|
||||
"city",
|
||||
"postal_expression",
|
||||
],
|
||||
city: ["country_code", "province_code", "city"],
|
||||
province_code: ["country_code", "province_code"],
|
||||
country_code: ["country_code"],
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the address has the required properties for the geo zones
|
||||
* constraints to build after. We are going from the narrowest to the broadest
|
||||
*/
|
||||
Object.entries(geoZoneRequirePropertyHierarchy).forEach(
|
||||
([prop, requiredProps]) => {
|
||||
if (address![prop]) {
|
||||
for (const requiredProp of requiredProps) {
|
||||
if (!address![requiredProp]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Missing required property ${requiredProp} for address when property ${prop} is set`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const geoZoneConstraints = Object.entries(geoZoneRequirePropertyHierarchy)
|
||||
.map(([prop, requiredProps]) => {
|
||||
if (address![prop]) {
|
||||
return requiredProps.reduce((geoZoneConstraint, prop) => {
|
||||
geoZoneConstraint.type =
|
||||
prop === "postal_expression"
|
||||
? "zip"
|
||||
: prop === "city"
|
||||
? "city"
|
||||
: prop === "province_code"
|
||||
? "province"
|
||||
: "country"
|
||||
geoZoneConstraint[prop] = address![prop]
|
||||
return geoZoneConstraint
|
||||
}, {} as Record<string, string | undefined>)
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((v): v is Record<string, any> => !!v)
|
||||
|
||||
return geoZoneConstraints
|
||||
}
|
||||
}
|
||||
@@ -78,8 +78,8 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.internal
|
||||
return await provider.validateFulfillmentData(optionData, data, context)
|
||||
}
|
||||
|
||||
async validateOption(data: any) {
|
||||
const provider = this.retrieveProviderRegistration(data.provider_id)
|
||||
async validateOption(providerId: string, data: Record<string, unknown>) {
|
||||
const provider = this.retrieveProviderRegistration(providerId)
|
||||
return await provider.validateOption(data)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export function isContextValid(
|
||||
} = {
|
||||
someAreValid: false,
|
||||
}
|
||||
) {
|
||||
): boolean {
|
||||
const { someAreValid } = options
|
||||
|
||||
const loopComparator = someAreValid ? rules.some : rules.every
|
||||
|
||||
Reference in new issue
Block a user