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:
7
.changeset/fast-buckets-marry.md
Normal file
7
.changeset/fast-buckets-marry.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"medusa-react": patch
|
||||
"@medusajs/medusa-js": patch
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
Add create-inventory-item endpoint
|
||||
5
.changeset/slow-beers-train.md
Normal file
5
.changeset/slow-beers-train.md
Normal file
@@ -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", () => {
|
||||
it("Lists inventory items with location", async () => {
|
||||
const api = useApi()
|
||||
|
||||
@@ -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 ClockIcon from "../../../../components/fundamentals/icons/clock-icon"
|
||||
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
||||
|
||||
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 moment from "moment"
|
||||
import { parse } from "iso8601-duration"
|
||||
import { removeNullish } from "../../../../utils/remove-nullish"
|
||||
import { useAdminUpdateDiscount } from "medusa-react"
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
|
||||
type displaySetting = {
|
||||
title: string
|
||||
|
||||
@@ -4,21 +4,24 @@ import {
|
||||
Order,
|
||||
StockLocationDTO,
|
||||
} from "@medusajs/medusa"
|
||||
import { useAdminStockLocations } from "medusa-react"
|
||||
import { useAdminRequestReturn, useAdminShippingOptions } from "medusa-react"
|
||||
import {
|
||||
useAdminRequestReturn,
|
||||
useAdminShippingOptions,
|
||||
useAdminStockLocations,
|
||||
} from "medusa-react"
|
||||
import React, { useContext, useEffect, useState } from "react"
|
||||
import Spinner from "../../../../components/atoms/spinner"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../components/molecules/modal/layered-modal"
|
||||
|
||||
import Button from "../../../../components/fundamentals/button"
|
||||
import CheckIcon from "../../../../components/fundamentals/icons/check-icon"
|
||||
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
|
||||
import IconTooltip from "../../../../components/molecules/icon-tooltip"
|
||||
import Modal from "../../../../components/molecules/modal"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../components/molecules/modal/layered-modal"
|
||||
import RMAShippingPrice from "../../../../components/molecules/rma-select-shipping"
|
||||
import Select from "../../../../components/molecules/select/next-select/select"
|
||||
// import Select from "../../../../components/molecules/select"
|
||||
import CurrencyInput from "../../../../components/organisms/currency-input"
|
||||
import RMASelectProductTable from "../../../../components/organisms/rma-select-product-table"
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
|
||||
@@ -10,9 +10,7 @@ export type EditFlowVariantFormType = {
|
||||
type Props = {
|
||||
form: UseFormReturn<EditFlowVariantFormType, any>
|
||||
locationLevels: InventoryLevelDTO[]
|
||||
refetchInventory: () => void
|
||||
isLoading: boolean
|
||||
itemId: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,13 +32,7 @@ type Props = {
|
||||
* )
|
||||
* }
|
||||
*/
|
||||
const EditFlowVariantForm = ({
|
||||
form,
|
||||
isLoading,
|
||||
locationLevels,
|
||||
refetchInventory,
|
||||
itemId,
|
||||
}: Props) => {
|
||||
const EditFlowVariantForm = ({ form, isLoading, locationLevels }: Props) => {
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
@@ -49,8 +41,6 @@ const EditFlowVariantForm = ({
|
||||
<>
|
||||
<VariantStockForm
|
||||
locationLevels={locationLevels}
|
||||
refetchInventory={refetchInventory}
|
||||
itemId={itemId}
|
||||
form={nestedForm(form, "stock")}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import React, { useMemo, useState, useContext } from "react"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
|
||||
import {
|
||||
useAdminCreateLocationLevel,
|
||||
useAdminDeleteLocationLevel,
|
||||
useAdminStockLocations,
|
||||
} from "medusa-react"
|
||||
import { useAdminStockLocations } from "medusa-react"
|
||||
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 Switch from "../../../../../components/atoms/switch"
|
||||
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"
|
||||
|
||||
export type VariantStockFormType = {
|
||||
manage_inventory: boolean
|
||||
manage_inventory?: boolean
|
||||
allow_backorder: boolean
|
||||
inventory_quantity: number | null
|
||||
sku: string | null
|
||||
ean: string | null
|
||||
upc: string | null
|
||||
barcode: string | null
|
||||
location_levels: InventoryLevelDTO[] | null
|
||||
location_levels?: Partial<InventoryLevelDTO>[] | null
|
||||
}
|
||||
|
||||
type Props = {
|
||||
itemId: string
|
||||
locationLevels: InventoryLevelDTO[]
|
||||
refetchInventory: () => void
|
||||
form: NestedForm<VariantStockFormType>
|
||||
}
|
||||
|
||||
const VariantStockForm = ({
|
||||
form,
|
||||
locationLevels,
|
||||
refetchInventory,
|
||||
itemId,
|
||||
}: Props) => {
|
||||
const VariantStockForm = ({ form, locationLevels }: Props) => {
|
||||
const locationLevelMap = useMemo(
|
||||
() => new Map(locationLevels.map((l) => [l.location_id, l])),
|
||||
[locationLevels]
|
||||
)
|
||||
|
||||
const layeredModalContext = useContext(LayeredModalContext)
|
||||
|
||||
const { stock_locations: locations, isLoading } = useAdminStockLocations()
|
||||
|
||||
const deleteLevel = useAdminDeleteLocationLevel(itemId)
|
||||
const createLevel = useAdminCreateLocationLevel(itemId)
|
||||
const { path, control, register, watch } = form
|
||||
|
||||
const { path, control, register } = form
|
||||
const manageInventory = watch(path("manage_inventory"))
|
||||
|
||||
const handleUpdateLocations = async (value) => {
|
||||
await Promise.all(
|
||||
value.removed.map(async (id) => {
|
||||
await deleteLevel.mutateAsync(id)
|
||||
})
|
||||
const {
|
||||
fields: selectedLocations,
|
||||
append,
|
||||
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(
|
||||
value.added.map(async (id) => {
|
||||
await createLevel.mutateAsync({
|
||||
stocked_quantity: 0,
|
||||
location_id: id,
|
||||
})
|
||||
})
|
||||
)
|
||||
removed.forEach((r) => remove(r))
|
||||
|
||||
refetchInventory()
|
||||
data.added.forEach((added) => {
|
||||
append({
|
||||
location_id: added,
|
||||
stocked_quantity: locationLevelMap.get(added)?.stocked_quantity ?? 0,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -111,88 +112,95 @@ const VariantStockForm = ({
|
||||
returns are made.
|
||||
</p>
|
||||
</div>
|
||||
<div className="gap-y-2xsmall flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="inter-base-semibold mb-2xsmall">Allow backorders</h3>
|
||||
<Controller
|
||||
control={control}
|
||||
name={path("allow_backorder")}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return <Switch checked={value} onCheckedChange={onChange} />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="inter-base-regular text-grey-50">
|
||||
When checked the product will be available for purchase despite the
|
||||
product being sold out
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col text-base">
|
||||
<h3 className="inter-base-semibold mb-2xsmall">Quantity</h3>
|
||||
{!isLoading && locations && (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="inter-base-regular text-grey-50 flex justify-between py-3">
|
||||
<div className="">Location</div>
|
||||
<div className="">In Stock</div>
|
||||
{manageInventory && (
|
||||
<>
|
||||
<div className="gap-y-2xsmall flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="inter-base-semibold mb-2xsmall">
|
||||
Allow backorders
|
||||
</h3>
|
||||
<Controller
|
||||
control={control}
|
||||
name={path("allow_backorder")}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return <Switch checked={value} onCheckedChange={onChange} />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{locationLevels.map((level, i) => {
|
||||
const locationDetails = locations.find(
|
||||
(l) => l.id === level.location_id
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={level.id} className="flex items-center py-3">
|
||||
<div className="inter-base-regular flex items-center">
|
||||
<IconBadge className="mr-base">
|
||||
<BuildingsIcon />
|
||||
</IconBadge>
|
||||
{locationDetails?.name}
|
||||
</div>
|
||||
<div className="ml-auto flex">
|
||||
<div className="mr-base text-small text-grey-50 flex flex-col">
|
||||
<span className="whitespace-nowrap text-right">
|
||||
{`${level.reserved_quantity} reserved`}
|
||||
</span>
|
||||
<span className="whitespace-nowrap text-right">{`${
|
||||
level.stocked_quantity - level.reserved_quantity
|
||||
} available`}</span>
|
||||
</div>
|
||||
<InputField
|
||||
placeholder={"0"}
|
||||
type="number"
|
||||
{...register(
|
||||
path(`location_levels.${i}.stocked_quantity`),
|
||||
{ valueAsNumber: true }
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<p className="inter-base-regular text-grey-50">
|
||||
When checked the product will be available for purchase despite
|
||||
the product being sold out
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
layeredModalContext.push(
|
||||
// @ts-ignore
|
||||
ManageLocationsScreen(
|
||||
layeredModalContext.pop,
|
||||
locationLevels,
|
||||
locations,
|
||||
handleUpdateLocations
|
||||
)
|
||||
)
|
||||
}}
|
||||
>
|
||||
Manage locations
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full flex-col text-base">
|
||||
<h3 className="inter-base-semibold mb-2xsmall">Quantity</h3>
|
||||
{!isLoading && locations && (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="inter-base-regular text-grey-50 flex justify-between py-3">
|
||||
<div className="">Location</div>
|
||||
<div className="">In Stock</div>
|
||||
</div>
|
||||
{selectedLocations.map((level, i) => {
|
||||
console.log(level)
|
||||
const locationDetails = locations.find(
|
||||
(l: StockLocationDTO) => l.id === level.location_id
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={level.id} className="flex items-center py-3">
|
||||
<div className="inter-base-regular flex items-center">
|
||||
<IconBadge className="mr-base">
|
||||
<BuildingsIcon />
|
||||
</IconBadge>
|
||||
{locationDetails?.name}
|
||||
</div>
|
||||
<div className="ml-auto flex">
|
||||
<div className="mr-base text-small text-grey-50 flex flex-col">
|
||||
<span className="whitespace-nowrap text-right">
|
||||
{`${level.reserved_quantity} reserved`}
|
||||
</span>
|
||||
<span className="whitespace-nowrap text-right">{`${
|
||||
level.stocked_quantity - level.reserved_quantity
|
||||
} available`}</span>
|
||||
</div>
|
||||
<InputField
|
||||
placeholder={"0"}
|
||||
type="number"
|
||||
{...register(
|
||||
path(`location_levels.${i}.stocked_quantity`),
|
||||
{ valueAsNumber: true }
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
layeredModalContext.push(
|
||||
// @ts-ignore
|
||||
ManageLocationsScreen(
|
||||
layeredModalContext.pop,
|
||||
selectedLocations,
|
||||
locations,
|
||||
handleUpdateLocations
|
||||
)
|
||||
)
|
||||
}}
|
||||
>
|
||||
Manage locations
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -200,7 +208,7 @@ const VariantStockForm = ({
|
||||
|
||||
export const ManageLocationsScreen = (
|
||||
pop: () => void,
|
||||
levels: InventoryLevelDTO[],
|
||||
levels: Partial<InventoryLevelDTO>[],
|
||||
locations: StockLocationDTO[],
|
||||
onSubmit: (value: any) => Promise<void>
|
||||
) => {
|
||||
@@ -218,7 +226,7 @@ export const ManageLocationsScreen = (
|
||||
}
|
||||
|
||||
type ManageLocationFormProps = {
|
||||
existingLevels: InventoryLevelDTO[]
|
||||
existingLevels: Partial<InventoryLevelDTO>[]
|
||||
locationOptions: StockLocationDTO[]
|
||||
onSubmit: (value: any) => Promise<void>
|
||||
}
|
||||
@@ -232,7 +240,7 @@ const ManageLocationsForm = ({
|
||||
const { pop } = layeredModalContext
|
||||
|
||||
const existingLocations = useMemo(() => {
|
||||
return existingLevels.map((level) => level.location_id)
|
||||
return existingLevels.map((level) => level.location_id!)
|
||||
}, [existingLevels])
|
||||
|
||||
const [selectedLocations, setSelectedLocations] =
|
||||
@@ -266,7 +274,7 @@ const ManageLocationsForm = ({
|
||||
(locationId: string) => !existingLocations.includes(locationId)
|
||||
)
|
||||
const removedLevels = existingLocations.filter(
|
||||
(locationId) => !selectedLocations.includes(locationId)
|
||||
(locationId) => !!locationId && !selectedLocations.includes(locationId)
|
||||
)
|
||||
|
||||
await onSubmit({
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
useAdminUpdateProduct,
|
||||
useAdminUpdateVariant,
|
||||
} 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 { 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 dialog = useImperativeDialog()
|
||||
@@ -70,8 +71,11 @@ const useEditProductActions = (productId: string) => {
|
||||
successMessage = "Variant was updated successfully"
|
||||
) => {
|
||||
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: () => {
|
||||
notification("Success", successMessage, "success")
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||
import {
|
||||
useAdminUpdateLocationLevel,
|
||||
useAdminVariantsInventory,
|
||||
} from "medusa-react"
|
||||
import React, { useContext } from "react"
|
||||
InventoryLevelDTO,
|
||||
Product,
|
||||
ProductVariant,
|
||||
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 Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../../components/molecules/modal/layered-modal"
|
||||
import EditFlowVariantForm, {
|
||||
EditFlowVariantFormType,
|
||||
} 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 useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
import { removeNullish } from "../../../../../utils/remove-nullish"
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
@@ -24,6 +28,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
||||
const { client } = useMedusa()
|
||||
const layeredModalContext = useContext(LayeredModalContext)
|
||||
const {
|
||||
// @ts-ignore
|
||||
@@ -32,31 +37,94 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
||||
refetch,
|
||||
} = 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 = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const { onUpdateVariant, updatingVariant } = useEditProductActions(product.id)
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const { location_levels } = data.stock
|
||||
|
||||
await Promise.all(
|
||||
location_levels.map(async (level) => {
|
||||
await updateLocationLevel({
|
||||
stockLocationId: level.location_id,
|
||||
stocked_quantity: level.stocked_quantity,
|
||||
})
|
||||
})
|
||||
)
|
||||
// / TODO: Call update location level with new values
|
||||
const onSubmit = async (data: EditFlowVariantFormType) => {
|
||||
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(
|
||||
deleteLocations.map(async (location: InventoryLevelDTO) => {
|
||||
await client.admin.inventoryItems.deleteLocationLevel(
|
||||
inventoryItemId!,
|
||||
location.id
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
onUpdateVariant(variant.id, createUpdatePayload(data), () => {
|
||||
refetch()
|
||||
@@ -71,8 +139,7 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
||||
</Modal.Header>
|
||||
{!isLoadingInventory && (
|
||||
<StockForm
|
||||
variantInventory={variantInventory}
|
||||
refetchInventory={refetch}
|
||||
variantInventory={variantInventory!}
|
||||
onSubmit={onSubmit}
|
||||
isLoadingInventory={isLoadingInventory}
|
||||
handleClose={handleClose}
|
||||
@@ -86,10 +153,15 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
|
||||
const StockForm = ({
|
||||
variantInventory,
|
||||
onSubmit,
|
||||
refetchInventory,
|
||||
isLoadingInventory,
|
||||
handleClose,
|
||||
updatingVariant,
|
||||
}: {
|
||||
variantInventory: VariantInventory
|
||||
onSubmit: (data: EditFlowVariantFormType) => void
|
||||
isLoadingInventory: boolean
|
||||
handleClose: () => void
|
||||
updatingVariant: boolean
|
||||
}) => {
|
||||
const form = useForm<EditFlowVariantFormType>({
|
||||
defaultValues: getEditVariantDefaultValues(variantInventory),
|
||||
@@ -99,32 +171,20 @@ const StockForm = ({
|
||||
formState: { isDirty },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = form
|
||||
|
||||
const locationLevels = watch("stock.location_levels")
|
||||
|
||||
const { location_levels } = variantInventory.inventory[0]
|
||||
|
||||
React.useEffect(() => {
|
||||
form.setValue("stock.location_levels", location_levels)
|
||||
}, [form, location_levels])
|
||||
const locationLevels = variantInventory.inventory[0]?.location_levels || []
|
||||
|
||||
const handleOnSubmit = handleSubmit((data) => {
|
||||
// @ts-ignore
|
||||
onSubmit(data)
|
||||
})
|
||||
|
||||
const itemId = variantInventory.inventory[0].id
|
||||
|
||||
return (
|
||||
<form onSubmit={handleOnSubmit} noValidate>
|
||||
<Modal.Content>
|
||||
<EditFlowVariantForm
|
||||
form={form}
|
||||
refetchInventory={refetchInventory}
|
||||
locationLevels={locationLevels || []}
|
||||
itemId={itemId}
|
||||
locationLevels={locationLevels}
|
||||
isLoading={isLoadingInventory}
|
||||
/>
|
||||
</Modal.Content>
|
||||
|
||||
@@ -106,9 +106,9 @@ const VariantsTable = ({ variants, actions }: Props) => {
|
||||
|
||||
const getTableRowActionables = (variant: ProductVariant) => {
|
||||
const inventoryManagementActions = []
|
||||
if (hasInventoryService && variant.manage_inventory) {
|
||||
if (hasInventoryService) {
|
||||
inventoryManagementActions.push({
|
||||
label: "Manage inventory", // TODO: Only add this item if variant.manageInventory is true
|
||||
label: "Manage inventory",
|
||||
icon: <BuildingsIcon size="20" />,
|
||||
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 {
|
||||
buildQuery,
|
||||
@@ -190,14 +190,34 @@ export default class InventoryLevelService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an inventory level by ID.
|
||||
* @param inventoryLevelId - The ID of the inventory level to delete.
|
||||
*/
|
||||
async delete(inventoryLevelId: string): Promise<void> {
|
||||
* Deletes inventory levels by inventory Item ID.
|
||||
* @param inventoryItemId - The ID or IDs of the inventory item to delete inventory levels for.
|
||||
*/
|
||||
async deleteByInventoryItemId(inventoryItemId: string | string[]): Promise<void> {
|
||||
const ids = Array.isArray(inventoryItemId) ? inventoryItemId : [inventoryItemId]
|
||||
await this.atomicPhase_(async (manager) => {
|
||||
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_
|
||||
.withTransaction(manager)
|
||||
|
||||
@@ -245,6 +245,10 @@ export default class InventoryService
|
||||
* @param inventoryItemId - the id of the inventory item to delete
|
||||
*/
|
||||
async deleteInventoryItem(inventoryItemId: string): Promise<void> {
|
||||
await this.inventoryLevelService_
|
||||
.withTransaction(this.activeManager_)
|
||||
.deleteByInventoryItemId(inventoryItemId)
|
||||
|
||||
return await this.inventoryItemService_
|
||||
.withTransaction(this.activeManager_)
|
||||
.delete(inventoryItemId)
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
|
||||
AdminInventoryItemsLocationLevelsRes,
|
||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||
AdminPostInventoryItemsReq,
|
||||
AdminPostInventoryItemsParams,
|
||||
} from "@medusajs/medusa"
|
||||
import { ResponsePromise } from "../../typings"
|
||||
import BaseResource from "../base"
|
||||
@@ -75,6 +77,28 @@ class AdminInventoryItemsResource extends BaseResource {
|
||||
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
|
||||
* @experimental This feature is under development and may change in the future.
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
AdminPostInventoryItemsInventoryItemReq,
|
||||
AdminPostInventoryItemsItemLocationLevelsLevelReq,
|
||||
AdminPostInventoryItemsItemLocationLevelsReq,
|
||||
AdminPostInventoryItemsReq,
|
||||
AdminPostInventoryItemsParams
|
||||
} from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import {
|
||||
@@ -17,6 +19,29 @@ import { adminInventoryItemsKeys } from "./queries"
|
||||
|
||||
// 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
|
||||
export const useAdminUpdateInventoryItem = (
|
||||
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"
|
||||
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"
|
||||
|
||||
@@ -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 {
|
||||
Brackets,
|
||||
EntityManager,
|
||||
@@ -10,26 +9,6 @@ import {
|
||||
IsNull,
|
||||
SelectQueryBuilder,
|
||||
} 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 {
|
||||
CreateProductVariantInput,
|
||||
FilterableProductVariantProps,
|
||||
@@ -37,8 +16,30 @@ import {
|
||||
ProductVariantPrice,
|
||||
UpdateProductVariantInput,
|
||||
} 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 { CartRepository } from "../repositories/cart"
|
||||
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"
|
||||
|
||||
class ProductVariantService extends TransactionBaseService {
|
||||
|
||||
Reference in New Issue
Block a user