From ac829fc67f7495b08f28e55923c59f0fd6320311 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 26 Feb 2024 15:59:55 +0100 Subject: [PATCH] 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 --- .changeset/loud-mirrors-perform.md | 5 + .../integration-tests/__fixtures__/index.ts | 39 ++ .../fulfillment-module-service.spec.ts | 545 +++++++++--------- .../services/fulfillment-module-service.ts | 100 +++- .../src/utils/__tests__/utils.spec.ts | 64 +- packages/fulfillment/src/utils/utils.ts | 21 +- .../src/fulfillment/common/shipping-option.ts | 3 + 7 files changed, 457 insertions(+), 320 deletions(-) create mode 100644 .changeset/loud-mirrors-perform.md create mode 100644 packages/fulfillment/integration-tests/__fixtures__/index.ts diff --git a/.changeset/loud-mirrors-perform.md b/.changeset/loud-mirrors-perform.md new file mode 100644 index 0000000000..d4b119c09b --- /dev/null +++ b/.changeset/loud-mirrors-perform.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(fulfillment): List shipping options filtered by context and rules diff --git a/packages/fulfillment/integration-tests/__fixtures__/index.ts b/packages/fulfillment/integration-tests/__fixtures__/index.ts new file mode 100644 index 0000000000..a62bc9bd44 --- /dev/null +++ b/packages/fulfillment/integration-tests/__fixtures__/index.ts @@ -0,0 +1,39 @@ +import { CreateShippingOptionDTO } from "@medusajs/types" + +export function generateCreateShippingOptionsData({ + name, + service_zone_id, + shipping_profile_id, + service_provider_id, + price_type, + rules, + type, + data, +}: Omit & { + price_type?: CreateShippingOptionDTO["price_type"] + name?: string + type?: CreateShippingOptionDTO["type"] +}): Required { + return { + service_zone_id: service_zone_id, + shipping_profile_id: shipping_profile_id, + service_provider_id: service_provider_id, + type: type ?? { + code: "test-type", + description: "test-description", + label: "test-label", + }, + data: data ?? { + amount: 1000, + }, + name: name ?? Math.random().toString(36).substring(7), + price_type: price_type ?? "flat", + rules: rules ?? [ + { + attribute: "weight", + operator: "eq", + value: "test", + }, + ], + } +} diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts index eb48b5a324..6cccf1c5ae 100644 --- a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service.spec.ts @@ -1,4 +1,4 @@ -import { Modules } from "@medusajs/modules-sdk" +import {Modules} from "@medusajs/modules-sdk" import { CreateFulfillmentSetDTO, CreateGeoZoneDTO, @@ -12,11 +12,21 @@ import { UpdateGeoZoneDTO, UpdateServiceZoneDTO, } from "@medusajs/types" -import { GeoZoneType } from "@medusajs/utils" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" +import {GeoZoneType} from "@medusajs/utils" +import {moduleIntegrationTestRunner, SuiteOptions} from "medusa-test-utils" +import {generateCreateShippingOptionsData} from "../__fixtures__" jest.setTimeout(100000) +// TODO: Temporary until the providers are sorted out +const createProvider = async (MikroOrmWrapper, providerId: string) => { + const [{ id }] = await MikroOrmWrapper.forkManager().execute( + `insert into service_provider (id) values ('${providerId}') returning id` + ) + + return id +} + moduleIntegrationTestRunner({ moduleName: Modules.FULFILLMENT, testSuite: ({ @@ -249,6 +259,177 @@ moduleIntegrationTestRunner({ ) }) }) + + describe("shipping options", () => { + it("should list shipping options with a filter", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + }, + ], + }) + + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) + + const [shippingOption1] = await service.createShippingOptions([ + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + service_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "in", + value: ["test"], + }, + ], + }), + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + service_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "eq", + value: "test", + }, + { + attribute: "test-attribute2.options", + operator: "in", + value: ["test", "test2"], + }, + ], + }), + ]) + + const listedOptions = await service.listShippingOptions({ + name: shippingOption1.name, + }) + + expect(listedOptions).toHaveLength(1) + expect(listedOptions[0].id).toEqual(shippingOption1.id) + }) + + it("should list shipping options with a context", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + service_zones: [ + { + name: "test", + }, + ], + }) + + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) + + const [shippingOption1, , shippingOption3] = + await service.createShippingOptions([ + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + service_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "in", + value: ["test"], + }, + ], + }), + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + service_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "in", + value: ["test-test"], + }, + ], + }), + generateCreateShippingOptionsData({ + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + service_provider_id: providerId, + rules: [ + { + attribute: "test-attribute", + operator: "eq", + value: "test", + }, + { + attribute: "test-attribute2.options", + operator: "in", + value: ["test", "test2"], + }, + ], + }), + ]) + + let listedOptions = await service.listShippingOptions({ + context: { + "test-attribute": "test", + "test-attribute2": { + options: "test2", + }, + }, + }) + + expect(listedOptions).toHaveLength(2) + expect(listedOptions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: shippingOption1.id }), + expect.objectContaining({ id: shippingOption3.id }), + ]) + ) + + listedOptions = await service.listShippingOptions({ + fulfillment_set_id: { $ne: fulfillmentSet.id }, + context: { + "test-attribute": "test", + "test-attribute2": { + options: "test2", + }, + }, + }) + + expect(listedOptions).toHaveLength(0) + + listedOptions = await service.listShippingOptions({ + fulfillment_set_type: "non-existing-type", + context: { + "test-attribute": "test", + "test-attribute2": { + options: "test2", + }, + }, + }) + + expect(listedOptions).toHaveLength(0) + }) + }) }) describe("mutations", () => { @@ -788,34 +969,16 @@ moduleIntegrationTestRunner({ fulfillment_set_id: fulfillmentSet.id, }) - // TODO: change that for a real provider instead of fake data manual inserted data - const [{ id: providerId }] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) - const createData: CreateShippingOptionDTO = { - name: "test-option", - price_type: "flat", + const createData: CreateShippingOptionDTO = generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, service_provider_id: providerId, - type: { - code: "test-type", - description: "test-description", - label: "test-label", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test-attribute", - operator: "in", - value: ["test-value"], - }, - ], - } + }) const createdShippingOption = await service.createShippingOptions( createData @@ -863,57 +1026,22 @@ moduleIntegrationTestRunner({ fulfillment_set_id: fulfillmentSet.id, }) - // TODO: change that for a real provider instead of fake data manual inserted data - const [{ id: providerId }] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) const createData: CreateShippingOptionDTO[] = [ - { - name: "test-option", - price_type: "flat", + generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, service_provider_id: providerId, - type: { - code: "test-type", - description: "test-description", - label: "test-label", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test-attribute", - operator: "eq", - value: "test-value", - }, - ], - }, - { - name: "test-option-2", - price_type: "calculated", + }), + generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, service_provider_id: providerId, - type: { - code: "test-type", - description: "test-description", - label: "test-label", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test-attribute", - operator: "eq", - value: "test-value", - }, - ], - }, + }) ] const createdShippingOptions = await service.createShippingOptions( @@ -968,34 +1096,23 @@ moduleIntegrationTestRunner({ fulfillment_set_id: fulfillmentSet.id, }) - // TODO: change that for a real provider instead of fake data manual inserted data - const [{ id: providerId }] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) - const createData: CreateShippingOptionDTO = { - name: "test-option", - price_type: "flat", + const createData: CreateShippingOptionDTO = generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, service_provider_id: providerId, - type: { - code: "test-type", - description: "test-description", - label: "test-label", - }, - data: { - amount: 1000, - }, rules: [ { attribute: "test-attribute", - operator: "invalid", + operator: "invalid" as any, value: "test-value", }, ], - } + }) const err = await service .createShippingOptions(createData) @@ -1029,28 +1146,13 @@ moduleIntegrationTestRunner({ "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" ) - const shippingOption = await service.createShippingOptions({ - name: "test-option", - price_type: "flat", - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: providerId, - type: { - code: "test-type", - description: "test-description", - label: "test-label", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test-attribute", - operator: "eq", - value: "test-value", - }, - ], - }) + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + service_provider_id: providerId, + }) + ) const ruleData = { attribute: "test-attribute", @@ -1835,33 +1937,16 @@ moduleIntegrationTestRunner({ type: "default", }) - const [serviceProvider] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) - const shippingOptionData = { - name: "test", - price_type: "flat", + const shippingOptionData = generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - } + service_provider_id: providerId, + }) const shippingOption = await service.createShippingOptions( shippingOptionData @@ -1873,7 +1958,7 @@ moduleIntegrationTestRunner({ price_type: "calculated", service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, + service_provider_id: providerId, type: { code: "updated-test", description: "updated-test", @@ -1955,33 +2040,16 @@ moduleIntegrationTestRunner({ type: "default", }) - const [serviceProvider] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) - const shippingOptionData = { - name: "test", - price_type: "flat", + const shippingOptionData = generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - } + service_provider_id: providerId, + }) const shippingOption = await service.createShippingOptions( shippingOptionData @@ -1993,7 +2061,7 @@ moduleIntegrationTestRunner({ price_type: "calculated", service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, + service_provider_id: providerId, data: { amount: 2000, }, @@ -2068,56 +2136,22 @@ moduleIntegrationTestRunner({ type: "default", }) - const [serviceProvider] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) const shippingOptionData = [ - { - name: "test", - price_type: "flat", + generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - }, - { - name: "test2", - price_type: "calculated", + service_provider_id: providerId, + }), + generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - }, + service_provider_id: providerId, + }) ] const shippingOptions = await service.createShippingOptions( @@ -2131,7 +2165,7 @@ moduleIntegrationTestRunner({ price_type: "calculated", service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, + service_provider_id: providerId, type: { code: "updated-test", description: "updated-test", @@ -2154,7 +2188,7 @@ moduleIntegrationTestRunner({ price_type: "calculated", service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, + service_provider_id: providerId, type: { code: "updated-test", description: "updated-test", @@ -2307,33 +2341,16 @@ moduleIntegrationTestRunner({ type: "default", }) - const [serviceProvider] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) - const shippingOptionData = { - name: "test", - price_type: "flat", + const shippingOptionData = generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - } + service_provider_id: providerId, + }) const shippingOption = await service.createShippingOptions( shippingOptionData @@ -2374,33 +2391,16 @@ moduleIntegrationTestRunner({ type: "default", }) - const [serviceProvider] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) - const shippingOptionData = { - name: "test", - price_type: "flat", + const shippingOptionData = generateCreateShippingOptionsData({ service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - } + service_provider_id: providerId, + }) const shippingOption = await service.createShippingOptions( shippingOptionData @@ -2444,33 +2444,18 @@ moduleIntegrationTestRunner({ name: "test", fulfillment_set_id: fulfillmentSet.id, }) - const [serviceProvider] = - await MikroOrmWrapper.forkManager().execute( - "insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id" - ) + const providerId = await createProvider( + MikroOrmWrapper, + "sp_jdafwfleiwuonl" + ) - const shippingOption = await service.createShippingOptions({ - name: "test", - price_type: "flat", - service_zone_id: serviceZone.id, - shipping_profile_id: shippingProfile.id, - service_provider_id: serviceProvider.id, - type: { - code: "test", - description: "test", - label: "test", - }, - data: { - amount: 1000, - }, - rules: [ - { - attribute: "test", - operator: "eq", - value: "test", - }, - ], - }) + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + service_provider_id: providerId, + }) + ) const updateData = { id: shippingOption.rules[0].id, diff --git a/packages/fulfillment/src/services/fulfillment-module-service.ts b/packages/fulfillment/src/services/fulfillment-module-service.ts index b64a9d9515..e2d1239ad9 100644 --- a/packages/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/fulfillment/src/services/fulfillment-module-service.ts @@ -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 = {} + ) { + 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 = {}, + sharedContext?: Context + ): Promise { + 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 diff --git a/packages/fulfillment/src/utils/__tests__/utils.spec.ts b/packages/fulfillment/src/utils/__tests__/utils.spec.ts index 7bdecf3adb..c72ff35a42 100644 --- a/packages/fulfillment/src/utils/__tests__/utils.spec.ts +++ b/packages/fulfillment/src/utils/__tests__/utils.spec.ts @@ -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) }) }) diff --git a/packages/fulfillment/src/utils/utils.ts b/packages/fulfillment/src/utils/utils.ts index 481172967d..8cbdcba7c4 100644 --- a/packages/fulfillment/src/utils/utils.ts +++ b/packages/fulfillment/src/utils/utils.ts @@ -10,8 +10,8 @@ import { isString, MedusaError, pickValueFromObject } from "@medusajs/utils" export type Rule = { attribute: string - operator: RuleOperator - value: string | string[] + operator: Lowercase + 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, 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): 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 diff --git a/packages/types/src/fulfillment/common/shipping-option.ts b/packages/types/src/fulfillment/common/shipping-option.ts index fe2ab7ca2a..a88989aabd 100644 --- a/packages/types/src/fulfillment/common/shipping-option.ts +++ b/packages/types/src/fulfillment/common/shipping-option.ts @@ -37,6 +37,8 @@ export interface FilterableShippingOptionProps extends BaseFilterable { id?: string | string[] | OperatorMap name?: string | string[] | OperatorMap + fulfillment_set_id?: string | string[] | OperatorMap + fulfillment_set_type?: string | string[] | OperatorMap price_type?: | ShippingOptionPriceType | ShippingOptionPriceType[] @@ -44,4 +46,5 @@ export interface FilterableShippingOptionProps service_zone?: FilterableServiceZoneProps shipping_option_type?: FilterableShippingOptionTypeProps rules?: FilterableShippingOptionRuleProps + context?: Record }