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:
5
.changeset/bright-cars-accept.md
Normal file
5
.changeset/bright-cars-accept.md
Normal file
@@ -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 () => {
|
||||
const salesChannel1 = (
|
||||
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", () => {
|
||||
it("should throw an error when required attributes are not passed", async () => {
|
||||
const { response } = await api
|
||||
|
||||
@@ -160,7 +160,9 @@ function prepareCancelOrderFulfillmentData({
|
||||
(i) => i.id === lineItemId
|
||||
) as OrderItemWithVariantDTO
|
||||
// find inventory items
|
||||
const iitems = orderItem!.variant?.inventory_items
|
||||
const iitems = orderItem!.variant?.manage_inventory
|
||||
? orderItem!.variant?.inventory_items
|
||||
: undefined
|
||||
// find fulfillment item
|
||||
const fitem = fulfillment.items.find(
|
||||
(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-to-chunks"
|
||||
export * from "./process-import-chunks"
|
||||
export * from "./dismiss-product-variants-inventory"
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep } from "../../common"
|
||||
import { updatePriceSetsStep } from "../../pricing"
|
||||
import { updateProductVariantsStep } from "../steps"
|
||||
import {
|
||||
dismissProductVariantsInventoryStep,
|
||||
updateProductVariantsStep,
|
||||
} from "../steps"
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* @example
|
||||
@@ -151,6 +154,32 @@ export const updateProductVariantsWorkflow = createWorkflow(
|
||||
|
||||
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
|
||||
const variantIds = transform({ input, updatedVariants }, (data) => {
|
||||
if ("product_variants" in data.input) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
useRemoteQueryStep,
|
||||
} from "../../common"
|
||||
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.
|
||||
@@ -444,6 +445,35 @@ export const updateProductsWorkflow = createWorkflow(
|
||||
const toUpdateInput = transform({ input }, prepareUpdateProductInput)
|
||||
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(
|
||||
{ input, updatedProducts },
|
||||
prepareSalesChannelLinks
|
||||
|
||||
Reference in New Issue
Block a user