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:
@@ -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
|
||||
|
||||
|
||||
-173
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user