Skip to content

Commit

Permalink
feat(core-flows, types, medusa): Add Update location level endpoint f…
Browse files Browse the repository at this point in the history
…or api-v2 (#6743)

* initialize update-location-level

* update middlewares

* readd middleware

* pr feedback
  • Loading branch information
pKorsholm authored Mar 25, 2024
1 parent 0168c81 commit aa15466
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 8 deletions.
96 changes: 96 additions & 0 deletions integration-tests/modules/__tests__/inventory/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/core-flows/src/inventory/steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
57 changes: 57 additions & 0 deletions packages/core-flows/src/inventory/steps/update-inventory-levels.ts
Original file line number Diff line number Diff line change
@@ -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[]
)
}
)
1 change: 1 addition & 0 deletions packages/core-flows/src/inventory/workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -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)
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
})
}
19 changes: 16 additions & 3 deletions packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
AdminGetInventoryItemsParams,
AdminPostInventoryItemsInventoryItemParams,
AdminPostInventoryItemsInventoryItemReq,
AdminPostInventoryItemsItemLocationLevelsLevelParams,
AdminPostInventoryItemsItemLocationLevelsLevelReq,
AdminPostInventoryItemsItemLocationLevelsReq,
AdminPostInventoryItemsReq,
} from "./validators"
Expand Down Expand Up @@ -39,18 +41,29 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["POST"],
matcher: "/admin/inventory-items",
middlewares: [
transformBody(AdminPostInventoryItemsReq),
transformQuery(
AdminGetInventoryItemsItemParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/inventory-items/:id/location-levels",
middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)],
},
{
method: ["POST"],
matcher: "/admin/inventory-items",
matcher: "/admin/inventory-items/:id/location-levels/:location_id",
middlewares: [
transformBody(AdminPostInventoryItemsReq),
transformBody(AdminPostInventoryItemsItemLocationLevelsLevelReq),
transformQuery(
AdminGetInventoryItemsItemParams,
AdminPostInventoryItemsItemLocationLevelsLevelParams,
QueryConfig.retrieveTransformQueryConfig
),
],
Expand Down
11 changes: 11 additions & 0 deletions packages/medusa/src/api-v2/admin/inventory-items/query-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions packages/medusa/src/api-v2/admin/inventory-items/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
IsObject,
IsOptional,
IsString,
Min,
ValidateNested,
} from "class-validator"
import { Transform, Type } from "class-transformer"
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions packages/types/src/inventory/common/inventory-level.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BaseFilterable, OperatorMap } from "../../dal"

import { NumericalComparisonOperator } from "../../common"

/**
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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>
}
2 changes: 1 addition & 1 deletion packages/types/src/inventory/mutations/inventory-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down

0 comments on commit aa15466

Please sign in to comment.