fix(core-flows): Avoid checking inventory items on fulfillment cancel for unmanaged inventory variants (#14177)
* Avoid checking existent inventory item on fulfillment cancellation for variants without managed inventory * Add changeset * Dismiss existent variant inventory links when updating to unmanaged inventory * Update input type and step name * Dismiss inventory when variant is updated to unmanaged inventory * Review changes * Fix * Fix * Comments * Include Map to avoid iterating unnecessarily
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(core-flows): Avoid checking inventory items on fulfillment cancel for unmanaged inventory variants
|
||||||
@@ -2356,6 +2356,125 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should update variant to manage_inventory false and unlink inventory items", async () => {
|
||||||
|
const inventoryItem1 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "inventory-item-1" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
const inventoryItem2 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "inventory-item-2" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
const createPayload = {
|
||||||
|
title: "Test product with inventory",
|
||||||
|
handle: "test-product-inventory",
|
||||||
|
options: [{ title: "size", values: ["large"] }],
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Variant with inventory",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
manage_inventory: true,
|
||||||
|
options: { size: "large" },
|
||||||
|
inventory_items: [
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem1.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem2.id,
|
||||||
|
required_quantity: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdProduct = (
|
||||||
|
await api.post("/admin/products", createPayload, {
|
||||||
|
...adminHeaders,
|
||||||
|
params: {
|
||||||
|
fields:
|
||||||
|
"variants.inventory_items.*,variants.inventory_items.inventory.*",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const variantWithInventory = createdProduct.variants[0]
|
||||||
|
|
||||||
|
expect(variantWithInventory.manage_inventory).toBe(true)
|
||||||
|
expect(variantWithInventory.inventory_items).toHaveLength(2)
|
||||||
|
expect(variantWithInventory.inventory_items).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: inventoryItem1.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: inventoryItem2.id,
|
||||||
|
required_quantity: 10,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: variantWithInventory.id,
|
||||||
|
manage_inventory: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProduct = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${createdProduct.id}`,
|
||||||
|
updatePayload,
|
||||||
|
{
|
||||||
|
...adminHeaders,
|
||||||
|
params: {
|
||||||
|
fields:
|
||||||
|
"variants.inventory_items.*,variants.inventory_items.inventory.*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const updatedVariant = updatedProduct.variants.find(
|
||||||
|
(v) => v.id === variantWithInventory.id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updatedVariant.manage_inventory).toBe(false)
|
||||||
|
expect(updatedVariant.inventory_items).toHaveLength(0)
|
||||||
|
expect(updatedVariant.inventory_items).toEqual([])
|
||||||
|
|
||||||
|
const inventoryItem1Response = await api.get(
|
||||||
|
`/admin/inventory-items/${inventoryItem1.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
const inventoryItem2Response = await api.get(
|
||||||
|
`/admin/inventory-items/${inventoryItem2.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(inventoryItem1Response.status).toEqual(200)
|
||||||
|
expect(inventoryItem2Response.status).toEqual(200)
|
||||||
|
expect(inventoryItem1Response.data.inventory_item.id).toBe(
|
||||||
|
inventoryItem1.id
|
||||||
|
)
|
||||||
|
expect(inventoryItem2Response.data.inventory_item.id).toBe(
|
||||||
|
inventoryItem2.id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("updates products sales channels", async () => {
|
it("updates products sales channels", async () => {
|
||||||
const salesChannel1 = (
|
const salesChannel1 = (
|
||||||
await api.post(
|
await api.post(
|
||||||
|
|||||||
@@ -649,6 +649,123 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id/variants/:variant_id", () => {
|
||||||
|
it("should update variant to manage_inventory false and unlink inventory items", async () => {
|
||||||
|
const inventoryItem1 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "inventory-item-variant-1" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
const inventoryItem2 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "inventory-item-variant-2" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
const createPayload = {
|
||||||
|
title: "Test product for variant update",
|
||||||
|
handle: "test-product-variant-update",
|
||||||
|
options: [{ title: "size", values: ["large"] }],
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Variant with inventory",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
manage_inventory: true,
|
||||||
|
options: { size: "large" },
|
||||||
|
inventory_items: [
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem1.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem2.id,
|
||||||
|
required_quantity: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdProduct = (
|
||||||
|
await api.post("/admin/products", createPayload, {
|
||||||
|
...adminHeaders,
|
||||||
|
params: {
|
||||||
|
fields:
|
||||||
|
"+variants.inventory_items.*,+variants.inventory_items.inventory.*",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const variantWithInventory = createdProduct.variants[0]
|
||||||
|
|
||||||
|
expect(variantWithInventory.manage_inventory).toBe(true)
|
||||||
|
expect(variantWithInventory.inventory_items).toHaveLength(2)
|
||||||
|
expect(variantWithInventory.inventory_items).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: inventoryItem1.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
inventory_item_id: inventoryItem2.id,
|
||||||
|
required_quantity: 10,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
manage_inventory: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProduct = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${createdProduct.id}/variants/${variantWithInventory.id}`,
|
||||||
|
updatePayload,
|
||||||
|
{
|
||||||
|
...adminHeaders,
|
||||||
|
params: {
|
||||||
|
fields:
|
||||||
|
"+variants.inventory_items.*,+variants.inventory_items.inventory.*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const updatedVariant = updatedProduct.variants.find(
|
||||||
|
(v) => v.id === variantWithInventory.id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updatedVariant.manage_inventory).toBe(false)
|
||||||
|
|
||||||
|
expect(updatedVariant.inventory_items).toHaveLength(0)
|
||||||
|
expect(updatedVariant.inventory_items).toEqual([])
|
||||||
|
|
||||||
|
const inventoryItem1Response = await api.get(
|
||||||
|
`/admin/inventory-items/${inventoryItem1.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
const inventoryItem2Response = await api.get(
|
||||||
|
`/admin/inventory-items/${inventoryItem2.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(inventoryItem1Response.status).toEqual(200)
|
||||||
|
expect(inventoryItem2Response.status).toEqual(200)
|
||||||
|
expect(inventoryItem1Response.data.inventory_item.id).toBe(
|
||||||
|
inventoryItem1.id
|
||||||
|
)
|
||||||
|
expect(inventoryItem2Response.data.inventory_item.id).toBe(
|
||||||
|
inventoryItem2.id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("POST /admin/products/:id/variants/:variant_id/inventory-items", () => {
|
describe("POST /admin/products/:id/variants/:variant_id/inventory-items", () => {
|
||||||
it("should throw an error when required attributes are not passed", async () => {
|
it("should throw an error when required attributes are not passed", async () => {
|
||||||
const { response } = await api
|
const { response } = await api
|
||||||
|
|||||||
@@ -160,7 +160,9 @@ function prepareCancelOrderFulfillmentData({
|
|||||||
(i) => i.id === lineItemId
|
(i) => i.id === lineItemId
|
||||||
) as OrderItemWithVariantDTO
|
) as OrderItemWithVariantDTO
|
||||||
// find inventory items
|
// find inventory items
|
||||||
const iitems = orderItem!.variant?.inventory_items
|
const iitems = orderItem!.variant?.manage_inventory
|
||||||
|
? orderItem!.variant?.inventory_items
|
||||||
|
: undefined
|
||||||
// find fulfillment item
|
// find fulfillment item
|
||||||
const fitem = fulfillment.items.find(
|
const fitem = fulfillment.items.find(
|
||||||
(i) => i.line_item_id === lineItemId
|
(i) => i.line_item_id === lineItemId
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
||||||
|
import { Link, Query } from "@medusajs/framework/modules-sdk"
|
||||||
|
import { LinkDefinition } from "@medusajs/types"
|
||||||
|
|
||||||
|
export const dismissProductVariantsInventoryStepId =
|
||||||
|
"dismiss-product-variants-inventory"
|
||||||
|
|
||||||
|
export type DismissProductVariantsInventoryStepInput = {
|
||||||
|
variantIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dismissVariantsInventory(
|
||||||
|
variantIds: string[],
|
||||||
|
query: Query,
|
||||||
|
link: Link
|
||||||
|
): Promise<Record<string, string[]>> {
|
||||||
|
const dismissedVariantInventoryItems: Record<string, string[]> = {}
|
||||||
|
if (!variantIds.length) {
|
||||||
|
return dismissedVariantInventoryItems
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: variantInventoryItems } = await query.graph({
|
||||||
|
entity: "product_variant_inventory_item",
|
||||||
|
fields: ["inventory_item_id", "variant_id"],
|
||||||
|
filters: {
|
||||||
|
variant_id: variantIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const variantInventoryItemsMap = new Map<string, string[]>()
|
||||||
|
for (const item of variantInventoryItems) {
|
||||||
|
variantInventoryItemsMap.set(item.variant_id, [
|
||||||
|
...(variantInventoryItemsMap.get(item.variant_id) ?? []),
|
||||||
|
item.inventory_item_id,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissLinks: LinkDefinition[] = []
|
||||||
|
for (const variantId of variantIds) {
|
||||||
|
if (!variantId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissedVariantInventoryItems[variantId] =
|
||||||
|
variantInventoryItemsMap.get(variantId) ?? []
|
||||||
|
|
||||||
|
for (const inventoryItemId of variantInventoryItemsMap.get(variantId) ??
|
||||||
|
[]) {
|
||||||
|
dismissLinks.push({
|
||||||
|
[Modules.PRODUCT]: { variant_id: variantId },
|
||||||
|
[Modules.INVENTORY]: { inventory_item_id: inventoryItemId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await link.dismiss(dismissLinks)
|
||||||
|
|
||||||
|
return dismissedVariantInventoryItems
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dismissProductVariantsInventoryStep = createStep(
|
||||||
|
dismissProductVariantsInventoryStepId,
|
||||||
|
async (data: DismissProductVariantsInventoryStepInput, { container }) => {
|
||||||
|
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
||||||
|
const link = container.resolve(ContainerRegistrationKeys.LINK)
|
||||||
|
const variantIds = data.variantIds || []
|
||||||
|
|
||||||
|
if (!variantIds.length) {
|
||||||
|
return new StepResponse(void 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissedVariantInventoryItems = await dismissVariantsInventory(
|
||||||
|
variantIds,
|
||||||
|
query as Query,
|
||||||
|
link
|
||||||
|
)
|
||||||
|
return new StepResponse(void 0, dismissedVariantInventoryItems)
|
||||||
|
},
|
||||||
|
async (dismissedVariantInventoryItems, { container }) => {
|
||||||
|
if (!dismissedVariantInventoryItems) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const linksToCreate: LinkDefinition[] = []
|
||||||
|
for (const [variantId, inventoryItemIds] of Object.entries(
|
||||||
|
dismissedVariantInventoryItems
|
||||||
|
)) {
|
||||||
|
for (const inventoryItemId of inventoryItemIds) {
|
||||||
|
linksToCreate.push({
|
||||||
|
[Modules.PRODUCT]: { variant_id: variantId },
|
||||||
|
[Modules.INVENTORY]: { inventory_item_id: inventoryItemId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = container.resolve(ContainerRegistrationKeys.LINK)
|
||||||
|
await link.create(linksToCreate)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -32,3 +32,4 @@ export * from "./get-variant-availability"
|
|||||||
export * from "./normalize-products"
|
export * from "./normalize-products"
|
||||||
export * from "./normalize-products-to-chunks"
|
export * from "./normalize-products-to-chunks"
|
||||||
export * from "./process-import-chunks"
|
export * from "./process-import-chunks"
|
||||||
|
export * from "./dismiss-product-variants-inventory"
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import {
|
|||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { emitEventStep } from "../../common"
|
import { emitEventStep } from "../../common"
|
||||||
import { updatePriceSetsStep } from "../../pricing"
|
import { updatePriceSetsStep } from "../../pricing"
|
||||||
import { updateProductVariantsStep } from "../steps"
|
import {
|
||||||
|
dismissProductVariantsInventoryStep,
|
||||||
|
updateProductVariantsStep,
|
||||||
|
} from "../steps"
|
||||||
import { getVariantPricingLinkStep } from "../steps/get-variant-pricing-link"
|
import { getVariantPricingLinkStep } from "../steps/get-variant-pricing-link"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,12 +60,12 @@ export const updateProductVariantsWorkflowId = "update-product-variants"
|
|||||||
* allows you to update custom data models linked to the product variants.
|
* allows you to update custom data models linked to the product variants.
|
||||||
*
|
*
|
||||||
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product-variant update.
|
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product-variant update.
|
||||||
*
|
*
|
||||||
* :::note
|
* :::note
|
||||||
*
|
*
|
||||||
* Learn more about adding rules to the product variant's prices in the Pricing Module's
|
* Learn more about adding rules to the product variant's prices in the Pricing Module's
|
||||||
* [Price Rules](https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules) documentation.
|
* [Price Rules](https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules) documentation.
|
||||||
*
|
*
|
||||||
* :::
|
* :::
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -151,6 +154,32 @@ export const updateProductVariantsWorkflow = createWorkflow(
|
|||||||
|
|
||||||
const updatedVariants = updateProductVariantsStep(updateWithoutPrices)
|
const updatedVariants = updateProductVariantsStep(updateWithoutPrices)
|
||||||
|
|
||||||
|
const variantsToDismissInventory = transform(
|
||||||
|
{ input, updatedVariants },
|
||||||
|
(data) => {
|
||||||
|
const variantIds: string[] = []
|
||||||
|
|
||||||
|
if ("product_variants" in data.input) {
|
||||||
|
for (const variant of data.input.product_variants) {
|
||||||
|
if (variant.id && variant.manage_inventory === false) {
|
||||||
|
variantIds.push(variant.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
data.input.update &&
|
||||||
|
data.input.update?.manage_inventory === false
|
||||||
|
) {
|
||||||
|
variantIds.push(...data.updatedVariants.map((v) => v.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return variantIds
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dismissProductVariantsInventoryStep({
|
||||||
|
variantIds: variantsToDismissInventory,
|
||||||
|
})
|
||||||
|
|
||||||
// We don't want to do any pricing updates if the prices didn't change
|
// We don't want to do any pricing updates if the prices didn't change
|
||||||
const variantIds = transform({ input, updatedVariants }, (data) => {
|
const variantIds = transform({ input, updatedVariants }, (data) => {
|
||||||
if ("product_variants" in data.input) {
|
if ("product_variants" in data.input) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
useRemoteQueryStep,
|
useRemoteQueryStep,
|
||||||
} from "../../common"
|
} from "../../common"
|
||||||
import { upsertVariantPricesWorkflow } from "./upsert-variant-prices"
|
import { upsertVariantPricesWorkflow } from "./upsert-variant-prices"
|
||||||
|
import { dismissProductVariantsInventoryStep } from "../steps/dismiss-product-variants-inventory"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update products that match a specified selector, along with custom data that's passed to the workflow's hooks.
|
* Update products that match a specified selector, along with custom data that's passed to the workflow's hooks.
|
||||||
@@ -444,6 +445,35 @@ export const updateProductsWorkflow = createWorkflow(
|
|||||||
const toUpdateInput = transform({ input }, prepareUpdateProductInput)
|
const toUpdateInput = transform({ input }, prepareUpdateProductInput)
|
||||||
const updatedProducts = updateProductsStep(toUpdateInput)
|
const updatedProducts = updateProductsStep(toUpdateInput)
|
||||||
|
|
||||||
|
const variantsToDismissInventory = transform(
|
||||||
|
{ input, updatedProducts },
|
||||||
|
(data) => {
|
||||||
|
const variantIds: string[] = []
|
||||||
|
|
||||||
|
if ("products" in data.input) {
|
||||||
|
for (const product of data.input.products) {
|
||||||
|
for (const variant of product.variants ?? []) {
|
||||||
|
if (variant.id && variant.manage_inventory === false) {
|
||||||
|
variantIds.push(variant.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.input.update?.variants?.length) {
|
||||||
|
for (const variant of data.input.update.variants) {
|
||||||
|
if (variant.id && variant.manage_inventory === false) {
|
||||||
|
variantIds.push(variant.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variantIds
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dismissProductVariantsInventoryStep({
|
||||||
|
variantIds: variantsToDismissInventory,
|
||||||
|
})
|
||||||
|
|
||||||
const salesChannelLinks = transform(
|
const salesChannelLinks = transform(
|
||||||
{ input, updatedProducts },
|
{ input, updatedProducts },
|
||||||
prepareSalesChannelLinks
|
prepareSalesChannelLinks
|
||||||
|
|||||||
Reference in New Issue
Block a user