From 2f58bf625f92c0282671b374666b3c4768b0ca5c Mon Sep 17 00:00:00 2001 From: ALHASAN ALSARRAJ Date: Wed, 22 Oct 2025 13:26:19 +0100 Subject: [PATCH] correct quantity deduction for bundle products with duplicate options --- .../_support/Step/Acceptance/Magento.php | 86 +++++++++++++++++++ dev/tests/acceptance/CheckoutCest.php | 38 ++++++++ .../GetSourceSelectionResultFromOrder.php | 26 +++++- 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/dev/tests/_support/Step/Acceptance/Magento.php b/dev/tests/_support/Step/Acceptance/Magento.php index 474cbf0..e2e5444 100644 --- a/dev/tests/_support/Step/Acceptance/Magento.php +++ b/dev/tests/_support/Step/Acceptance/Magento.php @@ -78,6 +78,92 @@ public function createSimpleProduct($sku, $qty, array $productDefinition = []) return $productId; } + public function createBundleProductWithDuplicateOptions($sku, $childSkus) + { + $I = $this; + + foreach ($childSkus as $child) { + $this->createSimpleProduct($child['sku'], 10); + } + + $options = []; + foreach ($childSkus as $index => $child) { + $options[] = [ + 'option_id' => $child['option_id'], + 'label' => 'Option ' . ($index + 1), + 'position' => $index + 1, + 'required' => true, + 'type' => 'select', + 'product_links' => [[ + 'sku' => $child['sku'], + 'qty' => 1, + 'price' => 0, + 'can_change_quantity' => true + ]] + ]; + } + + $I->amBearerAuthenticated(self::ACCESS_TOKEN); + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->sendPOSTAndVerifyResponseCodeIs200('V1/products', json_encode([ + 'product' => [ + 'sku' => $sku, + 'name' => 'Bundle Product With Duplicates', + 'price' => 0, + 'status' => 1, + 'type_id' => 'bundle', + 'visibility' => 4, + 'extension_attributes' => [ + 'bundle_product_options' => $options, + 'stock_item' => [ + 'qty' => 100, + 'is_in_stock' => true + ] + ], + 'custom_attributes' => [ + [ + 'attribute_code' => 'tax_class_id', + 'value' => 2 + ] + ] + ] + ])); + + return $sku; + } + + public function addBundleProductToQuote($cartId, $bundleSku, $selections) + { + $I = $this; + + $bundleOptions = []; + foreach ($selections as $optionId => $sku) { + $bundleOptions[] = [ + 'option_id' => $optionId, + 'sku' => $sku, + 'qty' => 1 + ]; + } + + $payload = [ + 'cartItem' => [ + 'quote_id' => $cartId, + 'sku' => $bundleSku, + 'qty' => 1, + 'product_type' => 'bundle', + 'product_option' => [ + 'extension_attributes' => [ + 'bundle_options' => $bundleOptions + ] + ] + ] + ]; + + $I->amBearerAuthenticated(self::ACCESS_TOKEN); + $I->haveHttpHeader('Content-Type', 'application/json'); + $I->sendPOSTAndVerifyResponseCodeIs200('V1/guest-carts/' . $cartId . '/items', json_encode($payload)); + } + /** * @return string|string[] */ diff --git a/dev/tests/acceptance/CheckoutCest.php b/dev/tests/acceptance/CheckoutCest.php index 3491f4d..19a5bb5 100644 --- a/dev/tests/acceptance/CheckoutCest.php +++ b/dev/tests/acceptance/CheckoutCest.php @@ -35,6 +35,44 @@ public function noInventoryIsReservedAndStockHasBeenDeducted(Step\Acceptance\Mag $I->assertEquals(9, $newQty, 'The quantity should have been decremented on creation of the order'); } + /** + * Covers issue where bundle child option is selected twice + * and validates stock is deducted for both instances. + * + * @depends noInventoryIsReservedAndStockHasBeenDeducted + * @param Step\Acceptance\Magento $I + */ + public function bundleWithDuplicateOptionsDeductsCorrectly(Step\Acceptance\Magento $I) + { + $productId = $I->createSimpleProduct('amp_bundle_child_duplicate', 10); + + $bundleSku = $I->createBundleProductWithDuplicateOptions( + 'amp_bundle_with_duplicates', + [ + ['sku' => 'amp_bundle_child_duplicate', 'option_id' => 'option_1'], + ['sku' => 'amp_bundle_child_duplicate', 'option_id' => 'option_2'] + ] + ); + + $cartId = $I->getGuestQuote(); + $I->addBundleProductToQuote( + $cartId, + $bundleSku, + [ + 'option_1' => 'amp_bundle_child_duplicate', + 'option_2' => 'amp_bundle_child_duplicate' + ] + ); + $I->completeGuestCheckout($cartId); + + $newQty = $I->grabFromDatabase( + 'cataloginventory_stock_item', + 'qty', + ['product_id' => $productId] + ); + $I->assertEquals(8, $newQty, 'Expected 2 units to be deducted when same bundle option is selected twice'); + } + /** * @depends noInventoryIsReservedAndStockHasBeenDeducted * @param Step\Acceptance\Magento $I diff --git a/src/Model/GetSourceSelectionResultFromOrder.php b/src/Model/GetSourceSelectionResultFromOrder.php index d92de76..41ff505 100644 --- a/src/Model/GetSourceSelectionResultFromOrder.php +++ b/src/Model/GetSourceSelectionResultFromOrder.php @@ -114,7 +114,7 @@ function (OrderItemInterface $orderItem) { ); $itemProductTypes = $this->getProductTypesBySkus->execute($itemsSkus); - $selectionRequestItems = []; + $skuQtyMap = []; /** @var \Magento\Sales\Model\Order\Item $orderItem */ foreach ($orderItems as $orderItem) { $itemSku = $this->getSkuFromOrderItem->execute($orderItem); @@ -124,13 +124,33 @@ function (OrderItemInterface $orderItem) { continue; } - $qty = $this->castQty($orderItem, $orderItem->getQtyOrdered()); + // Skip parent bundle items + if (!$orderItem->getParentItemId() && $itemProductTypes[$itemSku] === 'bundle') { + continue; + } + + $parentQty = 1; + if ($orderItem->getParentItem()) { + $parentQty = $orderItem->getParentItem()->getQtyOrdered(); + } + + $qty = $this->castQty($orderItem, $orderItem->getQtyOrdered()) * $parentQty; + if (!isset($skuQtyMap[$itemSku])) { + $skuQtyMap[$itemSku] = 0; + } + + $skuQtyMap[$itemSku] += $qty; + } + + $selectionRequestItems = []; + foreach ($skuQtyMap as $sku => $qty) { $selectionRequestItems[] = $this->itemRequestFactory->create([ - 'sku' => $itemSku, + 'sku' => $sku, 'qty' => $qty, ]); } + return $selectionRequestItems; }