feat: move create inventory to @medusajs/workflows (#5301)

**Why**
- We have some workflow-like flows in @medusajs/medusa. These should be moved over to the workflows package.
- Inventory Items <> Variant currently assume a 1-1 mapping. There should be support for a many-to-many mapping.

**What**
- PR introduces a feature flag for supporting many-to-many mappings for inventory and variants.
- Deletes legacy transaction handler in @medusajs/medusa.
- Adjusts existing createInventoryItems handler to remove dependency on variant data.

**Unkowns**
~~1. Couldn't find an existing test for the CreateProduct workflow. It should be tested that this still works as expected.~~
2. Have removed transaction managers as we should move to handling consistency through orchestration tooling. Are we ready for that?
This commit is contained in:
Sebastian Rindom
2023-10-11 18:01:56 +00:00
committed by GitHub
parent bbd9dd408f
commit 66413d094e
25 changed files with 480 additions and 281 deletions
@@ -1,15 +1,14 @@
import { FlagRouter, ManyToManyInventoryFeatureFlag } from "@medusajs/utils"
import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"
import {
ProductVariantInventoryService,
ProductVariantService,
} from "../../../../services"
createInventoryItems,
CreateInventoryItemActions,
pipe,
} from "@medusajs/workflows"
import { ProductVariantInventoryService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { IInventoryService } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { createInventoryItemTransaction } from "./transaction/create-inventory-item"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/inventory-items
@@ -78,54 +77,106 @@ import { validator } from "../../../../utils/validator"
*/
export default async (req, res) => {
const validated = await validator(AdminPostInventoryItemsReq, req.body)
const { variant_id, ...input } = validated
const { variant_id, ...inventoryItemInput } = req.validatedBody
const inventoryService: IInventoryService =
req.scope.resolve("inventoryService")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const productVariantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
let inventoryItems = await productVariantInventoryService.listByVariant(
variant_id
)
const createInventoryItemWorkflow = createInventoryItems(req.scope)
// TODO: this is a temporary fix to prevent duplicate inventory items since we don't support this functionality yet
if (inventoryItems.length) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory Item already exists for this variant"
if (!featureFlagRouter.isFeatureEnabled(ManyToManyInventoryFeatureFlag.key)) {
if (!variant_id) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"variant_id is required"
)
}
createInventoryItemWorkflow.appendAction(
"attachInventoryItems",
CreateInventoryItemActions.createInventoryItems,
{
invoke: pipe(
{
invoke: {
from: CreateInventoryItemActions.createInventoryItems,
alias: "createdItems",
},
},
generateAttachInventoryToVariantHandler(
variant_id,
productVariantInventoryService
)
),
compensate: pipe(
{
invoke: {
from: "attachInventoryItems",
alias: "attachedItems",
},
},
generateDetachInventoryItemFromVariantHandler(
productVariantInventoryService
)
),
}
)
}
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
await createInventoryItemTransaction(
{
manager: transactionManager,
inventoryService,
productVariantInventoryService,
productVariantService,
},
variant_id,
input
)
const { result } = await createInventoryItemWorkflow.run({
input: {
inventoryItems: [inventoryItemInput],
},
})
inventoryItems = await productVariantInventoryService.listByVariant(
variant_id
)
res.status(200).json({ inventory_item: result[0].inventoryItem })
}
const inventoryItem = await inventoryService.retrieveInventoryItem(
inventoryItems[0].inventory_item_id,
req.retrieveConfig
)
function generateDetachInventoryItemFromVariantHandler(
productVariantInventoryService: ProductVariantInventoryService
) {
return async ({ data }) => {
if (!data.attachedItems || !data.attachedItems.length) {
return
}
res.status(200).json({ inventory_item: inventoryItem })
const [variantId, inventoryItemId] = data.attachedItems
if (!variantId || !inventoryItemId) {
return
}
return await productVariantInventoryService.detachInventoryItem(
inventoryItemId,
variantId
)
}
}
function generateAttachInventoryToVariantHandler(
variantId: string,
productVariantInventoryService: ProductVariantInventoryService
) {
return async ({ data }) => {
let inventoryItems = await productVariantInventoryService.listByVariant(
variantId
)
// TODO: this is a temporary fix to prevent duplicate inventory
// items since we don't support this functionality yet
if (inventoryItems.length) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory Item already exists for this variant"
)
}
const inventoryItemId = data.createdItems[0].inventoryItem.id
await productVariantInventoryService.attachInventoryItem(
variantId,
inventoryItemId
)
return [variantId, inventoryItemId]
}
}
/**
@@ -192,6 +243,7 @@ export default async (req, res) => {
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostInventoryItemsReq {
@IsOptional()
@IsString()
variant_id: string
@@ -1,173 +0,0 @@
import {
DistributedTransaction,
TransactionHandlerType,
TransactionOrchestrator,
TransactionPayload,
TransactionState,
TransactionStepsDefinition,
} from "@medusajs/orchestration"
import { IInventoryService, InventoryItemDTO } from "@medusajs/types"
import {
ProductVariantInventoryService,
ProductVariantService,
} from "../../../../../services"
import { EntityManager } from "typeorm"
import { MedusaError } from "@medusajs/utils"
import { ulid } from "ulid"
enum actions {
createInventoryItem = "createInventoryItem",
attachInventoryItem = "attachInventoryItem",
}
const flow: TransactionStepsDefinition = {
next: {
action: actions.createInventoryItem,
saveResponse: true,
next: {
action: actions.attachInventoryItem,
noCompensation: true,
},
},
}
const createInventoryItemStrategy = new TransactionOrchestrator(
"create-inventory-item",
flow
)
type InjectedDependencies = {
manager: EntityManager
productVariantService: ProductVariantService
productVariantInventoryService: ProductVariantInventoryService
inventoryService: IInventoryService
}
type CreateInventoryItemInput = {
sku?: string
hs_code?: string
weight?: number
length?: number
height?: number
width?: number
origin_country?: string
mid_code?: string
material?: string
title?: string
description?: string
thumbnail?: string
metadata?: Record<string, unknown>
}
export const createInventoryItemTransaction = async (
dependencies: InjectedDependencies,
variantId: string,
input: CreateInventoryItemInput
): Promise<DistributedTransaction> => {
const {
manager,
productVariantService,
inventoryService,
productVariantInventoryService,
} = dependencies
const productVariantInventoryServiceTx =
productVariantInventoryService.withTransaction(manager)
const productVariantServiceTx = productVariantService.withTransaction(manager)
async function createInventoryItem(input: CreateInventoryItemInput) {
return await inventoryService!.createInventoryItem({
sku: input.sku,
origin_country: input.origin_country,
hs_code: input.hs_code,
mid_code: input.mid_code,
material: input.material,
weight: input.weight,
length: input.length,
height: input.height,
width: input.width,
title: input.title,
description: input.description,
thumbnail: input.thumbnail,
})
}
async function removeInventoryItem(inventoryItem: InventoryItemDTO) {
if (inventoryItem) {
await inventoryService!.deleteInventoryItem(inventoryItem.id)
}
}
async function attachInventoryItem(inventoryItem: InventoryItemDTO) {
const variant = await productVariantServiceTx.retrieve(variantId)
if (!variant.manage_inventory) {
return
}
await productVariantInventoryServiceTx.attachInventoryItem(
variant.id,
inventoryItem.id
)
}
async function transactionHandler(
actionId: string,
type: TransactionHandlerType,
payload: TransactionPayload
) {
const command = {
[actions.createInventoryItem]: {
[TransactionHandlerType.INVOKE]: async (
data: CreateInventoryItemInput
) => {
return await createInventoryItem(data)
},
[TransactionHandlerType.COMPENSATE]: async (
data: CreateInventoryItemInput,
{ invoke }
) => {
await removeInventoryItem(invoke[actions.createInventoryItem])
},
},
[actions.attachInventoryItem]: {
[TransactionHandlerType.INVOKE]: async (
data: CreateInventoryItemInput,
{ invoke }
) => {
const { [actions.createInventoryItem]: inventoryItem } = invoke
return await attachInventoryItem(inventoryItem)
},
},
}
return command[actionId][type](payload.data, payload.context)
}
const transaction = await createInventoryItemStrategy.beginTransaction(
ulid(),
transactionHandler,
input
)
await createInventoryItemStrategy.resume(transaction)
if (transaction.getState() !== TransactionState.DONE) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
transaction
.getErrors()
.map((err) => err.error?.message)
.join("\n")
)
}
return transaction
}
export const revertVariantTransaction = async (
transaction: DistributedTransaction
) => {
await createInventoryItemStrategy.cancelTransaction(transaction)
}