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:
Philip Korsholm
2024-03-25 07:43:41 +01:00
committed by GitHub
parent 0168c819da
commit aa154665de
11 changed files with 269 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*/ */