diff --git a/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx b/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx
index 8ce5d7d13f7e3..a138571b243a2 100644
--- a/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx
+++ b/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx
@@ -47,7 +47,10 @@ import {
isOptionEnabledInStore,
isReturnOption,
} from "../../../../../lib/shipping-options"
-import { FulfillmentSetType } from "../../../common/constants"
+import {
+ FulfillmentSetType,
+ ShippingOptionPriceType,
+} from "../../../common/constants"
type LocationGeneralSectionProps = {
location: HttpTypes.AdminStockLocation
@@ -167,6 +170,8 @@ function ShippingOption({
{
label: t("stockLocations.shippingOptions.pricing.action"),
icon: ,
+ disabled:
+ option.price_type === ShippingOptionPriceType.Calculated,
to: `/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${option.service_zone_id}/shipping-option/${option.id}/pricing`,
},
],
diff --git a/packages/core/core-flows/src/fulfillment/steps/set-shipping-options-prices.ts b/packages/core/core-flows/src/fulfillment/steps/set-shipping-options-prices.ts
index 9aad02092577f..f58b3babba0f9 100644
--- a/packages/core/core-flows/src/fulfillment/steps/set-shipping-options-prices.ts
+++ b/packages/core/core-flows/src/fulfillment/steps/set-shipping-options-prices.ts
@@ -25,7 +25,7 @@ interface PriceRegionId {
export type SetShippingOptionsPricesStepInput = {
id: string
- prices?: FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput["prices"]
+ prices?: FulfillmentWorkflow.UpdateShippingOptionPriceRecord[]
}[]
async function getCurrentShippingOptionPrices(
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 ab07f9280693c..e6dc50d55d126 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,26 +1,85 @@
import { FulfillmentWorkflow } from "@medusajs/framework/types"
-import { MedusaError, Modules } from "@medusajs/framework/utils"
+import {
+ MedusaError,
+ Modules,
+ ShippingOptionPriceType,
+} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
+type OptionsInput = (
+ | FulfillmentWorkflow.CreateShippingOptionsWorkflowInput
+ | FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput
+)[]
+
export const validateShippingOptionPricesStepId =
"validate-shipping-option-prices"
/**
- * Validate that regions exist for the shipping option prices.
+ * Validate that shipping options can be crated based on provided price configuration.
+ *
+ * For flat rate prices, it validates that regions exist for the shipping option prices.
+ * For calculated prices, it validates with the fulfillment provider if the price can be calculated.
*/
export const validateShippingOptionPricesStep = createStep(
validateShippingOptionPricesStepId,
- async (
- options: {
- prices?: FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput["prices"]
- }[],
- { container }
- ) => {
- const allPrices = options.flatMap((option) => option.prices ?? [])
+ async (options: OptionsInput, { container }) => {
+ const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT)
+
+ const optionIds = options.map(
+ (option) =>
+ (option as FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput).id
+ )
+
+ if (optionIds.length) {
+ /**
+ * This means we are validating an update of shipping options.
+ * We need to ensure that all shipping options have price_type set
+ * to correctly determine price updates.
+ *
+ * (On create, price_type must be defined already.)
+ */
+ const shippingOptions =
+ await fulfillmentModuleService.listShippingOptions(
+ {
+ id: optionIds,
+ },
+ { select: ["id", "price_type", "provider_id"] }
+ )
+
+ const optionsMap = new Map(
+ shippingOptions.map((option) => [option.id, option])
+ )
+
+ ;(
+ options as FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput[]
+ ).forEach((option) => {
+ option.price_type =
+ option.price_type ?? optionsMap.get(option.id)?.price_type
+ option.provider_id =
+ option.provider_id ?? optionsMap.get(option.id)?.provider_id
+ })
+ }
+
+ const flatRatePrices: FulfillmentWorkflow.UpdateShippingOptionPriceRecord[] =
+ []
+ const calculatedOptions: OptionsInput = []
+
+ options.forEach((option) => {
+ if (option.price_type === ShippingOptionPriceType.FLAT) {
+ flatRatePrices.push(...(option.prices ?? []))
+ }
+ if (option.price_type === ShippingOptionPriceType.CALCULATED) {
+ calculatedOptions.push(option)
+ }
+ })
+
+ await fulfillmentModuleService.validateShippingOptionsForPriceCalculation(
+ calculatedOptions as FulfillmentWorkflow.CreateShippingOptionsWorkflowInput[]
+ )
const regionIdSet = new Set()
- allPrices.forEach((price) => {
+ flatRatePrices.forEach((price) => {
if ("region_id" in price && price.region_id) {
regionIdSet.add(price.region_id)
}
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 dda9dbef0d213..e8a6de0caa38e 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
@@ -33,7 +33,13 @@ export const createShippingOptionsWorkflow = createWorkflow(
const data = transform(input, (data) => {
const shippingOptionsIndexToPrices = data.map((option, index) => {
- const prices = option.prices
+ /**
+ * Flat rate ShippingOptions always needs to provide a price array.
+ *
+ * For calculated pricing we create an "empty" price set
+ * so we can have simpler update flow for both cases and allow updating price_type.
+ */
+ const prices = (option as any).prices ?? []
return {
shipping_option_index: index,
prices,
diff --git a/packages/core/core-flows/src/fulfillment/workflows/update-shipping-options.ts b/packages/core/core-flows/src/fulfillment/workflows/update-shipping-options.ts
index 0c2a624b0645e..9f85d9eec93b9 100644
--- a/packages/core/core-flows/src/fulfillment/workflows/update-shipping-options.ts
+++ b/packages/core/core-flows/src/fulfillment/workflows/update-shipping-options.ts
@@ -12,6 +12,7 @@ import {
} from "../steps"
import { validateFulfillmentProvidersStep } from "../steps/validate-fulfillment-providers"
import { validateShippingOptionPricesStep } from "../steps/validate-shipping-option-prices"
+import { ShippingOptionPriceType } from "@medusajs/framework/utils"
export const updateShippingOptionsWorkflowId =
"update-shipping-options-workflow"
@@ -32,11 +33,22 @@ export const updateShippingOptionsWorkflow = createWorkflow(
const data = transform(input, (data) => {
const shippingOptionsIndexToPrices = data.map((option, index) => {
- const prices = option.prices
- delete option.prices
+ const prices = (
+ option as FulfillmentWorkflow.UpdateFlatRateShippingOptionInput
+ ).prices
+
+ delete (option as FulfillmentWorkflow.UpdateFlatRateShippingOptionInput)
+ .prices
+
+ /**
+ * When we are updating an option to be calculated, remove the prices.
+ */
+ const isCalculatedOption =
+ option.price_type === ShippingOptionPriceType.CALCULATED
+
return {
shipping_option_index: index,
- prices,
+ prices: isCalculatedOption ? [] : prices,
}
})
@@ -58,8 +70,10 @@ export const updateShippingOptionsWorkflow = createWorkflow(
(data) => {
const shippingOptionsPrices = data.shippingOptionsIndexToPrices.map(
({ shipping_option_index, prices }) => {
+ const option = data.shippingOptions[shipping_option_index]
+
return {
- id: data.shippingOptions[shipping_option_index].id,
+ id: option.id,
prices,
}
}
diff --git a/packages/core/types/src/fulfillment/provider.ts b/packages/core/types/src/fulfillment/provider.ts
index 613a496c9f256..6729bc490aece 100644
--- a/packages/core/types/src/fulfillment/provider.ts
+++ b/packages/core/types/src/fulfillment/provider.ts
@@ -12,6 +12,11 @@ export type FulfillmentOption = {
[k: string]: unknown
}
+export type CalculatedShippingOptionPrice = {
+ calculated_amount: number
+ is_calculated_price_tax_inclusive: boolean
+}
+
export interface IFulfillmentProvider {
/**
*
@@ -41,7 +46,7 @@ export interface IFulfillmentProvider {
*
* Check if the provider can calculate the fulfillment price.
*/
- canCalculate(data: Record): Promise
+ canCalculate(data: Record): Promise
/**
*
* Calculate the price for the given fulfillment option.
@@ -50,7 +55,7 @@ export interface IFulfillmentProvider {
optionData: Record,
data: Record,
context: Record
- ): Promise
+ ): Promise
/**
*
* Create a fulfillment for the given data.
diff --git a/packages/core/types/src/fulfillment/service.ts b/packages/core/types/src/fulfillment/service.ts
index a0642822ebdab..46017e7b34a85 100644
--- a/packages/core/types/src/fulfillment/service.ts
+++ b/packages/core/types/src/fulfillment/service.ts
@@ -2625,6 +2625,28 @@ export interface IFulfillmentModuleService extends IModuleService {
context: Record
): Promise
+ /**
+ * This method checks whether a shipping option can have calculated price.
+ *
+ * @param {FulfillmentTypes.CreateShippingOptionDTO[]} shippingOptionsData - The shipping options data to check.
+ * @returns {Promise} Whether the shipping options can have calculated price.
+ *
+ * @example
+ * const isValid =
+ * await fulfillmentModuleService.validateShippingOptionsForPriceCalculation(
+ * [
+ * {
+ * provider_id: "webshipper",
+ * price_type: "calculated",
+ * },
+ * ]
+ * )
+ */
+ validateShippingOptionsForPriceCalculation(
+ shippingOptionsData: CreateShippingOptionDTO[],
+ sharedContext?: Context
+ ): Promise
+
/**
* This method retrieves a paginated list of fulfillment providers based on optional filters and configuration.
*
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 bb207b8649afd..15a7563f2854c 100644
--- a/packages/core/types/src/workflow/fulfillment/create-shipping-options.ts
+++ b/packages/core/types/src/workflow/fulfillment/create-shipping-options.ts
@@ -1,28 +1,27 @@
-import { ShippingOptionDTO, ShippingOptionPriceType } from "../../fulfillment"
+import { ShippingOptionDTO } from "../../fulfillment"
import { RuleOperatorType } from "../../common"
-export interface CreateShippingOptionsWorkflowInput {
+type CreateFlatRateShippingOptionPriceRecord =
+ | {
+ currency_code: string
+ amount: number
+ }
+ | {
+ region_id: string
+ amount: number
+ }
+
+type CreateFlatShippingOptionInputBase = {
name: string
service_zone_id: string
shipping_profile_id: string
data?: Record
- price_type: ShippingOptionPriceType
provider_id: string
type: {
label: string
description: string
code: string
}
- prices: (
- | {
- currency_code: string
- amount: number
- }
- | {
- region_id: string
- amount: number
- }
- )[]
rules?: {
attribute: string
operator: RuleOperatorType
@@ -30,4 +29,17 @@ export interface CreateShippingOptionsWorkflowInput {
}[]
}
+type CreateFlatRateShippingOptionInput = CreateFlatShippingOptionInputBase & {
+ price_type: "flat"
+ prices: CreateFlatRateShippingOptionPriceRecord[]
+}
+
+type CreateCalculatedShippingOptionInput = CreateFlatShippingOptionInputBase & {
+ price_type: "calculated"
+}
+
+export type CreateShippingOptionsWorkflowInput =
+ | CreateFlatRateShippingOptionInput
+ | CreateCalculatedShippingOptionInput
+
export type CreateShippingOptionsWorkflowOutput = ShippingOptionDTO[]
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 4e1ad086a7e99..573be19684ba0 100644
--- a/packages/core/types/src/workflow/fulfillment/update-shipping-options.ts
+++ b/packages/core/types/src/workflow/fulfillment/update-shipping-options.ts
@@ -1,34 +1,18 @@
import { RuleOperatorType } from "../../common"
-import { ShippingOptionPriceType } from "../../fulfillment"
import { PriceRule } from "../../pricing"
-export interface UpdateShippingOptionsWorkflowInput {
+type UpdateFlatShippingOptionInputBase = {
id: string
name?: string
service_zone_id?: string
shipping_profile_id?: string
data?: Record
- price_type?: ShippingOptionPriceType
provider_id?: string
type?: {
label: string
description: string
code: string
}
- prices?: (
- | {
- id?: string
- currency_code?: string
- amount?: number
- rules?: PriceRule[]
- }
- | {
- id?: string
- region_id?: string
- amount?: number
- rules?: PriceRule[]
- }
- )[]
rules?: {
attribute: string
operator: RuleOperatorType
@@ -36,6 +20,35 @@ export interface UpdateShippingOptionsWorkflowInput {
}[]
}
+export type UpdateShippingOptionPriceRecord =
+ | {
+ id?: string
+ currency_code?: string
+ amount?: number
+ rules?: PriceRule[]
+ }
+ | {
+ id?: string
+ region_id?: string
+ amount?: number
+ rules?: PriceRule[]
+ }
+
+export type UpdateCalculatedShippingOptionInput =
+ UpdateFlatShippingOptionInputBase & {
+ price_type?: "calculated"
+ }
+
+export type UpdateFlatRateShippingOptionInput =
+ UpdateFlatShippingOptionInputBase & {
+ price_type?: "flat"
+ prices?: UpdateShippingOptionPriceRecord[]
+ }
+
+export type UpdateShippingOptionsWorkflowInput =
+ | UpdateFlatRateShippingOptionInput
+ | UpdateCalculatedShippingOptionInput
+
export type UpdateShippingOptionsWorkflowOutput = {
id: string
}[]
diff --git a/packages/core/utils/src/fulfillment/provider.ts b/packages/core/utils/src/fulfillment/provider.ts
index 53715f6a05043..781c2cf58dff8 100644
--- a/packages/core/utils/src/fulfillment/provider.ts
+++ b/packages/core/utils/src/fulfillment/provider.ts
@@ -1,4 +1,8 @@
-import { FulfillmentOption, IFulfillmentProvider } from "@medusajs/types"
+import {
+ CalculatedShippingOptionPrice,
+ FulfillmentOption,
+ IFulfillmentProvider,
+} from "@medusajs/types"
/**
* ### constructor
@@ -221,7 +225,7 @@ export class AbstractFulfillmentProviderService
optionData: Record,
data: Record,
context: Record
- ): Promise {
+ ): Promise {
throw Error("calculatePrice must be overridden by the child class")
}
@@ -345,7 +349,9 @@ export class AbstractFulfillmentProviderService
* }
* }
*/
- async createReturnFulfillment(fulfillment: Record): Promise {
+ async createReturnFulfillment(
+ fulfillment: Record
+ ): Promise {
throw Error("createReturn must be overridden by the child class")
}
diff --git a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts
index 486ecf19e663b..dd43d15ac299b 100644
--- a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts
+++ b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts
@@ -1970,6 +1970,34 @@ export default class FulfillmentModuleService
return !!shippingOptions.length
}
+ @InjectManager()
+ async validateShippingOptionsForPriceCalculation(
+ shippingOptionsData: FulfillmentTypes.CreateShippingOptionDTO[],
+ @MedusaContext() sharedContext: Context = {}
+ ): Promise {
+ const nonCalculatedOptions = shippingOptionsData.filter(
+ (option) => option.price_type !== "calculated"
+ )
+
+ if (nonCalculatedOptions.length) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Cannot calculate price for non-calculated shipping options: ${nonCalculatedOptions
+ .map((o) => o.name)
+ .join(", ")}`
+ )
+ }
+
+ const promises = shippingOptionsData.map((option) =>
+ this.fulfillmentProviderService_.canCalculate(
+ option.provider_id,
+ option as unknown as Record
+ )
+ )
+
+ return await promiseAll(promises)
+ }
+
@InjectTransactionManager()
// @ts-expect-error
async deleteShippingProfiles(
diff --git a/packages/modules/fulfillment/src/services/fulfillment-provider.ts b/packages/modules/fulfillment/src/services/fulfillment-provider.ts
index ca926f66d58dd..16876730095d8 100644
--- a/packages/modules/fulfillment/src/services/fulfillment-provider.ts
+++ b/packages/modules/fulfillment/src/services/fulfillment-provider.ts
@@ -100,6 +100,21 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.MedusaIn
return await provider.validateOption(data)
}
+ async canCalculate(providerId: string, data: Record) {
+ const provider = this.retrieveProviderRegistration(providerId)
+ return await provider.canCalculate(data)
+ }
+
+ async calculatePrice(
+ providerId: string,
+ optionData: Record,
+ data: Record,
+ context: Record
+ ) {
+ const provider = this.retrieveProviderRegistration(providerId)
+ return await provider.calculatePrice(optionData, data, context)
+ }
+
async createFulfillment(
providerId: string,
data: object,
diff --git a/packages/modules/providers/fulfillment-manual/src/services/manual-fulfillment.ts b/packages/modules/providers/fulfillment-manual/src/services/manual-fulfillment.ts
index 5734782c92169..d560a4e051d80 100644
--- a/packages/modules/providers/fulfillment-manual/src/services/manual-fulfillment.ts
+++ b/packages/modules/providers/fulfillment-manual/src/services/manual-fulfillment.ts
@@ -1,5 +1,8 @@
import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils"
-import { FulfillmentOption } from "@medusajs/types"
+import {
+ CalculatedShippingOptionPrice,
+ FulfillmentOption,
+} from "@medusajs/types"
// TODO rework type and DTO's
@@ -30,6 +33,18 @@ export class ManualFulfillmentService extends AbstractFulfillmentProviderService
return data
}
+ async calculatePrice(
+ optionData: Record,
+ data: Record,
+ context: Record
+ ): Promise {
+ throw new Error("Manual fulfillment does not support price calculation")
+ }
+
+ async canCalculate(): Promise {
+ return false
+ }
+
async validateOption(data: Record): Promise {
return true
}