diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml index f228e49355e8a..e12803e5ec8da 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -48,7 +48,7 @@ - + @@ -111,17 +111,17 @@ - + - + - + diff --git a/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php b/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php index 4875afdbeb14a..4ec714a53ac10 100644 --- a/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php +++ b/app/code/Magento/CatalogInventory/Block/Plugin/ProductView.php @@ -5,49 +5,42 @@ */ namespace Magento\CatalogInventory\Block\Plugin; -use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\Catalog\Block\Product\View; +use Magento\CatalogInventory\Model\Product\QuantityValidator; class ProductView { /** - * @var StockRegistryInterface + * @var QuantityValidator */ - private $stockRegistry; + private $productQuantityValidator; /** - * @param StockRegistryInterface $stockRegistry + * @param QuantityValidator $productQuantityValidator */ public function __construct( - StockRegistryInterface $stockRegistry + QuantityValidator $productQuantityValidator ) { - $this->stockRegistry = $stockRegistry; + $this->productQuantityValidator = $productQuantityValidator; } /** * Adds quantities validator. * - * @param \Magento\Catalog\Block\Product\View $block + * @param View $block * @param array $validators * @return array */ public function afterGetQuantityValidators( - \Magento\Catalog\Block\Product\View $block, + View $block, array $validators ) { - $stockItem = $this->stockRegistry->getStockItem( - $block->getProduct()->getId(), - $block->getProduct()->getStore()->getWebsiteId() + return array_merge( + $validators, + $this->productQuantityValidator->getData( + $block->getProduct()->getId(), + $block->getProduct()->getStore()->getWebsiteId() + ) ); - - $params = []; - if ($stockItem->getMaxSaleQty()) { - $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); - } - if ($stockItem->getQtyIncrements() > 0) { - $params['qtyIncrements'] = (float)$stockItem->getQtyIncrements(); - } - $validators['validate-item-quantity'] = $params; - - return $validators; } } diff --git a/app/code/Magento/CatalogInventory/Model/Product/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Product/QuantityValidator.php new file mode 100644 index 0000000000000..6cbef082e00f0 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/Product/QuantityValidator.php @@ -0,0 +1,51 @@ +stockRegistry->getStockItem($productId, $websiteId); + + if (!$stockItem) { + return []; + } + + $params = []; + $validators = []; + $params['minAllowed'] = $stockItem->getMinSaleQty(); + if ($stockItem->getMaxSaleQty()) { + $params['maxAllowed'] = $stockItem->getMaxSaleQty(); + } + if ($stockItem->getQtyIncrements() > 0) { + $params['qtyIncrements'] = (float) $stockItem->getQtyIncrements(); + } + $validators['validate-item-quantity'] = $params; + + return $validators; + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Block/Plugin/ProductViewTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Block/Plugin/ProductViewTest.php deleted file mode 100644 index 0ab50275e01f1..0000000000000 --- a/app/code/Magento/CatalogInventory/Test/Unit/Block/Plugin/ProductViewTest.php +++ /dev/null @@ -1,94 +0,0 @@ -stockItem = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods(['getMinSaleQty', 'getMaxSaleQty', 'getQtyIncrements']) - ->getMock(); - - $this->stockRegistry = $this->getMockBuilder(StockRegistryInterface::class) - ->getMock(); - - $this->block = $objectManager->getObject( - ProductView::class, - [ - 'stockRegistry' => $this->stockRegistry - ] - ); - } - - public function testAfterGetQuantityValidators() - { - $result = [ - 'validate-item-quantity' => [ - 'maxAllowed' => 5.0, - 'qtyIncrements' => 3.0 - ] - ]; - $validators = []; - $productViewBlock = $this->getMockBuilder(View::class) - ->disableOriginalConstructor() - ->getMock(); - $productMock = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->addMethods(['_wakeup']) - ->onlyMethods(['getId', 'getStore']) - ->getMock(); - $storeMock = $this->getMockBuilder(Store::class) - ->disableOriginalConstructor() - ->addMethods(['_wakeup']) - ->onlyMethods(['getWebsiteId']) - ->getMock(); - - $productViewBlock->expects($this->any())->method('getProduct')->willReturn($productMock); - $productMock->expects($this->once())->method('getId')->willReturn('productId'); - $productMock->expects($this->once())->method('getStore')->willReturn($storeMock); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn('websiteId'); - $this->stockRegistry->expects($this->once()) - ->method('getStockItem') - ->with('productId', 'websiteId') - ->willReturn($this->stockItem); - $this->stockItem->expects($this->any())->method('getMaxSaleQty')->willReturn(5); - $this->stockItem->expects($this->any())->method('getQtyIncrements')->willReturn(3); - - $this->assertEquals($result, $this->block->afterGetQuantityValidators($productViewBlock, $validators)); - } -} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Product/QuantityValidatorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Product/QuantityValidatorTest.php new file mode 100644 index 0000000000000..ee9227ae366db --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Product/QuantityValidatorTest.php @@ -0,0 +1,132 @@ +stockRegistry = $this->createMock(StockRegistryInterface::class); + + $this->quantityValidator = new QuantityValidator( + $this->stockRegistry + ); + } + + public function testGetDataWithMinMaxAndIncrements(): void + { + $stockItem = $this->createMock(StockItemInterface::class); + + $stockItem->method('getMinSaleQty') + ->willReturn(2.0); + + $stockItem->method('getMaxSaleQty') + ->willReturn(10.0); + + $stockItem->method('getQtyIncrements') + ->willReturn(2.0); + + $this->stockRegistry->expects($this->once()) + ->method('getStockItem') + ->with(self::PRODUCT_ID, self::WEBSITE_ID) + ->willReturn($stockItem); + + $expected = [ + 'validate-item-quantity' => [ + 'minAllowed' => 2.0, + 'maxAllowed' => 10.0, + 'qtyIncrements' => 2.0 + ] + ]; + + $result = $this->quantityValidator->getData(self::PRODUCT_ID, self::WEBSITE_ID); + $this->assertEquals($expected, $result); + } + + public function testReturnsEmptyArrayForNonExistentProductOrWebsite(): void + { + $this->stockRegistry->expects($this->once()) + ->method('getStockItem') + ->with(self::PRODUCT_ID, self::WEBSITE_ID) + ->willReturn(null); + + $result = $this->quantityValidator->getData(self::PRODUCT_ID, self::WEBSITE_ID); + $this->assertSame([], $result, 'Should return empty array when StockItem is not found'); + } + + public function testHandlesNullValuesFromStockItem(): void + { + $stockItem = $this->createMock(StockItemInterface::class); + $stockItem->method('getMinSaleQty') + ->willReturn(null); + $stockItem->method('getMaxSaleQty') + ->willReturn(null); + $stockItem->method('getQtyIncrements') + ->willReturn(null); + + $this->stockRegistry->expects($this->once()) + ->method('getStockItem') + ->with(self::PRODUCT_ID, self::WEBSITE_ID) + ->willReturn($stockItem); + + $expected = [ + 'validate-item-quantity' => [ + 'minAllowed' => null + ], + ]; + $result = $this->quantityValidator->getData(self::PRODUCT_ID, self::WEBSITE_ID); + $this->assertEquals($expected, $result); + } + + public function testHandlesInvalidValuesFromStockItem(): void + { + $stockItem = $this->createMock(StockItemInterface::class); + $stockItem->method('getMinSaleQty') + ->willReturn('not-a-number'); + $stockItem->method('getMaxSaleQty') + ->willReturn(-5); + $stockItem->method('getQtyIncrements') + ->willReturn(false); + + $this->stockRegistry->expects($this->once()) + ->method('getStockItem') + ->with(self::PRODUCT_ID, self::WEBSITE_ID) + ->willReturn($stockItem); + + $expected = [ + 'validate-item-quantity' => [ + 'minAllowed' => 'not-a-number', + 'maxAllowed' => -5 + ], + ]; + $result = $this->quantityValidator->getData(self::PRODUCT_ID, self::WEBSITE_ID); + $this->assertEquals($expected, $result); + } +} diff --git a/app/code/Magento/GroupedProduct/ViewModel/ValidateQuantity.php b/app/code/Magento/GroupedProduct/ViewModel/ValidateQuantity.php new file mode 100644 index 0000000000000..b8bf9bd6faad8 --- /dev/null +++ b/app/code/Magento/GroupedProduct/ViewModel/ValidateQuantity.php @@ -0,0 +1,46 @@ +serializer->serialize( + array_merge( + ['validate-grouped-qty' => '#super-product-table'], + $this->productQuantityValidator->getData($productId, $websiteId) + ) + ); + } +} diff --git a/app/code/Magento/GroupedProduct/view/frontend/layout/catalog_product_view_type_grouped.xml b/app/code/Magento/GroupedProduct/view/frontend/layout/catalog_product_view_type_grouped.xml index a774f384d947a..4a1f813484b3c 100644 --- a/app/code/Magento/GroupedProduct/view/frontend/layout/catalog_product_view_type_grouped.xml +++ b/app/code/Magento/GroupedProduct/view/frontend/layout/catalog_product_view_type_grouped.xml @@ -1,15 +1,19 @@ - + + + Magento\GroupedProduct\ViewModel\ValidateQuantity + + diff --git a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml index 996c61571563a..3662149d75211 100644 --- a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml +++ b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml @@ -1,31 +1,34 @@ -setPreconfiguredValue(); ?> -getProduct(); ?> -getAssociatedProducts(); ?> - 0; ?> +setPreconfiguredValue(); + $_product = $block->getProduct(); + $_associatedProducts = $block->getAssociatedProducts(); + $_hasAssociatedProducts = count($_associatedProducts) > 0; + $viewModel = $block->getData('validateQuantityViewModel'); +?>
- + - + isSaleable()): ?> - + @@ -34,8 +37,8 @@ - isSaleable()): ?> - @@ -85,7 +92,7 @@ diff --git a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_configure_type_grouped.xml b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_configure_type_grouped.xml index 92bb55ebd0ee1..27d63bad57c08 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_configure_type_grouped.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_configure_type_grouped.xml @@ -1,15 +1,19 @@ - + + + Magento\GroupedProduct\ViewModel\ValidateQuantity + + diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 72baf69740c9b..3f1df78dcfdee 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1643,6 +1643,9 @@ define([ isQtyIncrementsValid = typeof params.qtyIncrements === 'undefined' || resolveModulo(qty, $.mage.parseNumber(params.qtyIncrements)) === 0.0; + if ($(element).data('no-validation-for-zero-qty') === true && qty === 0) { + return true; + } result = qty > 0; if (result === false) {
escapeHtml(__('Grouped product items')) ?>escapeHtml(__('Grouped product items')) ?>
escapeHtml(__('Product Name')) ?>escapeHtml(__('Product Name')) ?> escapeHtml(__('Qty')) ?>escapeHtml(__('Qty')) ?>
- escapeHtml($_item->getName()) ?> + + escapeHtml($_item->getName()) ?> getCanShowProductPrice($_product)): ?> getCanShowProductPrice($_item)): ?> getProductPrice($_item) ?> @@ -43,21 +46,25 @@ + isSaleable()): ?>
-
- escapeHtml(__('Out of stock')) ?> +
+ escapeHtml(__('Out of stock')) ?>
- escapeHtml(__('No options of this product are available.')) ?> + escapeHtml(__('No options of this product are available.')) ?>