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:
1 parent
e77a02aca5
commit
84b8836cbf
14 files changed
+266
-139
No files matched your search
@@ -326,7 +326,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.skip("Create inventory item level", () => {
|
describe("Create inventory item level", () => {
|
||||||
let location1
|
let location1
|
||||||
let location2
|
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({})
|
const [{ id: inventoryItemId }] = await service.list({})
|
||||||
|
|
||||||
await api.post(
|
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", () => {
|
describe.skip("Create inventory items", () => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@medusajs/customer": "workspace:^",
|
"@medusajs/customer": "workspace:^",
|
||||||
"@medusajs/event-bus-local": "workspace:*",
|
"@medusajs/event-bus-local": "workspace:*",
|
||||||
"@medusajs/inventory": "workspace:^",
|
"@medusajs/inventory": "workspace:^",
|
||||||
|
"@medusajs/inventory-next": "workspace:^",
|
||||||
"@medusajs/medusa": "workspace:*",
|
"@medusajs/medusa": "workspace:*",
|
||||||
"@medusajs/modules-sdk": "workspace:^",
|
"@medusajs/modules-sdk": "workspace:^",
|
||||||
"@medusajs/pricing": "workspace:^",
|
"@medusajs/pricing": "workspace:^",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export * from "./definitions"
|
|||||||
export * from "./fulfillment"
|
export * from "./fulfillment"
|
||||||
export * as Handlers from "./handlers"
|
export * as Handlers from "./handlers"
|
||||||
export * from "./invite"
|
export * from "./invite"
|
||||||
|
export * from "./inventory"
|
||||||
export * from "./payment"
|
export * from "./payment"
|
||||||
export * from "./price-list"
|
export * from "./price-list"
|
||||||
export * from "./pricing"
|
export * from "./pricing"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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,
|
AdminGetInventoryItemsItemParams,
|
||||||
AdminGetInventoryItemsParams,
|
AdminGetInventoryItemsParams,
|
||||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||||
AdminPostInventoryItemsReq,
|
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
import { transformBody, transformQuery } from "../../../api/middlewares"
|
import { transformBody, transformQuery } from "../../../api/middlewares"
|
||||||
|
|
||||||
@@ -42,9 +41,4 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
matcher: "/admin/inventory-items/:id/location-levels",
|
matcher: "/admin/inventory-items/:id/location-levels",
|
||||||
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
|
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
method: ["POST"],
|
|
||||||
matcher: "/admin/inventory-items",
|
|
||||||
middlewares: [transformBody(AdminPostInventoryItemsReq)],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
@@ -135,134 +135,3 @@ export class AdminPostInventoryItemsItemLocationLevelsReq {
|
|||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
export class AdminPostInventoryItemsItemLocationLevelsParams extends FindParams {}
|
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
|
context?: Context
|
||||||
): Promise<void>
|
): 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.
|
* 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/customer": "workspace:^"
|
||||||
"@medusajs/event-bus-local": "workspace:*"
|
"@medusajs/event-bus-local": "workspace:*"
|
||||||
"@medusajs/inventory": "workspace:^"
|
"@medusajs/inventory": "workspace:^"
|
||||||
|
"@medusajs/inventory-next": "workspace:^"
|
||||||
"@medusajs/medusa": "workspace:*"
|
"@medusajs/medusa": "workspace:*"
|
||||||
"@medusajs/modules-sdk": "workspace:^"
|
"@medusajs/modules-sdk": "workspace:^"
|
||||||
"@medusajs/pricing": "workspace:^"
|
"@medusajs/pricing": "workspace:^"
|
||||||
|
|||||||
Reference in new issue
Block a user