From 67d3660abf3ceeac3f04006fe5e92d2fa7c0ccad Mon Sep 17 00:00:00 2001 From: William Bouchard <46496014+willbouch@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:02:36 -0400 Subject: [PATCH] feat(dashboard, core-flows): associate shipping option to type (#13226) * feat(dashboard, core-flows): allow associating shipping option type to a shipping option * edit as well * fix translation schema * fix some tests * changeset * add new test to update shipping option type * add new test to create shipping option with shipping option type * pr comments * pr comments * rename variable * make zod great again --- .changeset/gorgeous-cameras-own.md | 9 + .../admin/shipping-option.spec.ts | 360 +++++++++++++++++- .../src/i18n/translations/$schema.json | 4 + .../dashboard/src/i18n/translations/en.json | 1 + .../create-shipping-option-details-form.tsx | 40 +- .../create-shipping-options-form.tsx | 7 +- .../create-shipping-options-form/schema.ts | 1 + .../edit-shipping-option-form.tsx | 44 ++- .../steps/upsert-shipping-options.ts | 16 +- .../steps/validate-shipping-option-prices.ts | 11 +- .../workflows/create-shipping-options.ts | 5 +- .../framework/src/http/utils/validate-body.ts | 3 +- .../fulfillment/mutations/shipping-option.ts | 4 +- .../http/shipping-option/admin/payloads.ts | 16 +- .../fulfillment/create-shipping-options.ts | 6 +- .../fulfillment/update-shipping-options.ts | 4 + .../api/admin/shipping-options/validators.ts | 29 +- .../services/fulfillment-module-service.ts | 7 +- 18 files changed, 525 insertions(+), 42 deletions(-) create mode 100644 .changeset/gorgeous-cameras-own.md diff --git a/.changeset/gorgeous-cameras-own.md b/.changeset/gorgeous-cameras-own.md new file mode 100644 index 0000000000..2bacf638de --- /dev/null +++ b/.changeset/gorgeous-cameras-own.md @@ -0,0 +1,9 @@ +--- +"@medusajs/fulfillment": patch +"@medusajs/dashboard": patch +"@medusajs/core-flows": patch +"@medusajs/types": patch +"@medusajs/medusa": patch +--- + +feat(dashboard, core-flows): associate shipping option to type diff --git a/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts index 6ecab2a7f8..3da759a837 100644 --- a/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts @@ -17,6 +17,7 @@ medusaIntegrationTestRunner({ let appContainer let location let location2 + let type const shippingOptionRule = { operator: RuleOperator.EQ, @@ -92,6 +93,17 @@ medusaIntegrationTestRunner({ adminHeaders ) ).data.region + + type = ( + await api.post( + `/admin/shipping-option-types`, + { + label: "Test", + code: 'test', + }, + adminHeaders + ) + ).data.shipping_option_type }) describe("GET /admin/shipping-options", () => { @@ -404,6 +416,103 @@ medusaIntegrationTestRunner({ ) }) + it("should create a shipping option successfully with the provided shipping option type", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type_id: type.id, + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + { + region_id: region.id, + amount: 1000, + }, + { + region_id: region.id, + amount: 500, + rules: [ + { + attribute: "item_total", + operator: "gt", + value: 200, + }, + ], + }, + ], + rules: [shippingOptionRule], + } + + const response = await api.post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.shipping_option).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: shippingOptionPayload.name, + provider: expect.objectContaining({ + id: shippingOptionPayload.provider_id, + }), + price_type: shippingOptionPayload.price_type, + type: expect.objectContaining({ + id: type.id, + label: type.label, + description: type.description, + code: type.code, + }), + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + currency_code: "usd", + amount: 1000, + }), + expect.objectContaining({ + id: expect.any(String), + currency_code: "eur", + amount: 1000, + }), + expect.objectContaining({ + id: expect.any(String), + currency_code: "eur", + amount: 500, + rules_count: 2, + price_rules: expect.arrayContaining([ + expect.objectContaining({ + attribute: "item_total", + operator: "gt", + value: "200", + }), + expect.objectContaining({ + attribute: "region_id", + operator: "eq", + value: region.id, + }), + ]), + }), + ]), + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + operator: "eq", + attribute: "old_attr", + value: "old value", + }), + ]), + }) + ) + }) + it("should throw error when creating a price rule with a non white listed attribute", async () => { const shippingOptionPayload = { name: "Test shipping option", @@ -552,6 +661,72 @@ medusaIntegrationTestRunner({ "Providers (does-not-exist) are not enabled for the service location" ) }) + + it("should throw error if both type and type_id are missing", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + ], + rules: [shippingOptionRule], + } + + const error = await api + .post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + .catch((e) => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.message).toEqual( + "Invalid request: Exactly one of 'type' or 'type_id' must be provided, but not both" + ) + }) + + it("should throw error if both type and type_id are defined", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + type_id: "test_type_id", + price_type: "flat", + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + ], + rules: [shippingOptionRule], + } + + const error = await api + .post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + .catch((e) => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.message).toEqual( + "Invalid request: Exactly one of 'type' or 'type_id' must be provided, but not both" + ) + }) }) describe("POST /admin/shipping-options/:id", () => { @@ -637,7 +812,7 @@ medusaIntegrationTestRunner({ ], rules: [ { - // Un touched + // Untouched id: oldAttrRule.id, operator: RuleOperator.EQ, attribute: "old_attr", @@ -735,6 +910,135 @@ medusaIntegrationTestRunner({ ) }) + it("should update a shipping option with a provided shipping option type successfully", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + { + region_id: region.id, + amount: 1000, + }, + ], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "old_attr", + value: "old value", + }, + { + operator: RuleOperator.EQ, + attribute: "old_attr_2", + value: "true", + }, + ], + } + + const response = await api.post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + + const shippingOptionId = response.data.shipping_option.id + + const updateResponse = await api.post( + `/admin/shipping-options/${shippingOptionId}`, + { + name: "Updated shipping option", + type_id: type.id, + }, + adminHeaders + ) + + expect(updateResponse.status).toEqual(200) + expect(updateResponse.data.shipping_option).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "Updated shipping option", + type: expect.objectContaining({ + id: type.id, + label: type.label, + description: type.description, + code: type.code, + }), + }) + ) + }) + + it("should update a shipping option without providing shipping option type successfully", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + { + region_id: region.id, + amount: 1000, + }, + ], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "old_attr", + value: "old value", + }, + { + operator: RuleOperator.EQ, + attribute: "old_attr_2", + value: "true", + }, + ], + } + + const response = await api.post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + + const shippingOptionId = response.data.shipping_option.id + + const updateResponse = await api.post( + `/admin/shipping-options/${shippingOptionId}`, + { + name: "Updated shipping option" + }, + adminHeaders + ) + + expect(updateResponse.status).toEqual(200) + expect(updateResponse.data.shipping_option).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "Updated shipping option" + }) + ) + }) + it("should throw an error when provider does not belong to service location", async () => { const shippingOptionPayload = { name: "Test shipping option", @@ -785,6 +1089,60 @@ medusaIntegrationTestRunner({ "Providers (another_test-provider) are not enabled for the service location" ) }) + + it("should throw an error when type and type_id are both defined", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + { + region_id: region.id, + amount: 1000, + }, + ], + rules: [shippingOptionRule], + } + + const response = await api.post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + + const shippingOptionId = response.data.shipping_option.id + + const updateShippingOptionPayload = { + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + type_id: "test_type_id" + } + + const error = await api + .post( + `/admin/shipping-options/${shippingOptionId}`, + updateShippingOptionPayload, + adminHeaders + ) + .catch((e) => e) + + expect(error.response.status).toEqual(400) + expect(error.response.data.message).toEqual("Invalid request: Only one of 'type' or 'type_id' can be provided") + }) }) describe("DELETE /admin/shipping-options/:id", () => { diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index f73a9eda35..4884a90e85 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -6308,6 +6308,9 @@ "profile": { "type": "string" }, + "type": { + "type": "string" + }, "fulfillmentOption": { "type": "string" } @@ -6318,6 +6321,7 @@ "enableInStore", "provider", "profile", + "type", "fulfillmentOption" ], "additionalProperties": false diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 3e23efc2bd..605f78194e 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -1683,6 +1683,7 @@ }, "provider": "Fulfillment provider", "profile": "Shipping profile", + "type": "Shipping option type", "fulfillmentOption": "Fulfillment option" } }, diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx index 3d6c61bddb..c922a5e799 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx @@ -10,10 +10,7 @@ import { Combobox } from "../../../../../components/inputs/combobox" import { useComboboxData } from "../../../../../hooks/use-combobox-data" import { sdk } from "../../../../../lib/client" import { formatProvider } from "../../../../../lib/format-provider" -import { - FulfillmentSetType, - ShippingOptionPriceType, -} from "../../../common/constants" +import { FulfillmentSetType, ShippingOptionPriceType, } from "../../../common/constants" import { CreateShippingOptionSchema } from "./schema" type CreateShippingOptionDetailsFormProps = { @@ -49,6 +46,16 @@ export const CreateShippingOptionDetailsForm = ({ })), }) + const shippingOptionTypes = useComboboxData({ + queryFn: (params) => sdk.admin.shippingOptionType.list(params), + queryKey: ["shipping_option_types"], + getOptions: (data) => + data.shipping_option_types.map((type) => ({ + label: type.label, + value: type.id, + })), + }) + const fulfillmentProviders = useComboboxData({ queryFn: (params) => sdk.admin.fulfillmentProvider.list({ @@ -170,6 +177,31 @@ export const CreateShippingOptionDetailsForm = ({ ) }} /> + { + return ( + + + {t("stockLocations.shippingOptions.fields.type")} + + + + + + + ) + }} + />
diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx index 902faf8e22..1ad792917f 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx @@ -167,12 +167,7 @@ export function CreateShippingOptionsForm({ operator: "eq", }, ], - type: { - // TODO: FETCH TYPES - label: "Type label", - description: "Type description", - code: "type-code", - }, + type_id: data.shipping_option_type_id, }, { onSuccess: ({ shipping_option }) => { diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts index 34f188a3cd..a47b76d37b 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts @@ -13,6 +13,7 @@ export const CreateShippingOptionDetailsSchema = z.object({ shipping_profile_id: z.string().min(1), provider_id: z.string().min(1), fulfillment_option_id: z.string().min(1), + shipping_option_type_id: z.string().min(1), }) export const ShippingOptionConditionalPriceSchema = z.object({ diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx index 2fef4a41e8..777cf85436 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx @@ -31,6 +31,7 @@ const EditShippingOptionSchema = zod.object({ price_type: zod.nativeEnum(ShippingOptionPriceType), enabled_in_store: zod.boolean().optional(), shipping_profile_id: zod.string(), + shipping_option_type_id: zod.string(), }) export const EditShippingOptionForm = ({ @@ -54,12 +55,23 @@ export const EditShippingOptionForm = ({ defaultValue: shippingOption.shipping_profile_id, }) + const shippingOptionTypes = useComboboxData({ + queryFn: (params) => sdk.admin.shippingOptionType.list(params), + queryKey: ["shipping_option_types"], + getOptions: (data) => + data.shipping_option_types.map((type) => ({ + label: type.label, + value: type.id, + })), + }) + const form = useForm>({ defaultValues: { name: shippingOption.name, price_type: shippingOption.price_type as ShippingOptionPriceType, enabled_in_store: isOptionEnabledInStore(shippingOption), shipping_profile_id: shippingOption.shipping_profile_id, + shipping_option_type_id: shippingOption.type.id, }, resolver: zodResolver(EditShippingOptionSchema), }) @@ -92,6 +104,7 @@ export const EditShippingOptionForm = ({ price_type: values.price_type, shipping_profile_id: values.shipping_profile_id, rules, + type_id: values.shipping_option_type_id, }, { onSuccess: ({ shipping_option }) => { @@ -111,7 +124,10 @@ export const EditShippingOptionForm = ({ return ( - +
@@ -200,6 +216,32 @@ export const EditShippingOptionForm = ({ ) }} /> + + { + return ( + + + {t("stockLocations.shippingOptions.fields.type")} + + + + + + + ) + }} + />
diff --git a/packages/core/core-flows/src/fulfillment/steps/upsert-shipping-options.ts b/packages/core/core-flows/src/fulfillment/steps/upsert-shipping-options.ts index e9745df09b..14464685eb 100644 --- a/packages/core/core-flows/src/fulfillment/steps/upsert-shipping-options.ts +++ b/packages/core/core-flows/src/fulfillment/steps/upsert-shipping-options.ts @@ -4,12 +4,8 @@ import { ShippingOptionDTO, UpsertShippingOptionDTO, } from "@medusajs/framework/types" -import { - Modules, - arrayDifference, - getSelectsAndRelationsFromObjectArray, -} from "@medusajs/framework/utils" -import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { arrayDifference, getSelectsAndRelationsFromObjectArray, Modules, } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" /** * The data to create or update shipping options. @@ -65,7 +61,13 @@ export const upsertShippingOptionsStep = createStep( const upsertedShippingOptions: ShippingOptionDTO[] = await fulfillmentService.upsertShippingOptions( - input as UpsertShippingOptionDTO[] + input.map(inputItem => { + const upsertShippingOption = inputItem as UpsertShippingOptionDTO + if (inputItem.type_id) { + upsertShippingOption.type = inputItem.type_id + } + return upsertShippingOption; + }) ) const upsertedShippingOptionIds = upsertedShippingOptions.map((s) => s.id) diff --git a/packages/core/core-flows/src/fulfillment/steps/validate-shipping-option-prices.ts b/packages/core/core-flows/src/fulfillment/steps/validate-shipping-option-prices.ts index be6aca6fa3..9bb68f4ed8 100644 --- a/packages/core/core-flows/src/fulfillment/steps/validate-shipping-option-prices.ts +++ b/packages/core/core-flows/src/fulfillment/steps/validate-shipping-option-prices.ts @@ -1,10 +1,7 @@ import { FulfillmentWorkflow } from "@medusajs/framework/types" -import { - MedusaError, - Modules, - ShippingOptionPriceType, -} from "@medusajs/framework/utils" -import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { MedusaError, Modules, ShippingOptionPriceType, } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { CreateShippingOptionDTO } from "@medusajs/types" /** * The data to validate shipping option prices. @@ -96,7 +93,7 @@ export const validateShippingOptionPricesStep = createStep( const validation = await fulfillmentModuleService.validateShippingOptionsForPriceCalculation( - calculatedOptions as FulfillmentWorkflow.CreateShippingOptionsWorkflowInput[] + calculatedOptions as CreateShippingOptionDTO[] ) if (validation.some((v) => !v)) { diff --git a/packages/core/core-flows/src/fulfillment/workflows/create-shipping-options.ts b/packages/core/core-flows/src/fulfillment/workflows/create-shipping-options.ts index f4b7c17b84..40b6822658 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/create-shipping-options.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/create-shipping-options.ts @@ -6,10 +6,7 @@ import { WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { - createShippingOptionsPriceSetsStep, - upsertShippingOptionsStep, -} from "../steps" +import { createShippingOptionsPriceSetsStep, upsertShippingOptionsStep, } from "../steps" import { setShippingOptionsPriceSetsStep } from "../steps/set-shipping-options-price-sets" import { validateFulfillmentProvidersStep } from "../steps/validate-fulfillment-providers" import { validateShippingOptionPricesStep } from "../steps/validate-shipping-option-prices" diff --git a/packages/core/framework/src/http/utils/validate-body.ts b/packages/core/framework/src/http/utils/validate-body.ts index 18096bf716..4758ca6a9b 100644 --- a/packages/core/framework/src/http/utils/validate-body.ts +++ b/packages/core/framework/src/http/utils/validate-body.ts @@ -1,11 +1,12 @@ import { z } from "zod" import { NextFunction } from "express" import { MedusaRequest, MedusaResponse } from "../types" -import { zodValidator } from "../../zod/zod-helpers" +import { zodValidator } from "../../zod" export function validateAndTransformBody( zodSchema: | z.ZodObject + | z.ZodEffects | (( customSchema?: z.ZodOptional>> ) => z.ZodObject | z.ZodEffects) diff --git a/packages/core/types/src/fulfillment/mutations/shipping-option.ts b/packages/core/types/src/fulfillment/mutations/shipping-option.ts index 78ce919329..ad878ca45c 100644 --- a/packages/core/types/src/fulfillment/mutations/shipping-option.ts +++ b/packages/core/types/src/fulfillment/mutations/shipping-option.ts @@ -35,7 +35,7 @@ export interface CreateShippingOptionDTO { /** * The shipping option type associated with the shipping option. */ - type: CreateShippingOptionTypeDTO + type: CreateShippingOptionTypeDTO | string /** * The data necessary for the associated fulfillment provider to process the shipping option @@ -87,7 +87,7 @@ export interface UpdateShippingOptionDTO { * The shipping option type associated with the shipping option. */ type?: - | Omit + | CreateShippingOptionTypeDTO | string | { /** * The ID of the shipping option type. diff --git a/packages/core/types/src/http/shipping-option/admin/payloads.ts b/packages/core/types/src/http/shipping-option/admin/payloads.ts index 7d5fb495b3..7d025222af 100644 --- a/packages/core/types/src/http/shipping-option/admin/payloads.ts +++ b/packages/core/types/src/http/shipping-option/admin/payloads.ts @@ -156,7 +156,14 @@ export interface AdminCreateShippingOption { * Learn more in the [Shipping Option](https://docs.medusajs.com/resources/commerce-modules/fulfillment/shipping-option#shipping-profile-and-types) * documentation. */ - type: AdminCreateShippingOptionType + type?: AdminCreateShippingOptionType + /** + * The ID of the type of shipping option. + * + * Learn more in the [Shipping Option](https://docs.medusajs.com/resources/commerce-modules/fulfillment/shipping-option#shipping-profile-and-types) + * documentation. + */ + type_id?: string /** * The prices of the shipping option. */ @@ -254,6 +261,13 @@ export interface AdminUpdateShippingOption { * documentation. */ type?: AdminCreateShippingOptionType + /** + * The ID of the type of shipping option. + * + * Learn more in the [Shipping Option](https://docs.medusajs.com/resources/commerce-modules/fulfillment/shipping-option#shipping-profile-and-types) + * documentation. + */ + type_id?: string /** * The prices of the shipping option. */ diff --git a/packages/core/types/src/workflow/fulfillment/create-shipping-options.ts b/packages/core/types/src/workflow/fulfillment/create-shipping-options.ts index 71876e4c51..2976402d1d 100644 --- a/packages/core/types/src/workflow/fulfillment/create-shipping-options.ts +++ b/packages/core/types/src/workflow/fulfillment/create-shipping-options.ts @@ -39,7 +39,7 @@ type CreateFlatShippingOptionInputBase = { /** * The type of the shipping option. */ - type: { + type?: { /** * The label of the shipping option type. */ @@ -53,6 +53,10 @@ type CreateFlatShippingOptionInputBase = { */ code: string } + /** + * The ID of the type of shipping option. + */ + type_id?: string /** * The rules that determine when the shipping option is available. */ diff --git a/packages/core/types/src/workflow/fulfillment/update-shipping-options.ts b/packages/core/types/src/workflow/fulfillment/update-shipping-options.ts index f56fdd1f01..32547e8639 100644 --- a/packages/core/types/src/workflow/fulfillment/update-shipping-options.ts +++ b/packages/core/types/src/workflow/fulfillment/update-shipping-options.ts @@ -47,6 +47,10 @@ type UpdateFlatShippingOptionInputBase = { */ code: string } + /** + * The ID of the type of shipping option. + */ + type_id?: string /** * The rules that determine when the shipping option is available. */ diff --git a/packages/medusa/src/api/admin/shipping-options/validators.ts b/packages/medusa/src/api/admin/shipping-options/validators.ts index 58438b64ef..7d5aa795aa 100644 --- a/packages/medusa/src/api/admin/shipping-options/validators.ts +++ b/packages/medusa/src/api/admin/shipping-options/validators.ts @@ -10,6 +10,7 @@ import { createOperatorMap, createSelectParams, } from "../../utils/validators" +import { isDefined } from "@medusajs/utils" export type AdminGetShippingOptionParamsType = z.infer< typeof AdminGetShippingOptionParams @@ -127,9 +128,6 @@ export const AdminUpdateShippingOptionPriceWithRegion = z }) .strict() -export type AdminCreateShippingOptionType = z.infer< - typeof AdminCreateShippingOption -> export const AdminCreateShippingOption = z .object({ name: z.string(), @@ -138,13 +136,19 @@ export const AdminCreateShippingOption = z data: z.record(z.unknown()).optional(), price_type: z.nativeEnum(ShippingOptionPriceTypeEnum), provider_id: z.string(), - type: AdminCreateShippingOptionTypeObject, + type: AdminCreateShippingOptionTypeObject.optional(), + type_id: z.string().optional(), prices: AdminCreateShippingOptionPriceWithCurrency.or( AdminCreateShippingOptionPriceWithRegion ).array(), rules: AdminCreateShippingOptionRule.array().optional(), }) .strict() + .refine((data) => isDefined(data.type_id) !== isDefined(data.type), { + message: + "Exactly one of 'type' or 'type_id' must be provided, but not both", + path: ["type_id", "type"], + }) export type AdminUpdateShippingOptionType = z.infer< typeof AdminUpdateShippingOption @@ -157,6 +161,7 @@ export const AdminUpdateShippingOption = z provider_id: z.string().optional(), shipping_profile_id: z.string().optional(), type: AdminCreateShippingOptionTypeObject.optional(), + type_id: z.string().optional(), prices: AdminUpdateShippingOptionPriceWithCurrency.or( AdminUpdateShippingOptionPriceWithRegion ) @@ -167,3 +172,19 @@ export const AdminUpdateShippingOption = z .optional(), }) .strict() + .refine( + (data) => { + const hasType = isDefined(data.type) + const hasTypeId = isDefined(data.type_id) + + if (!hasType && !hasTypeId) { + return true + } + + return hasType !== hasTypeId + }, + { + message: "Only one of 'type' or 'type_id' can be provided", + path: ["type_id", "type"], + } + ) diff --git a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts index 1ba946ec96..767043a472 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts @@ -59,6 +59,7 @@ import { joinerConfig } from "../joiner-config" import { UpdateShippingOptionsInput } from "../types/service" import { buildCreatedShippingOptionEvents } from "../utils/events" import FulfillmentProviderService from "./fulfillment-provider" +import { isObject } from "@medusajs/utils" const generateMethodForModels = { FulfillmentSet, @@ -1408,9 +1409,9 @@ export default class FulfillmentModuleService dataArray.forEach((shippingOption) => { const existingShippingOption = existingShippingOptions.get( shippingOption.id - )! // Garuantueed to exist since the validation above have been performed + )! // Guaranteed to exist since the validation above have been performed - if (shippingOption.type && !("id" in shippingOption.type)) { + if (isObject(shippingOption.type) && !("id" in shippingOption.type)) { optionTypeDeletedIds.push(existingShippingOption.type.id) } @@ -1534,7 +1535,7 @@ export default class FulfillmentModuleService const createdOptionTypeIds = updatedShippingOptions .filter((so) => { const updateData = shippingOptionsData.find((sod) => sod.id === so.id) - return updateData?.type && !("id" in updateData.type) + return isObject(updateData?.type) && !("id" in updateData.type) }) .map((so) => so.type.id)