feat(core-flows, types, medusa): Add Update location level endpoint for api-v2 (#6743)
* initialize update-location-level * update middlewares * readd middleware * pr feedback
This commit is contained in:
@@ -293,6 +293,102 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
})
|
||||
|
||||
describe("Update inventory levels", () => {
|
||||
let locationId
|
||||
let inventoryItemId
|
||||
beforeEach(async () => {
|
||||
const invItemReps = await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "test-sku" },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
inventoryItemId = invItemReps.data.inventory_item.id
|
||||
|
||||
const stockLocation = await appContainer
|
||||
.resolve(ModuleRegistrationName.STOCK_LOCATION)
|
||||
.create({ name: "test-location" })
|
||||
|
||||
locationId = stockLocation.id
|
||||
|
||||
await api.post(
|
||||
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||
{
|
||||
location_id: locationId,
|
||||
stocked_quantity: 10,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
it("should update the stocked and incoming quantity for an inventory level", async () => {
|
||||
const result = await api.post(
|
||||
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
|
||||
{
|
||||
stocked_quantity: 15,
|
||||
incoming_quantity: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(result.status).toEqual(200)
|
||||
expect(result.data.inventory_item).toEqual(
|
||||
expect.objectContaining({
|
||||
id: inventoryItemId,
|
||||
location_levels: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
inventory_item_id: inventoryItemId,
|
||||
location_id: locationId,
|
||||
stocked_quantity: 15,
|
||||
reserved_quantity: 0,
|
||||
incoming_quantity: 5,
|
||||
metadata: null,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to update a non-existing location level", async () => {
|
||||
const error = await api
|
||||
.post(
|
||||
`/admin/inventory-items/${inventoryItemId}/location-levels/does-not-exist`,
|
||||
{
|
||||
stocked_quantity: 15,
|
||||
incoming_quantity: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.response.status).toEqual(404)
|
||||
expect(error.response.data).toEqual({
|
||||
type: "not_found",
|
||||
message: `Item ${inventoryItemId} is not stocked at location does-not-exist`,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail to update a non-existing inventory_item_id level", async () => {
|
||||
const error = await api
|
||||
.post(
|
||||
`/admin/inventory-items/does-not-exist/location-levels/${locationId}`,
|
||||
{
|
||||
stocked_quantity: 15,
|
||||
incoming_quantity: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.response.status).toEqual(404)
|
||||
expect(error.response.data).toEqual({
|
||||
type: "not_found",
|
||||
message: `Item does-not-exist is not stocked at location ${locationId}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Retrieve inventory item", () => {
|
||||
let location1 = "loc_1"
|
||||
let location2 = "loc_2"
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from "./create-inventory-levels"
|
||||
export * from "./validate-inventory-locations"
|
||||
export * from "./update-inventory-items"
|
||||
export * from "./delete-inventory-levels"
|
||||
export * from "./update-inventory-levels"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { IInventoryServiceNext, InventoryNext } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
convertItemResponseToUpdateRequest,
|
||||
getSelectsAndRelationsFromObjectArray,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export const updateInventoryLevelsStepId = "update-inventory-levels-step"
|
||||
export const updateInventoryLevelsStep = createStep(
|
||||
updateInventoryLevelsStepId,
|
||||
async (
|
||||
input: InventoryNext.BulkUpdateInventoryLevelInput[],
|
||||
{ container }
|
||||
) => {
|
||||
const inventoryService: IInventoryServiceNext = container.resolve(
|
||||
ModuleRegistrationName.INVENTORY
|
||||
)
|
||||
|
||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray(input)
|
||||
|
||||
const dataBeforeUpdate = await inventoryService.listInventoryLevels(
|
||||
{
|
||||
$or: input.map(({ inventory_item_id, location_id }) => ({
|
||||
inventory_item_id,
|
||||
location_id,
|
||||
})),
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const updatedLevels: InventoryNext.InventoryLevelDTO[] =
|
||||
await inventoryService.updateInventoryLevels(input)
|
||||
|
||||
return new StepResponse(updatedLevels, {
|
||||
dataBeforeUpdate,
|
||||
selects,
|
||||
relations,
|
||||
})
|
||||
},
|
||||
async (revertInput, { container }) => {
|
||||
if (!revertInput?.dataBeforeUpdate?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const { dataBeforeUpdate, selects, relations } = revertInput
|
||||
|
||||
const inventoryService = container.resolve(ModuleRegistrationName.INVENTORY)
|
||||
|
||||
await inventoryService.updateInventoryLevels(
|
||||
dataBeforeUpdate.map((data) =>
|
||||
convertItemResponseToUpdateRequest(data, selects, relations)
|
||||
) as InventoryNext.BulkUpdateInventoryLevelInput[]
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -3,3 +3,4 @@ export * from "./create-inventory-items"
|
||||
export * from "./create-inventory-levels"
|
||||
export * from "./update-inventory-items"
|
||||
export * from "./delete-inventory-levels"
|
||||
export * from "./update-inventory-levels"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { InventoryLevelDTO, InventoryNext } from "@medusajs/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
|
||||
import { updateInventoryLevelsStep } from "../steps/update-inventory-levels"
|
||||
|
||||
interface WorkflowInput {
|
||||
updates: InventoryNext.BulkUpdateInventoryLevelInput[]
|
||||
}
|
||||
export const updateInventoryLevelsWorkflowId =
|
||||
"update-inventory-levels-workflow"
|
||||
export const updateInventoryLevelsWorkflow = createWorkflow(
|
||||
updateInventoryLevelsWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>): WorkflowData<InventoryLevelDTO[]> => {
|
||||
return updateInventoryLevelsStep(input.updates)
|
||||
}
|
||||
)
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing"
|
||||
|
||||
import { AdminPostInventoryItemsItemLocationLevelsLevelReq } from "../../../validators"
|
||||
import { deleteInventoryLevelsWorkflow } from "@medusajs/core-flows"
|
||||
import { updateInventoryLevelsWorkflow } from "@medusajs/core-flows"
|
||||
|
||||
export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const { id, location_id } = req.params
|
||||
@@ -45,3 +47,36 @@ export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<AdminPostInventoryItemsItemLocationLevelsLevelReq>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { id: inventory_item_id, location_id } = req.params
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const { errors } = await updateInventoryLevelsWorkflow(req.scope).run({
|
||||
input: {
|
||||
updates: [{ inventory_item_id, location_id, ...req.validatedBody }],
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const [inventory_item] = await remoteQuery(
|
||||
remoteQueryObjectFromString({
|
||||
entryPoint: "inventory",
|
||||
variables: {
|
||||
id: inventory_item_id,
|
||||
},
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
})
|
||||
)
|
||||
|
||||
res.status(200).json({
|
||||
inventory_item,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
AdminGetInventoryItemsParams,
|
||||
AdminPostInventoryItemsInventoryItemParams,
|
||||
AdminPostInventoryItemsInventoryItemReq,
|
||||
AdminPostInventoryItemsItemLocationLevelsLevelParams,
|
||||
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||
AdminPostInventoryItemsReq,
|
||||
} from "./validators"
|
||||
@@ -39,11 +41,6 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/inventory-items/:id/location-levels",
|
||||
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/inventory-items",
|
||||
@@ -55,6 +52,22 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/inventory-items/:id/location-levels",
|
||||
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/inventory-items/:id/location-levels/:location_id",
|
||||
middlewares: [
|
||||
transformBody(AdminPostInventoryItemsItemLocationLevelsLevelReq),
|
||||
transformQuery(
|
||||
AdminPostInventoryItemsItemLocationLevelsLevelParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/inventory-items/:id",
|
||||
|
||||
@@ -45,6 +45,17 @@ export const retrieveTransformQueryConfig = {
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const retrieveLocationLevelsTransformQueryConfig = {
|
||||
defaults: defaultAdminLocationLevelFields,
|
||||
allowed: defaultAdminLocationLevelFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listLocationLevelsTransformQueryConfig = {
|
||||
...retrieveLocationLevelsTransformQueryConfig,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
export const listTransformQueryConfig = {
|
||||
...retrieveTransformQueryConfig,
|
||||
isList: true,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { Transform, Type } from "class-transformer"
|
||||
@@ -258,6 +259,31 @@ export class AdminPostInventoryItemsReq {
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema AdminPostInventoryItemsItemLocationLevelsLevelReq
|
||||
* type: object
|
||||
* properties:
|
||||
* stocked_quantity:
|
||||
* description: the total stock quantity of an inventory item at the given location ID
|
||||
* type: number
|
||||
* incoming_quantity:
|
||||
* description: the incoming stock quantity of an inventory item at the given location ID
|
||||
* type: number
|
||||
*/
|
||||
export class AdminPostInventoryItemsItemLocationLevelsLevelReq {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
incoming_quantity?: number
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
stocked_quantity?: number
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export class AdminPostInventoryItemsItemLocationLevelsLevelParams extends FindParams {}
|
||||
/**
|
||||
* @schema AdminPostInventoryItemsInventoryItemReq
|
||||
* type: object
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BaseFilterable, OperatorMap } from "../../dal"
|
||||
|
||||
import { NumericalComparisonOperator } from "../../common"
|
||||
|
||||
/**
|
||||
@@ -53,7 +55,8 @@ export interface InventoryLevelDTO {
|
||||
deleted_at: string | Date | null
|
||||
}
|
||||
|
||||
export interface FilterableInventoryLevelProps {
|
||||
export interface FilterableInventoryLevelProps
|
||||
extends BaseFilterable<FilterableInventoryLevelProps> {
|
||||
/**
|
||||
* Filter inventory levels by the ID of their associated inventory item.
|
||||
*/
|
||||
@@ -65,13 +68,13 @@ export interface FilterableInventoryLevelProps {
|
||||
/**
|
||||
* Filters to apply on inventory levels' `stocked_quantity` attribute.
|
||||
*/
|
||||
stocked_quantity?: number | NumericalComparisonOperator
|
||||
stocked_quantity?: number | OperatorMap<Number>
|
||||
/**
|
||||
* Filters to apply on inventory levels' `reserved_quantity` attribute.
|
||||
*/
|
||||
reserved_quantity?: number | NumericalComparisonOperator
|
||||
reserved_quantity?: number | OperatorMap<Number>
|
||||
/**
|
||||
* Filters to apply on inventory levels' `incoming_quantity` attribute.
|
||||
*/
|
||||
incoming_quantity?: number | NumericalComparisonOperator
|
||||
incoming_quantity?: number | OperatorMap<Number>
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface UpdateInventoryLevelInput {
|
||||
/**
|
||||
* id of the inventory level to update
|
||||
*/
|
||||
id: string
|
||||
id?: string
|
||||
/**
|
||||
* The stocked quantity of the associated inventory item in the associated location.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user