feat(core-flows, dashboard, fulfillment, fulfillment-manual, utils, types): create shipping options with calculated prices (#10495)

**What**
- support creating SO with calculated price
- support updating SO for both types of pricing
- update `validateShippingOptionPricesStep` to handle both SO price_types
- add the `validateShippingOptionsForPriceCalculation` method to `FulfillementModule`
- add `canCalculate` and `calculatePrice` to fulfillment provider service service / interface / manual provider
- disable SO pricing edit on Admin if SO price type is calculated

---

CLOSES CMRC-776
This commit is contained in:
Frane Polić
2024-12-11 09:38:44 +01:00
committed by GitHub
parent fad85a9d29
commit d8a92dbb2d
13 changed files with 253 additions and 53 deletions

View File

@@ -25,7 +25,7 @@ interface PriceRegionId {
export type SetShippingOptionsPricesStepInput = {
id: string
prices?: FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput["prices"]
prices?: FulfillmentWorkflow.UpdateShippingOptionPriceRecord[]
}[]
async function getCurrentShippingOptionPrices(

View File

@@ -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<string>()
allPrices.forEach((price) => {
flatRatePrices.forEach((price) => {
if ("region_id" in price && price.region_id) {
regionIdSet.add(price.region_id)
}

View File

@@ -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,

View File

@@ -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,
}
}