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:
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/inventory": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"@medusajs/workflows": patch
|
||||||
|
---
|
||||||
|
fix: move create inventory workflow to @medusajs/workflows
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
const path = require("path")
|
||||||
|
const { ProductVariantInventoryService } = require("@medusajs/medusa")
|
||||||
|
|
||||||
|
const {
|
||||||
|
bootstrapApp,
|
||||||
|
} = require("../../../../environment-helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../../environment-helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../../environment-helpers/use-api")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../../../helpers/admin-seeder")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
const {
|
||||||
|
simpleProductFactory,
|
||||||
|
simpleOrderFactory,
|
||||||
|
} = require("../../../../factories")
|
||||||
|
const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }
|
||||||
|
|
||||||
|
describe("Inventory Items endpoints", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
let variantId
|
||||||
|
let inventoryItems
|
||||||
|
let locationId
|
||||||
|
let location2Id
|
||||||
|
let location3Id
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd, verbose: true })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
// Set feature flag
|
||||||
|
const flagRouter = appContainer.resolve("featureFlagRouter")
|
||||||
|
flagRouter.setFlag("many_to_many_inventory", true)
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const flagRouter = appContainer.resolve("featureFlagRouter")
|
||||||
|
flagRouter.setFlag("many_to_many_inventory", false)
|
||||||
|
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Inventory Items", () => {
|
||||||
|
it("should create inventory item without variant id", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{
|
||||||
|
sku: "TABLE_LEG",
|
||||||
|
description: "Table Leg",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
/** @type {ProductVariantInventoryService} */
|
||||||
|
const productVariantInventoryService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
const inventoryItems = await inventoryService.list()
|
||||||
|
|
||||||
|
expect(inventoryItems.length).toEqual(1)
|
||||||
|
|
||||||
|
const variants = await productVariantInventoryService.listByItem([
|
||||||
|
inventoryItems[0].id,
|
||||||
|
])
|
||||||
|
expect(variants.length).toEqual(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -345,7 +345,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
|
|
||||||
const inventoryItemCreateRes = await api.post(
|
const inventoryItemCreateRes = await api.post(
|
||||||
`/admin/inventory-items`,
|
`/admin/inventory-items`,
|
||||||
{ variant_id: variantId },
|
{ variant_id: variantId, sku: "attach_this_to_variant" },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { initDb, useDb } from "../../../../environment-helpers/use-db"
|
||||||
|
import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app"
|
||||||
|
import {
|
||||||
|
createInventoryItems,
|
||||||
|
CreateInventoryItemActions,
|
||||||
|
pipe,
|
||||||
|
} from "@medusajs/workflows"
|
||||||
|
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
|
||||||
|
describe("CreateInventoryItem workflow", function () {
|
||||||
|
let medusaProcess
|
||||||
|
let medusaContainer
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
await initDb({ cwd } as any)
|
||||||
|
const { container } = await bootstrapApp({ cwd })
|
||||||
|
medusaContainer = container
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
|
||||||
|
medusaProcess.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should compensate all the invoke if something fails", async () => {
|
||||||
|
const workflow = createInventoryItems(medusaContainer)
|
||||||
|
|
||||||
|
workflow.appendAction(
|
||||||
|
"fail_step",
|
||||||
|
CreateInventoryItemActions.createInventoryItems,
|
||||||
|
{
|
||||||
|
invoke: pipe({}, async function failStep() {
|
||||||
|
throw new Error(`Failed`)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noCompensation: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const input: WorkflowTypes.InventoryWorkflow.CreateInventoryItemsWorkflowInputDTO =
|
||||||
|
{
|
||||||
|
inventoryItems: [
|
||||||
|
{
|
||||||
|
sku: "TABLE_LEG",
|
||||||
|
description: "Table Leg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result, errors, transaction } = await workflow.run({
|
||||||
|
input,
|
||||||
|
context: {},
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "fail_step",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: new Error(`Failed`),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(transaction.getState()).toEqual("reverted")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(result[0].inventoryItem).toEqual(
|
||||||
|
expect.objectContaining({ id: expect.any(String) })
|
||||||
|
)
|
||||||
|
|
||||||
|
const inventoryService: IInventoryService = medusaContainer.resolve(
|
||||||
|
ModuleRegistrationName.INVENTORY
|
||||||
|
)
|
||||||
|
|
||||||
|
const [inventoryItems] = await inventoryService.listInventoryItems(
|
||||||
|
{ id: result[0].inventoryItem.id },
|
||||||
|
{ withDeleted: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(inventoryItems[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: result[0].inventoryItem.id,
|
||||||
|
deleted_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/cache-inmemory": "workspace:*",
|
"@medusajs/cache-inmemory": "workspace:*",
|
||||||
"@medusajs/event-bus-local": "workspace:*",
|
"@medusajs/event-bus-local": "workspace:*",
|
||||||
|
"@medusajs/inventory": "workspace:^",
|
||||||
"@medusajs/medusa": "workspace:*",
|
"@medusajs/medusa": "workspace:*",
|
||||||
"@medusajs/product": "workspace:^",
|
"@medusajs/product": "workspace:^",
|
||||||
"faker": "^5.5.3",
|
"faker": "^5.5.3",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function buildQuery<TWhereKeys extends object, TEntity = unknown>(
|
|||||||
where: buildWhere<TWhereKeys, TEntity>(selector),
|
where: buildWhere<TWhereKeys, TEntity>(selector),
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("deleted_at" in selector) {
|
if ("deleted_at" in selector || config.withDeleted) {
|
||||||
query.withDeleted = true
|
query.withDeleted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ export function getListQuery(
|
|||||||
queryBuilder.select(legacySelect.map((s) => "inv_item." + s))
|
queryBuilder.select(legacySelect.map((s) => "inv_item." + s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.withDeleted) {
|
||||||
|
queryBuilder.withDeleted()
|
||||||
|
}
|
||||||
|
|
||||||
if (query.order) {
|
if (query.order) {
|
||||||
const toSelect: string[] = []
|
const toSelect: string[] = []
|
||||||
const parsed = Object.entries(query.order).reduce((acc, [k, v]) => {
|
const parsed = Object.entries(query.order).reduce((acc, [k, v]) => {
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
|
import { FlagRouter, ManyToManyInventoryFeatureFlag } from "@medusajs/utils"
|
||||||
import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"
|
import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"
|
||||||
import {
|
import {
|
||||||
ProductVariantInventoryService,
|
createInventoryItems,
|
||||||
ProductVariantService,
|
CreateInventoryItemActions,
|
||||||
} from "../../../../services"
|
pipe,
|
||||||
|
} from "@medusajs/workflows"
|
||||||
|
import { ProductVariantInventoryService } from "../../../../services"
|
||||||
|
|
||||||
import { EntityManager } from "typeorm"
|
|
||||||
import { FindParams } from "../../../../types/common"
|
import { FindParams } from "../../../../types/common"
|
||||||
import { IInventoryService } from "@medusajs/types"
|
|
||||||
import { MedusaError } from "@medusajs/utils"
|
import { MedusaError } from "@medusajs/utils"
|
||||||
import { createInventoryItemTransaction } from "./transaction/create-inventory-item"
|
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /admin/inventory-items
|
* @oas [post] /admin/inventory-items
|
||||||
@@ -78,54 +77,106 @@ import { validator } from "../../../../utils/validator"
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const validated = await validator(AdminPostInventoryItemsReq, req.body)
|
const { variant_id, ...inventoryItemInput } = req.validatedBody
|
||||||
const { variant_id, ...input } = validated
|
|
||||||
|
|
||||||
const inventoryService: IInventoryService =
|
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||||
req.scope.resolve("inventoryService")
|
|
||||||
const productVariantInventoryService: ProductVariantInventoryService =
|
const productVariantInventoryService: ProductVariantInventoryService =
|
||||||
req.scope.resolve("productVariantInventoryService")
|
req.scope.resolve("productVariantInventoryService")
|
||||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
|
||||||
"productVariantService"
|
|
||||||
)
|
|
||||||
|
|
||||||
let inventoryItems = await productVariantInventoryService.listByVariant(
|
const createInventoryItemWorkflow = createInventoryItems(req.scope)
|
||||||
variant_id
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: this is a temporary fix to prevent duplicate inventory items since we don't support this functionality yet
|
if (!featureFlagRouter.isFeatureEnabled(ManyToManyInventoryFeatureFlag.key)) {
|
||||||
if (inventoryItems.length) {
|
if (!variant_id) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
"Inventory Item already exists for this variant"
|
"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")
|
const { result } = await createInventoryItemWorkflow.run({
|
||||||
|
input: {
|
||||||
await manager.transaction(async (transactionManager) => {
|
inventoryItems: [inventoryItemInput],
|
||||||
await createInventoryItemTransaction(
|
},
|
||||||
{
|
|
||||||
manager: transactionManager,
|
|
||||||
inventoryService,
|
|
||||||
productVariantInventoryService,
|
|
||||||
productVariantService,
|
|
||||||
},
|
|
||||||
variant_id,
|
|
||||||
input
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
inventoryItems = await productVariantInventoryService.listByVariant(
|
res.status(200).json({ inventory_item: result[0].inventoryItem })
|
||||||
variant_id
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const inventoryItem = await inventoryService.retrieveInventoryItem(
|
function generateDetachInventoryItemFromVariantHandler(
|
||||||
inventoryItems[0].inventory_item_id,
|
productVariantInventoryService: ProductVariantInventoryService
|
||||||
req.retrieveConfig
|
) {
|
||||||
)
|
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"
|
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
|
||||||
*/
|
*/
|
||||||
export class AdminPostInventoryItemsReq {
|
export class AdminPostInventoryItemsReq {
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
variant_id: string
|
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)
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * as CartWorkflow from "./cart"
|
export * as CartWorkflow from "./cart"
|
||||||
export * as CommonWorkflow from "./common"
|
export * as CommonWorkflow from "./common"
|
||||||
export * as ProductWorkflow from "./product"
|
export * as ProductWorkflow from "./product"
|
||||||
|
export * as InventoryWorkflow from "./inventory"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface CreateInventoryItemInputDTO {
|
||||||
|
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 interface CreateInventoryItemsWorkflowInputDTO {
|
||||||
|
inventoryItems: CreateInventoryItemInputDTO[]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-inventory-items"
|
||||||
@@ -6,4 +6,4 @@ export * from "./sales-channels"
|
|||||||
export * from "./tax-inclusive-pricing"
|
export * from "./tax-inclusive-pricing"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
export * from "./workflows"
|
export * from "./workflows"
|
||||||
|
export * from "./many-to-many-inventory"
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FeatureFlagTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
export const ManyToManyInventoryFeatureFlag: FeatureFlagTypes.FlagSettings = {
|
||||||
|
key: "many_to_many_inventory",
|
||||||
|
default_val: false,
|
||||||
|
env_key: "MEDUSA_FF_MANY_TO_MANY_INVENTORY",
|
||||||
|
description:
|
||||||
|
"Enable capability to have many to many relationship between inventory items and variants",
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./cart"
|
export * from "./cart"
|
||||||
export * from "./product"
|
export * from "./product"
|
||||||
|
export * from "./inventory"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Workflows } from "../../definitions"
|
||||||
|
import {
|
||||||
|
TransactionStepsDefinition,
|
||||||
|
WorkflowManager,
|
||||||
|
} from "@medusajs/orchestration"
|
||||||
|
import { exportWorkflow, pipe } from "../../helper"
|
||||||
|
|
||||||
|
import { InventoryTypes, WorkflowTypes } from "@medusajs/types"
|
||||||
|
import { InventoryHandlers } from "../../handlers"
|
||||||
|
|
||||||
|
export enum CreateInventoryItemActions {
|
||||||
|
prepare = "prepare",
|
||||||
|
createInventoryItems = "createInventoryItems",
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowSteps: TransactionStepsDefinition = {
|
||||||
|
next: {
|
||||||
|
action: CreateInventoryItemActions.createInventoryItems,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = new Map([
|
||||||
|
[
|
||||||
|
CreateInventoryItemActions.createInventoryItems,
|
||||||
|
{
|
||||||
|
invoke: pipe(
|
||||||
|
{
|
||||||
|
inputAlias: CreateInventoryItemActions.prepare,
|
||||||
|
merge: true,
|
||||||
|
invoke: {
|
||||||
|
from: CreateInventoryItemActions.prepare,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InventoryHandlers.createInventoryItems
|
||||||
|
),
|
||||||
|
compensate: pipe(
|
||||||
|
{
|
||||||
|
merge: true,
|
||||||
|
invoke: {
|
||||||
|
from: CreateInventoryItemActions.createInventoryItems,
|
||||||
|
alias:
|
||||||
|
InventoryHandlers.removeInventoryItems.aliases.inventoryItems,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InventoryHandlers.removeInventoryItems
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
WorkflowManager.register(
|
||||||
|
Workflows.CreateInventoryItems,
|
||||||
|
workflowSteps,
|
||||||
|
handlers
|
||||||
|
)
|
||||||
|
|
||||||
|
export const createInventoryItems = exportWorkflow<
|
||||||
|
WorkflowTypes.InventoryWorkflow.CreateInventoryItemsWorkflowInputDTO,
|
||||||
|
{ tag: string; inventoryItem: InventoryTypes.InventoryItemDTO }[]
|
||||||
|
>(
|
||||||
|
Workflows.CreateInventoryItems,
|
||||||
|
CreateInventoryItemActions.createInventoryItems,
|
||||||
|
async (data) => data
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-inventory-item"
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MiddlewaresHandlers,
|
MiddlewaresHandlers,
|
||||||
ProductHandlers,
|
ProductHandlers,
|
||||||
} from "../../handlers"
|
} from "../../handlers"
|
||||||
|
import { prepareCreateInventoryItems } from "./prepare-create-inventory-items"
|
||||||
|
|
||||||
export enum CreateProductsActions {
|
export enum CreateProductsActions {
|
||||||
prepare = "prepare",
|
prepare = "prepare",
|
||||||
@@ -175,9 +176,10 @@ const handlers = new Map([
|
|||||||
merge: true,
|
merge: true,
|
||||||
invoke: {
|
invoke: {
|
||||||
from: CreateProductsActions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias: InventoryHandlers.createInventoryItems.aliases.products,
|
alias: prepareCreateInventoryItems.aliases.products,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
prepareCreateInventoryItems,
|
||||||
InventoryHandlers.createInventoryItems
|
InventoryHandlers.createInventoryItems
|
||||||
),
|
),
|
||||||
compensate: pipe(
|
compensate: pipe(
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ProductTypes } from "@medusajs/types"
|
||||||
|
import { WorkflowArguments } from "../../helper"
|
||||||
|
|
||||||
|
type AssociationTaggedVariant = ProductTypes.ProductVariantDTO & {
|
||||||
|
_associationTag?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectWithVariant = { variants: ProductTypes.ProductVariantDTO[] }
|
||||||
|
|
||||||
|
export async function prepareCreateInventoryItems({
|
||||||
|
data,
|
||||||
|
}: WorkflowArguments<{
|
||||||
|
products: ObjectWithVariant[]
|
||||||
|
}>) {
|
||||||
|
const taggedVariants = data.products.reduce<AssociationTaggedVariant[]>(
|
||||||
|
(acc, product: ObjectWithVariant) => {
|
||||||
|
const cleanVariants = product.variants.reduce<AssociationTaggedVariant[]>(
|
||||||
|
(acc, variant: AssociationTaggedVariant) => {
|
||||||
|
if (!variant.manage_inventory) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
variant._associationTag = variant.id
|
||||||
|
|
||||||
|
acc.push(variant)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
return acc.concat(cleanVariants)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
alias: prepareCreateInventoryItems.aliases.output,
|
||||||
|
value: {
|
||||||
|
inventoryItems: taggedVariants,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareCreateInventoryItems.aliases = {
|
||||||
|
products: "products",
|
||||||
|
output: "prepareCreateInventoryItemsOutput",
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ export enum Workflows {
|
|||||||
|
|
||||||
// Cart workflows
|
// Cart workflows
|
||||||
CreateCart = "create-cart",
|
CreateCart = "create-cart",
|
||||||
|
|
||||||
|
CreateInventoryItems = "create-inventory-items",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InputAlias {
|
export enum InputAlias {
|
||||||
@@ -16,4 +18,6 @@ export enum InputAlias {
|
|||||||
|
|
||||||
AttachedInventoryItems = "attachedInventoryItems",
|
AttachedInventoryItems = "attachedInventoryItems",
|
||||||
DetachedInventoryItems = "detachedInventoryItems",
|
DetachedInventoryItems = "detachedInventoryItems",
|
||||||
|
|
||||||
|
InventoryItemsInputData = "inventoryItemsInputData",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InventoryItemDTO, ProductTypes } from "@medusajs/types"
|
import { InventoryItemDTO } from "@medusajs/types"
|
||||||
import { WorkflowArguments } from "../../helper"
|
import { WorkflowArguments } from "../../helper"
|
||||||
|
|
||||||
export async function attachInventoryItems({
|
export async function attachInventoryItems({
|
||||||
@@ -7,12 +7,11 @@ export async function attachInventoryItems({
|
|||||||
data,
|
data,
|
||||||
}: WorkflowArguments<{
|
}: WorkflowArguments<{
|
||||||
inventoryItems: {
|
inventoryItems: {
|
||||||
variant: ProductTypes.ProductVariantDTO
|
tag: string
|
||||||
inventoryItem: InventoryItemDTO
|
inventoryItem: InventoryItemDTO
|
||||||
}[]
|
}[]
|
||||||
}>) {
|
}>) {
|
||||||
const { manager } = context
|
const { manager } = context
|
||||||
|
|
||||||
const productVariantInventoryService = container
|
const productVariantInventoryService = container
|
||||||
.resolve("productVariantInventoryService")
|
.resolve("productVariantInventoryService")
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
@@ -21,12 +20,10 @@ export async function attachInventoryItems({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const inventoryData = data.inventoryItems.map(
|
const inventoryData = data.inventoryItems.map(({ tag, inventoryItem }) => ({
|
||||||
({ variant, inventoryItem }) => ({
|
variantId: tag,
|
||||||
variantId: variant.id,
|
inventoryItemId: inventoryItem.id,
|
||||||
inventoryItemId: inventoryItem.id,
|
}))
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return await productVariantInventoryService.attachInventoryItem(inventoryData)
|
return await productVariantInventoryService.attachInventoryItem(inventoryData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import {
|
import { IInventoryService, InventoryItemDTO } from "@medusajs/types"
|
||||||
IInventoryService,
|
|
||||||
InventoryItemDTO,
|
|
||||||
ProductTypes,
|
|
||||||
} from "@medusajs/types"
|
|
||||||
import { WorkflowArguments } from "../../helper"
|
import { WorkflowArguments } from "../../helper"
|
||||||
|
|
||||||
type Result = {
|
type Result = {
|
||||||
variant: ProductTypes.ProductVariantDTO
|
tag: string
|
||||||
inventoryItem: InventoryItemDTO
|
inventoryItem: InventoryItemDTO
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
export async function createInventoryItems({
|
export async function createInventoryItems({
|
||||||
container,
|
container,
|
||||||
context,
|
|
||||||
data,
|
data,
|
||||||
}: WorkflowArguments<{
|
}: WorkflowArguments<{
|
||||||
products: ProductTypes.ProductDTO[]
|
inventoryItems: (InventoryItemDTO & { _associationTag?: string })[]
|
||||||
}>): Promise<Result | void> {
|
}>): Promise<Result | void> {
|
||||||
const inventoryService: IInventoryService =
|
const inventoryService: IInventoryService =
|
||||||
container.resolve("inventoryService")
|
container.resolve("inventoryService")
|
||||||
@@ -28,47 +23,27 @@ export async function createInventoryItems({
|
|||||||
return void 0
|
return void 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const variants = data.products.reduce(
|
|
||||||
(
|
|
||||||
acc: ProductTypes.ProductVariantDTO[],
|
|
||||||
product: ProductTypes.ProductDTO
|
|
||||||
) => {
|
|
||||||
return acc.concat(product.variants)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
variants.map(async (variant) => {
|
data.inventoryItems.map(async (item) => {
|
||||||
if (!variant.manage_inventory) {
|
const inventoryItem = await inventoryService!.createInventoryItem({
|
||||||
return
|
sku: item.sku!,
|
||||||
}
|
origin_country: item.origin_country!,
|
||||||
|
hs_code: item.hs_code!,
|
||||||
|
mid_code: item.mid_code!,
|
||||||
|
material: item.material!,
|
||||||
|
weight: item.weight!,
|
||||||
|
length: item.length!,
|
||||||
|
height: item.height!,
|
||||||
|
width: item.width!,
|
||||||
|
})
|
||||||
|
|
||||||
const inventoryItem = await inventoryService!.createInventoryItem(
|
return { tag: item._associationTag ?? inventoryItem.id, inventoryItem }
|
||||||
{
|
|
||||||
sku: variant.sku!,
|
|
||||||
origin_country: variant.origin_country!,
|
|
||||||
hs_code: variant.hs_code!,
|
|
||||||
mid_code: variant.mid_code!,
|
|
||||||
material: variant.material!,
|
|
||||||
weight: variant.weight!,
|
|
||||||
length: variant.length!,
|
|
||||||
height: variant.height!,
|
|
||||||
width: variant.width!,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transactionManager: (context.transactionManager ??
|
|
||||||
context.manager) as any,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return { variant, inventoryItem }
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return result.filter(Boolean) as Result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
createInventoryItems.aliases = {
|
createInventoryItems.aliases = {
|
||||||
products: "products",
|
payload: "payload",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InventoryItemDTO, ProductTypes } from "@medusajs/types"
|
import { InventoryItemDTO } from "@medusajs/types"
|
||||||
import { WorkflowArguments } from "../../helper"
|
import { WorkflowArguments } from "../../helper"
|
||||||
|
|
||||||
export async function detachInventoryItems({
|
export async function detachInventoryItems({
|
||||||
@@ -7,7 +7,7 @@ export async function detachInventoryItems({
|
|||||||
data,
|
data,
|
||||||
}: WorkflowArguments<{
|
}: WorkflowArguments<{
|
||||||
inventoryItems: {
|
inventoryItems: {
|
||||||
variant: ProductTypes.ProductVariantDTO
|
tag: string
|
||||||
inventoryItem: InventoryItemDTO
|
inventoryItem: InventoryItemDTO
|
||||||
}[]
|
}[]
|
||||||
}>): Promise<void> {
|
}>): Promise<void> {
|
||||||
@@ -22,10 +22,10 @@ export async function detachInventoryItems({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
data.inventoryItems.map(async ({ variant, inventoryItem }) => {
|
data.inventoryItems.map(async ({ tag, inventoryItem }) => {
|
||||||
return await productVariantInventoryService.detachInventoryItem(
|
return await productVariantInventoryService.detachInventoryItem(
|
||||||
inventoryItem.id,
|
inventoryItem.id,
|
||||||
variant.id
|
tag
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import { WorkflowArguments } from "../../helper"
|
|||||||
|
|
||||||
export async function removeInventoryItems({
|
export async function removeInventoryItems({
|
||||||
container,
|
container,
|
||||||
context,
|
|
||||||
data,
|
data,
|
||||||
}: WorkflowArguments<{
|
}: WorkflowArguments<{
|
||||||
inventoryItems: { inventoryItem: InventoryItemDTO }[]
|
inventoryItems: { inventoryItem: InventoryItemDTO }[]
|
||||||
}>) {
|
}>) {
|
||||||
const { manager } = context
|
|
||||||
const inventoryService = container.resolve("inventoryService")
|
const inventoryService = container.resolve("inventoryService")
|
||||||
|
|
||||||
if (!inventoryService) {
|
if (!inventoryService) {
|
||||||
@@ -20,8 +18,7 @@ export async function removeInventoryItems({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await inventoryService!.deleteInventoryItem(
|
return await inventoryService!.deleteInventoryItem(
|
||||||
data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id),
|
data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id)
|
||||||
{ transactionManager: manager }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6499,7 +6499,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@medusajs/inventory@workspace:packages/inventory":
|
"@medusajs/inventory@workspace:^, @medusajs/inventory@workspace:packages/inventory":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/inventory@workspace:packages/inventory"
|
resolution: "@medusajs/inventory@workspace:packages/inventory"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -26516,6 +26516,7 @@ __metadata:
|
|||||||
"@babel/node": ^7.12.10
|
"@babel/node": ^7.12.10
|
||||||
"@medusajs/cache-inmemory": "workspace:*"
|
"@medusajs/cache-inmemory": "workspace:*"
|
||||||
"@medusajs/event-bus-local": "workspace:*"
|
"@medusajs/event-bus-local": "workspace:*"
|
||||||
|
"@medusajs/inventory": "workspace:^"
|
||||||
"@medusajs/medusa": "workspace:*"
|
"@medusajs/medusa": "workspace:*"
|
||||||
"@medusajs/product": "workspace:^"
|
"@medusajs/product": "workspace:^"
|
||||||
babel-preset-medusa-package: "*"
|
babel-preset-medusa-package: "*"
|
||||||
|
|||||||
Reference in New Issue
Block a user