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", () => {
|
describe("Retrieve inventory item", () => {
|
||||||
let location1 = "loc_1"
|
let location1 = "loc_1"
|
||||||
let location2 = "loc_2"
|
let location2 = "loc_2"
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from "./create-inventory-levels"
|
|||||||
export * from "./validate-inventory-locations"
|
export * from "./validate-inventory-locations"
|
||||||
export * from "./update-inventory-items"
|
export * from "./update-inventory-items"
|
||||||
export * from "./delete-inventory-levels"
|
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 "./create-inventory-levels"
|
||||||
export * from "./update-inventory-items"
|
export * from "./update-inventory-items"
|
||||||
export * from "./delete-inventory-levels"
|
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"
|
} from "@medusajs/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing"
|
import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing"
|
||||||
|
|
||||||
|
import { AdminPostInventoryItemsItemLocationLevelsLevelReq } from "../../../validators"
|
||||||
import { deleteInventoryLevelsWorkflow } from "@medusajs/core-flows"
|
import { deleteInventoryLevelsWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { updateInventoryLevelsWorkflow } from "@medusajs/core-flows"
|
||||||
|
|
||||||
export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
|
export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
const { id, location_id } = req.params
|
const { id, location_id } = req.params
|
||||||
@@ -45,3 +47,36 @@ export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => {
|
|||||||
deleted: true,
|
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,
|
AdminGetInventoryItemsParams,
|
||||||
AdminPostInventoryItemsInventoryItemParams,
|
AdminPostInventoryItemsInventoryItemParams,
|
||||||
AdminPostInventoryItemsInventoryItemReq,
|
AdminPostInventoryItemsInventoryItemReq,
|
||||||
|
AdminPostInventoryItemsItemLocationLevelsLevelParams,
|
||||||
|
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
||||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||||
AdminPostInventoryItemsReq,
|
AdminPostInventoryItemsReq,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
@@ -39,11 +41,6 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
method: ["POST"],
|
|
||||||
matcher: "/admin/inventory-items/:id/location-levels",
|
|
||||||
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
matcher: "/admin/inventory-items",
|
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"],
|
method: ["POST"],
|
||||||
matcher: "/admin/inventory-items/:id",
|
matcher: "/admin/inventory-items/:id",
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ export const retrieveTransformQueryConfig = {
|
|||||||
isList: false,
|
isList: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const retrieveLocationLevelsTransformQueryConfig = {
|
||||||
|
defaults: defaultAdminLocationLevelFields,
|
||||||
|
allowed: defaultAdminLocationLevelFields,
|
||||||
|
isList: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listLocationLevelsTransformQueryConfig = {
|
||||||
|
...retrieveLocationLevelsTransformQueryConfig,
|
||||||
|
isList: true,
|
||||||
|
}
|
||||||
|
|
||||||
export const listTransformQueryConfig = {
|
export const listTransformQueryConfig = {
|
||||||
...retrieveTransformQueryConfig,
|
...retrieveTransformQueryConfig,
|
||||||
isList: true,
|
isList: true,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
IsObject,
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
Min,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { Transform, Type } from "class-transformer"
|
import { Transform, Type } from "class-transformer"
|
||||||
@@ -258,6 +259,31 @@ export class AdminPostInventoryItemsReq {
|
|||||||
metadata?: Record<string, unknown>
|
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
|
* @schema AdminPostInventoryItemsInventoryItemReq
|
||||||
* type: object
|
* type: object
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { BaseFilterable, OperatorMap } from "../../dal"
|
||||||
|
|
||||||
import { NumericalComparisonOperator } from "../../common"
|
import { NumericalComparisonOperator } from "../../common"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +55,8 @@ export interface InventoryLevelDTO {
|
|||||||
deleted_at: string | Date | null
|
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.
|
* 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.
|
* 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.
|
* 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.
|
* 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 of the inventory level to update
|
||||||
*/
|
*/
|
||||||
id: string
|
id?: string
|
||||||
/**
|
/**
|
||||||
* The stocked quantity of the associated inventory item in the associated location.
|
* The stocked quantity of the associated inventory item in the associated location.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user