feat(core-flows,modules-sdk,types,medusa,link-modules): adds variant <> inventory item link endpoints (#7576)
what: - adds variant inventory link management endpoints: ``` Link inventory item to variant POST /products/:id/variants/:vid/inventory-items Update variant's inventory item link POST /products/:id/variants/:vid/inventory-items/:iid Unlink variant's inventory item DELETE /products/:id/variants/:vid/inventory-items/:iid ``` - a batch endpoint that does the above 3 across variants ``` POST /products/:id/variants/inventory-items ```
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
createAdminUser,
|
||||
adminHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
@@ -2870,6 +2870,298 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/products/:id/variants/:variant_id/inventory-items", () => {
|
||||
it("should throw an error when required attributes are not passed", async () => {
|
||||
const { response } = await api
|
||||
.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
expect(response.data).toEqual({
|
||||
type: "invalid_data",
|
||||
message:
|
||||
"Invalid request: Field 'required_quantity' is required; Field 'inventory_item_id' is required",
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully adds inventory item to a variant", async () => {
|
||||
const inventoryItem = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "12345" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
const res = await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
{
|
||||
inventory_item_id: inventoryItem.id,
|
||||
required_quantity: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(res.status).toEqual(200)
|
||||
expect(res.data.variant.inventory_items[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
required_quantity: 5,
|
||||
inventory_item_id: inventoryItem.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
||||
let inventoryItem
|
||||
|
||||
beforeEach(async () => {
|
||||
inventoryItem = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "12345" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||
{
|
||||
inventory_item_id: inventoryItem.id,
|
||||
required_quantity: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when required attributes are not passed", async () => {
|
||||
const { response } = await api
|
||||
.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.status).toEqual(400)
|
||||
expect(response.data).toEqual({
|
||||
type: "invalid_data",
|
||||
message: "Invalid request: Field 'required_quantity' is required",
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully updates an inventory item link to a variant", async () => {
|
||||
const res = await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
{ required_quantity: 10 },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(res.status).toEqual(200)
|
||||
expect(res.data.variant.inventory_items)
|
||||
expect(res.data.variant.inventory_items).toEqual([
|
||||
expect.objectContaining({
|
||||
required_quantity: 10,
|
||||
inventory_item_id: inventoryItem.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("DELETE /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
||||
let inventoryItem
|
||||
|
||||
beforeEach(async () => {
|
||||
inventoryItem = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "12345" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||
{
|
||||
inventory_item_id: inventoryItem.id,
|
||||
required_quantity: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully deletes an inventory item link from a variant", async () => {
|
||||
await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||
{ inventory_item_id: inventoryItem.id, required_quantity: 5 },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const res = await api.delete(
|
||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(res.status).toEqual(200)
|
||||
expect(res.data.parent.inventory_items)
|
||||
expect(res.data.parent.inventory_items).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/batch", () => {
|
||||
let inventoryItemToUpdate
|
||||
let inventoryItemToDelete
|
||||
let inventoryItemToCreate
|
||||
let inventoryProduct
|
||||
let inventoryVariant1
|
||||
let inventoryVariant2
|
||||
let inventoryVariant3
|
||||
|
||||
beforeEach(async () => {
|
||||
inventoryProduct = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
{
|
||||
title: "product 1",
|
||||
variants: [
|
||||
{
|
||||
title: "variant 1",
|
||||
prices: [{ currency_code: "usd", amount: 100 }],
|
||||
},
|
||||
{
|
||||
title: "variant 2",
|
||||
prices: [{ currency_code: "usd", amount: 100 }],
|
||||
},
|
||||
{
|
||||
title: "variant 3",
|
||||
prices: [{ currency_code: "usd", amount: 100 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
inventoryVariant1 = inventoryProduct.variants[0]
|
||||
inventoryVariant2 = inventoryProduct.variants[1]
|
||||
inventoryVariant3 = inventoryProduct.variants[2]
|
||||
|
||||
inventoryItemToCreate = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "to-create" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
inventoryItemToUpdate = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "to-update" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
inventoryItemToDelete = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "to-delete" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}/inventory-items`,
|
||||
{
|
||||
inventory_item_id: inventoryItemToUpdate.id,
|
||||
required_quantity: 5,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}/inventory-items`,
|
||||
{
|
||||
inventory_item_id: inventoryItemToDelete.id,
|
||||
required_quantity: 10,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully creates, updates and deletes an inventory item link from a variant", async () => {
|
||||
const res = await api.post(
|
||||
`/admin/products/${baseProduct.id}/variants/inventory-items/batch`,
|
||||
{
|
||||
create: [
|
||||
{
|
||||
required_quantity: 15,
|
||||
inventory_item_id: inventoryItemToCreate.id,
|
||||
variant_id: inventoryVariant3.id,
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
required_quantity: 25,
|
||||
inventory_item_id: inventoryItemToUpdate.id,
|
||||
variant_id: inventoryVariant1.id,
|
||||
},
|
||||
],
|
||||
delete: [
|
||||
{
|
||||
inventory_item_id: inventoryItemToDelete.id,
|
||||
variant_id: inventoryVariant2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(res.status).toEqual(200)
|
||||
|
||||
const createdLinkVariant = (
|
||||
await api.get(
|
||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant3.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.variant
|
||||
|
||||
expect(createdLinkVariant.inventory_items[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
required_quantity: 15,
|
||||
inventory_item_id: inventoryItemToCreate.id,
|
||||
})
|
||||
)
|
||||
|
||||
const updatedLinkVariant = (
|
||||
await api.get(
|
||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.variant
|
||||
|
||||
expect(updatedLinkVariant.inventory_items[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
required_quantity: 25,
|
||||
inventory_item_id: inventoryItemToUpdate.id,
|
||||
})
|
||||
)
|
||||
|
||||
const deletedLinkVariant = (
|
||||
await api.get(
|
||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.variant
|
||||
|
||||
expect(deletedLinkVariant.inventory_items).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import {
|
||||
createLinksWorkflow,
|
||||
createLinksWorkflowId,
|
||||
dismissLinksWorkflow,
|
||||
dismissLinksWorkflowId,
|
||||
updateLinksWorkflow,
|
||||
updateLinksWorkflowId,
|
||||
} from "@medusajs/core-flows"
|
||||
import { Modules } from "@medusajs/utils"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
env: {},
|
||||
testSuite: ({ getContainer, api, dbConnection }) => {
|
||||
describe("Workflows: Common", () => {
|
||||
let appContainer
|
||||
let product
|
||||
let variant
|
||||
|
||||
beforeAll(async () => {
|
||||
appContainer = getContainer()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||
|
||||
product = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
{
|
||||
title: "product 1",
|
||||
variants: [
|
||||
{
|
||||
title: "variant 1",
|
||||
prices: [{ currency_code: "usd", amount: 100 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
variant = product.variants[0]
|
||||
})
|
||||
|
||||
describe("createLinksWorkflow", () => {
|
||||
describe("compensation", () => {
|
||||
it("should dismiss links when step throws an error", async () => {
|
||||
const workflow = createLinksWorkflow(appContainer)
|
||||
const workflowId = createLinksWorkflowId
|
||||
const inventoryItem = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "12345" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
workflow.appendAction("throw", workflowId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Fail`)
|
||||
},
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: [
|
||||
{
|
||||
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||
[Modules.INVENTORY]: { inventory_item_id: inventoryItem.id },
|
||||
data: { required_quantity: 10 },
|
||||
},
|
||||
],
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
message: `Fail`,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
const updatedVariant = (
|
||||
await api.get(
|
||||
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.variant
|
||||
|
||||
expect(updatedVariant.inventory_items).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateLinksWorkflow", () => {
|
||||
describe("compensation", () => {
|
||||
it("should revert link data when step throws an error", async () => {
|
||||
const workflow = updateLinksWorkflow(appContainer)
|
||||
const workflowId = updateLinksWorkflowId
|
||||
const originalQuantity = 5
|
||||
const newQuantity = 10
|
||||
const inventoryItem = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "12345" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
await api.post(
|
||||
`/admin/products/${product.id}/variants/${variant.id}/inventory-items`,
|
||||
{
|
||||
inventory_item_id: inventoryItem.id,
|
||||
required_quantity: originalQuantity,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
workflow.appendAction("throw", workflowId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Fail`)
|
||||
},
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: [
|
||||
{
|
||||
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||
[Modules.INVENTORY]: { inventory_item_id: inventoryItem.id },
|
||||
data: { required_quantity: newQuantity },
|
||||
},
|
||||
],
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
message: `Fail`,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
const updatedVariant = (
|
||||
await api.get(
|
||||
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.variant
|
||||
|
||||
expect(updatedVariant.inventory_items).toEqual([
|
||||
expect.objectContaining({
|
||||
required_quantity: originalQuantity,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should throw an error when a link is not found", async () => {
|
||||
const workflow = updateLinksWorkflow(appContainer)
|
||||
const workflowId = updateLinksWorkflowId
|
||||
|
||||
workflow.appendAction("throw", workflowId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Fail`)
|
||||
},
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: [
|
||||
{
|
||||
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||
[Modules.INVENTORY]: {
|
||||
inventory_item_id: "does-not-exist-id",
|
||||
},
|
||||
data: { required_quantity: 10 },
|
||||
},
|
||||
],
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "update-remote-links-step",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
message: `Could not find all existing links from data`,
|
||||
}),
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("dismissLinksWorkflow", () => {
|
||||
describe("compensation", () => {
|
||||
it("should recreate dismissed links when step throws an error", async () => {
|
||||
const originalQuantity = 10
|
||||
const workflow = dismissLinksWorkflow(appContainer)
|
||||
const workflowId = dismissLinksWorkflowId
|
||||
const inventoryItem = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{ sku: "12345" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
await api.post(
|
||||
`/admin/products/${product.id}/variants/${variant.id}/inventory-items`,
|
||||
{
|
||||
inventory_item_id: inventoryItem.id,
|
||||
required_quantity: originalQuantity,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
workflow.appendAction("throw", workflowId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Fail`)
|
||||
},
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: [
|
||||
{
|
||||
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||
[Modules.INVENTORY]: { inventory_item_id: inventoryItem.id },
|
||||
},
|
||||
],
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
message: `Fail`,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
const updatedVariant = (
|
||||
await api.get(
|
||||
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.variant
|
||||
|
||||
expect(updatedVariant.inventory_items).toEqual([
|
||||
expect.objectContaining({
|
||||
required_quantity: originalQuantity,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should pass dismiss step if link not found if next step throws error", async () => {
|
||||
const workflow = dismissLinksWorkflow(appContainer)
|
||||
const workflowId = dismissLinksWorkflowId
|
||||
|
||||
workflow.appendAction("throw", workflowId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Fail`)
|
||||
},
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: [
|
||||
{
|
||||
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||
[Modules.INVENTORY]: {
|
||||
inventory_item_id: "does-not-exist-id",
|
||||
},
|
||||
},
|
||||
],
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
message: `Fail`,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
const updatedVariant = (
|
||||
await api.get(
|
||||
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.variant
|
||||
|
||||
expect(updatedVariant.inventory_items).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user