Skip to content

Commit 55e8d99

Browse files
author
mastiuhin-olexandr
committed
MC-39272: Rest api PUT /V1/products/:sku/links calls does not update indexer by Save
1 parent 6418d48 commit 55e8d99

File tree

7 files changed

+292
-18
lines changed

7 files changed

+292
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
namespace Magento\Catalog\Plugin\Api\ProductLinkRepositoryInterface;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Api\ProductLinkRepositoryInterface;
12+
use Magento\Catalog\Model\Indexer\Product\Full as FullProductIndexer;
13+
14+
/**
15+
* Product reindexing after delete by id links plugin.
16+
*/
17+
class ReindexAfterDeleteByIdProductLinksPlugin
18+
{
19+
/**
20+
* @var FullProductIndexer
21+
*/
22+
private $fullProductIndexer;
23+
24+
/**
25+
* @var ProductRepositoryInterface
26+
*/
27+
private $productRepository;
28+
29+
/**
30+
* @param FullProductIndexer $fullProductIndexer
31+
* @param ProductRepositoryInterface $productRepository
32+
*/
33+
public function __construct(FullProductIndexer $fullProductIndexer, ProductRepositoryInterface $productRepository)
34+
{
35+
$this->fullProductIndexer = $fullProductIndexer;
36+
$this->productRepository = $productRepository;
37+
}
38+
39+
/**
40+
* Complex reindex after product links has been deleted.
41+
*
42+
* @param ProductLinkRepositoryInterface $subject
43+
* @param bool $result
44+
* @param string $sku
45+
* @param string $type
46+
* @param string $linkedProductSku
47+
* @return bool
48+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
49+
*/
50+
public function afterDeleteById(ProductLinkRepositoryInterface $subject, bool $result, $sku): bool
51+
{
52+
$product = $this->productRepository->get($sku);
53+
$this->fullProductIndexer->executeRow($product->getId());
54+
55+
return $result;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
namespace Magento\Catalog\Plugin\Api\ProductLinkRepositoryInterface;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Api\ProductLinkRepositoryInterface;
12+
use Magento\Catalog\Api\Data\ProductLinkInterface;
13+
use Magento\Catalog\Model\Indexer\Product\Full as FullProductIndexer;
14+
15+
/**
16+
* Product reindexing after save links plugin.
17+
*/
18+
class ReindexAfterSaveProductLinksPlugin
19+
{
20+
/**
21+
* @var FullProductIndexer
22+
*/
23+
private $fullProductIndexer;
24+
25+
/**
26+
* @var ProductRepositoryInterface
27+
*/
28+
private $productRepository;
29+
30+
/**
31+
* @param FullProductIndexer $fullProductIndexer
32+
* @param ProductRepositoryInterface $productRepository
33+
*/
34+
public function __construct(FullProductIndexer $fullProductIndexer, ProductRepositoryInterface $productRepository)
35+
{
36+
$this->fullProductIndexer = $fullProductIndexer;
37+
$this->productRepository = $productRepository;
38+
}
39+
40+
/**
41+
* Complex reindex after product links has been saved.
42+
*
43+
* @param ProductLinkRepositoryInterface $subject
44+
* @param bool $result
45+
* @param ProductLinkInterface $entity
46+
* @return bool
47+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
48+
*/
49+
public function afterSave(ProductLinkRepositoryInterface $subject, bool $result, ProductLinkInterface $entity): bool
50+
{
51+
$product = $this->productRepository->get($entity->getSku());
52+
$this->fullProductIndexer->executeRow($product->getId());
53+
54+
return $result;
55+
}
56+
}

app/code/Magento/Catalog/etc/webapi_rest/di.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,8 @@
4040
<argument name="deserializer" xsi:type="object">Magento\Catalog\Model\Product\Webapi\Rest\RequestTypeBasedDeserializer</argument>
4141
</arguments>
4242
</type>
43+
<type name="Magento\Catalog\Api\ProductLinkRepositoryInterface">
44+
<plugin name="reindex_after_save_product_links" type="Magento\Catalog\Plugin\Api\ProductLinkRepositoryInterface\ReindexAfterSaveProductLinksPlugin"/>
45+
<plugin name="reindex_after_delete_by_id_product_links" type="Magento\Catalog\Plugin\Api\ProductLinkRepositoryInterface\ReindexAfterDeleteByIdProductLinksPlugin"/>
46+
</type>
4347
</config>

app/code/Magento/Catalog/etc/webapi_soap/di.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,8 @@
4040
<argument name="deserializer" xsi:type="object">Magento\Framework\Webapi\Rest\Request\Deserializer\Xml</argument>
4141
</arguments>
4242
</type>
43+
<type name="Magento\Catalog\Api\ProductLinkRepositoryInterface">
44+
<plugin name="reindex_after_save_product_links" type="Magento\Catalog\Plugin\Api\ProductLinkRepositoryInterface\ReindexAfterSaveProductLinksPlugin"/>
45+
<plugin name="reindex_after_delete_by_id_product_links" type="Magento\Catalog\Plugin\Api\ProductLinkRepositoryInterface\ReindexAfterDeleteByIdProductLinksPlugin"/>
46+
</type>
4347
</config>

dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
* Copyright © Magento, Inc. All rights reserved.
55
* See COPYING.txt for license details.
66
*/
7+
declare(strict_types=1);
8+
79
namespace Magento\GroupedProduct\Api;
810

9-
use Magento\TestFramework\Helper\Bootstrap;
10-
use Magento\Indexer\Model\Config;
11+
use Magento\Catalog\Api\ProductLinkManagementInterface;
1112
use Magento\Framework\Indexer\IndexerRegistry;
1213
use Magento\Framework\Webapi\Rest\Request;
14+
use Magento\Indexer\Model\Config;
15+
use Magento\TestFramework\Helper\Bootstrap;
16+
use Magento\TestFramework\TestCase\WebapiAbstract;
1317

14-
class ProductLinkRepositoryTest extends \Magento\TestFramework\TestCase\WebapiAbstract
18+
class ProductLinkRepositoryTest extends WebapiAbstract
1519
{
1620
const SERVICE_NAME = 'catalogProductLinkRepositoryV1';
1721
const SERVICE_VERSION = 'V1';
@@ -55,7 +59,7 @@ public function testSave(): void
5559
'linked_product_sku' => 'simple-1',
5660
'position' => 3,
5761
'extension_attributes' => [
58-
'qty' => (float) 300.0000,
62+
'qty' => (float)300.0000,
5963
],
6064
];
6165

@@ -72,12 +76,15 @@ public function testSave(): void
7276
];
7377
$this->_webApiCall($serviceInfo, ['entity' => $productData]);
7478

75-
/** @var \Magento\Catalog\Api\ProductLinkManagementInterface $linkManagement */
76-
$linkManagement = $this->objectManager->get(\Magento\Catalog\Api\ProductLinkManagementInterface::class);
79+
/** @var ProductLinkManagementInterface $linkManagement */
80+
$linkManagement = $this->objectManager->get(ProductLinkManagementInterface::class);
7781
$actual = $linkManagement->getLinkedItemsByType($productSku, $linkType);
78-
array_walk($actual, function (&$item) {
79-
$item = $item->__toArray();
80-
});
82+
array_walk(
83+
$actual,
84+
function (&$item) {
85+
$item = $item->__toArray();
86+
}
87+
);
8188
$this->assertEquals($productData, $actual[2]);
8289
}
8390

@@ -98,7 +105,7 @@ public function testLinkWithScheduledIndex(): void
98105
'linked_product_sku' => $productSimple,
99106
'position' => 3,
100107
'extension_attributes' => [
101-
'qty' => (float) 300.0000,
108+
'qty' => (float)300.0000,
102109
],
103110
];
104111
$serviceInfo = [
@@ -124,6 +131,101 @@ public function testLinkWithScheduledIndex(): void
124131
$this->restoreIndexMode();
125132
}
126133

134+
/**
135+
* Verify empty out of stock grouped product is in stock after child has been added.
136+
*
137+
* @return void
138+
* @magentoApiDataFixture Magento/GroupedProduct/_files/empty_grouped_product.php
139+
* @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php
140+
*/
141+
public function testGroupedProductIsInStockAfterAddChild(): void
142+
{
143+
$productSku = 'grouped-product';
144+
self::assertFalse($this->isProductInStock($productSku));
145+
$items = [
146+
'sku' => $productSku,
147+
'link_type' => 'associated',
148+
'linked_product_type' => 'virtual',
149+
'linked_product_sku' => 'virtual-product',
150+
'position' => 3,
151+
'extension_attributes' => [
152+
'qty' => 1,
153+
],
154+
];
155+
$serviceInfo = [
156+
'rest' => [
157+
'resourcePath' => self::RESOURCE_PATH . $productSku . '/links',
158+
'httpMethod' => Request::HTTP_METHOD_PUT,
159+
],
160+
'soap' => [
161+
'service' => self::SERVICE_NAME,
162+
'serviceVersion' => self::SERVICE_VERSION,
163+
'operation' => self::SERVICE_NAME . 'Save',
164+
],
165+
];
166+
$this->_webApiCall($serviceInfo, ['entity' => $items]);
167+
self::assertTrue($this->isProductInStock($productSku));
168+
}
169+
170+
/**
171+
* Verify in stock grouped product is out stock after children have been removed.
172+
*
173+
* @return void
174+
* @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php
175+
*/
176+
public function testGroupedProductIsOutOfStockAfterRemoveChild(): void
177+
{
178+
$productSku = 'grouped';
179+
$childrenSkus = [
180+
'simple_11',
181+
'simple_22',
182+
];
183+
self::assertTrue($this->isProductInStock($productSku));
184+
185+
foreach ($childrenSkus as $childSku) {
186+
$serviceInfo = [
187+
'rest' => [
188+
'resourcePath' => self::RESOURCE_PATH . $productSku . '/links/associated/' . $childSku,
189+
'httpMethod' => Request::HTTP_METHOD_DELETE,
190+
],
191+
'soap' => [
192+
'service' => self::SERVICE_NAME,
193+
'serviceVersion' => self::SERVICE_VERSION,
194+
'operation' => self::SERVICE_NAME . 'DeleteById',
195+
],
196+
];
197+
$requestData = ['sku' => $productSku, 'type' => 'associated', 'linkedProductSku' => $childSku];
198+
$this->_webApiCall($serviceInfo, $requestData);
199+
}
200+
201+
self::assertFalse($this->isProductInStock($productSku));
202+
}
203+
204+
205+
/**
206+
* Check product stock status.
207+
*
208+
* @param string $productSku
209+
* @return bool
210+
*/
211+
private function isProductInStock(string $productSku): bool
212+
{
213+
$serviceInfo = [
214+
'rest' => [
215+
'resourcePath' => '/V1/stockStatuses/' . $productSku,
216+
'httpMethod' => Request::HTTP_METHOD_GET,
217+
],
218+
'soap' => [
219+
'service' => 'catalogInventoryStockRegistryV1',
220+
'serviceVersion' => self::SERVICE_VERSION,
221+
'operation' => 'catalogInventoryStockRegistryV1getStockStatusBySku',
222+
],
223+
];
224+
$result = $this->_webApiCall($serviceInfo, ['productSku' => $productSku]);
225+
226+
return (bool)$result['stock_status'];
227+
}
228+
127229
/**
128230
* @param string $productSku
129231
* @return array
@@ -139,11 +241,11 @@ private function buildSearchCriteria(string $productSku): array
139241
[
140242
'field' => 'search_term',
141243
'value' => $productSku,
142-
]
143-
]
144-
]
145-
]
146-
]
244+
],
245+
],
246+
],
247+
],
248+
],
147249
];
148250
}
149251

@@ -156,13 +258,13 @@ private function buildSearchServiceInfo(array $searchCriteria): array
156258
return [
157259
'rest' => [
158260
'resourcePath' => self::RESOURCE_PATH_SEARCH . '?' . http_build_query($searchCriteria),
159-
'httpMethod' => Request::HTTP_METHOD_GET
261+
'httpMethod' => Request::HTTP_METHOD_GET,
160262
],
161263
'soap' => [
162264
'service' => self::SERVICE_NAME_SEARCH,
163265
'serviceVersion' => self::SERVICE_VERSION,
164-
'operation' => self::SERVICE_NAME_SEARCH . 'Search'
165-
]
266+
'operation' => self::SERVICE_NAME_SEARCH . 'Search',
267+
],
166268
];
167269
}
168270

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
use Magento\Catalog\Api\ProductRepositoryInterface;
8+
use Magento\Catalog\Model\Product;
9+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
10+
use Magento\Catalog\Model\Product\Visibility;
11+
use Magento\GroupedProduct\Model\Product\Type\Grouped;
12+
use Magento\TestFramework\Helper\Bootstrap;
13+
14+
$objectManager = Bootstrap::getObjectManager();
15+
$productRepository = $objectManager->get(ProductRepositoryInterface::class);
16+
$product = Bootstrap::getObjectManager()->create(Product::class);
17+
$product->isObjectNew(true);
18+
$product->setTypeId(Grouped::TYPE_CODE)
19+
->setAttributeSetId(4)
20+
->setWebsiteIds([1])
21+
->setName('Grouped Product')
22+
->setSku('grouped-product')
23+
->setPrice(100)
24+
->setTaxClassId(0)
25+
->setVisibility(Visibility::VISIBILITY_BOTH)
26+
->setStatus(Status::STATUS_ENABLED)
27+
->setStockData(['use_config_manage_stock' => 1, 'qty' => 0, 'is_in_stock' => 1])
28+
->save();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
use Magento\Catalog\Api\ProductRepositoryInterface;
8+
use Magento\Framework\Exception\NoSuchEntityException;
9+
use Magento\Framework\Registry;
10+
use Magento\TestFramework\Helper\Bootstrap;
11+
12+
$registry = Bootstrap::getObjectManager()->get(Registry::class);
13+
$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class);
14+
$registry->unregister('isSecureArea');
15+
$registry->register('isSecureArea', true);
16+
try {
17+
$groupedProduct = $productRepository->get('grouped-product', false, null, true);
18+
$groupedProduct->delete();
19+
} catch (NoSuchEntityException $e) {
20+
//already deleted
21+
}
22+
$registry->unregister('isSecureArea');
23+
$registry->register('isSecureArea', false);

0 commit comments

Comments
 (0)