Skip to content

Commit 6940d20

Browse files
committed
MAGETWO-73528: Out of stock associated products to configurable are not full page cache cleaned #8009
1 parent 38bc4bf commit 6940d20

File tree

9 files changed

+176
-6
lines changed

9 files changed

+176
-6
lines changed

app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,8 @@
263263
<requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity>
264264
<requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity>
265265
</entity>
266+
<entity name="ApiSimpleSingleQty" extends="ApiSimpleOne">
267+
<data key="quantity">1</data>
268+
<requiredEntity type="product_extension_attribute">EavStock1</requiredEntity>
269+
</entity>
266270
</entities>

app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@
1111
<entity name="EavStockItem" type="product_extension_attribute">
1212
<requiredEntity type="stock_item">Qty_1000</requiredEntity>
1313
</entity>
14+
<entity name="EavStock1" type="product_extension_attribute">
15+
<requiredEntity type="stock_item">Qty_1</requiredEntity>
16+
</entity>
1417
</entities>

app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@
1212
<data key="qty">1000</data>
1313
<data key="is_in_stock">true</data>
1414
</entity>
15+
<entity name="Qty_1" type="stock_item">
16+
<data key="qty">1</data>
17+
<data key="is_in_stock">true</data>
18+
</entity>
1519
</entities>

app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111
use Magento\CatalogInventory\Api\StockConfigurationInterface;
1212
use Magento\Framework\App\ResourceConnection;
13+
use Magento\Framework\App\ObjectManager;
1314
use Magento\Framework\DB\Adapter\AdapterInterface;
1415
use Magento\Framework\Event\ManagerInterface;
16+
use Magento\Framework\EntityManager\MetadataPool;
1517
use Magento\Framework\Indexer\CacheContext;
1618
use Magento\CatalogInventory\Model\Stock;
1719
use Magento\Catalog\Model\Product;
@@ -46,25 +48,35 @@ class CacheCleaner
4648
*/
4749
private $connection;
4850

51+
/**
52+
* @var MetadataPool
53+
*/
54+
private $metadataPool;
55+
4956
/**
5057
* @param ResourceConnection $resource
5158
* @param StockConfigurationInterface $stockConfiguration
5259
* @param CacheContext $cacheContext
5360
* @param ManagerInterface $eventManager
61+
* @param MetadataPool|null $metadataPool
5462
*/
5563
public function __construct(
5664
ResourceConnection $resource,
5765
StockConfigurationInterface $stockConfiguration,
5866
CacheContext $cacheContext,
59-
ManagerInterface $eventManager
67+
ManagerInterface $eventManager,
68+
MetadataPool $metadataPool = null
6069
) {
6170
$this->resource = $resource;
6271
$this->stockConfiguration = $stockConfiguration;
6372
$this->cacheContext = $cacheContext;
6473
$this->eventManager = $eventManager;
74+
$this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class);
6575
}
6676

6777
/**
78+
* Clean cache by product ids.
79+
*
6880
* @param array $productIds
6981
* @param callable $reindex
7082
* @return void
@@ -76,22 +88,37 @@ public function clean(array $productIds, callable $reindex)
7688
$productStatusesAfter = $this->getProductStockStatuses($productIds);
7789
$productIds = $this->getProductIdsForCacheClean($productStatusesBefore, $productStatusesAfter);
7890
if ($productIds) {
79-
$this->cacheContext->registerEntities(Product::CACHE_TAG, $productIds);
91+
$this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds));
8092
$this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
8193
}
8294
}
8395

8496
/**
97+
* Get current stock statuses for product ids.
98+
*
8599
* @param array $productIds
86100
* @return array
87101
*/
88102
private function getProductStockStatuses(array $productIds)
89103
{
104+
$linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
105+
->getLinkField();
90106
$select = $this->getConnection()->select()
91107
->from(
92-
$this->resource->getTableName('cataloginventory_stock_status'),
108+
['css' => $this->resource->getTableName('cataloginventory_stock_status')],
93109
['product_id', 'stock_status', 'qty']
94-
)->where('product_id IN (?)', $productIds)
110+
)
111+
->joinLeft(
112+
['cpr' => $this->resource->getTableName('catalog_product_relation')],
113+
'css.product_id = cpr.child_id',
114+
[]
115+
)
116+
->joinLeft(
117+
['cpe' => $this->resource->getTableName('catalog_product_entity')],
118+
'cpr.parent_id = cpe.' . $linkField,
119+
['parent_id' => 'cpe.entity_id']
120+
)
121+
->where('product_id IN (?)', $productIds)
95122
->where('stock_id = ?', Stock::DEFAULT_STOCK_ID)
96123
->where('website_id = ?', $this->stockConfiguration->getDefaultScopeId());
97124

@@ -125,13 +152,18 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array
125152
if ($statusBefore['stock_status'] !== $statusAfter['stock_status']
126153
|| ($stockThresholdQty && $statusAfter['qty'] <= $stockThresholdQty)) {
127154
$productIds[] = $productId;
155+
if (isset($statusAfter['parent_id'])) {
156+
$productIds[] = $statusAfter['parent_id'];
157+
}
128158
}
129159
}
130160

131161
return $productIds;
132162
}
133163

134164
/**
165+
* Get database connection.
166+
*
135167
* @return AdapterInterface
136168
*/
137169
private function getConnection()
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
11+
<test name="AssociatedProductToConfigurableOutOfStockTest">
12+
<annotations>
13+
<features value="CatalogInventory"/>
14+
<stories value="Add/remove images and videos for all product types and category"/>
15+
<title value="Out of stock associated products to configurable are not full page cache cleaned "/>
16+
<description value="After last configurable product was ordered it becomes out of stock"/>
17+
<severity value="MAJOR"/>
18+
<testCaseId value="MAGETWO-96031"/>
19+
<group value="CatalogInventory"/>
20+
</annotations>
21+
22+
<before>
23+
<!--Create Configurable product-->
24+
<actionGroup ref="AdminCreateConfigurableProductChildQty1ActionGroup" stepKey="createConfigurableProduct">
25+
<argument name="productName" value="ApiConfigurableProduct"/>
26+
</actionGroup>
27+
<!-- Create customer -->
28+
<createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer">
29+
<field key="group_id">1</field>
30+
</createData>
31+
<!--Update index mode, reindex, flush cache-->
32+
<magentoCLI command="indexer:set-mode schedule" stepKey="setScheduleIndexMode"/>
33+
<magentoCLI command="indexer:reindex" stepKey="reindexBefore"/>
34+
<magentoCLI command="cache:flush" stepKey="cacheFlushBefore"/>
35+
</before>
36+
37+
<after>
38+
<!--Delete configurable product-->
39+
<deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/>
40+
<deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct"/>
41+
<deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/>
42+
<deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/>
43+
<!--Delete customer-->
44+
<deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/>
45+
<!--Update index mode, reindex, flush cache-->
46+
<magentoCLI command="indexer:set-mode realtime" stepKey="setRealTimeIndexMode"/>
47+
<magentoCLI command="indexer:reindex" stepKey="reindexAfter"/>
48+
<magentoCLI command="cache:flush" stepKey="cacheFlushAfter"/>
49+
</after>
50+
51+
<!-- Login as a customer -->
52+
<actionGroup ref="CustomerLoginOnStorefront" stepKey="signUpNewUser">
53+
<argument name="customer" value="$$createSimpleUsCustomer$$"/>
54+
</actionGroup>
55+
56+
<!-- Go to configurable product page -->
57+
<amOnPage url="{{StorefrontProductPage.url('apiconfigurableproduct')}}" stepKey="goToConfigurableProductPage"/>
58+
59+
<!-- Order product with single quantity -->
60+
<selectOption userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct.option[store_labels][1][label]$$"
61+
selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttributeCreateConfigurableProduct.attribute_id$$)}}"
62+
stepKey="configProductFillOption"
63+
/>
64+
<click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/>
65+
<waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/>
66+
<actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/>
67+
<actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectShippingMehod"/>
68+
<click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/>
69+
<actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/>
70+
<actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeorder">
71+
<argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" />
72+
<argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" />
73+
</actionGroup>
74+
75+
<!--Run cron to reindex products-->
76+
<magentoCLI command="cron:run --group='index'" stepKey="runCron"/>
77+
<magentoCLI command="cron:run --group='index'" stepKey="runCron1"/>
78+
79+
<!-- Go to configurable product page -->
80+
<amOnPage url="{{StorefrontProductPage.url('apiconfigurableproduct')}}" stepKey="goToConfigurableProductPage1"/>
81+
82+
<!-- Assert that ordered product with single quantity is not available for order -->
83+
<dontSee userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct.option[store_labels][1][label]$$"
84+
selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttributeCreateConfigurableProduct.attribute_id$$)}}"
85+
stepKey="assertOptionNotAvailable"
86+
/>
87+
88+
<!-- Logout customer on Storefront-->
89+
<actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutStorefront"/>
90+
</test>
91+
</tests>

app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Magento\Framework\DB\Adapter\AdapterInterface;
1313
use Magento\Framework\DB\Select;
1414
use Magento\Framework\Event\ManagerInterface;
15+
use Magento\Framework\EntityManager\MetadataPool;
1516
use Magento\Framework\Indexer\CacheContext;
1617
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
1718
use Magento\Catalog\Model\Product;
@@ -43,6 +44,11 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase
4344
*/
4445
private $cacheContextMock;
4546

47+
/**
48+
* @var MetadataPool |\PHPUnit_Framework_MockObject_MockObject
49+
*/
50+
private $metadataPoolMock;
51+
4652
/**
4753
* @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject
4854
*/
@@ -61,6 +67,8 @@ protected function setUp()
6167
->setMethods(['getStockThresholdQty'])->getMockForAbstractClass();
6268
$this->cacheContextMock = $this->getMockBuilder(CacheContext::class)->disableOriginalConstructor()->getMock();
6369
$this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class)->getMock();
70+
$this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class)
71+
->setMethods(['getMetadata', 'getLinkField'])->disableOriginalConstructor()->getMock();
6472
$this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock();
6573

6674
$this->resourceMock->expects($this->any())
@@ -73,7 +81,8 @@ protected function setUp()
7381
'resource' => $this->resourceMock,
7482
'stockConfiguration' => $this->stockConfigurationMock,
7583
'cacheContext' => $this->cacheContextMock,
76-
'eventManager' => $this->eventManagerMock
84+
'eventManager' => $this->eventManagerMock,
85+
'metadataPool' => $this->metadataPoolMock,
7786
]
7887
);
7988
}
@@ -90,6 +99,7 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto
9099
$productId = 123;
91100
$this->selectMock->expects($this->any())->method('from')->willReturnSelf();
92101
$this->selectMock->expects($this->any())->method('where')->willReturnSelf();
102+
$this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf();
93103
$this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock);
94104
$this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls(
95105
[
@@ -105,7 +115,10 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto
105115
->with(Product::CACHE_TAG, [$productId]);
106116
$this->eventManagerMock->expects($this->once())->method('dispatch')
107117
->with('clean_cache_by_tags', ['object' => $this->cacheContextMock]);
108-
118+
$this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata')
119+
->willReturnSelf();
120+
$this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField')
121+
->willReturn('row_id');
109122
$callback = function () {
110123
};
111124
$this->unit->clean([], $callback);
@@ -136,6 +149,7 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft
136149
$productId = 123;
137150
$this->selectMock->expects($this->any())->method('from')->willReturnSelf();
138151
$this->selectMock->expects($this->any())->method('where')->willReturnSelf();
152+
$this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf();
139153
$this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock);
140154
$this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls(
141155
[
@@ -149,6 +163,10 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft
149163
->willReturn($stockThresholdQty);
150164
$this->cacheContextMock->expects($this->never())->method('registerEntities');
151165
$this->eventManagerMock->expects($this->never())->method('dispatch');
166+
$this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata')
167+
->willReturnSelf();
168+
$this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField')
169+
->willReturn('row_id');
152170

153171
$callback = function () {
154172
};

app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd">
1111
<!-- Checkout select Check/Money Order payment -->
1212
<actionGroup name="CheckoutSelectCheckMoneyOrderPaymentActionGroup">
13+
<waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/>
1314
<waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/>
1415
<conditionalClick selector="{{CheckoutPaymentSection.checkMoneyOrderPayment}}" dependentSelector="{{CheckoutPaymentSection.checkMoneyOrderPayment}}" visible="true" stepKey="clickCheckMoneyOrderPayment"/>
1516
</actionGroup>
@@ -66,4 +67,10 @@
6667
<click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/>
6768
<see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/>
6869
</actionGroup>
70+
71+
<!-- Checkout select Flat Rate shipping method -->
72+
<actionGroup name="CheckoutSelectFlatRateShippingMethodActionGroup">
73+
<conditionalClick selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" dependentSelector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" visible="true" stepKey="selectFlatRateShippingMethod"/>
74+
<waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/>
75+
</actionGroup>
6976
</actionGroups>

app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,14 @@
6262
<requiredEntity createDataKey="createConfigChildProduct2"/>
6363
</createData>
6464
</actionGroup>
65+
66+
<actionGroup name="AdminCreateConfigurableProductChildQty1ActionGroup" extends="AdminCreateApiConfigurableProductActionGroup">
67+
<createData entity="ApiConfigurableProductWithOutCategory" stepKey="createConfigProduct">
68+
<field key="name">{{productName}}</field>
69+
</createData>
70+
<createData entity="ApiSimpleSingleQty" stepKey="createConfigChildProduct1">
71+
<requiredEntity createDataKey="createConfigProductAttribute"/>
72+
<requiredEntity createDataKey="getConfigAttributeOption1"/>
73+
</createData>
74+
</actionGroup>
6575
</actionGroups>

app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
<element name="productAttributeOptions" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/>
1414
<element name="stockIndication" type="block" selector=".stock" />
1515
<element name="productAttributeOptionsSelectButton" type="select" selector="#product-options-wrapper .super-attribute-select"/>
16+
<element name="optionByAttributeId" type="input" selector="#attribute{{var1}}" parameterized="true"/>
1617
</section>
1718
</sections>

0 commit comments

Comments
 (0)