feat(medusa, admin-ui, medusa-react, medusa-js): Allow toggling of manage inventory (#3435)
**What** - Toggle manage inventory in the inventory management modal **How** - Create/update/remove inventory item based on if `manage_inventory` is set and if an inventory item already exists - Move all stock location updates to when the modal is submitted - Add create-inventory-item endpoint in the core Fixes CORE-1196 Co-authored-by: Sebastian Rindom <7554214+srindom@users.noreply.github.com>
This commit is contained in:
co-authored by
Sebastian Rindom
parent
30a3203640
commit
fe9eea4c18
@@ -0,0 +1,230 @@
|
||||
import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
} from "../../../../services"
|
||||
import { IInventoryService } from "../../../../interfaces"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { createInventoryItemTransaction } from "./transaction/create-inventory-item"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { FindParams } from "../../../../types/common"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/inventory-items
|
||||
* operationId: "PostInventoryItems"
|
||||
* summary: "Create an Inventory Item."
|
||||
* description: "Creates an Inventory Item."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (query) expand {string} Comma separated list of relations to include in the results.
|
||||
* - (query) fields {string} Comma separated list of fields to include in the results.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/AdminPostInventoryItemsItemLocationLevelsReq"
|
||||
* x-codeSamples:
|
||||
* - lang: JavaScript
|
||||
* label: JS Client
|
||||
* source: |
|
||||
* import Medusa from "@medusajs/medusa-js"
|
||||
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||
* // must be previously logged in or use api token
|
||||
* medusa.admin.inventoryItems.create(inventoryItemId, {
|
||||
* variant_id: 'variant_123',
|
||||
* sku: "sku-123",
|
||||
* })
|
||||
* .then(({ inventory_item }) => {
|
||||
* console.log(inventory_item.id);
|
||||
* });
|
||||
* - lang: Shell
|
||||
* label: cURL
|
||||
* source: |
|
||||
* curl --location --request POST 'https://medusa-url.com/admin/inventory-items' \
|
||||
* --header 'Authorization: Bearer {api_token}' \
|
||||
* --header 'Content-Type: application/json' \
|
||||
* --data-raw '{
|
||||
* "variant_id": "variant_123",
|
||||
* "sku": "sku-123",
|
||||
* }'
|
||||
* security:
|
||||
* - api_token: []
|
||||
* - cookie_auth: []
|
||||
* tags:
|
||||
* - Inventory Items
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/AdminInventoryItemsRes"
|
||||
* "400":
|
||||
* $ref: "#/components/responses/400_error"
|
||||
* "401":
|
||||
* $ref: "#/components/responses/unauthorized"
|
||||
* "404":
|
||||
* $ref: "#/components/responses/not_found_error"
|
||||
* "409":
|
||||
* $ref: "#/components/responses/invalid_state_error"
|
||||
* "422":
|
||||
* $ref: "#/components/responses/invalid_request_error"
|
||||
* "500":
|
||||
* $ref: "#/components/responses/500_error"
|
||||
*/
|
||||
|
||||
export default async (req, res) => {
|
||||
const validated = await validator(AdminPostInventoryItemsReq, req.body)
|
||||
const { variant_id, ...input } = validated
|
||||
|
||||
const inventoryService: IInventoryService =
|
||||
req.scope.resolve("inventoryService")
|
||||
const productVariantInventoryService: ProductVariantInventoryService =
|
||||
req.scope.resolve("productVariantInventoryService")
|
||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
)
|
||||
|
||||
let inventoryItems = await productVariantInventoryService.listByVariant(
|
||||
variant_id
|
||||
)
|
||||
|
||||
// TODO: this is a temporary fix to prevent duplicate inventory items since we don't support this functionality yet
|
||||
if (inventoryItems.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Inventory Item already exists for this variant"
|
||||
)
|
||||
}
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await createInventoryItemTransaction(
|
||||
{
|
||||
manager: transactionManager,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
},
|
||||
variant_id,
|
||||
input
|
||||
)
|
||||
})
|
||||
|
||||
inventoryItems = await productVariantInventoryService.listByVariant(
|
||||
variant_id
|
||||
)
|
||||
|
||||
const inventoryItem = await inventoryService.retrieveInventoryItem(
|
||||
inventoryItems[0].inventory_item_id,
|
||||
req.retrieveConfig
|
||||
)
|
||||
|
||||
res.status(200).json({ inventory_item: inventoryItem })
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema AdminPostInventoryItemsReq
|
||||
* type: object
|
||||
* properties:
|
||||
* sku:
|
||||
* description: The unique SKU for the 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 for the Product Variant.
|
||||
* type: string
|
||||
* inventory_quantity:
|
||||
* description: The amount of stock kept for the Product Variant.
|
||||
* type: integer
|
||||
* default: 0
|
||||
* allow_backorder:
|
||||
* description: Whether the Product Variant can be purchased when out of stock.
|
||||
* type: boolean
|
||||
* manage_inventory:
|
||||
* description: Whether Medusa should keep track of the inventory for this Product Variant.
|
||||
* type: boolean
|
||||
* default: true
|
||||
* weight:
|
||||
* description: The wieght of the Product Variant.
|
||||
* type: number
|
||||
* length:
|
||||
* description: The length of the Product Variant.
|
||||
* type: number
|
||||
* height:
|
||||
* description: The height of the Product Variant.
|
||||
* type: number
|
||||
* width:
|
||||
* description: The width of the Product Variant.
|
||||
* type: number
|
||||
* origin_country:
|
||||
* description: The country of origin of the Product Variant.
|
||||
* type: string
|
||||
* mid_code:
|
||||
* description: The Manufacturer Identification code for the Product Variant.
|
||||
* type: string
|
||||
* material:
|
||||
* description: The material composition of the Product Variant.
|
||||
* type: string
|
||||
* metadata:
|
||||
* description: An optional set of key-value pairs with additional information.
|
||||
* type: object
|
||||
*/
|
||||
export class AdminPostInventoryItemsReq {
|
||||
@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
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export class AdminPostInventoryItemsParams extends FindParams {}
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
} from "./update-location-level"
|
||||
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
||||
import { ProductVariant } from "../../../../models"
|
||||
import {
|
||||
AdminPostInventoryItemsParams,
|
||||
AdminPostInventoryItemsReq,
|
||||
} from "./create-inventory-item"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -73,6 +77,17 @@ export default (app) => {
|
||||
middlewares.wrap(require("./create-location-level").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/",
|
||||
transformQuery(AdminPostInventoryItemsParams, {
|
||||
defaultFields: defaultAdminInventoryItemFields,
|
||||
defaultRelations: defaultAdminInventoryItemRelations,
|
||||
isList: false,
|
||||
}),
|
||||
transformBody(AdminPostInventoryItemsReq),
|
||||
middlewares.wrap(require("./create-inventory-item").default)
|
||||
)
|
||||
|
||||
route.get(
|
||||
"/:id/location-levels",
|
||||
transformQuery(AdminGetInventoryItemsItemLocationLevelsParams, {
|
||||
@@ -264,6 +279,7 @@ export type AdminInventoryItemsLocationLevelsRes = {
|
||||
}
|
||||
|
||||
export * from "./list-inventory-items"
|
||||
export * from "./create-inventory-item"
|
||||
export * from "./get-inventory-item"
|
||||
export * from "./update-inventory-item"
|
||||
export * from "./list-location-levels"
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
DistributedTransaction,
|
||||
TransactionHandlerType,
|
||||
TransactionOrchestrator,
|
||||
TransactionPayload,
|
||||
TransactionState,
|
||||
TransactionStepsDefinition,
|
||||
} from "../../../../../utils/transaction"
|
||||
import { ulid } from "ulid"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IInventoryService } from "../../../../../interfaces"
|
||||
import {
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
} from "../../../../../services"
|
||||
import { InventoryItemDTO } from "../../../../../types/inventory"
|
||||
import { ProductVariant } from "../../../../../models"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
enum actions {
|
||||
createInventoryItem = "createInventoryItem",
|
||||
attachInventoryItem = "attachInventoryItem",
|
||||
}
|
||||
|
||||
const flow: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: actions.createInventoryItem,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: actions.attachInventoryItem,
|
||||
noCompensation: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const createInventoryItemStrategy = new TransactionOrchestrator(
|
||||
"create-inventory-item",
|
||||
flow
|
||||
)
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
productVariantService: ProductVariantService
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
inventoryService: IInventoryService
|
||||
}
|
||||
|
||||
type CreateInventoryItemInput = {
|
||||
sku?: string
|
||||
hs_code?: string
|
||||
weight?: number
|
||||
length?: number
|
||||
height?: number
|
||||
width?: number
|
||||
origin_country?: string
|
||||
mid_code?: string
|
||||
material?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const createInventoryItemTransaction = async (
|
||||
dependencies: InjectedDependencies,
|
||||
variantId: string,
|
||||
input: CreateInventoryItemInput
|
||||
): Promise<DistributedTransaction> => {
|
||||
const {
|
||||
manager,
|
||||
productVariantService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
} = dependencies
|
||||
|
||||
const productVariantInventoryServiceTx =
|
||||
productVariantInventoryService.withTransaction(manager)
|
||||
|
||||
const productVariantServiceTx = productVariantService.withTransaction(manager)
|
||||
|
||||
const variant = await productVariantServiceTx.retrieve(variantId)
|
||||
|
||||
async function createInventoryItem(input: CreateInventoryItemInput) {
|
||||
return await inventoryService!.createInventoryItem({
|
||||
sku: variant.sku,
|
||||
origin_country: variant.origin_country,
|
||||
hs_code: variant.hs_code,
|
||||
mid_code: variant.mid_code,
|
||||
material: variant.material,
|
||||
weight: variant.weight,
|
||||
length: variant.length,
|
||||
height: variant.height,
|
||||
width: variant.width,
|
||||
})
|
||||
}
|
||||
|
||||
async function removeInventoryItem(inventoryItem: InventoryItemDTO) {
|
||||
if (inventoryItem) {
|
||||
await inventoryService!.deleteInventoryItem(inventoryItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function attachInventoryItem(
|
||||
variant: ProductVariant,
|
||||
inventoryItem: InventoryItemDTO
|
||||
) {
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
await productVariantInventoryServiceTx.attachInventoryItem(
|
||||
variant.id,
|
||||
inventoryItem.id
|
||||
)
|
||||
}
|
||||
|
||||
async function transactionHandler(
|
||||
actionId: string,
|
||||
type: TransactionHandlerType,
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
[actions.createInventoryItem]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateInventoryItemInput
|
||||
) => {
|
||||
return await createInventoryItem(data)
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: async (
|
||||
data: CreateInventoryItemInput,
|
||||
{ invoke }
|
||||
) => {
|
||||
await removeInventoryItem(invoke[actions.createInventoryItem])
|
||||
},
|
||||
},
|
||||
[actions.attachInventoryItem]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateInventoryItemInput,
|
||||
{ invoke }
|
||||
) => {
|
||||
const { [actions.createInventoryItem]: inventoryItem } = invoke
|
||||
|
||||
return await attachInventoryItem(variant, inventoryItem)
|
||||
},
|
||||
},
|
||||
}
|
||||
return command[actionId][type](payload.data, payload.context)
|
||||
}
|
||||
|
||||
const transaction = await createInventoryItemStrategy.beginTransaction(
|
||||
ulid(),
|
||||
transactionHandler,
|
||||
input
|
||||
)
|
||||
await createInventoryItemStrategy.resume(transaction)
|
||||
|
||||
if (transaction.getState() !== TransactionState.DONE) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
transaction
|
||||
.getErrors()
|
||||
.map((err) => err.error?.message)
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
return transaction
|
||||
}
|
||||
|
||||
export const revertVariantTransaction = async (
|
||||
transaction: DistributedTransaction
|
||||
) => {
|
||||
await createInventoryItemStrategy.cancelTransaction(transaction)
|
||||
}
|
||||
Reference in New Issue
Block a user