diff --git a/integration-tests/modules/__tests__/inventory/index.spec.ts b/integration-tests/modules/__tests__/inventory/index.spec.ts index 83ecd80d27..eee602fc79 100644 --- a/integration-tests/modules/__tests__/inventory/index.spec.ts +++ b/integration-tests/modules/__tests__/inventory/index.spec.ts @@ -417,32 +417,12 @@ medusaIntegrationTestRunner({ }) }) - describe.skip("Create inventory items", () => { + describe("Create inventory items", () => { it("should create inventory items", async () => { - const createResult = await api.post( - `/admin/products`, - { - title: "Test Product", - variants: [ - { - title: "Test Variant w. inventory 2", - sku: "MY_SKU1", - material: "material", - }, - ], - }, - adminHeaders - ) - - const inventoryItems = await service.list() - - expect(inventoryItems).toHaveLength(0) - const response = await api.post( `/admin/inventory-items`, { sku: "test-sku", - variant_id: createResult.data.product.variants[0].id, }, adminHeaders ) @@ -454,143 +434,6 @@ medusaIntegrationTestRunner({ }) ) }) - - it("should attach inventory items on creation", async () => { - const createResult = await api.post( - `/admin/products`, - { - title: "Test Product", - variants: [ - { - title: "Test Variant w. inventory 2", - sku: "MY_SKU1", - material: "material", - }, - ], - }, - adminHeaders - ) - - const inventoryItems = await service.list() - - expect(inventoryItems).toHaveLength(0) - - await api.post( - `/admin/inventory-items`, - { - sku: "test-sku", - variant_id: createResult.data.product.variants[0].id, - }, - adminHeaders - ) - - const remoteQuery = appContainer.resolve( - ContainerRegistrationKeys.REMOTE_QUERY - ) - - const query = remoteQueryObjectFromString({ - entryPoint: "product_variant_inventory_item", - variables: { - variant_id: createResult.data.product.variants[0].id, - }, - fields: ["inventory_item_id", "variant_id"], - }) - - const existingItems = await remoteQuery(query) - - expect(existingItems).toHaveLength(1) - expect(existingItems[0].variant_id).toEqual( - createResult.data.product.variants[0].id - ) - }) - }) - - describe.skip("Create inventory items", () => { - it("should create inventory items", async () => { - const createResult = await api.post( - `/admin/products`, - { - title: "Test Product", - variants: [ - { - title: "Test Variant w. inventory 2", - sku: "MY_SKU1", - material: "material", - }, - ], - }, - adminHeaders - ) - - const inventoryItems = await service.list({}) - - expect(inventoryItems).toHaveLength(0) - - const response = await api.post( - `/admin/inventory-items`, - { - sku: "test-sku", - variant_id: createResult.data.product.variants[0].id, - }, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.inventory_item).toEqual( - expect.objectContaining({ - sku: "test-sku", - }) - ) - }) - - it("should attach inventory items on creation", async () => { - const createResult = await api.post( - `/admin/products`, - { - title: "Test Product", - variants: [ - { - title: "Test Variant w. inventory 2", - sku: "MY_SKU1", - material: "material", - }, - ], - }, - adminHeaders - ) - - const inventoryItems = await service.list({}) - - expect(inventoryItems).toHaveLength(0) - - await api.post( - `/admin/inventory-items`, - { - sku: "test-sku", - variant_id: createResult.data.product.variants[0].id, - }, - adminHeaders - ) - - const remoteQuery = appContainer.resolve( - ContainerRegistrationKeys.REMOTE_QUERY - ) - - const query = remoteQueryObjectFromString({ - entryPoint: "product_variant_inventory_item", - variables: { - variant_id: createResult.data.product.variants[0].id, - }, - fields: ["inventory_item_id", "variant_id"], - }) - - const existingItems = await remoteQuery(query) - - expect(existingItems).toHaveLength(1) - expect(existingItems[0].variant_id).toEqual( - createResult.data.product.variants[0].id - ) - }) }) describe("List inventory items", () => { diff --git a/packages/core-flows/src/inventory/steps/attach-inventory-items.ts b/packages/core-flows/src/inventory/steps/attach-inventory-items.ts new file mode 100644 index 0000000000..e3ffddc37b --- /dev/null +++ b/packages/core-flows/src/inventory/steps/attach-inventory-items.ts @@ -0,0 +1,43 @@ +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { ContainerRegistrationKeys } from "@medusajs/utils" +import { InventoryItemDTO } from "@medusajs/types" + +export const attachInventoryItemToVariantsStepId = + "attach-inventory-items-to-variants-step" +export const attachInventoryItemToVariants = createStep( + attachInventoryItemToVariantsStepId, + async ( + input: { + inventoryItemId: string + tag?: string + }[], + { container } + ) => { + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + const linkDefinitions = input + .filter(({ tag }) => !!tag) + .map(({ inventoryItemId, tag }) => ({ + productService: { + variant_id: tag, + }, + inventoryService: { + inventory_item_id: inventoryItemId, + }, + })) + + const links = await remoteLink.create(linkDefinitions) + + return new StepResponse(links, linkDefinitions) + }, + async (linkDefinitions, { container }) => { + if (!linkDefinitions?.length) { + return + } + + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + await remoteLink.dismiss(linkDefinitions) + } +) diff --git a/packages/core-flows/src/inventory/steps/create-inventory-items.ts b/packages/core-flows/src/inventory/steps/create-inventory-items.ts new file mode 100644 index 0000000000..efe2365eca --- /dev/null +++ b/packages/core-flows/src/inventory/steps/create-inventory-items.ts @@ -0,0 +1,34 @@ +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { CreateInventoryItemInput } from "@medusajs/types" +import { IInventoryServiceNext } from "@medusajs/types" +import { InventoryNext } from "@medusajs/types" +import { ModuleRegistrationName } from "../../../../modules-sdk/dist" +import { promiseAll } from "@medusajs/utils" + +export const createInventoryItemsStepId = "create-inventory-items" +export const createInventoryItemsStep = createStep( + createInventoryItemsStepId, + async (data: InventoryNext.CreateInventoryItemInput[], { container }) => { + const inventoryService: IInventoryServiceNext = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + const createdItems: InventoryNext.InventoryItemDTO[] = + await inventoryService.create(data) + + return new StepResponse( + createdItems, + createdItems.map((i) => i.id) + ) + }, + async (data: string[] | undefined, { container }) => { + if (!data?.length) { + return + } + + const inventoryService = container.resolve(ModuleRegistrationName.INVENTORY) + + await inventoryService!.delete(data) + } +) diff --git a/packages/core-flows/src/inventory/steps/index.ts b/packages/core-flows/src/inventory/steps/index.ts index 26b4abc742..8171259165 100644 --- a/packages/core-flows/src/inventory/steps/index.ts +++ b/packages/core-flows/src/inventory/steps/index.ts @@ -1,2 +1,5 @@ +export * from "./attach-inventory-items" +export * from "./create-inventory-items" +export * from "./validate-singular-inventory-items-for-tags" export * from "./create-inventory-levels" export * from "./validate-inventory-locations" diff --git a/packages/core-flows/src/inventory/steps/validate-singular-inventory-items-for-tags.ts b/packages/core-flows/src/inventory/steps/validate-singular-inventory-items-for-tags.ts new file mode 100644 index 0000000000..bc1cbcf6c1 --- /dev/null +++ b/packages/core-flows/src/inventory/steps/validate-singular-inventory-items-for-tags.ts @@ -0,0 +1,45 @@ +import { + ContainerRegistrationKeys, + MedusaError, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { InventoryNext } from "@medusajs/types" +import { Modules } from "@medusajs/modules-sdk" + +export const validateInventoryItemsForCreateStepId = + "validate-inventory-items-for-create-step" +export const validateInventoryItemsForCreate = createStep( + validateInventoryItemsForCreateStepId, + async ( + input: { + tag?: string + }[], + { container } + ) => { + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + const linkService = remoteLink.getLinkModule( + Modules.PRODUCT, + "variant_id", + Modules.INVENTORY, + "inventory_item_id" + ) + + const existingItems = await linkService.list( + { variant_id: input.map((i) => i.tag) }, + { select: ["variant_id", "inventory_item_id"] } + ) + + if (existingItems.length) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Inventory items already exist for variants with ids: " + + existingItems.map((i) => i.variant_id).join(", ") + ) + } + + return new StepResponse(input) + } +) diff --git a/packages/core-flows/src/inventory/workflows/create-inventory-items.ts b/packages/core-flows/src/inventory/workflows/create-inventory-items.ts new file mode 100644 index 0000000000..f077e353aa --- /dev/null +++ b/packages/core-flows/src/inventory/workflows/create-inventory-items.ts @@ -0,0 +1,26 @@ +import { + WorkflowData, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { + attachInventoryItemToVariants, + createInventoryItemsStep, + validateInventoryItemsForCreate, +} from "../steps" + +import { InventoryNext } from "@medusajs/types" + +interface WorkflowInput { + items: InventoryNext.CreateInventoryItemInput[] +} + +export const createInventoryItemsWorkflowId = "create-inventory-items-workflow" +export const createInventoryItemsWorkflow = createWorkflow( + createInventoryItemsWorkflowId, + (input: WorkflowData) => { + const items = createInventoryItemsStep(input.items) + + return items + } +) diff --git a/packages/core-flows/src/inventory/workflows/index.ts b/packages/core-flows/src/inventory/workflows/index.ts index c8fa3b339b..46378681d5 100644 --- a/packages/core-flows/src/inventory/workflows/index.ts +++ b/packages/core-flows/src/inventory/workflows/index.ts @@ -1 +1,2 @@ +export * from "./create-inventory-items" export * from "./create-inventory-levels" diff --git a/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts b/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts index 47bc320384..dd3dfbf883 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts @@ -4,6 +4,7 @@ import { AdminGetInventoryItemsItemParams, AdminGetInventoryItemsParams, AdminPostInventoryItemsItemLocationLevelsReq, + AdminPostInventoryItemsReq, } from "./validators" import { transformBody, transformQuery } from "../../../api/middlewares" @@ -14,7 +15,7 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [ { method: "ALL", matcher: "/admin/inventory-items*", - middlewares: [authenticate("admin", ["session", "bearer"])], + middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], }, { method: ["GET"], @@ -41,4 +42,15 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/admin/inventory-items/:id/location-levels", middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)], }, + { + method: ["POST"], + matcher: "/admin/inventory-items", + middlewares: [ + transformBody(AdminPostInventoryItemsReq), + transformQuery( + AdminGetInventoryItemsItemParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts b/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts index 4dbbd6eb64..f78976be27 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts @@ -1,8 +1,5 @@ import { InventoryNext } from "@medusajs/types" -export const defaultAdminInventoryItemRelations = [] -export const allowedAdminInventoryItemRelations = [] - // eslint-disable-next-line max-len export const defaultAdminLocationLevelFields: (keyof InventoryNext.InventoryLevelDTO)[] = [ @@ -44,9 +41,8 @@ export const defaultAdminInventoryItemFields = [ ] export const retrieveTransformQueryConfig = { - defaultFields: defaultAdminInventoryItemFields, - defaultRelations: defaultAdminInventoryItemRelations, - allowedRelations: allowedAdminInventoryItemRelations, + defaults: defaultAdminInventoryItemFields, + allowed: defaultAdminInventoryItemFields, isList: false, } diff --git a/packages/medusa/src/api-v2/admin/inventory-items/route.ts b/packages/medusa/src/api-v2/admin/inventory-items/route.ts index bc32976c57..0892821f05 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/route.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/route.ts @@ -7,6 +7,33 @@ import { remoteQueryObjectFromString, } from "@medusajs/utils" +import { AdminPostInventoryItemsReq } from "./validators" +import { createInventoryItemsWorkflow } from "@medusajs/core-flows" + +// Create inventory-item +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const { result } = await createInventoryItemsWorkflow(req.scope).run({ + input: { items: [req.validatedBody] }, + }) + + const [inventory_item] = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "inventory_items", + variables: { + id: result[0].id, + }, + fields: req.retrieveConfig.select as string[], + }) + ) + + res.status(200).json({ inventory_item }) +} + // List inventory-items export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api-v2/admin/inventory-items/validators.ts b/packages/medusa/src/api-v2/admin/inventory-items/validators.ts index b88bbd2713..90a93fa634 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/validators.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/validators.ts @@ -135,3 +135,125 @@ export class AdminPostInventoryItemsItemLocationLevelsReq { // eslint-disable-next-line export class AdminPostInventoryItemsItemLocationLevelsParams extends FindParams {} + +/** + * @schema AdminPostInventoryItemsReq + * type: object + * description: "The details of the inventory item to create." + * properties: + * sku: + * description: The unique SKU of the associated Product Variant. + * type: string + * ean: + * description: The EAN number of the item. + * type: string + * upc: + * description: The UPC number of the item. + * type: string + * barcode: + * description: A generic GTIN field for the Product Variant. + * type: string + * hs_code: + * description: The Harmonized System code of the Inventory Item. May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * inventory_quantity: + * description: The amount of stock kept of the associated Product Variant. + * type: integer + * default: 0 + * allow_backorder: + * description: Whether the associated Product Variant can be purchased when out of stock. + * type: boolean + * manage_inventory: + * description: Whether Medusa should keep track of the inventory for the associated Product Variant. + * type: boolean + * default: true + * weight: + * description: The weight of the Inventory Item. May be used in shipping rate calculations. + * type: number + * length: + * description: The length of the Inventory Item. May be used in shipping rate calculations. + * type: number + * height: + * description: The height of the Inventory Item. May be used in shipping rate calculations. + * type: number + * width: + * description: The width of the Inventory Item. May be used in shipping rate calculations. + * type: number + * origin_country: + * description: The country in which the Inventory Item was produced. May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * mid_code: + * description: The Manufacturers Identification code that identifies the manufacturer of the Inventory Item. May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * material: + * description: The material and composition that the Inventory Item is made of, May be used by Fulfillment Providers to pass customs information to shipping carriers. + * type: string + * title: + * description: The inventory item's title. + * type: string + * description: + * description: The inventory item's description. + * type: string + * thumbnail: + * description: The inventory item's thumbnail. + * type: string + * metadata: + * description: An optional set of key-value pairs with additional information. + * type: object + * externalDocs: + * description: "Learn about the metadata attribute, and how to delete and update it." + * url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute" + */ +export class AdminPostInventoryItemsReq { + @IsString() + @IsOptional() + sku?: string + + @IsString() + @IsOptional() + hs_code?: string + + @IsNumber() + @IsOptional() + weight?: number + + @IsNumber() + @IsOptional() + length?: number + + @IsNumber() + @IsOptional() + height?: number + + @IsNumber() + @IsOptional() + width?: number + + @IsString() + @IsOptional() + origin_country?: string + + @IsString() + @IsOptional() + mid_code?: string + + @IsString() + @IsOptional() + material?: string + + @IsString() + @IsOptional() + title?: string + + @IsString() + @IsOptional() + description?: string + + @IsString() + @IsOptional() + thumbnail?: string + + @IsObject() + @IsOptional() + metadata?: Record +}