From 0a5b2b5fa78d724d3d58f450dba9dbd2c589adc0 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 5 Jun 2024 15:27:54 +0200 Subject: [PATCH] chore: fix inventory items specs (#7620) --- .../inventory/admin/inventory.spec.ts | 1034 ++++------------- .../reservations/admin/reservations.spec.ts | 463 ++++++++ .../api/admin/inventory-items/validators.ts | 1 - 3 files changed, 704 insertions(+), 794 deletions(-) create mode 100644 integration-tests/http/__tests__/reservations/admin/reservations.spec.ts diff --git a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts index 2f5ea6de88..6043f6dded 100644 --- a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts +++ b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts @@ -1,9 +1,8 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" import { adminHeaders, createAdminUser, } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" -import { getProductFixture } from "../../../../helpers/fixtures" jest.setTimeout(30000) @@ -16,24 +15,13 @@ medusaIntegrationTestRunner({ beforeEach(async () => { await createAdminUser(dbConnection, adminHeaders, getContainer()) + stockLocation1 = ( - await api.post( - `/admin/stock-locations`, - { - name: "loc1", - }, - adminHeaders - ) + await api.post(`/admin/stock-locations`, { name: "loc1" }, adminHeaders) ).data.stock_location stockLocation2 = ( - await api.post( - `/admin/stock-locations`, - { - name: "loc2", - }, - adminHeaders - ) + await api.post(`/admin/stock-locations`, { name: "loc2" }, adminHeaders) ).data.stock_location inventoryItem1 = ( @@ -57,190 +45,14 @@ medusaIntegrationTestRunner({ inventoryItem2 = ( await api.post( `/admin/inventory-items`, - { - sku: "second", - origin_country: "UK", - hs_code: "hs001", - }, + { sku: "second", origin_country: "UK", hs_code: "hs001" }, adminHeaders ) ).data.inventory_item }) describe("Inventory Items", () => { - it.skip("should create, update and delete the inventory location levels", async () => { - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 17, - incoming_quantity: 2, - }, - adminHeaders - ) - - const stockLevel = ( - await api.get( - `/admin/inventory-items/${inventoryItem1.id}/location-levels?location_id[]=${stockLocation1.id}`, - adminHeaders - ) - ).data.inventory_levels[0] - - expect(stockLevel.location_id).toEqual(stockLocation1.id) - expect(stockLevel.inventory_item_id).toEqual(inventoryItem1.id) - expect(stockLevel.stocked_quantity).toEqual(17) - expect(stockLevel.incoming_quantity).toEqual(2) - - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`, - { - stocked_quantity: 21, - incoming_quantity: 0, - }, - adminHeaders - ) - - const newStockLevel = ( - await api.get( - `/admin/inventory-items/${inventoryItem1.id}/location-levels?location_id[]=${stockLocation1.id}`, - adminHeaders - ) - ).data.inventory_levels[0] - - expect(newStockLevel.stocked_quantity).toEqual(21) - expect(newStockLevel.incoming_quantity).toEqual(0) - - await api.delete( - `/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`, - adminHeaders - ) - const deletedStockLevel = ( - await api.get( - `/admin/inventory-items/${inventoryItem1.id}/location-levels?location_id[]=${stockLocation1.id}`, - adminHeaders - ) - ).data.inventory_levels[0] - - expect(deletedStockLevel.message).toEqual( - `Inventory level for item ${inventoryItem1.id} and location ${stockLocation1.id} not found` - ) - }) - - it.skip("should fail to update the location level to negative quantity", async () => { - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 17, - incoming_quantity: 2, - }, - adminHeaders - ) - - const res = await api - .post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`, - { - incoming_quantity: -1, - stocked_quantity: -1, - }, - adminHeaders - ) - .catch((error) => error) - - expect(res.response.status).toEqual(400) - expect(res.response.data).toEqual({ - type: "invalid_data", - message: - "incoming_quantity must not be less than 0, stocked_quantity must not be less than 0", - }) - }) - - it.skip("should create the inventory item", async () => { - const product = ( - await api.post( - "/admin/products", - getProductFixture({ title: "test1" }), - adminHeaders - ) - ).data.product - - const variantId = product.variants[0].id - - let variantInventoryRes = await api.get( - `/admin/variants/${variantId}/inventory`, - adminHeaders - ) - - expect(variantInventoryRes.data).toEqual({ - variant: { - id: variantId, - inventory: [], - sales_channel_availability: [], - }, - }) - expect(variantInventoryRes.status).toEqual(200) - - const inventoryItemCreateRes = await api.post( - `/admin/inventory-items`, - { variant_id: variantId, sku: "attach_this_to_variant" }, - adminHeaders - ) - - variantInventoryRes = await api.get( - `/admin/variants/${variantId}/inventory`, - adminHeaders - ) - - expect(variantInventoryRes.data).toEqual({ - variant: expect.objectContaining({ - id: variantId, - inventory: [ - expect.objectContaining({ - ...inventoryItemCreateRes.data.inventory_item, - }), - ], - }), - }) - expect(variantInventoryRes.status).toEqual(200) - }) - - it.skip("should list the location levels based on id param constraint", async () => { - const inventoryItemId = inventoryItem1.id - - await api.post( - `/admin/inventory-items/${inventoryItemId}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 10, - }, - adminHeaders - ) - - await api.post( - `/admin/inventory-items/${inventoryItemId}/location-levels`, - { - location_id: stockLocation2.id, - stocked_quantity: 5, - }, - adminHeaders - ) - - const result = await api.get( - `/admin/inventory-items/${inventoryItemId}/location-levels?location_id[]=${stockLocation1.id}`, - adminHeaders - ) - - expect(result.status).toEqual(200) - expect(result.data.inventory_item.inventory_levels).toHaveLength(1) - expect(result.data.inventory_item.inventory_levels[0]).toEqual( - expect.objectContaining({ - stocked_quantity: 10, - }) - ) - }) - - describe("List inventory levels", () => { + describe("GET /admin/inventory-items/:id/location-levels", () => { beforeEach(async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, @@ -250,6 +62,7 @@ medusaIntegrationTestRunner({ }, adminHeaders ) + await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, { @@ -288,30 +101,28 @@ medusaIntegrationTestRunner({ ]) ) }) - }) - describe("Update inventory item", () => { - it("should update the inventory item", async () => { - const response = await api.post( - `/admin/inventory-items/${inventoryItem1.id}`, - { - mid_code: "updated mid_code", - weight: 120, - }, + it("should list the location levels based on id param constraint", async () => { + const inventoryItemId = inventoryItem1.id + + const result = await api.get( + `/admin/inventory-items/${inventoryItemId}/location-levels?location_id[]=${stockLocation1.id}`, adminHeaders ) - expect(response.data.inventory_item).toEqual( - expect.objectContaining({ - sku: "12345", - mid_code: "updated mid_code", - weight: 120, - }) + expect(result.status).toEqual(200) + expect(result.data.inventory_levels).toHaveLength(1) + expect(result.data.inventory_levels).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + stocked_quantity: 10, + }), + ]) ) }) }) - describe("Bulk create/delete inventory levels", () => { + describe("POST /admin/inventory-items/:id/location-levels/batch", () => { beforeEach(async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, @@ -348,7 +159,7 @@ medusaIntegrationTestRunner({ }) }) - describe("Delete inventory levels", () => { + describe("DELETE /admin/inventory-items/:id/location-levels/:id", () => { beforeEach(async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, @@ -401,7 +212,7 @@ medusaIntegrationTestRunner({ }) }) - describe("Update inventory levels", () => { + describe("POST /admin/inventory-items/:id/location-levels/:id", () => { beforeEach(async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, @@ -479,90 +290,39 @@ medusaIntegrationTestRunner({ message: `Item does-not-exist is not stocked at location ${stockLocation1.id}`, }) }) - }) - describe("Retrieve inventory item", () => { - it("should retrieve the inventory item", async () => { + it("should fail to update the location level to negative quantity", async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, { location_id: stockLocation1.id, - stocked_quantity: 15, - incoming_quantity: 5, - }, - adminHeaders - ) - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: stockLocation2.id, - stocked_quantity: 7, - incoming_quantity: 0, - reserved_quantity: 1, + stocked_quantity: 17, + incoming_quantity: 2, }, adminHeaders ) - const response = await api.get( - `/admin/inventory-items/${inventoryItem1.id}`, - adminHeaders - ) + const res = await api + .post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`, + { + incoming_quantity: -1, + stocked_quantity: -1, + }, + adminHeaders + ) + .catch((error) => error) - expect(response.data.inventory_item).toEqual( - expect.objectContaining({ - id: inventoryItem1.id, - sku: "12345", - origin_country: "UK", - hs_code: "hs001", - material: "material", - mid_code: "mids", - requires_shipping: true, - weight: 300, - length: 100, - height: 200, - width: 150, - stocked_quantity: 22, - reserved_quantity: 1, - location_levels: [ - expect.objectContaining({ - id: expect.any(String), - inventory_item_id: inventoryItem1.id, - location_id: stockLocation1.id, - stocked_quantity: 15, - reserved_quantity: 0, - incoming_quantity: 5, - available_quantity: 15, - metadata: null, - }), - expect.objectContaining({ - id: expect.any(String), - inventory_item_id: inventoryItem1.id, - location_id: stockLocation2.id, - stocked_quantity: 7, - reserved_quantity: 1, - incoming_quantity: 0, - available_quantity: 6, - metadata: null, - }), - ], - }) - ) - }) - - it("should throw if inventory item doesn't exist", async () => { - const error = await api - .get(`/admin/inventory-items/does-not-exist`, adminHeaders) - .catch((e) => e) - - expect(error.response.status).toEqual(404) - expect(error.response.data).toEqual({ - type: "not_found", - message: "Inventory item with id: does-not-exist was not found", + expect(res.response.status).toEqual(400) + expect(res.response.data).toEqual({ + type: "invalid_data", + message: + "Invalid request: Value for field 'stocked_quantity' too small, expected at least: '0'; Value for field 'incoming_quantity' too small, expected at least: '0'", }) }) }) - describe("Create inventory item level", () => { + describe("POST /admin/inventory-items/:id/location-levels", () => { it("should create location levels for an inventory item", async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, @@ -622,26 +382,7 @@ medusaIntegrationTestRunner({ }) }) - describe("Create inventory items", () => { - it("should create inventory items", async () => { - const response = await api.post( - `/admin/inventory-items`, - { - sku: "test-sku", - }, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.inventory_item).toEqual( - expect.objectContaining({ - sku: "test-sku", - }) - ) - }) - }) - - describe("List inventory items", () => { + describe("GET /admin/inventory-items", () => { it("should list the inventory items", async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, @@ -761,8 +502,141 @@ medusaIntegrationTestRunner({ }) }) - describe("delete inventory item", () => { - it.skip("should remove associated levels and reservations when deleting an inventory item", async () => { + describe("GET /admin/inventory-items/:id", () => { + it("should retrieve the inventory item", async () => { + await api.post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { + location_id: stockLocation1.id, + stocked_quantity: 15, + incoming_quantity: 5, + }, + adminHeaders + ) + + await api.post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { + location_id: stockLocation2.id, + stocked_quantity: 7, + incoming_quantity: 0, + }, + adminHeaders + ) + + await api.post( + `/admin/reservations`, + { + line_item_id: "line-item-id-1", + inventory_item_id: inventoryItem1.id, + location_id: stockLocation2.id, + description: "test description", + quantity: 1, + }, + adminHeaders + ) + + const response = await api.get( + `/admin/inventory-items/${inventoryItem1.id}`, + adminHeaders + ) + + expect(response.data.inventory_item).toEqual( + expect.objectContaining({ + id: inventoryItem1.id, + sku: "12345", + origin_country: "UK", + hs_code: "hs001", + material: "material", + mid_code: "mids", + requires_shipping: true, + weight: 300, + length: 100, + height: 200, + width: 150, + stocked_quantity: 22, + reserved_quantity: 1, + location_levels: [ + expect.objectContaining({ + id: expect.any(String), + inventory_item_id: inventoryItem1.id, + location_id: stockLocation1.id, + stocked_quantity: 15, + reserved_quantity: 0, + incoming_quantity: 5, + available_quantity: 15, + metadata: null, + }), + expect.objectContaining({ + id: expect.any(String), + inventory_item_id: inventoryItem1.id, + location_id: stockLocation2.id, + stocked_quantity: 7, + reserved_quantity: 1, + incoming_quantity: 0, + available_quantity: 6, + metadata: null, + }), + ], + }) + ) + }) + + it("should throw if inventory item doesn't exist", async () => { + const error = await api + .get(`/admin/inventory-items/does-not-exist`, adminHeaders) + .catch((e) => e) + + expect(error.response.status).toEqual(404) + expect(error.response.data).toEqual({ + type: "not_found", + message: "Inventory item with id: does-not-exist was not found", + }) + }) + }) + + describe("POST /admin/inventory-items", () => { + it("should create inventory items", async () => { + const response = await api.post( + `/admin/inventory-items`, + { + sku: "test-sku", + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.inventory_item).toEqual( + expect.objectContaining({ + sku: "test-sku", + }) + ) + }) + }) + + describe("POST /admin/inventory-items/:id", () => { + it("should update the inventory item", async () => { + const response = await api.post( + `/admin/inventory-items/${inventoryItem1.id}`, + { + mid_code: "updated mid_code", + weight: 120, + }, + adminHeaders + ) + + expect(response.data.inventory_item).toEqual( + expect.objectContaining({ + sku: "12345", + mid_code: "updated mid_code", + weight: 120, + }) + ) + }) + }) + + describe("DELETE /admin/inventory-items/:id", () => { + it("should remove associated levels and reservations when deleting an inventory item", async () => { await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, { @@ -771,11 +645,21 @@ medusaIntegrationTestRunner({ }, adminHeaders ) + await api.post( `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { location_id: stockLocation1.id }, + adminHeaders + ) + + await api.post( + `/admin/reservations`, { + line_item_id: "line-item-id-1", + inventory_item_id: inventoryItem1.id, location_id: stockLocation1.id, - reserved_quantity: 5, + description: "test description", + quantity: 5, }, adminHeaders ) @@ -795,7 +679,7 @@ medusaIntegrationTestRunner({ adminHeaders ) ).data - expect(levelsResponse.count).toEqual(1) + expect(levelsResponse.count).toEqual(2) const res = await api.delete( `/admin/inventory-items/${inventoryItem1.id}`, @@ -810,6 +694,7 @@ medusaIntegrationTestRunner({ adminHeaders ) ).data + expect(reservationsResponseAfterDelete.count).toEqual(0) const levelsResponseAfterDelete = ( @@ -818,507 +703,70 @@ medusaIntegrationTestRunner({ adminHeaders ) ).data + expect(levelsResponseAfterDelete.count).toEqual(0) }) - it.skip("should remove the product variant associations when deleting an inventory item", async () => { - // const secondVariantId = "test-2" - // const variantId = "test" - // const remoteLinks = appContainer.resolve( - // ContainerRegistrationKeys.REMOTE_LINK - // ) - // const remoteQuery = appContainer.resolve( - // ContainerRegistrationKeys.REMOTE_QUERY - // ) - // await remoteLinks.create([ - // { - // productService: { - // variant_id: variantId, - // }, - // inventoryService: { - // inventory_item_id: invItem.id, - // }, - // }, - // { - // productService: { - // variant_id: secondVariantId, - // }, - // inventoryService: { - // inventory_item_id: invItem.id, - // }, - // }, - // ]) - // let links = await remoteQuery( - // remoteQueryObjectFromString({ - // entryPoint: "product_variant_inventory_item", - // variables: { - // filter: { variant_id: [variantId, secondVariantId] }, - // }, - // fields: ["variant_id", "inventory_item_id"], - // }) - // ) - // expect(links).toHaveLength(2) - // expect(links).toEqual( - // expect.arrayContaining([ - // { - // variant_id: "test", - // inventory_item_id: invItem.id, - // }, - // { - // variant_id: "test-2", - // inventory_item_id: invItem.id, - // }, - // ]) - // ) - // await api.delete(`/admin/inventory-items/${invItem.id}`, adminHeaders) - // links = await remoteQuery( - // remoteQueryObjectFromString({ - // entryPoint: "product_variant_inventory_item", - // variables: { - // filter: { variant_id: [variantId, secondVariantId] }, - // }, - // fields: ["variant_id", "inventory_item_id"], - // }) - // ) - // expect(links).toHaveLength(0) - // expect(links).toEqual([]) - }) - }) - }) - - describe("Reservation items", () => { - it.skip("Create reservation item throws if available item quantity is less than reservation quantity", async () => { - // const orderRes = await api.get( - // `/admin/orders/${order.id}`, - // adminHeaders - // ) - // expect(orderRes.data.order.items[0].quantity).toBe(2) - // expect(orderRes.data.order.items[0].fulfilled_quantity).toBeFalsy() - // const payload = { - // quantity: 1, - // inventory_item_id: inventoryItem.id, - // line_item_id: lineItemId, - // location_id: locationId, - // } - // const res = await api - // .post(`/admin/reservations`, payload, adminHeaders) - // .catch((err) => err) - // expect(res.response.status).toBe(400) - // expect(res.response.data).toEqual({ - // type: "invalid_data", - // message: - // "The reservation quantity cannot be greater than the unfulfilled line item quantity", - // }) - }) - - it.skip("Update reservation item throws if available item quantity is less than reservation quantity", async () => { - // const orderRes = await api.get( - // `/admin/orders/${order.id}`, - // adminHeaders - // ) - // expect(orderRes.data.order.items[0].quantity).toBe(2) - // expect(orderRes.data.order.items[0].fulfilled_quantity).toBeFalsy() - // const payload = { - // quantity: 3, - // } - // const res = await api - // .post( - // `/admin/reservations/${reservationItem.id}`, - // payload, - // adminHeaders - // ) - // .catch((err) => err) - // expect(res.response.status).toBe(400) - // expect(res.response.data).toEqual({ - // type: "invalid_data", - // message: - // "The reservation quantity cannot be greater than the unfulfilled line item quantity", - // }) - }) - - describe("Create reservation item", () => { - beforeEach(async () => { - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 100, - }, - adminHeaders - ) - }) - - it("should create a reservation item", async () => { - const reservationResponse = await api.post( - `/admin/reservations`, - { - line_item_id: "line-item-id-1", - inventory_item_id: inventoryItem1.id, - location_id: stockLocation1.id, - description: "test description", - quantity: 1, - }, - adminHeaders - ) - - expect(reservationResponse.status).toEqual(200) - expect(reservationResponse.data.reservation).toEqual( - expect.objectContaining({ - line_item_id: "line-item-id-1", - inventory_item_id: inventoryItem1.id, - location_id: stockLocation1.id, - description: "test description", - quantity: 1, - }) - ) - }) - }) - - describe("Update reservation item", () => { - let reservationId - - beforeEach(async () => { - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 100, - }, - adminHeaders - ) - - const reservationResponse = await api.post( - `/admin/reservations`, - { - line_item_id: "line-item-id-1", - inventory_item_id: inventoryItem1.id, - location_id: stockLocation1.id, - description: "test description", - quantity: 1, - }, - adminHeaders - ) - reservationId = reservationResponse.data.reservation.id - }) - - it("should update a reservation item description", async () => { - const reservationResponse = await api.post( - `/admin/reservations/${reservationId}`, - { - description: "test description 1", - }, - adminHeaders - ) - - expect(reservationResponse.status).toEqual(200) - expect(reservationResponse.data.reservation).toEqual( - expect.objectContaining({ - description: "test description 1", - }) - ) - }) - - it("should update a reservation item", async () => { - const reservationResponse = await api.post( - `/admin/reservations/${reservationId}`, - { - quantity: 3, - }, - adminHeaders - ) - - expect(reservationResponse.status).toEqual(200) - expect(reservationResponse.data.reservation).toEqual( - expect.objectContaining({ - quantity: 3, - }) - ) - }) - }) - - describe("Delete reservation item", () => { - let reservationId - - beforeEach(async () => { - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 100, - }, - adminHeaders - ) - - const reservationResponse = await api.post( - `/admin/reservations`, - { - line_item_id: "line-item-id-1", - inventory_item_id: inventoryItem1.id, - location_id: stockLocation1.id, - description: "test description", - quantity: 1, - }, - adminHeaders - ) - reservationId = reservationResponse.data.reservation.id - }) - - it("should update a reservation item", async () => { - const reservationResponse = await api.delete( - `/admin/reservations/${reservationId}`, - adminHeaders - ) - - expect(reservationResponse.status).toEqual(200) - expect(reservationResponse.data).toEqual( - expect.objectContaining({ - id: reservationId, - object: "reservation", - deleted: true, - }) - ) - - let error - await api - .get(`/admin/reservations/${reservationId}`, adminHeaders) - .catch((err) => { - error = err - }) - - expect(error.response.status).toBe(404) - }) - }) - - describe("List reservation items", () => { - let reservation - let reservation2 - let scId - - beforeEach(async () => { - const scResponse = await api.post( - `/admin/sales-channels`, - { name: "test" }, - adminHeaders - ) - scId = scResponse.data.sales_channel.id - - await api.post( - `/admin/stock-locations/${stockLocation1.id}/sales-channels`, - { - add: [scId], - }, - adminHeaders - ) - await api.post( - `/admin/inventory-items/${inventoryItem1.id}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 100, - }, - adminHeaders - ) - - await api.post( - `/admin/inventory-items/${inventoryItem2.id}/location-levels`, - { - location_id: stockLocation1.id, - stocked_quantity: 100, - }, - adminHeaders - ) - - const reservationResponse = await api.post( - `/admin/reservations`, - { - line_item_id: "line-item-id-1", - inventory_item_id: inventoryItem1.id, - location_id: stockLocation1.id, - quantity: 2, - }, - adminHeaders - ) - - reservation = reservationResponse.data.reservation - - const reservationResponse2 = await api.post( - `/admin/reservations`, - { - line_item_id: "line-item-id-2", - inventory_item_id: inventoryItem2.id, - location_id: stockLocation1.id, - description: "test description", - quantity: 1, - }, - adminHeaders - ) - - reservation2 = reservationResponse2.data.reservation - }) - - it("lists reservation items", async () => { - const reservationsRes = await api - .get(`/admin/reservations`, adminHeaders) - .catch(console.warn) - expect(reservationsRes.data.reservations.length).toBe(2) - expect(reservationsRes.data.reservations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: reservation.id, - }), - expect.objectContaining({ - id: reservation2.id, - }), - ]) - ) - }) - - describe("Filters reservation items", () => { - it("filters by location", async () => { - const reservationsRes = await api.get( - `/admin/reservations?location_id[]=${stockLocation1.id}`, + it("should remove the product variant associations when deleting an inventory item", async () => { + const product = ( + await api.post( + "/admin/products", + { + title: "product 1", + variants: [ + { + title: "variant 1", + prices: [{ currency_code: "usd", amount: 100 }], + inventory_items: [ + { + inventory_item_id: inventoryItem1.id, + required_quantity: 10, + }, + ], + }, + { + title: "variant 2", + prices: [{ currency_code: "usd", amount: 100 }], + inventory_items: [ + { + inventory_item_id: inventoryItem1.id, + required_quantity: 5, + }, + ], + }, + ], + }, adminHeaders ) - expect(reservationsRes.data.reservations.length).toBe(2) - expect(reservationsRes.data.reservations[0].location_id).toBe( - stockLocation1.id - ) - }) + ).data.product - it("filters by itemID", async () => { - const reservationsRes = await api.get( - `/admin/reservations?inventory_item_id[]=${inventoryItem1.id}`, + const variant1 = product.variants[0] + const variant2 = product.variants[1] + + await api.delete( + `/admin/inventory-items/${inventoryItem1.id}`, + adminHeaders + ) + + const updatedVariant1 = ( + await api.get( + `/admin/products/${product.id}/variants/${variant1.id}?fields=inventory_items.inventory.*,inventory_items.*`, adminHeaders ) - expect(reservationsRes.data.reservations.length).toBe(1) - expect(reservationsRes.data.reservations[0].inventory_item_id).toBe( - inventoryItem1.id - ) - }) + ).data.variant - it("filters by quantity", async () => { - const reservationsRes = await api.get( - `/admin/reservations?quantity[$gt]=1`, + expect(updatedVariant1.inventory_items).toHaveLength(0) + + const updatedVariant2 = ( + await api.get( + `/admin/products/${product.id}/variants/${variant2.id}?fields=inventory_items.inventory.*,inventory_items.*`, adminHeaders ) + ).data.variant - expect(reservationsRes.data.reservations.length).toBe(1) - expect(reservationsRes.data.reservations[0].id).toBe(reservation.id) - }) - - it("filters by date", async () => { - const reservationsRes = await api.get( - `/admin/reservations?created_at[$gte]=${new Date( - reservation2.created_at - ).toISOString()}`, - adminHeaders - ) - - expect(reservationsRes.data.reservations.length).toBe(1) - expect(reservationsRes.data.reservations[0].id).toBe( - reservation2.id - ) - }) - - it("filters by description using equals", async () => { - const reservationsRes = await api - .get( - `/admin/reservations?description=test%20description`, - adminHeaders - ) - .catch(console.warn) - - expect(reservationsRes.data.reservations.length).toBe(1) - expect(reservationsRes.data.reservations[0].id).toBe( - reservation2.id - ) - }) - - it("filters by description using equals removes results", async () => { - const reservationsRes = await api.get( - `/admin/reservations?description=description`, - adminHeaders - ) - - expect(reservationsRes.data.reservations.length).toBe(0) - }) - - it("filters by description using contains", async () => { - const reservationsRes = await api.get( - `/admin/reservations?description[$ilike]=%descri%`, - adminHeaders - ) - - expect(reservationsRes.data.reservations.length).toBe(1) - expect(reservationsRes.data.reservations[0].id).toBe( - reservation2.id - ) - }) - - it("filters by description using starts_with", async () => { - const reservationsRes = await api - .get( - `/admin/reservations?description[$ilike]=test%`, - adminHeaders - ) - .catch(console.log) - - expect(reservationsRes.data.reservations.length).toBe(1) - expect(reservationsRes.data.reservations[0].id).toBe( - reservation2.id - ) - }) - - it("filters by description using starts_with removes results", async () => { - const reservationsRes = await api.get( - `/admin/reservations?description[$ilike]=description%`, - adminHeaders - ) - - expect(reservationsRes.data.reservations.length).toBe(0) - }) - - it("filters by description using ends_with", async () => { - const reservationsRes = await api.get( - `/admin/reservations?description[$ilike]=%test`, - adminHeaders - ) - - expect(reservationsRes.data.reservations.length).toBe(0) - }) - - it("filters by description using ends_with removes results", async () => { - const reservationsRes = await api.get( - `/admin/reservations?description[$ilike]=%description`, - adminHeaders - ) - - expect(reservationsRes.data.reservations.length).toBe(1) - expect(reservationsRes.data.reservations[0].id).toBe( - reservation2.id - ) - }) + expect(updatedVariant2.inventory_items).toHaveLength(0) }) }) - - it.skip("lists reservations with inventory_items and line items", async () => { - const res = await api.get( - `/admin/reservations?expand=line_item,inventory_item`, - adminHeaders - ) - - expect(res.status).toEqual(200) - expect(res.data.reservations.length).toEqual(1) - expect(res.data.reservations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - inventory_item: expect.objectContaining({}), - line_item: expect.objectContaining({ - order: expect.objectContaining({}), - }), - }), - ]) - ) - }) }) }, }) diff --git a/integration-tests/http/__tests__/reservations/admin/reservations.spec.ts b/integration-tests/http/__tests__/reservations/admin/reservations.spec.ts new file mode 100644 index 0000000000..1f026d6acf --- /dev/null +++ b/integration-tests/http/__tests__/reservations/admin/reservations.spec.ts @@ -0,0 +1,463 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + let inventoryItem1 + let inventoryItem2 + let stockLocation1 + let stockLocation2 + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders, getContainer()) + + stockLocation1 = ( + await api.post(`/admin/stock-locations`, { name: "loc1" }, adminHeaders) + ).data.stock_location + + stockLocation2 = ( + await api.post(`/admin/stock-locations`, { name: "loc2" }, adminHeaders) + ).data.stock_location + + inventoryItem1 = ( + await api.post( + `/admin/inventory-items`, + { + sku: "12345", + origin_country: "UK", + hs_code: "hs001", + mid_code: "mids", + material: "material", + weight: 300, + length: 100, + height: 200, + width: 150, + }, + adminHeaders + ) + ).data.inventory_item + + inventoryItem2 = ( + await api.post( + `/admin/inventory-items`, + { sku: "second", origin_country: "UK", hs_code: "hs001" }, + adminHeaders + ) + ).data.inventory_item + }) + + describe("POST /admin/reservations", () => { + beforeEach(async () => { + await api.post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { + location_id: stockLocation1.id, + stocked_quantity: 100, + }, + adminHeaders + ) + }) + + it("should create a reservation item", async () => { + const reservationResponse = await api.post( + `/admin/reservations`, + { + line_item_id: "line-item-id-1", + inventory_item_id: inventoryItem1.id, + location_id: stockLocation1.id, + description: "test description", + quantity: 1, + }, + adminHeaders + ) + + expect(reservationResponse.status).toEqual(200) + expect(reservationResponse.data.reservation).toEqual( + expect.objectContaining({ + line_item_id: "line-item-id-1", + inventory_item_id: inventoryItem1.id, + location_id: stockLocation1.id, + description: "test description", + quantity: 1, + }) + ) + }) + + it("should throw if available quantity is less than reservation quantity", async () => { + const payload = { + quantity: 100000, + inventory_item_id: inventoryItem1.id, + line_item_id: "line-item-id", + location_id: stockLocation1.id, + } + + const res = await api + .post(`/admin/reservations`, payload, adminHeaders) + .catch((err) => err) + + expect(res.response.status).toBe(400) + expect(res.response.data).toEqual({ + type: "not_allowed", + message: `Not enough stock available for item ${inventoryItem1.id} at location ${stockLocation1.id}`, + }) + }) + }) + + describe("POST /admin/reservations/:id", () => { + let reservationId + + beforeEach(async () => { + await api.post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { + location_id: stockLocation1.id, + stocked_quantity: 100, + }, + adminHeaders + ) + + const reservationResponse = await api.post( + `/admin/reservations`, + { + line_item_id: "line-item-id-1", + inventory_item_id: inventoryItem1.id, + location_id: stockLocation1.id, + description: "test description", + quantity: 1, + }, + adminHeaders + ) + reservationId = reservationResponse.data.reservation.id + }) + + // TODO: Implement this on the module + it.skip("should throw if available quantity is less than reservation quantity", async () => { + const payload = { quantity: 100000 } + const res = await api + .post(`/admin/reservations/${reservationId}`, payload, adminHeaders) + .catch((err) => err) + + expect(res.response.status).toBe(400) + expect(res.response.data).toEqual({ + type: "not_allowed", + message: `Not enough stock available for item ${inventoryItem1.id} at location ${stockLocation1.id}`, + }) + }) + + it("should update a reservation item description", async () => { + const reservationResponse = await api.post( + `/admin/reservations/${reservationId}`, + { + description: "test description 1", + }, + adminHeaders + ) + + expect(reservationResponse.status).toEqual(200) + expect(reservationResponse.data.reservation).toEqual( + expect.objectContaining({ + description: "test description 1", + }) + ) + }) + + it("should update a reservation item", async () => { + const reservationResponse = await api.post( + `/admin/reservations/${reservationId}`, + { + quantity: 3, + }, + adminHeaders + ) + + expect(reservationResponse.status).toEqual(200) + expect(reservationResponse.data.reservation).toEqual( + expect.objectContaining({ + quantity: 3, + }) + ) + }) + }) + + describe("DELETE /admin/reservations/:id", () => { + let reservationId + + beforeEach(async () => { + await api.post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { + location_id: stockLocation1.id, + stocked_quantity: 100, + }, + adminHeaders + ) + + const reservationResponse = await api.post( + `/admin/reservations`, + { + line_item_id: "line-item-id-1", + inventory_item_id: inventoryItem1.id, + location_id: stockLocation1.id, + description: "test description", + quantity: 1, + }, + adminHeaders + ) + reservationId = reservationResponse.data.reservation.id + }) + + it("should update a reservation item", async () => { + const reservationResponse = await api.delete( + `/admin/reservations/${reservationId}`, + adminHeaders + ) + + expect(reservationResponse.status).toEqual(200) + expect(reservationResponse.data).toEqual( + expect.objectContaining({ + id: reservationId, + object: "reservation", + deleted: true, + }) + ) + + let error + await api + .get(`/admin/reservations/${reservationId}`, adminHeaders) + .catch((err) => { + error = err + }) + + expect(error.response.status).toBe(404) + }) + }) + + describe("GET /admin/reservations", () => { + let reservation + let reservation2 + let scId + + beforeEach(async () => { + const scResponse = await api.post( + `/admin/sales-channels`, + { name: "test" }, + adminHeaders + ) + scId = scResponse.data.sales_channel.id + + await api.post( + `/admin/stock-locations/${stockLocation1.id}/sales-channels`, + { + add: [scId], + }, + adminHeaders + ) + await api.post( + `/admin/inventory-items/${inventoryItem1.id}/location-levels`, + { + location_id: stockLocation1.id, + stocked_quantity: 100, + }, + adminHeaders + ) + + await api.post( + `/admin/inventory-items/${inventoryItem2.id}/location-levels`, + { + location_id: stockLocation1.id, + stocked_quantity: 100, + }, + adminHeaders + ) + + const reservationResponse = await api.post( + `/admin/reservations`, + { + line_item_id: "line-item-id-1", + inventory_item_id: inventoryItem1.id, + location_id: stockLocation1.id, + quantity: 2, + }, + adminHeaders + ) + + reservation = reservationResponse.data.reservation + + const reservationResponse2 = await api.post( + `/admin/reservations`, + { + line_item_id: "line-item-id-2", + inventory_item_id: inventoryItem2.id, + location_id: stockLocation1.id, + description: "test description", + quantity: 1, + }, + adminHeaders + ) + + reservation2 = reservationResponse2.data.reservation + }) + + it("lists reservation items", async () => { + const reservationsRes = await api + .get(`/admin/reservations`, adminHeaders) + .catch(console.warn) + + expect(reservationsRes.data.reservations.length).toBe(2) + expect(reservationsRes.data.reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: reservation.id, + }), + expect.objectContaining({ + id: reservation2.id, + }), + ]) + ) + }) + + it("lists reservations with inventory_items", async () => { + const res = await api.get( + `/admin/reservations?fields=%2binventory_item.*`, + adminHeaders + ) + + expect(res.status).toEqual(200) + expect(res.data.reservations.length).toEqual(2) + expect(res.data.reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + inventory_item: expect.objectContaining({ + id: inventoryItem1.id, + }), + }), + expect.objectContaining({ + inventory_item: expect.objectContaining({ + id: inventoryItem2.id, + }), + }), + ]) + ) + }) + + describe("Filters reservation items", () => { + it("filters by location", async () => { + const reservationsRes = await api.get( + `/admin/reservations?location_id[]=${stockLocation1.id}`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(2) + expect(reservationsRes.data.reservations[0].location_id).toBe( + stockLocation1.id + ) + }) + + it("filters by itemID", async () => { + const reservationsRes = await api.get( + `/admin/reservations?inventory_item_id[]=${inventoryItem1.id}`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].inventory_item_id).toBe( + inventoryItem1.id + ) + }) + + it("filters by quantity", async () => { + const reservationsRes = await api.get( + `/admin/reservations?quantity[$gt]=1`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation.id) + }) + + it("filters by date", async () => { + const reservationsRes = await api.get( + `/admin/reservations?created_at[$gte]=${new Date( + reservation2.created_at + ).toISOString()}`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using equals", async () => { + const reservationsRes = await api + .get( + `/admin/reservations?description=test%20description`, + adminHeaders + ) + .catch(console.warn) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using equals removes results", async () => { + const reservationsRes = await api.get( + `/admin/reservations?description=description`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(0) + }) + + it("filters by description using contains", async () => { + const reservationsRes = await api.get( + `/admin/reservations?description[$ilike]=%descri%`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using starts_with", async () => { + const reservationsRes = await api + .get(`/admin/reservations?description[$ilike]=test%`, adminHeaders) + .catch(console.log) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + + it("filters by description using starts_with removes results", async () => { + const reservationsRes = await api.get( + `/admin/reservations?description[$ilike]=description%`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(0) + }) + + it("filters by description using ends_with", async () => { + const reservationsRes = await api.get( + `/admin/reservations?description[$ilike]=%test`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(0) + }) + + it("filters by description using ends_with removes results", async () => { + const reservationsRes = await api.get( + `/admin/reservations?description[$ilike]=%description`, + adminHeaders + ) + + expect(reservationsRes.data.reservations.length).toBe(1) + expect(reservationsRes.data.reservations[0].id).toBe(reservation2.id) + }) + }) + }) + }, +}) diff --git a/packages/medusa/src/api/admin/inventory-items/validators.ts b/packages/medusa/src/api/admin/inventory-items/validators.ts index 97c94d34d9..7f11888834 100644 --- a/packages/medusa/src/api/admin/inventory-items/validators.ts +++ b/packages/medusa/src/api/admin/inventory-items/validators.ts @@ -72,7 +72,6 @@ export const AdminCreateInventoryLocationLevel = z location_id: z.string(), stocked_quantity: z.number().min(0).optional(), incoming_quantity: z.number().min(0).optional(), - reserved_quantity: z.number().min(0).optional(), }) .strict()