diff --git a/integration-tests/modules/__tests__/inventory/index.spec.ts b/integration-tests/modules/__tests__/inventory/index.spec.ts index 9a35c66ecf..895f4fcb79 100644 --- a/integration-tests/modules/__tests__/inventory/index.spec.ts +++ b/integration-tests/modules/__tests__/inventory/index.spec.ts @@ -89,31 +89,6 @@ medusaIntegrationTestRunner({ ) }) - it.skip("should update the inventory item", async () => { - const inventoryItemId = inventoryItems[0].id - - const response = await api.post( - `/admin/inventory-items/${inventoryItemId}`, - { - mid_code: "updated mid_code", - weight: 120, - }, - adminHeaders - ) - - expect(response.data.inventory_item).toEqual( - expect.objectContaining({ - origin_country: "UK", - hs_code: "hs001", - mid_code: "updated mid_code", - weight: 120, - length: 100, - height: 200, - width: 150, - }) - ) - }) - it.skip("should fail to update the location level to negative quantity", async () => { const inventoryItemId = inventoryItems[0].id @@ -229,6 +204,40 @@ medusaIntegrationTestRunner({ ) }) + describe("Update inventory item", () => { + let inventoryItemId + beforeEach(async () => { + const inventoryItemResponse = await api.post( + `/admin/inventory-items`, + { + sku: "test-sku", + }, + adminHeaders + ) + + inventoryItemId = inventoryItemResponse.data.inventory_item.id + }) + + it("should update the inventory item", async () => { + const response = await api.post( + `/admin/inventory-items/${inventoryItemId}`, + { + mid_code: "updated mid_code", + weight: 120, + }, + adminHeaders + ) + + expect(response.data.inventory_item).toEqual( + expect.objectContaining({ + sku: "test-sku", + mid_code: "updated mid_code", + weight: 120, + }) + ) + }) + }) + describe("Delete inventory levels", () => { const locationId = "loc_1" let inventoryItem diff --git a/packages/core-flows/src/inventory/steps/index.ts b/packages/core-flows/src/inventory/steps/index.ts index 182f6a7b31..3a5ee70de4 100644 --- a/packages/core-flows/src/inventory/steps/index.ts +++ b/packages/core-flows/src/inventory/steps/index.ts @@ -5,4 +5,5 @@ export * from "./create-inventory-items" export * from "./validate-singular-inventory-items-for-tags" export * from "./create-inventory-levels" export * from "./validate-inventory-locations" +export * from "./update-inventory-items" export * from "./delete-inventory-levels" diff --git a/packages/core-flows/src/inventory/steps/update-inventory-items.ts b/packages/core-flows/src/inventory/steps/update-inventory-items.ts new file mode 100644 index 0000000000..b201bf1fdc --- /dev/null +++ b/packages/core-flows/src/inventory/steps/update-inventory-items.ts @@ -0,0 +1,49 @@ +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 updateInventoryItemsStepId = "update-inventory-items-step" +export const updateInventoryItemsStep = createStep( + updateInventoryItemsStepId, + async (input: InventoryNext.UpdateInventoryItemInput[], { container }) => { + const inventoryService = container.resolve( + ModuleRegistrationName.INVENTORY + ) + const { selects, relations } = getSelectsAndRelationsFromObjectArray(input) + + const dataBeforeUpdate = await inventoryService.list( + { id: input.map(({ id }) => id) }, + {} + ) + + const updatedInventoryItems = await inventoryService.update(input) + + return new StepResponse(updatedInventoryItems, { + dataBeforeUpdate, + selects, + relations, + }) + }, + async (revertInput, { container }) => { + if (!revertInput?.dataBeforeUpdate?.length) { + return + } + + const { dataBeforeUpdate, selects, relations } = revertInput + + const inventoryService = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + await inventoryService.update( + dataBeforeUpdate.map((data) => + convertItemResponseToUpdateRequest(data, selects, relations) + ) + ) + } +) diff --git a/packages/core-flows/src/inventory/workflows/index.ts b/packages/core-flows/src/inventory/workflows/index.ts index 22db2aa542..68e0ef85a5 100644 --- a/packages/core-flows/src/inventory/workflows/index.ts +++ b/packages/core-flows/src/inventory/workflows/index.ts @@ -1,4 +1,5 @@ export * from "./delete-inventory-items" export * from "./create-inventory-items" export * from "./create-inventory-levels" +export * from "./update-inventory-items" export * from "./delete-inventory-levels" diff --git a/packages/core-flows/src/inventory/workflows/update-inventory-items.ts b/packages/core-flows/src/inventory/workflows/update-inventory-items.ts new file mode 100644 index 0000000000..c976d1897a --- /dev/null +++ b/packages/core-flows/src/inventory/workflows/update-inventory-items.ts @@ -0,0 +1,17 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" + +import { InventoryNext } from "@medusajs/types" +import { updateInventoryItemsStep } from "../steps" + +interface WorkflowInput { + updates: InventoryNext.UpdateInventoryItemInput[] +} +export const updateInventoryItemsWorkflowId = "update-inventory-items-workflow" +export const updateInventoryItemsWorkflow = createWorkflow( + updateInventoryItemsWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + return updateInventoryItemsStep(input.updates) + } +) diff --git a/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts b/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts index b5cb180a0b..0f1785d73e 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts @@ -4,8 +4,12 @@ import { remoteQueryObjectFromString, } from "@medusajs/utils" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { + deleteInventoryItemWorkflow, + updateInventoryItemsWorkflow, +} from "@medusajs/core-flows" -import { deleteInventoryItemWorkflow } from "@medusajs/core-flows" +import { AdminPostInventoryItemsInventoryItemReq } from "../validators" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { id } = req.params @@ -38,6 +42,36 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { }) } +// Update inventory item +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + await updateInventoryItemsWorkflow(req.scope).run({ + input: { + updates: [{ id, ...req.validatedBody }], + }, + }) + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const [inventory_item] = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "inventory", + variables: { + id, + }, + fields: req.retrieveConfig.select as string[], + }) + ) + + res.status(200).json({ + inventory_item, + }) +} + export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { const id = req.params.id const deleteInventoryItems = deleteInventoryItemWorkflow(req.scope) diff --git a/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts b/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts index dd3dfbf883..b06a6f4137 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts @@ -3,6 +3,8 @@ import * as QueryConfig from "./query-config" import { AdminGetInventoryItemsItemParams, AdminGetInventoryItemsParams, + AdminPostInventoryItemsInventoryItemParams, + AdminPostInventoryItemsInventoryItemReq, AdminPostInventoryItemsItemLocationLevelsReq, AdminPostInventoryItemsReq, } from "./validators" @@ -53,4 +55,15 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/inventory-items/:id", + middlewares: [ + transformBody(AdminPostInventoryItemsInventoryItemReq), + transformQuery( + AdminPostInventoryItemsInventoryItemParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api-v2/admin/inventory-items/validators.ts b/packages/medusa/src/api-v2/admin/inventory-items/validators.ts index 90a93fa634..6e68df12e7 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/validators.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/validators.ts @@ -257,3 +257,102 @@ export class AdminPostInventoryItemsReq { @IsOptional() metadata?: Record } + +/** + * @schema AdminPostInventoryItemsInventoryItemReq + * type: object + * description: "The attributes to update in an inventory item." + * properties: + * 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 + * 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 + * weight: + * description: The weight 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 + * length: + * description: The length of the Inventory Item. May be used in shipping rate calculations. + * type: number + * 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 + * requires_shipping: + * description: Whether the item requires shipping. + * type: boolean + */ + +export class AdminPostInventoryItemsInventoryItemReq { + @IsString() + @IsOptional() + sku?: string + + @IsOptional() + @IsString() + origin_country?: string + + @IsOptional() + @IsString() + hs_code?: string + + @IsOptional() + @IsString() + mid_code?: string + + @IsOptional() + @IsString() + material?: string + + @IsOptional() + @IsNumber() + weight?: number + + @IsOptional() + @IsNumber() + height?: number + + @IsOptional() + @IsNumber() + length?: number + + @IsOptional() + @IsNumber() + width?: number + + @IsString() + @IsOptional() + title?: string + + @IsString() + @IsOptional() + description?: string + + @IsString() + @IsOptional() + thumbnail?: string + + @IsBoolean() + @IsOptional() + requires_shipping?: boolean +} + +export class AdminPostInventoryItemsInventoryItemParams extends FindParams {}