feat: Create shipping options workflow (#6962)
**What** - Create main workflow - Update create pricing rule types step to be idempotent - update remote joiner to support granular isList confguration on field aliases - Add full workflow integration tests
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
CreatePriceSetDTO,
|
||||
IPricingModuleService,
|
||||
IRegionModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
interface PriceCurrencyCode {
|
||||
currency_code: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
interface PriceRegionId {
|
||||
region_id: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
type StepInput = {
|
||||
id: string
|
||||
prices: (PriceCurrencyCode | PriceRegionId)[]
|
||||
}[]
|
||||
|
||||
function buildPriceSet(
|
||||
prices: StepInput[0]["prices"],
|
||||
regionToCurrencyMap: Map<string, string>
|
||||
): CreatePriceSetDTO {
|
||||
const rules: CreatePriceSetDTO["rules"] = []
|
||||
|
||||
const shippingOptionPrices = prices.map((price) => {
|
||||
if ("currency_code" in price) {
|
||||
return {
|
||||
currency_code: price.currency_code,
|
||||
amount: price.amount,
|
||||
}
|
||||
}
|
||||
|
||||
rules.push({
|
||||
rule_attribute: "region_id",
|
||||
})
|
||||
|
||||
return {
|
||||
currency_code: regionToCurrencyMap.get(price.region_id)!,
|
||||
amount: price.amount,
|
||||
rules: {
|
||||
region_id: price.region_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return { rules, prices: shippingOptionPrices }
|
||||
}
|
||||
|
||||
export const createShippingOptionsPriceSetsStepId =
|
||||
"add-shipping-options-prices-step"
|
||||
export const createShippingOptionsPriceSetsStep = createStep(
|
||||
createShippingOptionsPriceSetsStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
if (!data?.length) {
|
||||
return new StepResponse([], [])
|
||||
}
|
||||
|
||||
const regionIds = data
|
||||
.map((input) => input.prices)
|
||||
.flat()
|
||||
.filter((price): price is PriceRegionId => {
|
||||
return "region_id" in price
|
||||
})
|
||||
.map((price) => price.region_id)
|
||||
|
||||
let regionToCurrencyMap: Map<string, string> = new Map()
|
||||
|
||||
if (regionIds.length) {
|
||||
const regionService = container.resolve<IRegionModuleService>(
|
||||
ModuleRegistrationName.REGION
|
||||
)
|
||||
const regions = await regionService.list(
|
||||
{
|
||||
id: [...new Set(regionIds)],
|
||||
},
|
||||
{
|
||||
select: ["id", "currency_code"],
|
||||
}
|
||||
)
|
||||
|
||||
regionToCurrencyMap = new Map(
|
||||
regions.map((region) => [region.id, region.currency_code])
|
||||
)
|
||||
}
|
||||
|
||||
const priceSetsData = data.map((input) =>
|
||||
buildPriceSet(input.prices, regionToCurrencyMap)
|
||||
)
|
||||
|
||||
const pricingService = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
const priceSets = await pricingService.create(priceSetsData)
|
||||
|
||||
const shippingOptionPriceSetLinData = data.map((input, index) => {
|
||||
return {
|
||||
id: input.id,
|
||||
priceSetId: priceSets[index].id,
|
||||
}
|
||||
})
|
||||
|
||||
return new StepResponse(
|
||||
shippingOptionPriceSetLinData,
|
||||
priceSets.map((priceSet) => priceSet.id)
|
||||
)
|
||||
},
|
||||
async (priceSetIds, { container }) => {
|
||||
if (!priceSetIds?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const pricingService = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
await pricingService.delete(priceSetIds)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
FulfillmentWorkflow,
|
||||
IFulfillmentModuleService,
|
||||
ShippingOptionDTO,
|
||||
} from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
type StepInput = Omit<
|
||||
FulfillmentWorkflow.CreateShippingOptionsWorkflowInput,
|
||||
"prices"
|
||||
>[]
|
||||
|
||||
export const createShippingOptionsStepId = "create-shipping-options-step"
|
||||
export const createShippingOptionsStep = createStep(
|
||||
createShippingOptionsStepId,
|
||||
async (input: StepInput, { container }) => {
|
||||
if (!input?.length) {
|
||||
return new StepResponse([], [])
|
||||
}
|
||||
|
||||
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
const createdShippingOptions: ShippingOptionDTO[] =
|
||||
await fulfillmentService.createShippingOptions(input)
|
||||
|
||||
const shippingOptionIds = createdShippingOptions.map((s) => s.id)
|
||||
|
||||
return new StepResponse(createdShippingOptions, shippingOptionIds)
|
||||
},
|
||||
async (shippingOptionIds, { container }) => {
|
||||
if (!shippingOptionIds?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
await fulfillmentService.deleteShippingOptions(shippingOptionIds)
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./add-rules-to-fulfillment-shipping-option"
|
||||
export * from "./create-fulfillment-set"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
export * from "./create-shipping-options"
|
||||
export * from "./add-shipping-options-prices"
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { RemoteLink } from "@medusajs/modules-sdk"
|
||||
import { RemoteQueryFunction } from "@medusajs/types"
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
LINKS,
|
||||
Modules,
|
||||
promiseAll,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
type SetShippingOptionsPriceSetsStepInput = {
|
||||
id: string
|
||||
price_sets?: string[]
|
||||
}[]
|
||||
|
||||
interface FilteredSetShippingOptionsPriceSetsStepInput {
|
||||
id: string
|
||||
price_sets: string[]
|
||||
}
|
||||
|
||||
type LinkItems = {
|
||||
[Modules.FULFILLMENT]: { shipping_option_id: string }
|
||||
[Modules.PRICING]: { price_set_id: string }
|
||||
}[]
|
||||
|
||||
async function getCurrentShippingOptionPriceSetsLinks(
|
||||
shippingOptionIds: string[],
|
||||
{ remoteQuery }: { remoteQuery: RemoteQueryFunction }
|
||||
): Promise<LinkItems> {
|
||||
const query = remoteQueryObjectFromString({
|
||||
service: LINKS.ShippingOptionPriceSet,
|
||||
variables: {
|
||||
filters: { shipping_option_id: shippingOptionIds },
|
||||
take: null,
|
||||
},
|
||||
fields: ["shipping_option_id", "price_set_id"],
|
||||
})
|
||||
|
||||
const shippingOptionPriceSetLinks = (await remoteQuery(query)) as {
|
||||
shipping_option_id: string
|
||||
price_set_id: string
|
||||
}[]
|
||||
|
||||
return shippingOptionPriceSetLinks.map((shippingOption) => {
|
||||
return {
|
||||
[Modules.FULFILLMENT]: {
|
||||
shipping_option_id: shippingOption.shipping_option_id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: shippingOption.price_set_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const setShippingOptionsPriceSetsStepId =
|
||||
"set-shipping-options-price-sets-step"
|
||||
export const setShippingOptionsPriceSetsStep = createStep(
|
||||
setShippingOptionsPriceSetsStepId,
|
||||
async (data: SetShippingOptionsPriceSetsStepInput, { container }) => {
|
||||
if (!data.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const dataInputToProcess = data.filter((inputData) => {
|
||||
return inputData.price_sets?.length
|
||||
}) as FilteredSetShippingOptionsPriceSetsStepInput[]
|
||||
|
||||
if (!dataInputToProcess.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve<RemoteLink>(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
const remoteQuery = container.resolve<RemoteQueryFunction>(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
const shippingOptionIds = dataInputToProcess.map(
|
||||
(inputData) => inputData.id
|
||||
)
|
||||
const currentExistingLinks = await getCurrentShippingOptionPriceSetsLinks(
|
||||
shippingOptionIds,
|
||||
{ remoteQuery }
|
||||
)
|
||||
|
||||
const linksToRemove: LinkItems = currentExistingLinks
|
||||
.filter((existingLink) => {
|
||||
return !dataInputToProcess.some((input) => {
|
||||
return (
|
||||
input.id === existingLink[Modules.FULFILLMENT].shipping_option_id &&
|
||||
input.price_sets.includes(
|
||||
existingLink[Modules.PRICING].price_set_id
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
.map((link) => {
|
||||
return {
|
||||
[Modules.FULFILLMENT]: {
|
||||
shipping_option_id: link[Modules.FULFILLMENT].shipping_option_id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: link[Modules.PRICING].price_set_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const linksToCreate = dataInputToProcess
|
||||
.map((inputData) => {
|
||||
return inputData.price_sets.map((priceSet) => {
|
||||
const alreadyExists = currentExistingLinks.some((link) => {
|
||||
return (
|
||||
link[Modules.FULFILLMENT].shipping_option_id === inputData.id &&
|
||||
link[Modules.PRICING].price_set_id === priceSet
|
||||
)
|
||||
})
|
||||
|
||||
if (alreadyExists) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
[Modules.FULFILLMENT]: { shipping_option_id: inputData.id },
|
||||
[Modules.PRICING]: { price_set_id: priceSet },
|
||||
}
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
.filter((d): d is LinkItems[0] => !!d)
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
|
||||
if (linksToRemove.length) {
|
||||
promises.push(remoteLink.dismiss(linksToRemove))
|
||||
}
|
||||
|
||||
if (linksToCreate.length) {
|
||||
promises.push(remoteLink.create(linksToCreate))
|
||||
}
|
||||
|
||||
await promiseAll(promises)
|
||||
|
||||
return new StepResponse(void 0, {
|
||||
linksToCreate: linksToRemove,
|
||||
linksToRemove: linksToCreate,
|
||||
})
|
||||
},
|
||||
async (rollbackData, { container }) => {
|
||||
if (!rollbackData) {
|
||||
return
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve<RemoteLink>(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
|
||||
if (rollbackData.linksToRemove.length) {
|
||||
promises.push(remoteLink.dismiss(rollbackData.linksToRemove))
|
||||
}
|
||||
|
||||
if (rollbackData.linksToCreate.length) {
|
||||
promises.push(remoteLink.create(rollbackData.linksToCreate))
|
||||
}
|
||||
|
||||
await promiseAll(promises)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
import { CreateRuleTypeDTO, FulfillmentWorkflow } from "@medusajs/types"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
createShippingOptionsPriceSetsStep,
|
||||
createShippingOptionsStep,
|
||||
} from "../steps"
|
||||
import { setShippingOptionsPriceSetsStep } from "../steps/set-shipping-options-price-sets"
|
||||
import { createPricingRuleTypesStep } from "../../pricing"
|
||||
|
||||
export const createShippingOptionsWorkflowId =
|
||||
"create-shipping-options-workflow"
|
||||
export const createShippingOptionsWorkflow = createWorkflow(
|
||||
createShippingOptionsWorkflowId,
|
||||
(
|
||||
input: WorkflowData<
|
||||
FulfillmentWorkflow.CreateShippingOptionsWorkflowInput[]
|
||||
>
|
||||
): WorkflowData<FulfillmentWorkflow.CreateShippingOptionsWorkflowOutput> => {
|
||||
const data = transform(input, (data) => {
|
||||
const shippingOptionsIndexToPrices = data.map((option, index) => {
|
||||
return {
|
||||
shipping_option_index: index,
|
||||
prices: option.prices,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
shippingOptions: data,
|
||||
shippingOptionsIndexToPrices,
|
||||
}
|
||||
})
|
||||
|
||||
const createdShippingOptions = createShippingOptionsStep(
|
||||
data.shippingOptions
|
||||
)
|
||||
|
||||
const normalizedShippingOptionsPrices = transform(
|
||||
{
|
||||
shippingOptions: createdShippingOptions,
|
||||
shippingOptionsIndexToPrices: data.shippingOptionsIndexToPrices,
|
||||
},
|
||||
(data) => {
|
||||
const ruleTypes = new Set<CreateRuleTypeDTO>()
|
||||
const shippingOptionsPrices = data.shippingOptionsIndexToPrices.map(
|
||||
({ shipping_option_index, prices }) => {
|
||||
prices.forEach((price) => {
|
||||
if ("region_id" in price) {
|
||||
ruleTypes.add({
|
||||
name: "region_id",
|
||||
rule_attribute: "region_id",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: data.shippingOptions[shipping_option_index].id,
|
||||
prices,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
shippingOptionsPrices,
|
||||
ruleTypes: Array.from(ruleTypes) as CreateRuleTypeDTO[],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
createPricingRuleTypesStep(normalizedShippingOptionsPrices.ruleTypes)
|
||||
|
||||
const shippingOptionsPriceSetsLinkData = createShippingOptionsPriceSetsStep(
|
||||
normalizedShippingOptionsPrices.shippingOptionsPrices
|
||||
)
|
||||
|
||||
const normalizedLinkData = transform(
|
||||
{
|
||||
shippingOptionsPriceSetsLinkData,
|
||||
},
|
||||
(data) => {
|
||||
return data.shippingOptionsPriceSetsLinkData.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
price_sets: [item.priceSetId],
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
setShippingOptionsPriceSetsStep(normalizedLinkData)
|
||||
|
||||
return createdShippingOptions
|
||||
}
|
||||
)
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./add-rules-to-fulfillment-shipping-option"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
export * from "./create-shipping-options"
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { CreateRuleTypeDTO, IPricingModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const createPricingRuleTypesStepId = "create-pricing-rule-types"
|
||||
export const createPricingRuleTypesStep = createStep(
|
||||
createPricingRuleTypesStepId,
|
||||
async (data: CreateRuleTypeDTO[], { container }) => {
|
||||
if (!data?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const pricingModule = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
const ruleTypes = await pricingModule.createRuleTypes(data)
|
||||
const existingRuleTypes = await pricingModule.listRuleTypes({
|
||||
rule_attribute: data.map((d) => d.rule_attribute),
|
||||
})
|
||||
|
||||
const existingRuleTypeAttributes = new Set(
|
||||
existingRuleTypes.map((ruleType) => ruleType.rule_attribute)
|
||||
)
|
||||
|
||||
const ruleTypesToCreate = data.filter(
|
||||
(dataItem) => !existingRuleTypeAttributes.has(dataItem.rule_attribute)
|
||||
)
|
||||
|
||||
const ruleTypes = await pricingModule.createRuleTypes(ruleTypesToCreate)
|
||||
|
||||
return new StepResponse(
|
||||
ruleTypes,
|
||||
|
||||
Reference in New Issue
Block a user