Skip to content

Commit 75cacce

Browse files
committed
MAGETWO-94104: [2.3] Quantity Increments of selected simple within a Configurable Product does not work
1 parent 6d3dab2 commit 75cacce

File tree

6 files changed

+192
-45
lines changed

6 files changed

+192
-45
lines changed

app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
use Magento\CatalogInventory\Model\Stock;
1818
use Magento\Framework\Event\Observer;
1919
use Magento\Framework\Exception\LocalizedException;
20+
use Magento\Quote\Model\Quote\Item;
2021

2122
/**
2223
* @api
2324
* @since 100.0.2
25+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2426
*/
2527
class QuantityValidator
2628
{
@@ -67,7 +69,7 @@ public function __construct(
6769
* Add error information to Quote Item
6870
*
6971
* @param \Magento\Framework\DataObject $result
70-
* @param \Magento\Quote\Model\Quote\Item $quoteItem
72+
* @param Item $quoteItem
7173
* @param bool $removeError
7274
* @return void
7375
*/
@@ -100,7 +102,7 @@ private function addErrorInfoToQuote($result, $quoteItem)
100102
*/
101103
public function validate(Observer $observer)
102104
{
103-
/* @var $quoteItem \Magento\Quote\Model\Quote\Item */
105+
/* @var $quoteItem Item */
104106
$quoteItem = $observer->getEvent()->getItem();
105107
if (!$quoteItem ||
106108
!$quoteItem->getProductId() ||
@@ -175,35 +177,11 @@ public function validate(Observer $observer)
175177
$qty = $product->getTypeInstance()->prepareQuoteItemQty($qty, $product);
176178
$quoteItem->setData('qty', $qty);
177179
if ($stockStatus) {
178-
$result = $this->stockState->checkQtyIncrements(
179-
$product->getId(),
180-
$qty,
181-
$product->getStore()->getWebsiteId()
182-
);
183-
if ($result->getHasError()) {
184-
$quoteItem->addErrorInfo(
185-
'cataloginventory',
186-
Data::ERROR_QTY_INCREMENTS,
187-
$result->getMessage()
188-
);
189-
190-
$quoteItem->getQuote()->addErrorInfo(
191-
$result->getQuoteMessageIndex(),
192-
'cataloginventory',
193-
Data::ERROR_QTY_INCREMENTS,
194-
$result->getQuoteMessage()
195-
);
196-
} else {
197-
// Delete error from item and its quote, if it was set due to qty problems
198-
$this->_removeErrorsFromQuoteAndItem(
199-
$quoteItem,
200-
Data::ERROR_QTY_INCREMENTS
201-
);
202-
}
180+
$this->checkOptionsQtyIncrements($quoteItem, $options);
203181
}
182+
204183
// variable to keep track if we have previously encountered an error in one of the options
205184
$removeError = true;
206-
207185
foreach ($options as $option) {
208186
$result = $option->getStockStateResult();
209187
if ($result->getHasError()) {
@@ -228,10 +206,47 @@ public function validate(Observer $observer)
228206
}
229207
}
230208

209+
/**
210+
* Verifies product options quantity increments.
211+
*
212+
* @param Item $quoteItem
213+
* @param array $options
214+
* @return void
215+
*/
216+
private function checkOptionsQtyIncrements(Item $quoteItem, array $options): void
217+
{
218+
$removeErrors = true;
219+
foreach ($options as $option) {
220+
$result = $this->stockState->checkQtyIncrements(
221+
$option->getProduct()->getId(),
222+
$quoteItem->getData('qty'),
223+
$option->getProduct()->getStore()->getWebsiteId()
224+
);
225+
if ($result->getHasError()) {
226+
$quoteItem->getQuote()->addErrorInfo(
227+
$result->getQuoteMessageIndex(),
228+
'cataloginventory',
229+
Data::ERROR_QTY_INCREMENTS,
230+
$result->getQuoteMessage()
231+
);
232+
233+
$removeErrors = false;
234+
}
235+
}
236+
237+
if ($removeErrors) {
238+
// Delete error from item and its quote, if it was set due to qty problems
239+
$this->_removeErrorsFromQuoteAndItem(
240+
$quoteItem,
241+
Data::ERROR_QTY_INCREMENTS
242+
);
243+
}
244+
}
245+
231246
/**
232247
* Removes error statuses from quote and item, set by this observer
233248
*
234-
* @param \Magento\Quote\Model\Quote\Item $item
249+
* @param Item $item
235250
* @param int $code
236251
* @return void
237252
*/

app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,6 @@ public function getStockItem(
6767
* define that stock item is child for composite product
6868
*/
6969
$stockItem->setIsChildItem(true);
70-
/**
71-
* don't check qty increments value for option product
72-
*/
73-
$stockItem->setSuppressCheckQtyIncrements(true);
7470

7571
return $stockItem;
7672
}

app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ public function testInitializeWhenResultIsDecimalGetBackordersMessageHasOptionQt
151151
$this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock));
152152

153153
$this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true);
154-
$this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true);
155154
$this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true));
156155

157156
$this->stockRegistry
@@ -222,7 +221,6 @@ public function testInitializeWhenResultNotDecimalGetBackordersMessageHasOptionQ
222221
$this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock));
223222

224223
$this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true);
225-
$this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true);
226224
$this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true));
227225

228226
$this->stockRegistry

app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,13 @@ public function testValidateWithOptions()
278278
{
279279
$optionMock = $this->getMockBuilder(OptionItem::class)
280280
->disableOriginalConstructor()
281-
->setMethods(['setHasError', 'getStockStateResult'])
281+
->setMethods(['setHasError', 'getStockStateResult', 'getProduct'])
282282
->getMock();
283283
$optionMock->expects($this->once())
284284
->method('getStockStateResult')
285285
->willReturn($this->resultMock);
286+
$optionMock->method('getProduct')
287+
->willReturn($this->productMock);
286288
$this->stockRegistryMock->expects($this->at(0))
287289
->method('getStockItem')
288290
->willReturn($this->stockItemMock);
@@ -319,7 +321,7 @@ public function testValidateWithOptionsAndError()
319321
{
320322
$optionMock = $this->getMockBuilder(OptionItem::class)
321323
->disableOriginalConstructor()
322-
->setMethods(['setHasError', 'getStockStateResult'])
324+
->setMethods(['setHasError', 'getStockStateResult', 'getProduct'])
323325
->getMock();
324326
$this->stockRegistryMock->expects($this->at(0))
325327
->method('getStockItem')
@@ -330,6 +332,8 @@ public function testValidateWithOptionsAndError()
330332
$optionMock->expects($this->once())
331333
->method('getStockStateResult')
332334
->willReturn($this->resultMock);
335+
$optionMock->method('getProduct')
336+
->willReturn($this->productMock);
333337
$options = [$optionMock];
334338
$this->createInitialStub(1);
335339
$this->setUpStubForQuantity(1, true);
@@ -360,7 +364,7 @@ public function testValidateAndRemoveErrorsFromQuote()
360364
{
361365
$optionMock = $this->getMockBuilder(OptionItem::class)
362366
->disableOriginalConstructor()
363-
->setMethods(['setHasError', 'getStockStateResult'])
367+
->setMethods(['setHasError', 'getStockStateResult', 'getProduct'])
364368
->getMock();
365369
$quoteItem = $this->getMockBuilder(Item::class)
366370
->disableOriginalConstructor()
@@ -369,6 +373,8 @@ public function testValidateAndRemoveErrorsFromQuote()
369373
$optionMock->expects($this->once())
370374
->method('getStockStateResult')
371375
->willReturn($this->resultMock);
376+
$optionMock->method('getProduct')
377+
->willReturn($this->productMock);
372378
$this->stockRegistryMock->expects($this->at(0))
373379
->method('getStockItem')
374380
->willReturn($this->stockItemMock);

dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Quote/Item/QuantityValidatorTest.php

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55
*/
66
namespace Magento\CatalogInventory\Model\Quote\Item;
77

8+
use Magento\Catalog\Model\Product;
9+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
10+
use Magento\CatalogInventory\Api\Data\StockItemInterface;
11+
use Magento\CatalogInventory\Model\Stock\StockItemRepository;
12+
use Magento\CatalogInventory\Model\StockState;
13+
use Magento\CatalogInventory\Observer\QuantityValidatorObserver;
14+
use Magento\Eav\Model\Config;
15+
use Magento\Framework\Exception\CouldNotSaveException;
16+
use Magento\Framework\Exception\LocalizedException;
17+
use Magento\Quote\Api\Data\CartInterface;
18+
use Magento\Quote\Model\Quote;
819
use Magento\TestFramework\Helper\Bootstrap;
920
use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\Initializer\Option;
1021
use Magento\Framework\Event\Observer;
11-
use Magento\CatalogInventory\Model\StockState;
12-
use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator;
13-
use Magento\CatalogInventory\Observer\QuantityValidatorObserver;
1422
use Magento\Framework\Event;
1523
use Magento\Catalog\Api\ProductRepositoryInterface;
1624
use Magento\Framework\DataObject;
@@ -93,7 +101,7 @@ public function testQuoteWithOptions()
93101

94102
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
95103
$productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
96-
/** @var $product \Magento\Catalog\Model\Product */
104+
/** @var $product Product */
97105
$product = $productRepository->get('bundle-product');
98106
$resultMock = $this->createMock(DataObject::class);
99107
$this->stockState->expects($this->any())->method('checkQtyIncrements')->willReturn($resultMock);
@@ -117,7 +125,7 @@ public function testQuoteWithOptionsWithErrors()
117125
$session = $this->objectManager->create(Session::class);
118126
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
119127
$productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
120-
/** @var $product \Magento\Catalog\Model\Product */
128+
/** @var $product Product */
121129
$product = $productRepository->get('bundle-product');
122130
/* @var $quoteItem \Magento\Quote\Model\Quote\Item */
123131
$quoteItem = $this->_getQuoteItemIdByProductId($session->getQuote(), $product->getId());
@@ -132,7 +140,7 @@ public function testQuoteWithOptionsWithErrors()
132140
$resultMock->expects($this->any())->method('getHasError')->willReturn(true);
133141
$this->setMockStockStateResultToQuoteItemOptions($quoteItem, $resultMock);
134142
$this->observer->execute($this->observerMock);
135-
$this->assertCount(2, $quoteItem->getErrorInfos(), 'Expected 2 errors in QuoteItem');
143+
$this->assertCount(1, $quoteItem->getErrorInfos(), 'Expected 1 error in QuoteItem');
136144
}
137145

138146
/**
@@ -155,10 +163,100 @@ private function setMockStockStateResultToQuoteItemOptions($quoteItem, $resultMo
155163
$this->fail('Test failed since Quote Item does not have Qty options.');
156164
}
157165

166+
/**
167+
* Tests quantity verifications for configurable product.
168+
*
169+
* @param int $quantity - quantity of configurable option.
170+
* @param string $errorMessage - expected error message.
171+
* @return void
172+
* @throws CouldNotSaveException
173+
* @throws LocalizedException
174+
* @dataProvider quantityDataProvider
175+
* @magentoDataFixture Magento/CatalogInventory/_files/configurable_options_advanced_inventory.php
176+
* @magentoDbIsolation enabled
177+
* @magentoAppIsolation enabled
178+
*/
179+
public function testConfigurableWithOptions(int $quantity, string $errorMessage): void
180+
{
181+
/** @var ProductRepositoryInterface $productRepository */
182+
$productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
183+
/** @var Product $product */
184+
$product = $productRepository->get('configurable');
185+
$product->setStatus(Status::STATUS_ENABLED)
186+
->setData('is_salable', true);
187+
$productRepository->save($product);
188+
189+
/** @var StockItemRepository $stockItemRepository */
190+
$stockItemRepository = $this->objectManager->create(StockItemRepository::class);
191+
192+
/** @var StockItemInterface $stockItem */
193+
$stockItem = $stockItemRepository->get($product->getExtensionAttributes()
194+
->getStockItem()
195+
->getItemId());
196+
$stockItem->setIsInStock(true)
197+
->setQty(1000);
198+
$stockItemRepository->save($stockItem);
199+
200+
/** @var Config $eavConfig */
201+
$eavConfig = $this->objectManager->get(Config::class);
202+
/** @var $attribute */
203+
$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable');
204+
205+
$request = $this->objectManager->create(DataObject::class);
206+
$request->setData(
207+
[
208+
'product_id' => $product->getId(),
209+
'selected_configurable_option' => 1,
210+
'super_attribute' => [
211+
$attribute->getAttributeId() => $attribute->getOptions()[1]->getValue()
212+
],
213+
'qty' => $quantity
214+
]
215+
);
216+
217+
if (!empty($errorMessage)) {
218+
$this->expectException(LocalizedException::class);
219+
$this->expectExceptionMessage($errorMessage);
220+
}
221+
222+
/** @var Quote $cart */
223+
$cart = $this->objectManager->create(CartInterface::class);
224+
$result = $cart->addProduct($product, $request);
225+
226+
if (empty($errorMessage)) {
227+
self::assertEquals('Configurable Product', $result->getName());
228+
}
229+
}
230+
231+
/**
232+
* Provides request quantity for configurable option
233+
* and corresponding error message.
234+
*
235+
* @return array
236+
*/
237+
public function quantityDataProvider(): array
238+
{
239+
return [
240+
[
241+
'quantity' => 1,
242+
'error' => 'The fewest you may purchase is 500.'
243+
],
244+
[
245+
'quantity' => 501,
246+
'error' => 'You can buy Configurable OptionOption 1 only in quantities of 500 at a time'
247+
],
248+
[
249+
'quantity' => 1000,
250+
'error' => ''
251+
],
252+
253+
];
254+
}
255+
158256
/**
159257
* Gets \Magento\Quote\Model\Quote\Item from \Magento\Quote\Model\Quote by product id
160258
*
161-
* @param \Magento\Quote\Model\Quote $quote
259+
* @param Quote $quote
162260
* @param $productId
163261
* @return \Magento\Quote\Model\Quote\Item
164262
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Catalog\Api\Data\ProductInterface;
9+
use Magento\CatalogInventory\Api\Data\StockItemInterface;
10+
use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
11+
use Magento\TestFramework\Helper\Bootstrap;
12+
13+
require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/product_configurable.php';
14+
15+
$objectManager = Bootstrap::getObjectManager();
16+
17+
/** @var StockItemRepositoryInterface $stockItemRepository */
18+
$stockItemRepository = $objectManager->get(StockItemRepositoryInterface::class);
19+
20+
/** @var ProductInterface $product */
21+
$product = $productRepository->get('simple_10');
22+
23+
/** @var StockItemInterface $stockItem */
24+
$stockItem = $product->getExtensionAttributes()->getStockItem();
25+
$stockItem->setIsInStock(true)
26+
->setQty(10000)
27+
->setUseConfigMinSaleQty(false)
28+
->setMinSaleQty(500)
29+
->setUseConfigEnableQtyInc(false)
30+
->setEnableQtyIncrements(true)
31+
->setUseConfigQtyIncrements(false)
32+
->setQtyIncrements(500);
33+
34+
$stockItemRepository->save($stockItem);

0 commit comments

Comments
 (0)