From a3df2ea58ee44004970f64da0470e8e55d21faf0 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Sun, 16 Nov 2025 21:02:32 +0100 Subject: [PATCH 1/2] Fix: Preserve original grouped product qty to avoid overwritten values in GraphQL --- .../Test/Unit/Helper/ProductTestHelper.php | 32 ++++++-- .../Product/Link/ProductEntity/Converter.php | 2 +- .../Model/Product/Type/Grouped.php | 3 +- .../Unit/Model/Product/Link/ConverterTest.php | 82 +++++++++++++++++++ .../Unit/Model/Product/Type/GroupedTest.php | 54 ++++++++++++ 5 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Link/ConverterTest.php diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ProductTestHelper.php b/app/code/Magento/Catalog/Test/Unit/Helper/ProductTestHelper.php index 1c5dd96653f18..4df505d6f6540 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ProductTestHelper.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ProductTestHelper.php @@ -583,7 +583,7 @@ public function getData($key = '', $index = null) if (!is_array($this->data)) { $this->data = []; } - + // Check if there's a callback set for getData if (isset($this->data['get_data_callback'])) { return call_user_func($this->data['get_data_callback'], $key); @@ -594,17 +594,17 @@ public function getData($key = '', $index = null) return $this->data['product_data'] ?? []; } $productData = $this->data['product_data'] ?? []; - + // If key doesn't exist, return null if (!isset($productData[$key])) { return null; } - + // If index is provided and value is an array, return indexed value if ($index !== null && is_array($productData[$key])) { return $productData[$key][$index] ?? null; } - + return $productData[$key]; } @@ -621,7 +621,7 @@ public function setData($key, $value = null): self if (!is_array($this->data)) { $this->data = []; } - + // Use separate productData array for getData/setData to avoid conflicts if (!isset($this->data['product_data'])) { $this->data['product_data'] = []; @@ -1624,4 +1624,26 @@ public function getCost() { return $this->getData('cost') ?? $this->cost; } + + /** + * Get initial qty + * + * @return int|null + */ + public function getInitialQty(): ?int + { + return $this->getData('initial_qty'); + } + + /** + * Set parent product id for tests. + * + * @param int|null $initialQty + * @return $this + */ + public function setInitialQty(?int $initialQty) + { + $this->setData('initial_qty', $initialQty); + return $this; + } } diff --git a/app/code/Magento/GroupedProduct/Model/Product/Link/ProductEntity/Converter.php b/app/code/Magento/GroupedProduct/Model/Product/Link/ProductEntity/Converter.php index a0b25d8eb72dc..54f7bb96fe6e7 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Link/ProductEntity/Converter.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Link/ProductEntity/Converter.php @@ -20,7 +20,7 @@ public function convert(\Magento\Catalog\Model\Product $product) 'sku' => $product->getSku(), 'position' => $product->getPosition(), 'custom_attributes' => [ - ['attribute_code' => 'qty', 'value' => $product->getQty()], + ['attribute_code' => 'qty', 'value' => $product->getInitialQty() ?? $product->getQty()], ] ]; } diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index 493e15b4d9785..3d9368bfa0b36 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -228,6 +228,7 @@ public function getAssociatedProducts($product) ); foreach ($collection as $item) { + $item->setInitialQty($item->getQty()); $associatedProducts[] = $item; } @@ -280,7 +281,7 @@ public function setSaleableStatus($product) return $this; } - /** + /**testGetAssociatedProductsSetsInitialQty * Return all assigned status filters * * @param \Magento\Catalog\Model\Product $product diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Link/ConverterTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Link/ConverterTest.php new file mode 100644 index 0000000000000..2d4f5b4d1d349 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Link/ConverterTest.php @@ -0,0 +1,82 @@ +converter = new Converter(); + } + + public function testConvertUsesInitialQtyWhenAvailable(): void + { + $product = $this->createPartialMock( + ProductTestHelper::class, + ['getTypeId', 'getSku', 'getQty', 'getPosition', 'getInitialQty'] + ); + + $product->method('getTypeId')->willReturn('simple'); + $product->method('getSku')->willReturn('sku-123'); + $product->method('getPosition')->willReturn(5); + + // Qty override + $product->method('getInitialQty')->willReturn(10); + $product->method('getQty')->willReturn(2); // fallback should NOT be used + + $result = $this->converter->convert($product); + + $this->assertSame('simple', $result['type']); + $this->assertSame('sku-123', $result['sku']); + $this->assertSame(5, $result['position']); + + $this->assertSame( + [ + ['attribute_code' => 'qty', 'value' => 10] + ], + $result['custom_attributes'] + ); + } + + public function testConvertFallsBackToQtyIfInitialQtyIsNull(): void + { + $product = $this->createPartialMock( + ProductTestHelper::class, + ['getTypeId', 'getSku', 'getQty', 'getPosition', 'getInitialQty'] + ); + + $product->method('getTypeId')->willReturn('simple'); + $product->method('getSku')->willReturn('sku-456'); + $product->method('getPosition')->willReturn(7); + $product->method('getInitialQty')->willReturn(null); + $product->method('getQty')->willReturn(3); + + $result = $this->converter->convert($product); + + $this->assertSame( + [ + ['attribute_code' => 'qty', 'value' => 3] + ], + $result['custom_attributes'] + ); + } +} diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php index ea8a7404777b0..2555576399695 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php @@ -7,6 +7,7 @@ namespace Magento\GroupedProduct\Test\Unit\Model\Product\Type; +use Magento\Catalog\Test\Unit\Helper\ProductTestHelper; use PHPUnit\Framework\Attributes\DataProvider; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; @@ -22,6 +23,7 @@ use Magento\GroupedProduct\Model\Product\Type\Grouped; use Magento\GroupedProduct\Model\ResourceModel\Product\Link; use Magento\MediaStorage\Helper\File\Storage\Database; +use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -164,6 +166,58 @@ public function testGetAssociatedProducts(): void $this->assertEquals($associatedProducts, $this->_model->getAssociatedProducts($this->product)); } + /** + * Verify get associated products with setting initial qty + * + * @return void + */ + public function testGetAssociatedProductsSetsInitialQty(): void + { + $this->product->expects($this->atLeastOnce()) + ->method('hasData') + ->willReturn(false); + $savedValue = null; + $this->product->expects($this->atLeastOnce()) + ->method('setData') + ->willReturnCallback(function ($key, $value) use (&$savedValue) { + $savedValue = $value; + return null; + }); + $this->product->expects($this->atLeastOnce()) + ->method('getData') + ->willReturnCallback(function () use (&$savedValue) { + return $savedValue; + }); + $itemMock = $this->createMock(ProductTestHelper::class); + $itemMock->expects($this->once()) + ->method('getQty') + ->willReturn(10); + $itemMock->expects($this->once()) + ->method('setInitialQty') + ->with(10); + $collectionMock = $this->createMock(Collection::class); + $collectionMock->method('addAttributeToSelect')->willReturnSelf(); + $collectionMock->method('addFilterByRequiredOptions')->willReturnSelf(); + $collectionMock->method('setPositionOrder')->willReturnSelf(); + $collectionMock->method('addStoreFilter')->willReturnSelf(); + $collectionMock->method('addAttributeToFilter')->willReturnSelf(); + $collectionMock->method('setFlag')->willReturnSelf(); + $collectionMock->method('setIsStrongMode')->willReturnSelf(); + $collectionMock->expects($this->once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$itemMock])); + $linkMock = $this->createMock(Product\Link::class); + $linkMock->expects($this->once()) + ->method('getProductCollection') + ->willReturn($collectionMock); + $this->product->expects($this->once()) + ->method('getLinkInstance') + ->willReturn($linkMock); + $result = $this->_model->getAssociatedProducts($this->product); + $this->assertCount(1, $result); + $this->assertSame($itemMock, $result[0]); + } + /** * Verify able to set status filter * From f09038d7ac649705948ca772615719b2f380fa8c Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Sun, 16 Nov 2025 21:10:35 +0100 Subject: [PATCH 2/2] Fix: Preserve original grouped product qty to avoid overwritten values in GraphQL --- app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index 3d9368bfa0b36..a87a61b05b52f 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -281,7 +281,7 @@ public function setSaleableStatus($product) return $this; } - /**testGetAssociatedProductsSetsInitialQty + /** * Return all assigned status filters * * @param \Magento\Catalog\Model\Product $product