From 84b8836cbf70111c63331636a19c91289d7238c7 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:28:57 +0100 Subject: [PATCH] Feat(core-flows, inventory-next, medusa, types): Add create location level endpoint (#6695) * initialize create location levels * add enough from pr to make code build and test * fix integration tests * pr feedback * fix errors * rm dto * Update packages/core-flows/src/inventory/steps/validate-inventory-locations.ts Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .../modules/__tests__/inventory/index.spec.ts | 113 ++++++++++++++- integration-tests/plugins/package.json | 1 + packages/core-flows/src/index.ts | 1 + packages/core-flows/src/inventory/index.ts | 2 + .../steps/create-inventory-levels.ts | 34 +++++ .../core-flows/src/inventory/steps/index.ts | 2 + .../steps/validate-inventory-locations.ts | 40 ++++++ .../workflows/create-inventory-levels.ts | 20 +++ .../src/inventory/workflows/index.ts | 1 + .../[id]/location-levels/route.ts | 48 +++++++ .../admin/inventory-items/middlewares.ts | 6 - .../admin/inventory-items/validators.ts | 131 ------------------ packages/types/src/inventory/service-next.ts | 5 + yarn.lock | 1 + 14 files changed, 266 insertions(+), 139 deletions(-) create mode 100644 packages/core-flows/src/inventory/index.ts create mode 100644 packages/core-flows/src/inventory/steps/create-inventory-levels.ts create mode 100644 packages/core-flows/src/inventory/steps/index.ts create mode 100644 packages/core-flows/src/inventory/steps/validate-inventory-locations.ts create mode 100644 packages/core-flows/src/inventory/workflows/create-inventory-levels.ts create mode 100644 packages/core-flows/src/inventory/workflows/index.ts create mode 100644 packages/medusa/src/api-v2/admin/inventory-items/[id]/location-levels/route.ts diff --git a/integration-tests/modules/__tests__/inventory/index.spec.ts b/integration-tests/modules/__tests__/inventory/index.spec.ts index 41abf19912..83ecd80d27 100644 --- a/integration-tests/modules/__tests__/inventory/index.spec.ts +++ b/integration-tests/modules/__tests__/inventory/index.spec.ts @@ -326,7 +326,7 @@ medusaIntegrationTestRunner({ }) }) - describe.skip("Create inventory item level", () => { + describe("Create inventory item level", () => { let location1 let location2 @@ -357,7 +357,7 @@ medusaIntegrationTestRunner({ }) }) - it("should list the inventory items", async () => { + it("should create location levels for an inventory item", async () => { const [{ id: inventoryItemId }] = await service.list({}) await api.post( @@ -394,6 +394,115 @@ medusaIntegrationTestRunner({ }), ]) }) + + it("should fail to create a location level for an inventory item", async () => { + const [{ id: inventoryItemId }] = await service.list({}) + + const error = await api + .post( + `/admin/inventory-items/${inventoryItemId}/location-levels`, + { + location_id: "{location1.id}", + stocked_quantity: 10, + }, + adminHeaders + ) + .catch((error) => error) + + expect(error.response.status).toEqual(404) + expect(error.response.data).toEqual({ + type: "not_found", + message: "Stock locations with ids: {location1.id} was not found", + }) + }) + }) + + describe.skip("Create inventory items", () => { + it("should create inventory items", async () => { + const createResult = await api.post( + `/admin/products`, + { + title: "Test Product", + variants: [ + { + title: "Test Variant w. inventory 2", + sku: "MY_SKU1", + material: "material", + }, + ], + }, + adminHeaders + ) + + const inventoryItems = await service.list() + + expect(inventoryItems).toHaveLength(0) + + const response = await api.post( + `/admin/inventory-items`, + { + sku: "test-sku", + variant_id: createResult.data.product.variants[0].id, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.inventory_item).toEqual( + expect.objectContaining({ + sku: "test-sku", + }) + ) + }) + + it("should attach inventory items on creation", async () => { + const createResult = await api.post( + `/admin/products`, + { + title: "Test Product", + variants: [ + { + title: "Test Variant w. inventory 2", + sku: "MY_SKU1", + material: "material", + }, + ], + }, + adminHeaders + ) + + const inventoryItems = await service.list() + + expect(inventoryItems).toHaveLength(0) + + await api.post( + `/admin/inventory-items`, + { + sku: "test-sku", + variant_id: createResult.data.product.variants[0].id, + }, + adminHeaders + ) + + const remoteQuery = appContainer.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const query = remoteQueryObjectFromString({ + entryPoint: "product_variant_inventory_item", + variables: { + variant_id: createResult.data.product.variants[0].id, + }, + fields: ["inventory_item_id", "variant_id"], + }) + + const existingItems = await remoteQuery(query) + + expect(existingItems).toHaveLength(1) + expect(existingItems[0].variant_id).toEqual( + createResult.data.product.variants[0].id + ) + }) }) describe.skip("Create inventory items", () => { diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index 314bc3d342..e9a223407c 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -16,6 +16,7 @@ "@medusajs/customer": "workspace:^", "@medusajs/event-bus-local": "workspace:*", "@medusajs/inventory": "workspace:^", + "@medusajs/inventory-next": "workspace:^", "@medusajs/medusa": "workspace:*", "@medusajs/modules-sdk": "workspace:^", "@medusajs/pricing": "workspace:^", diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index 0b9e59d627..c11f3e9186 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -7,6 +7,7 @@ export * from "./definitions" export * from "./fulfillment" export * as Handlers from "./handlers" export * from "./invite" +export * from "./inventory" export * from "./payment" export * from "./price-list" export * from "./pricing" diff --git a/packages/core-flows/src/inventory/index.ts b/packages/core-flows/src/inventory/index.ts new file mode 100644 index 0000000000..e84516860c --- /dev/null +++ b/packages/core-flows/src/inventory/index.ts @@ -0,0 +1,2 @@ +export * from "./workflows" +export * from "./steps" diff --git a/packages/core-flows/src/inventory/steps/create-inventory-levels.ts b/packages/core-flows/src/inventory/steps/create-inventory-levels.ts new file mode 100644 index 0000000000..9453c81cf1 --- /dev/null +++ b/packages/core-flows/src/inventory/steps/create-inventory-levels.ts @@ -0,0 +1,34 @@ +import { + CreateInventoryLevelInput, + IInventoryServiceNext, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const createInventoryLevelsStepId = "create-inventory-levels" +export const createInventoryLevelsStep = createStep( + createInventoryLevelsStepId, + async (data: CreateInventoryLevelInput[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + const inventoryLevels = await service.createInventoryLevels(data) + return new StepResponse( + inventoryLevels, + inventoryLevels.map((level) => level.id) + ) + }, + async (ids, { container }) => { + if (!ids?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + await service.deleteInventoryLevels(ids) + } +) diff --git a/packages/core-flows/src/inventory/steps/index.ts b/packages/core-flows/src/inventory/steps/index.ts new file mode 100644 index 0000000000..26b4abc742 --- /dev/null +++ b/packages/core-flows/src/inventory/steps/index.ts @@ -0,0 +1,2 @@ +export * from "./create-inventory-levels" +export * from "./validate-inventory-locations" diff --git a/packages/core-flows/src/inventory/steps/validate-inventory-locations.ts b/packages/core-flows/src/inventory/steps/validate-inventory-locations.ts new file mode 100644 index 0000000000..cd93e2560c --- /dev/null +++ b/packages/core-flows/src/inventory/steps/validate-inventory-locations.ts @@ -0,0 +1,40 @@ +import { + ContainerRegistrationKeys, + MedusaError, + arrayDifference, + remoteQueryObjectFromString, +} from "@medusajs/utils" + +import { CreateInventoryLevelInput } from "@medusajs/types" +import { createStep } from "@medusajs/workflows-sdk" + +export const validateInventoryLocationsStepId = "validate-inventory-levels-step" +export const validateInventoryLocationsStep = createStep( + validateInventoryLocationsStepId, + async (data: CreateInventoryLevelInput[], { container }) => { + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const locationQuery = remoteQueryObjectFromString({ + entryPoint: "stock_location", + variables: { + id: data.map((d) => d.location_id), + }, + fields: ["id"], + }) + + const stockLocations = await remoteQuery(locationQuery) + + const diff = arrayDifference( + data.map((d) => d.location_id), + stockLocations.map((l) => l.id) + ) + if (diff.length > 0) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Stock locations with ids: ${diff.join(", ")} was not found` + ) + } + } +) diff --git a/packages/core-flows/src/inventory/workflows/create-inventory-levels.ts b/packages/core-flows/src/inventory/workflows/create-inventory-levels.ts new file mode 100644 index 0000000000..e39b4f4385 --- /dev/null +++ b/packages/core-flows/src/inventory/workflows/create-inventory-levels.ts @@ -0,0 +1,20 @@ +import { CreateInventoryLevelInput, InventoryLevelDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { + createInventoryLevelsStep, + validateInventoryLocationsStep, +} from "../steps" + +interface WorkflowInput { + inventory_levels: CreateInventoryLevelInput[] +} +export const createInventoryLevelsWorkflowId = + "create-inventory-levels-workflow" +export const createInventoryLevelsWorkflow = createWorkflow( + createInventoryLevelsWorkflowId, + (input: WorkflowData): WorkflowData => { + validateInventoryLocationsStep(input.inventory_levels) + + return createInventoryLevelsStep(input.inventory_levels) + } +) diff --git a/packages/core-flows/src/inventory/workflows/index.ts b/packages/core-flows/src/inventory/workflows/index.ts new file mode 100644 index 0000000000..c8fa3b339b --- /dev/null +++ b/packages/core-flows/src/inventory/workflows/index.ts @@ -0,0 +1 @@ +export * from "./create-inventory-levels" diff --git a/packages/medusa/src/api-v2/admin/inventory-items/[id]/location-levels/route.ts b/packages/medusa/src/api-v2/admin/inventory-items/[id]/location-levels/route.ts new file mode 100644 index 0000000000..0bd3293efb --- /dev/null +++ b/packages/medusa/src/api-v2/admin/inventory-items/[id]/location-levels/route.ts @@ -0,0 +1,48 @@ +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" + +import { AdminPostInventoryItemsItemLocationLevelsReq } from "../../validators" +import { MedusaError } from "@medusajs/utils" +import { createInventoryLevelsWorkflow } from "@medusajs/core-flows" +import { defaultAdminInventoryItemFields } from "../../query-config" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const workflow = createInventoryLevelsWorkflow(req.scope) + const { errors } = await workflow.run({ + input: { + inventory_levels: [ + { + inventory_item_id: id, + ...req.validatedBody, + }, + ], + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const itemQuery = remoteQueryObjectFromString({ + entryPoint: "inventory_items", + variables: { + id, + }, + fields: defaultAdminInventoryItemFields, + }) + + const [inventory_item] = await remoteQuery(itemQuery) + + res.status(200).json({ inventory_item }) +} 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 99bf73c42e..47bc320384 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/middlewares.ts @@ -4,7 +4,6 @@ import { AdminGetInventoryItemsItemParams, AdminGetInventoryItemsParams, AdminPostInventoryItemsItemLocationLevelsReq, - AdminPostInventoryItemsReq, } from "./validators" import { transformBody, transformQuery } from "../../../api/middlewares" @@ -42,9 +41,4 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/admin/inventory-items/:id/location-levels", middlewares: [transformBody(AdminPostInventoryItemsItemLocationLevelsReq)], }, - { - method: ["POST"], - matcher: "/admin/inventory-items", - middlewares: [transformBody(AdminPostInventoryItemsReq)], - }, ] 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 c339a74095..b88bbd2713 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/validators.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/validators.ts @@ -135,134 +135,3 @@ export class AdminPostInventoryItemsItemLocationLevelsReq { // eslint-disable-next-line export class AdminPostInventoryItemsItemLocationLevelsParams extends FindParams {} - -/** - * @schema AdminPostInventoryItemsReq - * type: object - * description: "The details of the inventory item to create." - * required: - * - variant_id - * properties: - * variant_id: - * description: The ID of the variant to create the inventory item for. - * type: string - * sku: - * description: The unique SKU of the associated Product Variant. - * type: string - * ean: - * description: The EAN number of the item. - * type: string - * upc: - * description: The UPC number of the item. - * type: string - * barcode: - * description: A generic GTIN field for the Product Variant. - * type: string - * 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 - * inventory_quantity: - * description: The amount of stock kept of the associated Product Variant. - * type: integer - * default: 0 - * allow_backorder: - * description: Whether the associated Product Variant can be purchased when out of stock. - * type: boolean - * manage_inventory: - * description: Whether Medusa should keep track of the inventory for the associated Product Variant. - * type: boolean - * default: true - * weight: - * description: The weight 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 - * 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 - * 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 - * 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 - * metadata: - * description: An optional set of key-value pairs with additional information. - * type: object - * externalDocs: - * description: "Learn about the metadata attribute, and how to delete and update it." - * url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute" - */ -export class AdminPostInventoryItemsReq { - @IsOptional() - @IsString() - variant_id: string - - @IsString() - @IsOptional() - sku?: string - - @IsString() - @IsOptional() - hs_code?: string - - @IsNumber() - @IsOptional() - weight?: number - - @IsNumber() - @IsOptional() - length?: number - - @IsNumber() - @IsOptional() - height?: number - - @IsNumber() - @IsOptional() - width?: number - - @IsString() - @IsOptional() - origin_country?: string - - @IsString() - @IsOptional() - mid_code?: string - - @IsString() - @IsOptional() - material?: string - - @IsString() - @IsOptional() - title?: string - - @IsString() - @IsOptional() - description?: string - - @IsString() - @IsOptional() - thumbnail?: string - - @IsObject() - @IsOptional() - metadata?: Record -} diff --git a/packages/types/src/inventory/service-next.ts b/packages/types/src/inventory/service-next.ts index a3e9659be8..bc4b20c711 100644 --- a/packages/types/src/inventory/service-next.ts +++ b/packages/types/src/inventory/service-next.ts @@ -790,6 +790,11 @@ export interface IInventoryServiceNext extends IModuleService { context?: Context ): Promise + deleteInventoryLevels( + inventoryLevelIds: string | string[], + context?: Context + ): Promise + /** * This method is used to adjust the inventory level's stocked quantity. The inventory level is identified by the IDs of its associated inventory item and location. * diff --git a/yarn.lock b/yarn.lock index 4d6c7f5f51..f5d6092732 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31891,6 +31891,7 @@ __metadata: "@medusajs/customer": "workspace:^" "@medusajs/event-bus-local": "workspace:*" "@medusajs/inventory": "workspace:^" + "@medusajs/inventory-next": "workspace:^" "@medusajs/medusa": "workspace:*" "@medusajs/modules-sdk": "workspace:^" "@medusajs/pricing": "workspace:^"