Feat(core-flows, inventory-next, medusa, types): Add create inventory item endpoint (#6693)

* init create inventory item

* fix integration tests

* pr feedback

* rename to validate

* pr feedback

* bulk delete

* undo change w/ naming

* update dto body

* move integration test

* pr fixes

* fix for pr review

* revert medusa-config changes

* pr feedback

* fix build
This commit is contained in:
Philip Korsholm
2024-03-19 09:20:30 +01:00
committed by GitHub
parent 0219a8677b
commit 390bc3e72f
11 changed files with 317 additions and 165 deletions

View File

@@ -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", () => {

View File

@@ -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)
}
)

View File

@@ -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)
}
)

View File

@@ -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"

View File

@@ -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)
}
)

View File

@@ -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<WorkflowInput>) => {
const items = createInventoryItemsStep(input.items)
return items
}
)

View File

@@ -1 +1,2 @@
export * from "./create-inventory-items"
export * from "./create-inventory-levels"

View File

@@ -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
),
],
},
]

View File

@@ -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,
}

View File

@@ -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<AdminPostInventoryItemsReq>,
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,

View File

@@ -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<string, unknown>
}