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:
Adrien de Peretti
2024-04-06 11:51:48 +00:00
committed by GitHub
parent 5e30b8cce6
commit 65794f4bb5
20 changed files with 804 additions and 20 deletions
@@ -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,