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)