feat(medusa): Add inventory for variants (#2970)
* initial get-inventory impl * add inventory management to create-variant * update create-variant endpoint * move if statement * use middleware for module checking * add endpoint to medusa-js * export return type from get-inventory * add jsdoc * revert create variant * rename variable * initial setInventoryPattern for variant and product endpoints * remove cache * sort imports * add sales channel info to inventory calculations * add missing import * remove promise.all from single promise * update oas * initial feedback * checkout joinLevels from develop * add variant middleware * add comments
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
InventoryItemDTO,
|
||||
InventoryLevelDTO,
|
||||
} from "../../../../types/inventory"
|
||||
import ProductVariantInventoryService from "../../../../services/product-variant-inventory"
|
||||
import {
|
||||
SalesChannelLocationService,
|
||||
SalesChannelService,
|
||||
} from "../../../../services"
|
||||
import { SalesChannel } from "../../../../models"
|
||||
import { IInventoryService } from "../../../../interfaces"
|
||||
import ProductVariantService from "../../../../services/product-variant"
|
||||
import { joinLevels } from "../inventory-items/utils/join-levels"
|
||||
|
||||
/**
|
||||
* @oas [get] /variants/{id}/inventory
|
||||
* operationId: "GetVariantsVariantInventory"
|
||||
* summary: "Get inventory of Variant."
|
||||
* description: "Returns the available inventory of a Variant."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id {string} The Product Variant id to get inventory for.
|
||||
* 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.variants.list()
|
||||
* .then(({ variants, limit, offset, count }) => {
|
||||
* console.log(variants.length)
|
||||
* })
|
||||
* - lang: Shell
|
||||
* label: cURL
|
||||
* source: |
|
||||
* curl --location --request GET 'https://medusa-url.com/admin/variants' \
|
||||
* --header 'Authorization: Bearer {api_token}'
|
||||
* security:
|
||||
* - api_token: []
|
||||
* - cookie_auth: []
|
||||
* tags:
|
||||
* - Product Variant
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* variant:
|
||||
* type: object
|
||||
* $ref: "#/components/schemas/AdminGetVariantsVariantInventoryRes"
|
||||
* "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 { id } = req.params
|
||||
|
||||
const inventoryService: IInventoryService =
|
||||
req.scope.resolve("inventoryService")
|
||||
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
|
||||
"salesChannelLocationService"
|
||||
)
|
||||
const channelService: SalesChannelService = req.scope.resolve(
|
||||
"salesChannelService"
|
||||
)
|
||||
const productVariantInventoryService: ProductVariantInventoryService =
|
||||
req.scope.resolve("productVariantInventoryService")
|
||||
|
||||
const variantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
)
|
||||
|
||||
const variant = await variantService.retrieve(id, { select: ["id"] })
|
||||
|
||||
const responseVariant: AdminGetVariantsVariantInventoryRes = {
|
||||
id: variant.id,
|
||||
inventory: [],
|
||||
sales_channel_availability: [],
|
||||
}
|
||||
|
||||
const [rawChannels] = await channelService.listAndCount({})
|
||||
const channels: SalesChannelDTO[] = await Promise.all(
|
||||
rawChannels.map(async (channel) => {
|
||||
const locations = await channelLocationService.listLocations(channel.id)
|
||||
return {
|
||||
...channel,
|
||||
locations,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const inventory =
|
||||
await productVariantInventoryService.listInventoryItemsByVariant(variant.id)
|
||||
responseVariant.inventory = await joinLevels(inventory, [], inventoryService)
|
||||
|
||||
// TODO: adjust for required quantity
|
||||
if (inventory.length) {
|
||||
responseVariant.sales_channel_availability = await Promise.all(
|
||||
channels.map(async (channel) => {
|
||||
if (!channel.locations.length) {
|
||||
return {
|
||||
channel_name: channel.name as string,
|
||||
channel_id: channel.id as string,
|
||||
available_quantity: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const quantity = await inventoryService.retrieveAvailableQuantity(
|
||||
inventory[0].id,
|
||||
channel.locations
|
||||
)
|
||||
|
||||
return {
|
||||
channel_name: channel.name as string,
|
||||
channel_id: channel.id as string,
|
||||
available_quantity: quantity,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
res.json({
|
||||
variant: responseVariant,
|
||||
})
|
||||
}
|
||||
|
||||
type SalesChannelDTO = Omit<SalesChannel, "beforeInsert"> & {
|
||||
locations: string[]
|
||||
}
|
||||
|
||||
type ResponseInventoryItem = Partial<InventoryItemDTO> & {
|
||||
location_levels?: InventoryLevelDTO[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema AdminGetVariantsVariantInventoryRes
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* description: the id of the variant
|
||||
* type: string
|
||||
* inventory:
|
||||
* description: the stock location address ID
|
||||
* type: string
|
||||
* sales_channel_availability:
|
||||
* type: object
|
||||
* description: An optional key-value map with additional details
|
||||
* properties:
|
||||
* channel_name:
|
||||
* description: Sales channel name
|
||||
* type: string
|
||||
* channel_id:
|
||||
* description: Sales channel id
|
||||
* type: string
|
||||
* available_quantity:
|
||||
* description: Available quantity in sales channel
|
||||
* type: number
|
||||
*/
|
||||
export type AdminGetVariantsVariantInventoryRes = {
|
||||
id: string
|
||||
inventory: ResponseInventoryItem[]
|
||||
sales_channel_availability: {
|
||||
channel_name: string
|
||||
channel_id: string
|
||||
available_quantity: number
|
||||
}[]
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ProductVariant } from "../../../../models/product-variant"
|
||||
import { PaginatedResponse } from "../../../../types/common"
|
||||
import { PricedVariant } from "../../../../types/pricing"
|
||||
import middlewares, { transformQuery } from "../../../middlewares"
|
||||
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
||||
import { AdminGetVariantsParams } from "./list-variants"
|
||||
|
||||
const route = Router()
|
||||
@@ -21,6 +22,15 @@ export default (app) => {
|
||||
middlewares.wrap(require("./list-variants").default)
|
||||
)
|
||||
|
||||
route.get(
|
||||
"/:id/inventory",
|
||||
checkRegisteredModules({
|
||||
inventoryService:
|
||||
"Inventory is not enabled. Please add an Inventory module to enable this functionality.",
|
||||
}),
|
||||
middlewares.wrap(require("./get-inventory").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -72,3 +82,4 @@ export type AdminVariantsListRes = PaginatedResponse & {
|
||||
}
|
||||
|
||||
export * from "./list-variants"
|
||||
export * from "./get-inventory"
|
||||
|
||||
Reference in New Issue
Block a user