Feat(core-flows, inventory-next, medusa, types): Add create location level endpoint (#6695)
* initialize create location levels * add enough from pr to make code build and test * fix integration tests * pr feedback * fix errors * rm dto * Update packages/core-flows/src/inventory/steps/validate-inventory-locations.ts Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
@@ -326,7 +326,7 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip("Create inventory item level", () => {
|
||||
describe("Create inventory item level", () => {
|
||||
let location1
|
||||
let location2
|
||||
|
||||
@@ -357,7 +357,7 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
})
|
||||
|
||||
it("should list the inventory items", async () => {
|
||||
it("should create location levels for an inventory item", async () => {
|
||||
const [{ id: inventoryItemId }] = await service.list({})
|
||||
|
||||
await api.post(
|
||||
@@ -394,6 +394,115 @@ medusaIntegrationTestRunner({
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should fail to create a location level for an inventory item", async () => {
|
||||
const [{ id: inventoryItemId }] = await service.list({})
|
||||
|
||||
const error = await api
|
||||
.post(
|
||||
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||
{
|
||||
location_id: "{location1.id}",
|
||||
stocked_quantity: 10,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((error) => error)
|
||||
|
||||
expect(error.response.status).toEqual(404)
|
||||
expect(error.response.data).toEqual({
|
||||
type: "not_found",
|
||||
message: "Stock locations with ids: {location1.id} was not found",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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.skip("Create inventory items", () => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@medusajs/customer": "workspace:^",
|
||||
"@medusajs/event-bus-local": "workspace:*",
|
||||
"@medusajs/inventory": "workspace:^",
|
||||
"@medusajs/inventory-next": "workspace:^",
|
||||
"@medusajs/medusa": "workspace:*",
|
||||
"@medusajs/modules-sdk": "workspace:^",
|
||||
"@medusajs/pricing": "workspace:^",
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from "./definitions"
|
||||
export * from "./fulfillment"
|
||||
export * as Handlers from "./handlers"
|
||||
export * from "./invite"
|
||||
export * from "./inventory"
|
||||
export * from "./payment"
|
||||
export * from "./price-list"
|
||||
export * from "./pricing"
|
||||
|
||||
2
packages/core-flows/src/inventory/index.ts
Normal file
2
packages/core-flows/src/inventory/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./workflows"
|
||||
export * from "./steps"
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
CreateInventoryLevelInput,
|
||||
IInventoryServiceNext,
|
||||
} from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export const createInventoryLevelsStepId = "create-inventory-levels"
|
||||
export const createInventoryLevelsStep = createStep(
|
||||
createInventoryLevelsStepId,
|
||||
async (data: CreateInventoryLevelInput[], { container }) => {
|
||||
const service = container.resolve<IInventoryServiceNext>(
|
||||
ModuleRegistrationName.INVENTORY
|
||||
)
|
||||
|
||||
const inventoryLevels = await service.createInventoryLevels(data)
|
||||
return new StepResponse(
|
||||
inventoryLevels,
|
||||
inventoryLevels.map((level) => level.id)
|
||||
)
|
||||
},
|
||||
async (ids, { container }) => {
|
||||
if (!ids?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = container.resolve<IInventoryServiceNext>(
|
||||
ModuleRegistrationName.INVENTORY
|
||||
)
|
||||
|
||||
await service.deleteInventoryLevels(ids)
|
||||
}
|
||||
)
|
||||
2
packages/core-flows/src/inventory/steps/index.ts
Normal file
2
packages/core-flows/src/inventory/steps/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./create-inventory-levels"
|
||||
export * from "./validate-inventory-locations"
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
arrayDifference,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { CreateInventoryLevelInput } from "@medusajs/types"
|
||||
import { createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const validateInventoryLocationsStepId = "validate-inventory-levels-step"
|
||||
export const validateInventoryLocationsStep = createStep(
|
||||
validateInventoryLocationsStepId,
|
||||
async (data: CreateInventoryLevelInput[], { container }) => {
|
||||
const remoteQuery = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
const locationQuery = remoteQueryObjectFromString({
|
||||
entryPoint: "stock_location",
|
||||
variables: {
|
||||
id: data.map((d) => d.location_id),
|
||||
},
|
||||
fields: ["id"],
|
||||
})
|
||||
|
||||
const stockLocations = await remoteQuery(locationQuery)
|
||||
|
||||
const diff = arrayDifference(
|
||||
data.map((d) => d.location_id),
|
||||
stockLocations.map((l) => l.id)
|
||||
)
|
||||
if (diff.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Stock locations with ids: ${diff.join(", ")} was not found`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CreateInventoryLevelInput, InventoryLevelDTO } from "@medusajs/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
createInventoryLevelsStep,
|
||||
validateInventoryLocationsStep,
|
||||
} from "../steps"
|
||||
|
||||
interface WorkflowInput {
|
||||
inventory_levels: CreateInventoryLevelInput[]
|
||||
}
|
||||
export const createInventoryLevelsWorkflowId =
|
||||
"create-inventory-levels-workflow"
|
||||
export const createInventoryLevelsWorkflow = createWorkflow(
|
||||
createInventoryLevelsWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>): WorkflowData<InventoryLevelDTO[]> => {
|
||||
validateInventoryLocationsStep(input.inventory_levels)
|
||||
|
||||
return createInventoryLevelsStep(input.inventory_levels)
|
||||
}
|
||||
)
|
||||
1
packages/core-flows/src/inventory/workflows/index.ts
Normal file
1
packages/core-flows/src/inventory/workflows/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./create-inventory-levels"
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||
|
||||
import { AdminPostInventoryItemsItemLocationLevelsReq } from "../../validators"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { createInventoryLevelsWorkflow } from "@medusajs/core-flows"
|
||||
import { defaultAdminInventoryItemFields } from "../../query-config"
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<AdminPostInventoryItemsItemLocationLevelsReq>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { id } = req.params
|
||||
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const workflow = createInventoryLevelsWorkflow(req.scope)
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
inventory_levels: [
|
||||
{
|
||||
inventory_item_id: id,
|
||||
...req.validatedBody,
|
||||
},
|
||||
],
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const itemQuery = remoteQueryObjectFromString({
|
||||
entryPoint: "inventory_items",
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
fields: defaultAdminInventoryItemFields,
|
||||
})
|
||||
|
||||
const [inventory_item] = await remoteQuery(itemQuery)
|
||||
|
||||
res.status(200).json({ inventory_item })
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
AdminGetInventoryItemsItemParams,
|
||||
AdminGetInventoryItemsParams,
|
||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||
AdminPostInventoryItemsReq,
|
||||
} from "./validators"
|
||||
import { transformBody, transformQuery } from "../../../api/middlewares"
|
||||
|
||||
@@ -42,9 +41,4 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
matcher: "/admin/inventory-items/:id/location-levels",
|
||||
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/inventory-items",
|
||||
middlewares: [transformBody(AdminPostInventoryItemsReq)],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -135,134 +135,3 @@ 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."
|
||||
* required:
|
||||
* - variant_id
|
||||
* properties:
|
||||
* variant_id:
|
||||
* description: The ID of the variant to create the inventory item for.
|
||||
* type: string
|
||||
* 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 {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
variant_id: string
|
||||
|
||||
@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>
|
||||
}
|
||||
|
||||
@@ -790,6 +790,11 @@ export interface IInventoryServiceNext extends IModuleService {
|
||||
context?: Context
|
||||
): Promise<void>
|
||||
|
||||
deleteInventoryLevels(
|
||||
inventoryLevelIds: string | string[],
|
||||
context?: Context
|
||||
): Promise<void>
|
||||
|
||||
/**
|
||||
* This method is used to adjust the inventory level's stocked quantity. The inventory level is identified by the IDs of its associated inventory item and location.
|
||||
*
|
||||
|
||||
@@ -31891,6 +31891,7 @@ __metadata:
|
||||
"@medusajs/customer": "workspace:^"
|
||||
"@medusajs/event-bus-local": "workspace:*"
|
||||
"@medusajs/inventory": "workspace:^"
|
||||
"@medusajs/inventory-next": "workspace:^"
|
||||
"@medusajs/medusa": "workspace:*"
|
||||
"@medusajs/modules-sdk": "workspace:^"
|
||||
"@medusajs/pricing": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user