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
This commit is contained in:
William Bouchard authored and GitHub committed 2025-08-19 11:02:36 -04:00
1 parent b1ee204369
commit 67d3660abf
18 files changed
+524 -41

No files matched your search

+9
View File
@@ -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
@@ -17,6 +17,7 @@ medusaIntegrationTestRunner({
let appContainer let appContainer
let location let location
let location2 let location2
let type
const shippingOptionRule = { const shippingOptionRule = {
operator: RuleOperator.EQ, operator: RuleOperator.EQ,
@@ -92,6 +93,17 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
).data.region ).data.region
type = (
await api.post(
`/admin/shipping-option-types`,
{
label: "Test",
code: 'test',
},
adminHeaders
)
).data.shipping_option_type
}) })
describe("GET /admin/shipping-options", () => { 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 () => { it("should throw error when creating a price rule with a non white listed attribute", async () => {
const shippingOptionPayload = { const shippingOptionPayload = {
name: "Test shipping option", name: "Test shipping option",
@@ -552,6 +661,72 @@ medusaIntegrationTestRunner({
"Providers (does-not-exist) are not enabled for the service location" "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", () => { describe("POST /admin/shipping-options/:id", () => {
@@ -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 () => { it("should throw an error when provider does not belong to service location", async () => {
const shippingOptionPayload = { const shippingOptionPayload = {
name: "Test shipping option", name: "Test shipping option",
@@ -785,6 +1089,60 @@ medusaIntegrationTestRunner({
"Providers (another_test-provider) are not enabled for the service location" "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", () => { describe("DELETE /admin/shipping-options/:id", () => {
@@ -6308,6 +6308,9 @@
"profile": { "profile": {
"type": "string" "type": "string"
}, },
"type": {
"type": "string"
},
"fulfillmentOption": { "fulfillmentOption": {
"type": "string" "type": "string"
} }
@@ -6318,6 +6321,7 @@
"enableInStore", "enableInStore",
"provider", "provider",
"profile", "profile",
"type",
"fulfillmentOption" "fulfillmentOption"
], ],
"additionalProperties": false "additionalProperties": false
@@ -1683,6 +1683,7 @@
}, },
"provider": "Fulfillment provider", "provider": "Fulfillment provider",
"profile": "Shipping profile", "profile": "Shipping profile",
"type": "Shipping option type",
"fulfillmentOption": "Fulfillment option" "fulfillmentOption": "Fulfillment option"
} }
}, },
@@ -10,10 +10,7 @@ import { Combobox } from "../../../../../components/inputs/combobox"
import { useComboboxData } from "../../../../../hooks/use-combobox-data" import { useComboboxData } from "../../../../../hooks/use-combobox-data"
import { sdk } from "../../../../../lib/client" import { sdk } from "../../../../../lib/client"
import { formatProvider } from "../../../../../lib/format-provider" import { formatProvider } from "../../../../../lib/format-provider"
import { import { FulfillmentSetType, ShippingOptionPriceType, } from "../../../common/constants"
FulfillmentSetType,
ShippingOptionPriceType,
} from "../../../common/constants"
import { CreateShippingOptionSchema } from "./schema" import { CreateShippingOptionSchema } from "./schema"
type CreateShippingOptionDetailsFormProps = { 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({ const fulfillmentProviders = useComboboxData({
queryFn: (params) => queryFn: (params) =>
sdk.admin.fulfillmentProvider.list({ sdk.admin.fulfillmentProvider.list({
@@ -170,6 +177,31 @@ export const CreateShippingOptionDetailsForm = ({
) )
}} }}
/> />
<Form.Field
control={form.control}
name="shipping_option_type_id"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("stockLocations.shippingOptions.fields.type")}
</Form.Label>
<Form.Control>
<Combobox
{...field}
options={shippingOptionTypes.options}
searchValue={shippingOptionTypes.searchValue}
onSearchValueChange={
shippingOptionTypes.onSearchValueChange
}
disabled={shippingOptionTypes.disabled}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div> </div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -167,12 +167,7 @@ export function CreateShippingOptionsForm({
operator: "eq", operator: "eq",
}, },
], ],
type: { type_id: data.shipping_option_type_id,
// TODO: FETCH TYPES
label: "Type label",
description: "Type description",
code: "type-code",
},
}, },
{ {
onSuccess: ({ shipping_option }) => { onSuccess: ({ shipping_option }) => {
@@ -13,6 +13,7 @@ export const CreateShippingOptionDetailsSchema = z.object({
shipping_profile_id: z.string().min(1), shipping_profile_id: z.string().min(1),
provider_id: z.string().min(1), provider_id: z.string().min(1),
fulfillment_option_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({ export const ShippingOptionConditionalPriceSchema = z.object({
@@ -31,6 +31,7 @@ const EditShippingOptionSchema = zod.object({
price_type: zod.nativeEnum(ShippingOptionPriceType), price_type: zod.nativeEnum(ShippingOptionPriceType),
enabled_in_store: zod.boolean().optional(), enabled_in_store: zod.boolean().optional(),
shipping_profile_id: zod.string(), shipping_profile_id: zod.string(),
shipping_option_type_id: zod.string(),
}) })
export const EditShippingOptionForm = ({ export const EditShippingOptionForm = ({
@@ -54,12 +55,23 @@ export const EditShippingOptionForm = ({
defaultValue: shippingOption.shipping_profile_id, 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<zod.infer<typeof EditShippingOptionSchema>>({ const form = useForm<zod.infer<typeof EditShippingOptionSchema>>({
defaultValues: { defaultValues: {
name: shippingOption.name, name: shippingOption.name,
price_type: shippingOption.price_type as ShippingOptionPriceType, price_type: shippingOption.price_type as ShippingOptionPriceType,
enabled_in_store: isOptionEnabledInStore(shippingOption), enabled_in_store: isOptionEnabledInStore(shippingOption),
shipping_profile_id: shippingOption.shipping_profile_id, shipping_profile_id: shippingOption.shipping_profile_id,
shipping_option_type_id: shippingOption.type.id,
}, },
resolver: zodResolver(EditShippingOptionSchema), resolver: zodResolver(EditShippingOptionSchema),
}) })
@@ -92,6 +104,7 @@ export const EditShippingOptionForm = ({
price_type: values.price_type, price_type: values.price_type,
shipping_profile_id: values.shipping_profile_id, shipping_profile_id: values.shipping_profile_id,
rules, rules,
type_id: values.shipping_option_type_id,
}, },
{ {
onSuccess: ({ shipping_option }) => { onSuccess: ({ shipping_option }) => {
@@ -111,7 +124,10 @@ export const EditShippingOptionForm = ({
return ( return (
<RouteDrawer.Form form={form}> <RouteDrawer.Form form={form}>
<KeyboundForm onSubmit={handleSubmit} className="flex flex-1 flex-col overflow-hidden"> <KeyboundForm
onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden"
>
<RouteDrawer.Body className="overflow-y-auto"> <RouteDrawer.Body className="overflow-y-auto">
<div className="flex flex-col gap-y-8"> <div className="flex flex-col gap-y-8">
<div className="flex flex-col gap-y-8"> <div className="flex flex-col gap-y-8">
@@ -200,6 +216,32 @@ export const EditShippingOptionForm = ({
) )
}} }}
/> />
<Form.Field
control={form.control}
name="shipping_option_type_id"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("stockLocations.shippingOptions.fields.type")}
</Form.Label>
<Form.Control>
<Combobox
{...field}
options={shippingOptionTypes.options}
searchValue={shippingOptionTypes.searchValue}
onSearchValueChange={
shippingOptionTypes.onSearchValueChange
}
disabled={shippingOptionTypes.disabled}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div> </div>
<Divider /> <Divider />
@@ -4,12 +4,8 @@ import {
ShippingOptionDTO, ShippingOptionDTO,
UpsertShippingOptionDTO, UpsertShippingOptionDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import { arrayDifference, getSelectsAndRelationsFromObjectArray, Modules, } from "@medusajs/framework/utils"
Modules, import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
arrayDifference,
getSelectsAndRelationsFromObjectArray,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
/** /**
* The data to create or update shipping options. * The data to create or update shipping options.
@@ -65,7 +61,13 @@ export const upsertShippingOptionsStep = createStep(
const upsertedShippingOptions: ShippingOptionDTO[] = const upsertedShippingOptions: ShippingOptionDTO[] =
await fulfillmentService.upsertShippingOptions( 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) const upsertedShippingOptionIds = upsertedShippingOptions.map((s) => s.id)
@@ -1,10 +1,7 @@
import { FulfillmentWorkflow } from "@medusajs/framework/types" import { FulfillmentWorkflow } from "@medusajs/framework/types"
import { import { MedusaError, Modules, ShippingOptionPriceType, } from "@medusajs/framework/utils"
MedusaError, import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
Modules, import { CreateShippingOptionDTO } from "@medusajs/types"
ShippingOptionPriceType,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
/** /**
* The data to validate shipping option prices. * The data to validate shipping option prices.
@@ -96,7 +93,7 @@ export const validateShippingOptionPricesStep = createStep(
const validation = const validation =
await fulfillmentModuleService.validateShippingOptionsForPriceCalculation( await fulfillmentModuleService.validateShippingOptionsForPriceCalculation(
calculatedOptions as FulfillmentWorkflow.CreateShippingOptionsWorkflowInput[] calculatedOptions as CreateShippingOptionDTO[]
) )
if (validation.some((v) => !v)) { if (validation.some((v) => !v)) {
@@ -6,10 +6,7 @@ import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { import { createShippingOptionsPriceSetsStep, upsertShippingOptionsStep, } from "../steps"
createShippingOptionsPriceSetsStep,
upsertShippingOptionsStep,
} from "../steps"
import { setShippingOptionsPriceSetsStep } from "../steps/set-shipping-options-price-sets" import { setShippingOptionsPriceSetsStep } from "../steps/set-shipping-options-price-sets"
import { validateFulfillmentProvidersStep } from "../steps/validate-fulfillment-providers" import { validateFulfillmentProvidersStep } from "../steps/validate-fulfillment-providers"
import { validateShippingOptionPricesStep } from "../steps/validate-shipping-option-prices" import { validateShippingOptionPricesStep } from "../steps/validate-shipping-option-prices"
@@ -1,11 +1,12 @@
import { z } from "zod" import { z } from "zod"
import { NextFunction } from "express" import { NextFunction } from "express"
import { MedusaRequest, MedusaResponse } from "../types" import { MedusaRequest, MedusaResponse } from "../types"
import { zodValidator } from "../../zod/zod-helpers" import { zodValidator } from "../../zod"
export function validateAndTransformBody( export function validateAndTransformBody(
zodSchema: zodSchema:
| z.ZodObject<any, any> | z.ZodObject<any, any>
| z.ZodEffects<any, any>
| (( | ((
customSchema?: z.ZodOptional<z.ZodNullable<z.ZodObject<any, any>>> customSchema?: z.ZodOptional<z.ZodNullable<z.ZodObject<any, any>>>
) => z.ZodObject<any, any> | z.ZodEffects<any, any>) ) => z.ZodObject<any, any> | z.ZodEffects<any, any>)
@@ -35,7 +35,7 @@ export interface CreateShippingOptionDTO {
/** /**
* The shipping option type associated with the shipping option. * 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 * 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. * The shipping option type associated with the shipping option.
*/ */
type?: type?:
| Omit<CreateShippingOptionTypeDTO, "shipping_option_id"> | CreateShippingOptionTypeDTO | string
| { | {
/** /**
* The ID of the shipping option type. * The ID of the shipping option type.
@@ -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) * Learn more in the [Shipping Option](https://docs.medusajs.com/resources/commerce-modules/fulfillment/shipping-option#shipping-profile-and-types)
* documentation. * 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. * The prices of the shipping option.
*/ */
@@ -254,6 +261,13 @@ export interface AdminUpdateShippingOption {
* documentation. * 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. * The prices of the shipping option.
*/ */
@@ -39,7 +39,7 @@ type CreateFlatShippingOptionInputBase = {
/** /**
* The type of the shipping option. * The type of the shipping option.
*/ */
type: { type?: {
/** /**
* The label of the shipping option type. * The label of the shipping option type.
*/ */
@@ -53,6 +53,10 @@ type CreateFlatShippingOptionInputBase = {
*/ */
code: string code: string
} }
/**
* The ID of the type of shipping option.
*/
type_id?: string
/** /**
* The rules that determine when the shipping option is available. * The rules that determine when the shipping option is available.
*/ */
@@ -47,6 +47,10 @@ type UpdateFlatShippingOptionInputBase = {
*/ */
code: string code: string
} }
/**
* The ID of the type of shipping option.
*/
type_id?: string
/** /**
* The rules that determine when the shipping option is available. * The rules that determine when the shipping option is available.
*/ */
@@ -10,6 +10,7 @@ import {
createOperatorMap, createOperatorMap,
createSelectParams, createSelectParams,
} from "../../utils/validators" } from "../../utils/validators"
import { isDefined } from "@medusajs/utils"
export type AdminGetShippingOptionParamsType = z.infer< export type AdminGetShippingOptionParamsType = z.infer<
typeof AdminGetShippingOptionParams typeof AdminGetShippingOptionParams
@@ -127,9 +128,6 @@ export const AdminUpdateShippingOptionPriceWithRegion = z
}) })
.strict() .strict()
export type AdminCreateShippingOptionType = z.infer<
typeof AdminCreateShippingOption
>
export const AdminCreateShippingOption = z export const AdminCreateShippingOption = z
.object({ .object({
name: z.string(), name: z.string(),
@@ -138,13 +136,19 @@ export const AdminCreateShippingOption = z
data: z.record(z.unknown()).optional(), data: z.record(z.unknown()).optional(),
price_type: z.nativeEnum(ShippingOptionPriceTypeEnum), price_type: z.nativeEnum(ShippingOptionPriceTypeEnum),
provider_id: z.string(), provider_id: z.string(),
type: AdminCreateShippingOptionTypeObject, type: AdminCreateShippingOptionTypeObject.optional(),
type_id: z.string().optional(),
prices: AdminCreateShippingOptionPriceWithCurrency.or( prices: AdminCreateShippingOptionPriceWithCurrency.or(
AdminCreateShippingOptionPriceWithRegion AdminCreateShippingOptionPriceWithRegion
).array(), ).array(),
rules: AdminCreateShippingOptionRule.array().optional(), rules: AdminCreateShippingOptionRule.array().optional(),
}) })
.strict() .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< export type AdminUpdateShippingOptionType = z.infer<
typeof AdminUpdateShippingOption typeof AdminUpdateShippingOption
@@ -157,6 +161,7 @@ export const AdminUpdateShippingOption = z
provider_id: z.string().optional(), provider_id: z.string().optional(),
shipping_profile_id: z.string().optional(), shipping_profile_id: z.string().optional(),
type: AdminCreateShippingOptionTypeObject.optional(), type: AdminCreateShippingOptionTypeObject.optional(),
type_id: z.string().optional(),
prices: AdminUpdateShippingOptionPriceWithCurrency.or( prices: AdminUpdateShippingOptionPriceWithCurrency.or(
AdminUpdateShippingOptionPriceWithRegion AdminUpdateShippingOptionPriceWithRegion
) )
@@ -167,3 +172,19 @@ export const AdminUpdateShippingOption = z
.optional(), .optional(),
}) })
.strict() .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"],
}
)
@@ -59,6 +59,7 @@ import { joinerConfig } from "../joiner-config"
import { UpdateShippingOptionsInput } from "../types/service" import { UpdateShippingOptionsInput } from "../types/service"
import { buildCreatedShippingOptionEvents } from "../utils/events" import { buildCreatedShippingOptionEvents } from "../utils/events"
import FulfillmentProviderService from "./fulfillment-provider" import FulfillmentProviderService from "./fulfillment-provider"
import { isObject } from "@medusajs/utils"
const generateMethodForModels = { const generateMethodForModels = {
FulfillmentSet, FulfillmentSet,
@@ -1408,9 +1409,9 @@ export default class FulfillmentModuleService
dataArray.forEach((shippingOption) => { dataArray.forEach((shippingOption) => {
const existingShippingOption = existingShippingOptions.get( const existingShippingOption = existingShippingOptions.get(
shippingOption.id 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) optionTypeDeletedIds.push(existingShippingOption.type.id)
} }
@@ -1534,7 +1535,7 @@ export default class FulfillmentModuleService
const createdOptionTypeIds = updatedShippingOptions const createdOptionTypeIds = updatedShippingOptions
.filter((so) => { .filter((so) => {
const updateData = shippingOptionsData.find((sod) => sod.id === so.id) 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) .map((so) => so.type.id)