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>
This commit is contained in:
Philip Korsholm
2024-03-18 10:28:57 +01:00
committed by GitHub
parent e77a02aca5
commit 84b8836cbf
14 changed files with 266 additions and 139 deletions

View File

@@ -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", () => {

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export * from "./workflows"
export * from "./steps"

View File

@@ -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<IInventoryServiceNext>(
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<IInventoryServiceNext>(
ModuleRegistrationName.INVENTORY
)
await service.deleteInventoryLevels(ids)
}
)

View File

@@ -0,0 +1,2 @@
export * from "./create-inventory-levels"
export * from "./validate-inventory-locations"

View File

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

View File

@@ -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<WorkflowInput>): WorkflowData<InventoryLevelDTO[]> => {
validateInventoryLocationsStep(input.inventory_levels)
return createInventoryLevelsStep(input.inventory_levels)
}
)

View File

@@ -0,0 +1 @@
export * from "./create-inventory-levels"

View File

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

View File

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

View File

@@ -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<string, unknown>
}

View File

@@ -790,6 +790,11 @@ export interface IInventoryServiceNext extends IModuleService {
context?: Context
): Promise<void>
deleteInventoryLevels(
inventoryLevelIds: string | string[],
context?: Context
): Promise<void>
/**
* 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.
*

View File

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