From 5228b14ca94d3a73e1af519fc0c1b12fcaefd462 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Mon, 6 May 2024 14:36:55 -0300 Subject: [PATCH] feat(medusa): complete cart create reservation (#7250) --- .changeset/wise-zebras-shave.md | 7 +++ .../cart/store/cart.workflows.spec.ts | 3 +- .../__tests__/cart/store/carts.spec.ts | 50 +++++++++++++++-- .../cart/steps/confirm-inventory.ts | 4 +- .../src/definition/cart/steps/index.ts | 2 +- .../cart/steps/reserve-inventory.ts | 51 ++++++++++++++++++ .../cart/steps/validate-variant-prices.ts | 31 +++++++++++ .../cart/steps/validate-variants-existence.ts | 38 ------------- .../utils/prepare-confirm-inventory-input.ts | 3 ++ .../definition/cart/workflows/add-to-cart.ts | 3 ++ .../cart/workflows/complete-cart.ts | 7 ++- .../workflows/confirm-variant-inventory.ts | 53 +++++++++++-------- .../definition/cart/workflows/create-carts.ts | 3 ++ .../list-shipping-options-for-cart.ts | 2 +- .../workflows/update-line-item-in-cart.ts | 3 ++ .../src/order/workflows/create-orders.ts | 5 +- packages/core/types/src/cart/workflows.ts | 2 - .../core/utils/src/common/deep-flat-map.ts | 5 ++ 18 files changed, 195 insertions(+), 77 deletions(-) create mode 100644 .changeset/wise-zebras-shave.md create mode 100644 packages/core/core-flows/src/definition/cart/steps/reserve-inventory.ts create mode 100644 packages/core/core-flows/src/definition/cart/steps/validate-variant-prices.ts delete mode 100644 packages/core/core-flows/src/definition/cart/steps/validate-variants-existence.ts diff --git a/.changeset/wise-zebras-shave.md b/.changeset/wise-zebras-shave.md new file mode 100644 index 0000000000..20bb955b67 --- /dev/null +++ b/.changeset/wise-zebras-shave.md @@ -0,0 +1,7 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +Create stock reservation on complete cart flow diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index 4f387ff73c..f5f845b98b 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -709,10 +709,9 @@ medusaIntegrationTestRunner({ expect(errors).toEqual([ { - action: "confirm-item-inventory-as-step", + action: "validate-variant-prices", handlerType: "invoke", error: expect.objectContaining({ - // TODO: FIX runAsStep nested errors message: expect.stringContaining( `Variants with IDs ${product.variants[0].id} do not have a price` ), diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index e2f79607a4..df9132414a 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -1480,6 +1480,8 @@ medusaIntegrationTestRunner({ let shippingProfile let fulfillmentSet let shippingOption + let stockLocation + let inventoryItem beforeEach(async () => { await setupTaxStructure(taxModule) @@ -1510,7 +1512,6 @@ medusaIntegrationTestRunner({ variants: [ { title: "Test variant", - inventory_quantity: 10, prices: [ { currency_code: "usd", @@ -1528,7 +1529,7 @@ medusaIntegrationTestRunner({ ) ).data.product - const stockLocation = ( + stockLocation = ( await api.post( `/admin/stock-locations`, { name: "test location" }, @@ -1536,6 +1537,25 @@ medusaIntegrationTestRunner({ ) ).data.stock_location + inventoryItem = ( + await api.post( + `/admin/inventory-items`, + { + sku: "12345", + }, + adminHeaders + ) + ).data.inventory_item + + await api.post( + `/admin/inventory-items/${inventoryItem.id}/location-levels`, + { + location_id: stockLocation.id, + stocked_quantity: 10, + }, + adminHeaders + ) + await api.post( `/admin/stock-locations/${stockLocation.id}/sales-channels`, { add: [salesChannel.id] }, @@ -1563,6 +1583,14 @@ medusaIntegrationTestRunner({ [Modules.STOCK_LOCATION]: { stock_location_id: stockLocation.id }, [Modules.FULFILLMENT]: { fulfillment_set_id: fulfillmentSet.id }, }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, ]) shippingOption = ( @@ -1590,7 +1618,7 @@ medusaIntegrationTestRunner({ ).data.shipping_option }) - it("should create an order", async () => { + it("should create an order and create item reservations", async () => { const cartResponse = await api.post(`/store/carts`, { currency_code: "usd", email: "tony@stark-industries.com", @@ -1602,8 +1630,7 @@ medusaIntegrationTestRunner({ province: "ny", postal_code: "94016", }, - // TODO: inventory isn't being managed on a product level - // sales_channel_id: salesChannel.id, + sales_channel_id: salesChannel.id, items: [{ quantity: 1, variant_id: product.variants[0].id }], }) @@ -1661,6 +1688,19 @@ medusaIntegrationTestRunner({ }), }) ) + + const reservation = await api.get(`/admin/reservations`, adminHeaders) + const reservationItem = reservation.data.reservations[0] + + expect(reservationItem).toEqual( + expect.objectContaining({ + id: expect.stringContaining("resitem_"), + location_id: stockLocation.id, + inventory_item_id: inventoryItem.id, + quantity: cartResponse.data.cart.items[0].quantity, + line_item_id: cartResponse.data.cart.items[0].id, + }) + ) }) }) }) diff --git a/packages/core/core-flows/src/definition/cart/steps/confirm-inventory.ts b/packages/core/core-flows/src/definition/cart/steps/confirm-inventory.ts index a26eaae88c..03d4f02a75 100644 --- a/packages/core/core-flows/src/definition/cart/steps/confirm-inventory.ts +++ b/packages/core/core-flows/src/definition/cart/steps/confirm-inventory.ts @@ -1,5 +1,5 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IInventoryService } from "@medusajs/types" +import { IInventoryServiceNext } from "@medusajs/types" import { promiseAll } from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" import { MedusaError } from "medusa-core-utils" @@ -18,7 +18,7 @@ export const confirmInventoryStepId = "confirm-inventory-step" export const confirmInventoryStep = createStep( confirmInventoryStepId, async (data: StepInput, { container }) => { - const inventoryService = container.resolve( + const inventoryService = container.resolve( ModuleRegistrationName.INVENTORY ) diff --git a/packages/core/core-flows/src/definition/cart/steps/index.ts b/packages/core/core-flows/src/definition/cart/steps/index.ts index 4d19d7592c..624a9e516c 100644 --- a/packages/core/core-flows/src/definition/cart/steps/index.ts +++ b/packages/core/core-flows/src/definition/cart/steps/index.ts @@ -22,4 +22,4 @@ export * from "./set-tax-lines-for-items" export * from "./update-cart-promotions" export * from "./update-carts" export * from "./validate-cart-shipping-options" -export * from "./validate-variants-existence" +export * from "./validate-variant-prices" diff --git a/packages/core/core-flows/src/definition/cart/steps/reserve-inventory.ts b/packages/core/core-flows/src/definition/cart/steps/reserve-inventory.ts new file mode 100644 index 0000000000..32f0216e2e --- /dev/null +++ b/packages/core/core-flows/src/definition/cart/steps/reserve-inventory.ts @@ -0,0 +1,51 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IInventoryServiceNext } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + items: { + id?: string + inventory_item_id: string + required_quantity: number + allow_backorder: boolean + quantity: number + location_ids: string[] + }[] +} + +export const reserveInventoryStepId = "reserve-inventory-step" +export const reserveInventoryStep = createStep( + reserveInventoryStepId, + async (data: StepInput, { container }) => { + const inventoryService = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + const items = data.items.map((item) => ({ + line_item_id: item.id, + inventory_item_id: item.inventory_item_id, + quantity: item.required_quantity * item.quantity, + allow_backorder: item.allow_backorder, + location_id: item.location_ids[0], + })) + + const reservations = await inventoryService.createReservationItems(items) + + return new StepResponse(void 0, { + reservations: reservations.map((r) => r.id), + }) + }, + async (data, { container }) => { + if (!data) { + return + } + + const inventoryService = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + await inventoryService.deleteReservationItems(data.reservations) + + return new StepResponse() + } +) diff --git a/packages/core/core-flows/src/definition/cart/steps/validate-variant-prices.ts b/packages/core/core-flows/src/definition/cart/steps/validate-variant-prices.ts new file mode 100644 index 0000000000..5b63ca69be --- /dev/null +++ b/packages/core/core-flows/src/definition/cart/steps/validate-variant-prices.ts @@ -0,0 +1,31 @@ +import { BigNumberInput } from "@medusajs/types" +import { MedusaError, isDefined } from "@medusajs/utils" +import { createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + variants: { + id: string + calculated_price?: BigNumberInput + }[] +} + +export const validateVariantPricesStepId = "validate-variant-prices" +export const validateVariantPricesStep = createStep( + validateVariantPricesStepId, + async (data: StepInput, { container }) => { + const priceNotFound: string[] = [] + + for (const variant of data.variants) { + if (!isDefined(variant.calculated_price)) { + priceNotFound.push(variant.id) + } + } + + if (priceNotFound.length > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Variants with IDs ${priceNotFound.join(", ")} do not have a price` + ) + } + } +) diff --git a/packages/core/core-flows/src/definition/cart/steps/validate-variants-existence.ts b/packages/core/core-flows/src/definition/cart/steps/validate-variants-existence.ts deleted file mode 100644 index dfcc0da1cf..0000000000 --- a/packages/core/core-flows/src/definition/cart/steps/validate-variants-existence.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IProductModuleService } from "@medusajs/types" -import { MedusaError } from "@medusajs/utils" -import { StepResponse, createStep } from "@medusajs/workflows-sdk" - -interface StepInput { - variantIds: string[] -} - -export const validateVariantsExistStepId = "validate-variants-exist" -export const validateVariantsExistStep = createStep( - validateVariantsExistStepId, - async (data: StepInput, { container }) => { - const productModuleService = container.resolve( - ModuleRegistrationName.PRODUCT - ) - - const variants = await productModuleService.listVariants( - { id: data.variantIds }, - { select: ["id"] } - ) - - const variantIdToData = new Set(variants.map((v) => v.id)) - - const notFoundVariants = new Set( - [...data.variantIds].filter((x) => !variantIdToData.has(x)) - ) - - if (notFoundVariants.size) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Variants with IDs ${[...notFoundVariants].join(", ")} do not exist` - ) - } - - return new StepResponse(Array.from(variants.map((v) => v.id))) - } -) diff --git a/packages/core/core-flows/src/definition/cart/utils/prepare-confirm-inventory-input.ts b/packages/core/core-flows/src/definition/cart/utils/prepare-confirm-inventory-input.ts index 4cbb379620..8a24996dd8 100644 --- a/packages/core/core-flows/src/definition/cart/utils/prepare-confirm-inventory-input.ts +++ b/packages/core/core-flows/src/definition/cart/utils/prepare-confirm-inventory-input.ts @@ -8,6 +8,7 @@ interface ConfirmInventoryPreparationInput { required_quantity: number }[] items: { + id?: string variant_id?: string quantity: BigNumberInput }[] @@ -20,6 +21,7 @@ interface ConfirmInventoryPreparationInput { } interface ConfirmInventoryItem { + id?: string inventory_item_id: string required_quantity: number allow_backorder: boolean @@ -63,6 +65,7 @@ export const prepareConfirmInventoryInput = ({ } itemsToConfirm.push({ + id: item.id, inventory_item_id: variantInventoryItem.inventory_item_id, required_quantity: variantInventoryItem.required_quantity, allow_backorder: !!variant.allow_backorder, diff --git a/packages/core/core-flows/src/definition/cart/workflows/add-to-cart.ts b/packages/core/core-flows/src/definition/cart/workflows/add-to-cart.ts index 34c96cf5c2..b0318a5f1f 100644 --- a/packages/core/core-flows/src/definition/cart/workflows/add-to-cart.ts +++ b/packages/core/core-flows/src/definition/cart/workflows/add-to-cart.ts @@ -11,6 +11,7 @@ import { useRemoteQueryStep } from "../../../common/steps/use-remote-query" import { addToCartStep, refreshCartShippingMethodsStep } from "../steps" import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions" import { updateTaxLinesStep } from "../steps/update-tax-lines" +import { validateVariantPricesStep } from "../steps/validate-variant-prices" import { cartFieldsForRefreshSteps, productVariantsFields, @@ -51,6 +52,8 @@ export const addToCartWorkflow = createWorkflow( throw_if_key_not_found: true, }) + validateVariantPricesStep({ variants }) + confirmVariantInventoryWorkflow.runAsStep({ input: { sales_channel_id: input.cart.sales_channel_id as string, diff --git a/packages/core/core-flows/src/definition/cart/workflows/complete-cart.ts b/packages/core/core-flows/src/definition/cart/workflows/complete-cart.ts index 28451e91fc..99ab1c1e3f 100644 --- a/packages/core/core-flows/src/definition/cart/workflows/complete-cart.ts +++ b/packages/core/core-flows/src/definition/cart/workflows/complete-cart.ts @@ -6,6 +6,7 @@ import { } from "@medusajs/workflows-sdk" import { useRemoteQueryStep } from "../../../common" import { createOrderFromCartStep } from "../steps" +import { reserveInventoryStep } from "../steps/reserve-inventory" import { updateTaxLinesStep } from "../steps/update-tax-lines" import { completeCartFields } from "../utils/fields" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" @@ -40,6 +41,7 @@ export const completeCartWorkflow = createWorkflow( data.cart.items.forEach((item) => { allItems.push({ id: item.id, + variant_id: item.variant_id, quantity: item.quantity, }) @@ -54,9 +56,8 @@ export const completeCartWorkflow = createWorkflow( } ) - confirmVariantInventoryWorkflow.runAsStep({ + const formatedInventoryItems = confirmVariantInventoryWorkflow.runAsStep({ input: { - ignore_price_check: true, sales_channel_id, variants, items, @@ -65,6 +66,8 @@ export const completeCartWorkflow = createWorkflow( updateTaxLinesStep({ cart_or_cart_id: cart, force_tax_calculation: true }) + reserveInventoryStep(formatedInventoryItems) + const finalCart = useRemoteQueryStep({ entry_point: "cart", fields: completeCartFields, diff --git a/packages/core/core-flows/src/definition/cart/workflows/confirm-variant-inventory.ts b/packages/core/core-flows/src/definition/cart/workflows/confirm-variant-inventory.ts index c93bfe7619..c2ad97cafc 100644 --- a/packages/core/core-flows/src/definition/cart/workflows/confirm-variant-inventory.ts +++ b/packages/core/core-flows/src/definition/cart/workflows/confirm-variant-inventory.ts @@ -1,5 +1,5 @@ import { ConfirmVariantInventoryWorkflowInputDTO } from "@medusajs/types" -import { MedusaError, deepFlatMap, isDefined } from "@medusajs/utils" +import { MedusaError, deepFlatMap } from "@medusajs/utils" import { WorkflowData, createWorkflow, @@ -8,13 +8,25 @@ import { import { confirmInventoryStep } from "../steps" import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input" +interface Output { + items: { + id?: string + inventory_item_id: string + required_quantity: number + allow_backorder: boolean + quantity: number + location_ids: string[] + }[] +} + export const confirmVariantInventoryWorkflowId = "confirm-item-inventory" export const confirmVariantInventoryWorkflow = createWorkflow( confirmVariantInventoryWorkflowId, - (input: WorkflowData) => { + ( + input: WorkflowData + ): WorkflowData => { const confirmInventoryInput = transform({ input }, (data) => { const productVariantInventoryItems = new Map() - const priceNotFound: string[] = [] const stockLocationIds = new Set() const allVariants = new Map() let hasSalesChannelStockLocation = false @@ -26,6 +38,10 @@ export const confirmVariantInventoryWorkflow = createWorkflow( data.input, "variants.inventory_items.inventory.location_levels.stock_locations.sales_channels", ({ variants, inventory_items, stock_locations, sales_channels }) => { + if (!variants) { + return + } + if ( !hasSalesChannelStockLocation && sales_channels?.id === salesChannelId @@ -33,19 +49,19 @@ export const confirmVariantInventoryWorkflow = createWorkflow( hasSalesChannelStockLocation = true } - if (!isDefined(variants.calculated_price)) { - priceNotFound.push(variants.id) + if (stock_locations) { + stockLocationIds.add(stock_locations.id) } - stockLocationIds.add(stock_locations.id) - - const inventoryItemId = inventory_items.inventory_item_id - if (!productVariantInventoryItems.has(inventoryItemId)) { - productVariantInventoryItems.set(inventoryItemId, { - variant_id: inventory_items.variant_id, - inventory_item_id: inventoryItemId, - required_quantity: inventory_items.required_quantity, - }) + if (inventory_items) { + const inventoryItemId = inventory_items.inventory_item_id + if (!productVariantInventoryItems.has(inventoryItemId)) { + productVariantInventoryItems.set(inventoryItemId, { + variant_id: inventory_items.variant_id, + inventory_item_id: inventoryItemId, + required_quantity: inventory_items.required_quantity, + }) + } } if (!allVariants.has(variants.id)) { @@ -72,13 +88,6 @@ export const confirmVariantInventoryWorkflow = createWorkflow( ) } - if (priceNotFound.length && !data.input.ignore_price_check) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Variants with IDs ${priceNotFound.join(", ")} do not have a price` - ) - } - const items = prepareConfirmInventoryInput({ product_variant_inventory_items: Array.from( productVariantInventoryItems.values() @@ -92,5 +101,7 @@ export const confirmVariantInventoryWorkflow = createWorkflow( }) confirmInventoryStep(confirmInventoryInput) + + return confirmInventoryInput } ) diff --git a/packages/core/core-flows/src/definition/cart/workflows/create-carts.ts b/packages/core/core-flows/src/definition/cart/workflows/create-carts.ts index e323666e32..5e63eef5c8 100644 --- a/packages/core/core-flows/src/definition/cart/workflows/create-carts.ts +++ b/packages/core/core-flows/src/definition/cart/workflows/create-carts.ts @@ -15,6 +15,7 @@ import { } from "../steps" import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions" import { updateTaxLinesStep } from "../steps/update-tax-lines" +import { validateVariantPricesStep } from "../steps/validate-variant-prices" import { productVariantsFields } from "../utils/fields" import { prepareLineItemData } from "../utils/prepare-line-item-data" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" @@ -68,6 +69,8 @@ export const createCartWorkflow = createWorkflow( throw_if_key_not_found: true, }) + validateVariantPricesStep({ variants }) + confirmVariantInventoryWorkflow.runAsStep({ input: { sales_channel_id: salesChannel.id, diff --git a/packages/core/core-flows/src/definition/cart/workflows/list-shipping-options-for-cart.ts b/packages/core/core-flows/src/definition/cart/workflows/list-shipping-options-for-cart.ts index 8b4d8eb267..58668f1002 100644 --- a/packages/core/core-flows/src/definition/cart/workflows/list-shipping-options-for-cart.ts +++ b/packages/core/core-flows/src/definition/cart/workflows/list-shipping-options-for-cart.ts @@ -78,7 +78,7 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( ({ shipping_options }) => { const { calculated_price, ...options } = shipping_options ?? {} - if (!calculated_price) { + if (options?.id && !calculated_price) { optionsMissingPrices.push(options.id) } diff --git a/packages/core/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts b/packages/core/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts index 1cda2b69be..643cffc6e1 100644 --- a/packages/core/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts +++ b/packages/core/core-flows/src/definition/cart/workflows/update-line-item-in-cart.ts @@ -8,6 +8,7 @@ import { useRemoteQueryStep } from "../../../common/steps/use-remote-query" import { updateLineItemsStep } from "../../line-item/steps" import { refreshCartShippingMethodsStep } from "../steps" import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions" +import { validateVariantPricesStep } from "../steps/validate-variant-prices" import { cartFieldsForRefreshSteps, productVariantsFields, @@ -47,6 +48,8 @@ export const updateLineItemInCartWorkflow = createWorkflow( throw_if_key_not_found: true, }) + validateVariantPricesStep({ variants }) + const items = transform({ input }, (data) => { return [data.input.item] }) diff --git a/packages/core/core-flows/src/order/workflows/create-orders.ts b/packages/core/core-flows/src/order/workflows/create-orders.ts index 75fba6f0e3..4e343e126b 100644 --- a/packages/core/core-flows/src/order/workflows/create-orders.ts +++ b/packages/core/core-flows/src/order/workflows/create-orders.ts @@ -13,7 +13,6 @@ import { findOrCreateCustomerStep } from "../../definition/cart/steps/find-or-cr import { findSalesChannelStep } from "../../definition/cart/steps/find-sales-channel" import { getVariantPriceSetsStep } from "../../definition/cart/steps/get-variant-price-sets" import { getVariantsStep } from "../../definition/cart/steps/get-variants" -import { validateVariantsExistStep } from "../../definition/cart/steps/validate-variants-existence" import { prepareConfirmInventoryInput } from "../../definition/cart/utils/prepare-confirm-inventory-input" import { prepareLineItemData } from "../../definition/cart/utils/prepare-line-item-data" import { createOrdersStep, updateOrderTaxLinesStep } from "../steps" @@ -39,8 +38,8 @@ export const createOrdersWorkflow = createWorkflow( findOrCreateCustomerStep({ customerId: input.customer_id, email: input.email, - }), - validateVariantsExistStep({ variantIds }) + }) + // validateVariantsExistStep({ variantIds }) ) const variants = getVariantsStep({ diff --git a/packages/core/types/src/cart/workflows.ts b/packages/core/types/src/cart/workflows.ts index ffd4559eda..027a057724 100644 --- a/packages/core/types/src/cart/workflows.ts +++ b/packages/core/types/src/cart/workflows.ts @@ -124,8 +124,6 @@ export interface CompleteCartWorkflowInputDTO { } export interface ConfirmVariantInventoryWorkflowInputDTO { - ignore_price_check?: boolean - sales_channel_id: string variants: { id: string diff --git a/packages/core/utils/src/common/deep-flat-map.ts b/packages/core/utils/src/common/deep-flat-map.ts index 988b7cf779..3be83e3a17 100644 --- a/packages/core/utils/src/common/deep-flat-map.ts +++ b/packages/core/utils/src/common/deep-flat-map.ts @@ -83,6 +83,11 @@ export function deepFlatMap( } } else { if (Array.isArray(element[currentKey])) { + if (element[currentKey].length === 0) { + callback({ ...context }) + continue + } + element[currentKey].forEach((item) => { stack.push({ element: item,