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:
1 parent
30a3203640
commit
fe9eea4c18
18 files changed
+765
-140
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"medusa-react": patch
|
||||||
|
"@medusajs/medusa-js": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add create-inventory-item endpoint
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/admin-ui": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(admin-ui): create/update/delete inventory items according to inventory items on the variant
|
||||||
@@ -278,6 +278,62 @@ describe("Inventory Items endpoints", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.only("Creates an inventory item using the api", async () => {
|
||||||
|
const product = await simpleProductFactory(dbConnection, {})
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const productRes = await api.get(
|
||||||
|
`/admin/products/${product.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const variantId = productRes.data.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 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
variantInventoryRes = await api.get(
|
||||||
|
`/admin/variants/${variantId}/inventory`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(variantInventoryRes.data).toEqual({
|
||||||
|
variant: {
|
||||||
|
id: variantId,
|
||||||
|
inventory: [
|
||||||
|
expect.objectContaining({
|
||||||
|
...inventoryItemCreateRes.data.inventory_item,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
sales_channel_availability: [
|
||||||
|
expect.objectContaining({
|
||||||
|
available_quantity: 0,
|
||||||
|
channel_name: "Default Sales Channel",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(variantInventoryRes.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
describe("List inventory items", () => {
|
describe("List inventory items", () => {
|
||||||
it("Lists inventory items with location", async () => {
|
it("Lists inventory items with location", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|||||||
+8
-7
@@ -1,14 +1,15 @@
|
|||||||
import { Discount } from "@medusajs/medusa"
|
|
||||||
import { parse } from "iso8601-duration"
|
|
||||||
import { useAdminUpdateDiscount } from "medusa-react"
|
|
||||||
import moment from "moment"
|
|
||||||
import { ReactNode } from "react"
|
import { ReactNode } from "react"
|
||||||
import ClockIcon from "../../../../components/fundamentals/icons/clock-icon"
|
|
||||||
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
|
||||||
import { ActionType } from "../../../../components/molecules/actionables"
|
import { ActionType } from "../../../../components/molecules/actionables"
|
||||||
import useNotification from "../../../../hooks/use-notification"
|
import ClockIcon from "../../../../components/fundamentals/icons/clock-icon"
|
||||||
|
import { Discount } from "@medusajs/medusa"
|
||||||
|
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
||||||
import { getErrorMessage } from "../../../../utils/error-messages"
|
import { getErrorMessage } from "../../../../utils/error-messages"
|
||||||
|
import moment from "moment"
|
||||||
|
import { parse } from "iso8601-duration"
|
||||||
import { removeNullish } from "../../../../utils/remove-nullish"
|
import { removeNullish } from "../../../../utils/remove-nullish"
|
||||||
|
import { useAdminUpdateDiscount } from "medusa-react"
|
||||||
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
|
|
||||||
type displaySetting = {
|
type displaySetting = {
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -4,21 +4,24 @@ import {
|
|||||||
Order,
|
Order,
|
||||||
StockLocationDTO,
|
StockLocationDTO,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { useAdminStockLocations } from "medusa-react"
|
import {
|
||||||
import { useAdminRequestReturn, useAdminShippingOptions } from "medusa-react"
|
useAdminRequestReturn,
|
||||||
|
useAdminShippingOptions,
|
||||||
|
useAdminStockLocations,
|
||||||
|
} from "medusa-react"
|
||||||
import React, { useContext, useEffect, useState } from "react"
|
import React, { useContext, useEffect, useState } from "react"
|
||||||
import Spinner from "../../../../components/atoms/spinner"
|
import Spinner from "../../../../components/atoms/spinner"
|
||||||
|
import LayeredModal, {
|
||||||
|
LayeredModalContext,
|
||||||
|
} from "../../../../components/molecules/modal/layered-modal"
|
||||||
|
|
||||||
import Button from "../../../../components/fundamentals/button"
|
import Button from "../../../../components/fundamentals/button"
|
||||||
import CheckIcon from "../../../../components/fundamentals/icons/check-icon"
|
import CheckIcon from "../../../../components/fundamentals/icons/check-icon"
|
||||||
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
|
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
|
||||||
import IconTooltip from "../../../../components/molecules/icon-tooltip"
|
import IconTooltip from "../../../../components/molecules/icon-tooltip"
|
||||||
import Modal from "../../../../components/molecules/modal"
|
import Modal from "../../../../components/molecules/modal"
|
||||||
import LayeredModal, {
|
|
||||||
LayeredModalContext,
|
|
||||||
} from "../../../../components/molecules/modal/layered-modal"
|
|
||||||
import RMAShippingPrice from "../../../../components/molecules/rma-select-shipping"
|
import RMAShippingPrice from "../../../../components/molecules/rma-select-shipping"
|
||||||
import Select from "../../../../components/molecules/select/next-select/select"
|
import Select from "../../../../components/molecules/select/next-select/select"
|
||||||
// import Select from "../../../../components/molecules/select"
|
|
||||||
import CurrencyInput from "../../../../components/organisms/currency-input"
|
import CurrencyInput from "../../../../components/organisms/currency-input"
|
||||||
import RMASelectProductTable from "../../../../components/organisms/rma-select-product-table"
|
import RMASelectProductTable from "../../../../components/organisms/rma-select-product-table"
|
||||||
import useNotification from "../../../../hooks/use-notification"
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
|
|||||||
+1
-11
@@ -10,9 +10,7 @@ export type EditFlowVariantFormType = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
form: UseFormReturn<EditFlowVariantFormType, any>
|
form: UseFormReturn<EditFlowVariantFormType, any>
|
||||||
locationLevels: InventoryLevelDTO[]
|
locationLevels: InventoryLevelDTO[]
|
||||||
refetchInventory: () => void
|
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
itemId: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,13 +32,7 @@ type Props = {
|
|||||||
* )
|
* )
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
const EditFlowVariantForm = ({
|
const EditFlowVariantForm = ({ form, isLoading, locationLevels }: Props) => {
|
||||||
form,
|
|
||||||
isLoading,
|
|
||||||
locationLevels,
|
|
||||||
refetchInventory,
|
|
||||||
itemId,
|
|
||||||
}: Props) => {
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -49,8 +41,6 @@ const EditFlowVariantForm = ({
|
|||||||
<>
|
<>
|
||||||
<VariantStockForm
|
<VariantStockForm
|
||||||
locationLevels={locationLevels}
|
locationLevels={locationLevels}
|
||||||
refetchInventory={refetchInventory}
|
|
||||||
itemId={itemId}
|
|
||||||
form={nestedForm(form, "stock")}
|
form={nestedForm(form, "stock")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+50
-42
@@ -1,13 +1,9 @@
|
|||||||
import React, { useMemo, useState, useContext } from "react"
|
import React, { useMemo, useState, useContext } from "react"
|
||||||
import Modal from "../../../../../components/molecules/modal"
|
import Modal from "../../../../../components/molecules/modal"
|
||||||
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
|
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
|
||||||
import {
|
import { useAdminStockLocations } from "medusa-react"
|
||||||
useAdminCreateLocationLevel,
|
|
||||||
useAdminDeleteLocationLevel,
|
|
||||||
useAdminStockLocations,
|
|
||||||
} from "medusa-react"
|
|
||||||
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
|
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
|
||||||
import { Controller } from "react-hook-form"
|
import { Controller, useFieldArray } from "react-hook-form"
|
||||||
import Button from "../../../../../components/fundamentals/button"
|
import Button from "../../../../../components/fundamentals/button"
|
||||||
import Switch from "../../../../../components/atoms/switch"
|
import Switch from "../../../../../components/atoms/switch"
|
||||||
import InputField from "../../../../../components/molecules/input"
|
import InputField from "../../../../../components/molecules/input"
|
||||||
@@ -16,55 +12,60 @@ import IconBadge from "../../../../../components/fundamentals/icon-badge"
|
|||||||
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
|
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
|
||||||
|
|
||||||
export type VariantStockFormType = {
|
export type VariantStockFormType = {
|
||||||
manage_inventory: boolean
|
manage_inventory?: boolean
|
||||||
allow_backorder: boolean
|
allow_backorder: boolean
|
||||||
inventory_quantity: number | null
|
inventory_quantity: number | null
|
||||||
sku: string | null
|
sku: string | null
|
||||||
ean: string | null
|
ean: string | null
|
||||||
upc: string | null
|
upc: string | null
|
||||||
barcode: string | null
|
barcode: string | null
|
||||||
location_levels: InventoryLevelDTO[] | null
|
location_levels?: Partial<InventoryLevelDTO>[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
itemId: string
|
|
||||||
locationLevels: InventoryLevelDTO[]
|
locationLevels: InventoryLevelDTO[]
|
||||||
refetchInventory: () => void
|
|
||||||
form: NestedForm<VariantStockFormType>
|
form: NestedForm<VariantStockFormType>
|
||||||
}
|
}
|
||||||
|
|
||||||
const VariantStockForm = ({
|
const VariantStockForm = ({ form, locationLevels }: Props) => {
|
||||||
form,
|
const locationLevelMap = useMemo(
|
||||||
locationLevels,
|
() => new Map(locationLevels.map((l) => [l.location_id, l])),
|
||||||
refetchInventory,
|
[locationLevels]
|
||||||
itemId,
|
)
|
||||||
}: Props) => {
|
|
||||||
const layeredModalContext = useContext(LayeredModalContext)
|
const layeredModalContext = useContext(LayeredModalContext)
|
||||||
|
|
||||||
const { stock_locations: locations, isLoading } = useAdminStockLocations()
|
const { stock_locations: locations, isLoading } = useAdminStockLocations()
|
||||||
|
|
||||||
const deleteLevel = useAdminDeleteLocationLevel(itemId)
|
const { path, control, register, watch } = form
|
||||||
const createLevel = useAdminCreateLocationLevel(itemId)
|
|
||||||
|
|
||||||
const { path, control, register } = form
|
const manageInventory = watch(path("manage_inventory"))
|
||||||
|
|
||||||
const handleUpdateLocations = async (value) => {
|
const {
|
||||||
await Promise.all(
|
fields: selectedLocations,
|
||||||
value.removed.map(async (id) => {
|
append,
|
||||||
await deleteLevel.mutateAsync(id)
|
remove,
|
||||||
|
} = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: path("location_levels"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleUpdateLocations = async (data: {
|
||||||
|
added: string[]
|
||||||
|
removed: string[]
|
||||||
|
}) => {
|
||||||
|
const removed = data.removed.map((r) =>
|
||||||
|
selectedLocations.findIndex((sl) => sl.location_id === r)
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(
|
removed.forEach((r) => remove(r))
|
||||||
value.added.map(async (id) => {
|
|
||||||
await createLevel.mutateAsync({
|
|
||||||
stocked_quantity: 0,
|
|
||||||
location_id: id,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
refetchInventory()
|
data.added.forEach((added) => {
|
||||||
|
append({
|
||||||
|
location_id: added,
|
||||||
|
stocked_quantity: locationLevelMap.get(added)?.stocked_quantity ?? 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -111,9 +112,13 @@ const VariantStockForm = ({
|
|||||||
returns are made.
|
returns are made.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{manageInventory && (
|
||||||
|
<>
|
||||||
<div className="gap-y-2xsmall flex flex-col">
|
<div className="gap-y-2xsmall flex flex-col">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="inter-base-semibold mb-2xsmall">Allow backorders</h3>
|
<h3 className="inter-base-semibold mb-2xsmall">
|
||||||
|
Allow backorders
|
||||||
|
</h3>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={path("allow_backorder")}
|
name={path("allow_backorder")}
|
||||||
@@ -123,8 +128,8 @@ const VariantStockForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="inter-base-regular text-grey-50">
|
<p className="inter-base-regular text-grey-50">
|
||||||
When checked the product will be available for purchase despite the
|
When checked the product will be available for purchase despite
|
||||||
product being sold out
|
the product being sold out
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col text-base">
|
<div className="flex w-full flex-col text-base">
|
||||||
@@ -135,9 +140,10 @@ const VariantStockForm = ({
|
|||||||
<div className="">Location</div>
|
<div className="">Location</div>
|
||||||
<div className="">In Stock</div>
|
<div className="">In Stock</div>
|
||||||
</div>
|
</div>
|
||||||
{locationLevels.map((level, i) => {
|
{selectedLocations.map((level, i) => {
|
||||||
|
console.log(level)
|
||||||
const locationDetails = locations.find(
|
const locationDetails = locations.find(
|
||||||
(l) => l.id === level.location_id
|
(l: StockLocationDTO) => l.id === level.location_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -183,7 +189,7 @@ const VariantStockForm = ({
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
ManageLocationsScreen(
|
ManageLocationsScreen(
|
||||||
layeredModalContext.pop,
|
layeredModalContext.pop,
|
||||||
locationLevels,
|
selectedLocations,
|
||||||
locations,
|
locations,
|
||||||
handleUpdateLocations
|
handleUpdateLocations
|
||||||
)
|
)
|
||||||
@@ -193,6 +199,8 @@ const VariantStockForm = ({
|
|||||||
Manage locations
|
Manage locations
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -200,7 +208,7 @@ const VariantStockForm = ({
|
|||||||
|
|
||||||
export const ManageLocationsScreen = (
|
export const ManageLocationsScreen = (
|
||||||
pop: () => void,
|
pop: () => void,
|
||||||
levels: InventoryLevelDTO[],
|
levels: Partial<InventoryLevelDTO>[],
|
||||||
locations: StockLocationDTO[],
|
locations: StockLocationDTO[],
|
||||||
onSubmit: (value: any) => Promise<void>
|
onSubmit: (value: any) => Promise<void>
|
||||||
) => {
|
) => {
|
||||||
@@ -218,7 +226,7 @@ export const ManageLocationsScreen = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ManageLocationFormProps = {
|
type ManageLocationFormProps = {
|
||||||
existingLevels: InventoryLevelDTO[]
|
existingLevels: Partial<InventoryLevelDTO>[]
|
||||||
locationOptions: StockLocationDTO[]
|
locationOptions: StockLocationDTO[]
|
||||||
onSubmit: (value: any) => Promise<void>
|
onSubmit: (value: any) => Promise<void>
|
||||||
}
|
}
|
||||||
@@ -232,7 +240,7 @@ const ManageLocationsForm = ({
|
|||||||
const { pop } = layeredModalContext
|
const { pop } = layeredModalContext
|
||||||
|
|
||||||
const existingLocations = useMemo(() => {
|
const existingLocations = useMemo(() => {
|
||||||
return existingLevels.map((level) => level.location_id)
|
return existingLevels.map((level) => level.location_id!)
|
||||||
}, [existingLevels])
|
}, [existingLevels])
|
||||||
|
|
||||||
const [selectedLocations, setSelectedLocations] =
|
const [selectedLocations, setSelectedLocations] =
|
||||||
@@ -266,7 +274,7 @@ const ManageLocationsForm = ({
|
|||||||
(locationId: string) => !existingLocations.includes(locationId)
|
(locationId: string) => !existingLocations.includes(locationId)
|
||||||
)
|
)
|
||||||
const removedLevels = existingLocations.filter(
|
const removedLevels = existingLocations.filter(
|
||||||
(locationId) => !selectedLocations.includes(locationId)
|
(locationId) => !!locationId && !selectedLocations.includes(locationId)
|
||||||
)
|
)
|
||||||
|
|
||||||
await onSubmit({
|
await onSubmit({
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import {
|
|||||||
useAdminUpdateProduct,
|
useAdminUpdateProduct,
|
||||||
useAdminUpdateVariant,
|
useAdminUpdateVariant,
|
||||||
} from "medusa-react"
|
} from "medusa-react"
|
||||||
import { useNavigate } from "react-router-dom"
|
|
||||||
import useImperativeDialog from "../../../../hooks/use-imperative-dialog"
|
|
||||||
import useNotification from "../../../../hooks/use-notification"
|
|
||||||
import { getErrorMessage } from "../../../../utils/error-messages"
|
import { getErrorMessage } from "../../../../utils/error-messages"
|
||||||
import { removeNullish } from "../../../../utils/remove-nullish"
|
import { removeNullish } from "../../../../utils/remove-nullish"
|
||||||
|
import useImperativeDialog from "../../../../hooks/use-imperative-dialog"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import useNotification from "../../../../hooks/use-notification"
|
||||||
|
|
||||||
const useEditProductActions = (productId: string) => {
|
const useEditProductActions = (productId: string) => {
|
||||||
const dialog = useImperativeDialog()
|
const dialog = useImperativeDialog()
|
||||||
@@ -70,8 +71,11 @@ const useEditProductActions = (productId: string) => {
|
|||||||
successMessage = "Variant was updated successfully"
|
successMessage = "Variant was updated successfully"
|
||||||
) => {
|
) => {
|
||||||
updateVariant.mutate(
|
updateVariant.mutate(
|
||||||
// @ts-ignore - TODO fix type on request
|
{
|
||||||
{ variant_id: id, ...removeNullish(payload) },
|
variant_id: id,
|
||||||
|
...removeNullish(payload),
|
||||||
|
manage_inventory: payload.manage_inventory,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notification("Success", successMessage, "success")
|
notification("Success", successMessage, "success")
|
||||||
|
|||||||
+101
-41
@@ -1,20 +1,24 @@
|
|||||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
|
||||||
import {
|
import {
|
||||||
useAdminUpdateLocationLevel,
|
InventoryLevelDTO,
|
||||||
useAdminVariantsInventory,
|
Product,
|
||||||
} from "medusa-react"
|
ProductVariant,
|
||||||
import React, { useContext } from "react"
|
VariantInventory,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import { useMedusa } from "medusa-react"
|
||||||
|
import { useAdminVariantsInventory } from "medusa-react"
|
||||||
|
import { useContext } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import Button from "../../../../../components/fundamentals/button"
|
|
||||||
import Modal from "../../../../../components/molecules/modal"
|
|
||||||
import LayeredModal, {
|
|
||||||
LayeredModalContext,
|
|
||||||
} from "../../../../../components/molecules/modal/layered-modal"
|
|
||||||
import EditFlowVariantForm, {
|
import EditFlowVariantForm, {
|
||||||
EditFlowVariantFormType,
|
EditFlowVariantFormType,
|
||||||
} from "../../../components/variant-inventory-form/edit-flow-variant-form"
|
} from "../../../components/variant-inventory-form/edit-flow-variant-form"
|
||||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
import LayeredModal, {
|
||||||
|
LayeredModalContext,
|
||||||
|
} from "../../../../../components/molecules/modal/layered-modal"
|
||||||
|
import Button from "../../../../../components/fundamentals/button"
|
||||||
|
import Modal from "../../../../../components/molecules/modal"
|
||||||
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
|
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
|
||||||
|
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||||
|
import { removeNullish } from "../../../../../utils/remove-nullish"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -24,6 +28,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
||||||
|
const { client } = useMedusa()
|
||||||
const layeredModalContext = useContext(LayeredModalContext)
|
const layeredModalContext = useContext(LayeredModalContext)
|
||||||
const {
|
const {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -32,30 +37,93 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
|||||||
refetch,
|
refetch,
|
||||||
} = useAdminVariantsInventory(variant.id)
|
} = useAdminVariantsInventory(variant.id)
|
||||||
|
|
||||||
const itemId = variantInventory?.inventory[0]?.id
|
const variantInventoryItem = variantInventory?.inventory[0]
|
||||||
|
const itemId = variantInventoryItem?.id
|
||||||
|
|
||||||
const { mutate: updateLocationLevel } = useAdminUpdateLocationLevel(
|
|
||||||
itemId || ""
|
|
||||||
)
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onUpdateVariant, updatingVariant } = useEditProductActions(product.id)
|
const { onUpdateVariant, updatingVariant } = useEditProductActions(product.id)
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = async (data: EditFlowVariantFormType) => {
|
||||||
const { location_levels } = data.stock
|
const locationLevels = data.stock.location_levels || []
|
||||||
|
const manageInventory = data.stock.manage_inventory
|
||||||
|
delete data.stock.manage_inventory
|
||||||
|
delete data.stock.location_levels
|
||||||
|
|
||||||
|
let inventoryItemId: string | undefined = itemId
|
||||||
|
|
||||||
|
const upsertPayload = removeNullish(data.stock)
|
||||||
|
|
||||||
|
if (variantInventoryItem) {
|
||||||
|
// variant inventory exists and we can remove location levels
|
||||||
|
// (it's important to do this before potentially deleting the inventory item)
|
||||||
|
const deleteLocations = manageInventory
|
||||||
|
? variantInventoryItem?.location_levels?.filter(
|
||||||
|
(itemLevel: InventoryLevelDTO) => {
|
||||||
|
return !locationLevels.find(
|
||||||
|
(level) => level.location_id === itemLevel.location_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) ?? []
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (inventoryItemId) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
location_levels.map(async (level) => {
|
deleteLocations.map(async (location: InventoryLevelDTO) => {
|
||||||
await updateLocationLevel({
|
await client.admin.inventoryItems.deleteLocationLevel(
|
||||||
stockLocationId: level.location_id,
|
inventoryItemId!,
|
||||||
stocked_quantity: level.stocked_quantity,
|
location.id
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// / TODO: Call update location level with new values
|
}
|
||||||
delete data.stock.location_levels
|
|
||||||
|
if (!manageInventory) {
|
||||||
|
// has an inventory item but no longer wants to manage inventory
|
||||||
|
await client.admin.inventoryItems.delete(itemId!)
|
||||||
|
inventoryItemId = undefined
|
||||||
|
} else {
|
||||||
|
// has an inventory item and wants to update inventory
|
||||||
|
await client.admin.inventoryItems.update(itemId!, upsertPayload)
|
||||||
|
}
|
||||||
|
} else if (manageInventory) {
|
||||||
|
// does not have an inventory item but wants to manage inventory
|
||||||
|
const { inventory_item } = await client.admin.inventoryItems.create({
|
||||||
|
variant_id: variant.id,
|
||||||
|
...upsertPayload,
|
||||||
|
})
|
||||||
|
inventoryItemId = inventory_item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// If some inventory Item exists update location levels
|
||||||
|
if (inventoryItemId) {
|
||||||
|
await Promise.all(
|
||||||
|
locationLevels.map(async (level) => {
|
||||||
|
if (!level.location_id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (level.id) {
|
||||||
|
await client.admin.inventoryItems.updateLocationLevel(
|
||||||
|
inventoryItemId!,
|
||||||
|
level.location_id,
|
||||||
|
{
|
||||||
|
stocked_quantity: level.stocked_quantity,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await client.admin.inventoryItems.createLocationLevel(
|
||||||
|
inventoryItemId!,
|
||||||
|
{
|
||||||
|
location_id: level.location_id,
|
||||||
|
stocked_quantity: level.stocked_quantity!,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
onUpdateVariant(variant.id, createUpdatePayload(data), () => {
|
onUpdateVariant(variant.id, createUpdatePayload(data), () => {
|
||||||
@@ -71,8 +139,7 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
|||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
{!isLoadingInventory && (
|
{!isLoadingInventory && (
|
||||||
<StockForm
|
<StockForm
|
||||||
variantInventory={variantInventory}
|
variantInventory={variantInventory!}
|
||||||
refetchInventory={refetch}
|
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
isLoadingInventory={isLoadingInventory}
|
isLoadingInventory={isLoadingInventory}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
@@ -86,10 +153,15 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
|||||||
const StockForm = ({
|
const StockForm = ({
|
||||||
variantInventory,
|
variantInventory,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
refetchInventory,
|
|
||||||
isLoadingInventory,
|
isLoadingInventory,
|
||||||
handleClose,
|
handleClose,
|
||||||
updatingVariant,
|
updatingVariant,
|
||||||
|
}: {
|
||||||
|
variantInventory: VariantInventory
|
||||||
|
onSubmit: (data: EditFlowVariantFormType) => void
|
||||||
|
isLoadingInventory: boolean
|
||||||
|
handleClose: () => void
|
||||||
|
updatingVariant: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const form = useForm<EditFlowVariantFormType>({
|
const form = useForm<EditFlowVariantFormType>({
|
||||||
defaultValues: getEditVariantDefaultValues(variantInventory),
|
defaultValues: getEditVariantDefaultValues(variantInventory),
|
||||||
@@ -99,32 +171,20 @@ const StockForm = ({
|
|||||||
formState: { isDirty },
|
formState: { isDirty },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
|
||||||
} = form
|
} = form
|
||||||
|
|
||||||
const locationLevels = watch("stock.location_levels")
|
const locationLevels = variantInventory.inventory[0]?.location_levels || []
|
||||||
|
|
||||||
const { location_levels } = variantInventory.inventory[0]
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
form.setValue("stock.location_levels", location_levels)
|
|
||||||
}, [form, location_levels])
|
|
||||||
|
|
||||||
const handleOnSubmit = handleSubmit((data) => {
|
const handleOnSubmit = handleSubmit((data) => {
|
||||||
// @ts-ignore
|
|
||||||
onSubmit(data)
|
onSubmit(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
const itemId = variantInventory.inventory[0].id
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleOnSubmit} noValidate>
|
<form onSubmit={handleOnSubmit} noValidate>
|
||||||
<Modal.Content>
|
<Modal.Content>
|
||||||
<EditFlowVariantForm
|
<EditFlowVariantForm
|
||||||
form={form}
|
form={form}
|
||||||
refetchInventory={refetchInventory}
|
locationLevels={locationLevels}
|
||||||
locationLevels={locationLevels || []}
|
|
||||||
itemId={itemId}
|
|
||||||
isLoading={isLoadingInventory}
|
isLoading={isLoadingInventory}
|
||||||
/>
|
/>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
|
|||||||
@@ -106,9 +106,9 @@ const VariantsTable = ({ variants, actions }: Props) => {
|
|||||||
|
|
||||||
const getTableRowActionables = (variant: ProductVariant) => {
|
const getTableRowActionables = (variant: ProductVariant) => {
|
||||||
const inventoryManagementActions = []
|
const inventoryManagementActions = []
|
||||||
if (hasInventoryService && variant.manage_inventory) {
|
if (hasInventoryService) {
|
||||||
inventoryManagementActions.push({
|
inventoryManagementActions.push({
|
||||||
label: "Manage inventory", // TODO: Only add this item if variant.manageInventory is true
|
label: "Manage inventory",
|
||||||
icon: <BuildingsIcon size="20" />,
|
icon: <BuildingsIcon size="20" />,
|
||||||
onClick: () => updateVariantInventory(variant),
|
onClick: () => updateVariantInventory(variant),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DeepPartial, EntityManager, FindManyOptions } from "typeorm"
|
import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm"
|
||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
import {
|
import {
|
||||||
buildQuery,
|
buildQuery,
|
||||||
@@ -190,14 +190,34 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an inventory level by ID.
|
* Deletes inventory levels by inventory Item ID.
|
||||||
* @param inventoryLevelId - The ID of the inventory level to delete.
|
* @param inventoryItemId - The ID or IDs of the inventory item to delete inventory levels for.
|
||||||
*/
|
*/
|
||||||
async delete(inventoryLevelId: string): Promise<void> {
|
async deleteByInventoryItemId(inventoryItemId: string | string[]): Promise<void> {
|
||||||
|
const ids = Array.isArray(inventoryItemId) ? inventoryItemId : [inventoryItemId]
|
||||||
await this.atomicPhase_(async (manager) => {
|
await this.atomicPhase_(async (manager) => {
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
await levelRepository.delete({ id: inventoryLevelId })
|
await levelRepository.delete({ inventory_item_id: In(ids) })
|
||||||
|
|
||||||
|
await this.eventBusService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.emit(InventoryLevelService.Events.DELETED, {
|
||||||
|
inventory_item_id: inventoryItemId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an inventory level by ID.
|
||||||
|
* @param inventoryLevelId - The ID or IDs of the inventory level to delete.
|
||||||
|
*/
|
||||||
|
async delete(inventoryLevelId: string | string[]): Promise<void> {
|
||||||
|
const ids = Array.isArray(inventoryLevelId) ? inventoryLevelId : [inventoryLevelId]
|
||||||
|
await this.atomicPhase_(async (manager) => {
|
||||||
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
|
await levelRepository.delete({ id: In(ids) })
|
||||||
|
|
||||||
await this.eventBusService_
|
await this.eventBusService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ export default class InventoryService
|
|||||||
* @param inventoryItemId - the id of the inventory item to delete
|
* @param inventoryItemId - the id of the inventory item to delete
|
||||||
*/
|
*/
|
||||||
async deleteInventoryItem(inventoryItemId: string): Promise<void> {
|
async deleteInventoryItem(inventoryItemId: string): Promise<void> {
|
||||||
|
await this.inventoryLevelService_
|
||||||
|
.withTransaction(this.activeManager_)
|
||||||
|
.deleteByInventoryItemId(inventoryItemId)
|
||||||
|
|
||||||
return await this.inventoryItemService_
|
return await this.inventoryItemService_
|
||||||
.withTransaction(this.activeManager_)
|
.withTransaction(this.activeManager_)
|
||||||
.delete(inventoryItemId)
|
.delete(inventoryItemId)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
|
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
|
||||||
AdminInventoryItemsLocationLevelsRes,
|
AdminInventoryItemsLocationLevelsRes,
|
||||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||||
|
AdminPostInventoryItemsReq,
|
||||||
|
AdminPostInventoryItemsParams,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { ResponsePromise } from "../../typings"
|
import { ResponsePromise } from "../../typings"
|
||||||
import BaseResource from "../base"
|
import BaseResource from "../base"
|
||||||
@@ -75,6 +77,28 @@ class AdminInventoryItemsResource extends BaseResource {
|
|||||||
return this.client.request("DELETE", path, undefined, {}, customHeaders)
|
return this.client.request("DELETE", path, undefined, {}, customHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an Inventory Item
|
||||||
|
* @experimental This feature is under development and may change in the future.
|
||||||
|
* To use this feature please install @medusajs/inventory
|
||||||
|
* @description creates an Inventory Item
|
||||||
|
* @returns the created Inventory Item
|
||||||
|
*/
|
||||||
|
create(
|
||||||
|
payload: AdminPostInventoryItemsReq,
|
||||||
|
query?: AdminPostInventoryItemsParams,
|
||||||
|
customHeaders: Record<string, any> = {}
|
||||||
|
): ResponsePromise<AdminInventoryItemsRes> {
|
||||||
|
let path = `/admin/inventory-items`
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const queryString = qs.stringify(query)
|
||||||
|
path += `?${queryString}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.client.request("POST", path, payload, {}, customHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a list of Inventory Items
|
* Retrieve a list of Inventory Items
|
||||||
* @experimental This feature is under development and may change in the future.
|
* @experimental This feature is under development and may change in the future.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
AdminPostInventoryItemsInventoryItemReq,
|
AdminPostInventoryItemsInventoryItemReq,
|
||||||
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
||||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||||
|
AdminPostInventoryItemsReq,
|
||||||
|
AdminPostInventoryItemsParams
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { Response } from "@medusajs/medusa-js"
|
import { Response } from "@medusajs/medusa-js"
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +19,29 @@ import { adminInventoryItemsKeys } from "./queries"
|
|||||||
|
|
||||||
// inventory item
|
// inventory item
|
||||||
|
|
||||||
|
// create inventory item
|
||||||
|
export const useAdminCreateInventoryItem = (
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
Response<AdminInventoryItemsRes>,
|
||||||
|
Error,
|
||||||
|
AdminPostInventoryItemsReq
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { client } = useMedusa()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
(payload: AdminPostInventoryItemsReq, query?: AdminPostInventoryItemsParams) =>
|
||||||
|
client.admin.inventoryItems.create(payload, query),
|
||||||
|
buildOptions(
|
||||||
|
queryClient,
|
||||||
|
[adminInventoryItemsKeys.lists()],
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// update inventory item
|
// update inventory item
|
||||||
export const useAdminUpdateInventoryItem = (
|
export const useAdminUpdateInventoryItem = (
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
|
|||||||
@@ -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"
|
} from "./update-location-level"
|
||||||
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
||||||
import { ProductVariant } from "../../../../models"
|
import { ProductVariant } from "../../../../models"
|
||||||
|
import {
|
||||||
|
AdminPostInventoryItemsParams,
|
||||||
|
AdminPostInventoryItemsReq,
|
||||||
|
} from "./create-inventory-item"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -73,6 +77,17 @@ export default (app) => {
|
|||||||
middlewares.wrap(require("./create-location-level").default)
|
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(
|
route.get(
|
||||||
"/:id/location-levels",
|
"/:id/location-levels",
|
||||||
transformQuery(AdminGetInventoryItemsItemLocationLevelsParams, {
|
transformQuery(AdminGetInventoryItemsItemLocationLevelsParams, {
|
||||||
@@ -264,6 +279,7 @@ export type AdminInventoryItemsLocationLevelsRes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export * from "./list-inventory-items"
|
export * from "./list-inventory-items"
|
||||||
|
export * from "./create-inventory-item"
|
||||||
export * from "./get-inventory-item"
|
export * from "./get-inventory-item"
|
||||||
export * from "./update-inventory-item"
|
export * from "./update-inventory-item"
|
||||||
export * from "./list-location-levels"
|
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)
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
|
||||||
import {
|
import {
|
||||||
Brackets,
|
Brackets,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
@@ -10,26 +9,6 @@ import {
|
|||||||
IsNull,
|
IsNull,
|
||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import {
|
|
||||||
IPriceSelectionStrategy,
|
|
||||||
PriceSelectionContext,
|
|
||||||
TransactionBaseService,
|
|
||||||
} from "../interfaces"
|
|
||||||
import {
|
|
||||||
MoneyAmount,
|
|
||||||
Product,
|
|
||||||
ProductOptionValue,
|
|
||||||
ProductVariant,
|
|
||||||
} from "../models"
|
|
||||||
import { CartRepository } from "../repositories/cart"
|
|
||||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
|
||||||
import { ProductRepository } from "../repositories/product"
|
|
||||||
import { ProductOptionValueRepository } from "../repositories/product-option-value"
|
|
||||||
import {
|
|
||||||
FindWithRelationsOptions,
|
|
||||||
ProductVariantRepository,
|
|
||||||
} from "../repositories/product-variant"
|
|
||||||
import { FindConfig } from "../types/common"
|
|
||||||
import {
|
import {
|
||||||
CreateProductVariantInput,
|
CreateProductVariantInput,
|
||||||
FilterableProductVariantProps,
|
FilterableProductVariantProps,
|
||||||
@@ -37,8 +16,30 @@ import {
|
|||||||
ProductVariantPrice,
|
ProductVariantPrice,
|
||||||
UpdateProductVariantInput,
|
UpdateProductVariantInput,
|
||||||
} from "../types/product-variant"
|
} from "../types/product-variant"
|
||||||
|
import {
|
||||||
|
FindWithRelationsOptions,
|
||||||
|
ProductVariantRepository,
|
||||||
|
} from "../repositories/product-variant"
|
||||||
|
import {
|
||||||
|
IPriceSelectionStrategy,
|
||||||
|
PriceSelectionContext,
|
||||||
|
TransactionBaseService,
|
||||||
|
} from "../interfaces"
|
||||||
|
import { MedusaError, isDefined } from "medusa-core-utils"
|
||||||
|
import {
|
||||||
|
MoneyAmount,
|
||||||
|
Product,
|
||||||
|
ProductOptionValue,
|
||||||
|
ProductVariant,
|
||||||
|
} from "../models"
|
||||||
import { buildQuery, buildRelations, setMetadata } from "../utils"
|
import { buildQuery, buildRelations, setMetadata } from "../utils"
|
||||||
|
|
||||||
|
import { CartRepository } from "../repositories/cart"
|
||||||
import EventBusService from "./event-bus"
|
import EventBusService from "./event-bus"
|
||||||
|
import { FindConfig } from "../types/common"
|
||||||
|
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||||
|
import { ProductOptionValueRepository } from "../repositories/product-option-value"
|
||||||
|
import { ProductRepository } from "../repositories/product"
|
||||||
import RegionService from "./region"
|
import RegionService from "./region"
|
||||||
|
|
||||||
class ProductVariantService extends TransactionBaseService {
|
class ProductVariantService extends TransactionBaseService {
|
||||||
|
|||||||
Reference in new issue
Block a user