From 378c1395a7ddbfc4ef830870bed7e9690ee3fa66 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Mon, 11 Dec 2017 15:06:18 +0200 Subject: [PATCH 1/7] MAGETWO-59163: Category product count incorporating products with visibility set to search only --- .../Adapter/Mysql/Filter/AliasResolver.php | 3 ++- .../Adapter/Mysql/Filter/Preprocessor.php | 5 ++++- .../ResourceModel/Fulltext/Collection.php | 8 ++++++++ .../Search/FilterMapper/ExclusionStrategy.php | 18 +++++++++++------- .../Magento/Framework/Search/Search.php | 10 +++++++--- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php index 52a2c6ff40c99..d649e7e09c009 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php @@ -35,7 +35,8 @@ public function getAlias(\Magento\Framework\Search\Request\FilterInterface $filt $alias = 'price_index'; break; case 'category_ids': - $alias = 'category_ids_index'; + case 'visibility': + $alias = 'category_products_index'; break; default: $alias = $field . RequestGenerator::FILTER_SUFFIX; diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index 056524452645e..fa363df490411 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -141,7 +141,10 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu $query ); } elseif ($filter->getField() === 'category_ids') { - return 'category_ids_index.category_id = ' . (int) $filter->getValue(); + return "{$this->aliasResolver->getAlias($filter)}.category_id = " + . (int) $filter->getValue(); + } elseif ($filter->getField() === 'visibility') { + return "{$this->aliasResolver->getAlias($filter)}." . $query; } elseif ($attribute->isStatic()) { $alias = $this->aliasResolver->getAlias($filter); $resultQuery = str_replace( diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 1f7d94b9c5f39..aa518b4a1593b 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -270,6 +270,14 @@ public function addFieldToFilter($field, $condition = null) $this->filterBuilder->setValue($condition); $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); } else { + if (empty($condition['from']) && empty($condition['to'])) { + $this->filterBuilder->setField($field); + $this->filterBuilder->setValue($condition); + $this->filterBuilder->setConditionType('in'); + $this->searchCriteriaBuilder->addFilter( + $this->filterBuilder->create() + ); + } if (!empty($condition['from'])) { $this->filterBuilder->setField("{$field}.from"); $this->filterBuilder->setValue($condition['from']); diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php index 03675f27cac21..8ca30c23c3637 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php @@ -70,14 +70,18 @@ public function apply( [] ); $isApplied = true; - } elseif ('category_ids' === $field) { + } elseif ('category_ids' === $field || $field === 'visibility') { $alias = $this->aliasResolver->getAlias($filter); - $tableName = $this->resourceConnection->getTableName('catalog_category_product_index'); - $select->joinInner( - [$alias => $tableName], - 'search_index.entity_id = category_ids_index.product_id', - [] - ); + if (!array_key_exists($alias, $select->getPart('from'))) { + $tableName = $this->resourceConnection->getTableName( + 'catalog_category_product_index' + ); + $select->joinInner( + [$alias => $tableName], + "search_index.entity_id = $alias.product_id", + [] + ); + } $isApplied = true; } diff --git a/lib/internal/Magento/Framework/Search/Search.php b/lib/internal/Magento/Framework/Search/Search.php index 9ad9d8b1500e2..92f7d113b2aae 100644 --- a/lib/internal/Magento/Framework/Search/Search.php +++ b/lib/internal/Magento/Framework/Search/Search.php @@ -84,15 +84,19 @@ public function search(SearchCriteriaInterface $searchCriteria) */ private function addFieldToFilter($field, $condition = null) { - if (!is_array($condition) || !in_array(key($condition), ['from', 'to'], true)) { - $this->requestBuilder->bind($field, $condition); - } else { + if (is_array($condition) + && ( + !empty($condition['from']) || !empty($condition['to']) + ) + ) { if (!empty($condition['from'])) { $this->requestBuilder->bind("{$field}.from", $condition['from']); } if (!empty($condition['to'])) { $this->requestBuilder->bind("{$field}.to", $condition['to']); } + } else { + $this->requestBuilder->bind($field, $condition); } return $this; From b0aa60adae8028a8684299e49f5609780717db6c Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Mon, 11 Dec 2017 15:29:45 +0200 Subject: [PATCH 2/7] MAGETWO-59163: Category product count incorporating products with visibility set to search only --- .../Mysql/Filter/AliasResolverTest.php | 6 ++- .../Adapter/Mysql/Filter/PreprocessorTest.php | 42 ++++++++++++++++--- .../Framework/Api/Search/SearchTest.php | 5 +++ .../ResourceModel/Fulltext/CollectionTest.php | 1 + 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php index cb7b12fc698ef..e0f3e0b11d56b 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php @@ -63,7 +63,11 @@ public function aliasDataProvider() ], 'category_ids' => [ 'field' => 'category_ids', - 'alias' => 'category_ids_index', + 'alias' => 'category_products_index', + ], + 'visibility' => [ + 'field' => 'visibility', + 'alias' => 'category_products_index', ], ]; } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php index c8dd43c74a10d..d9d436a50d263 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php @@ -203,9 +203,9 @@ public function testProcessPrice() public function processCategoryIdsDataProvider() { return [ - ['5', 'category_ids_index.category_id = 5'], - [3, 'category_ids_index.category_id = 3'], - ["' and 1 = 0", 'category_ids_index.category_id = 0'], + ['5', ':alias.category_id = 5'], + [3, ':alias.category_id = 3'], + ["' and 1 = 0", ':alias.category_id = 0'], ]; } @@ -218,7 +218,11 @@ public function testProcessCategoryIds($categoryId, $expectedResult) { $isNegation = false; $query = 'SELECT category_ids FROM catalog_product_entity'; + $tableAlias = 'category__alias'; + $this->aliasResolver->expects($this->atLeastOnce()) + ->method('getAlias') + ->willReturn($tableAlias); $this->filter->expects($this->exactly(3)) ->method('getField') ->will($this->returnValue('category_ids')); @@ -233,9 +237,35 @@ public function testProcessCategoryIds($categoryId, $expectedResult) ->will($this->returnValue($this->attribute)); $actualResult = $this->target->process($this->filter, $isNegation, $query); + $expectedResult = strtr($expectedResult, [':alias' => $tableAlias]); $this->assertSame($expectedResult, $this->removeWhitespaces($actualResult)); } + public function testProcessVisibilityIds() + { + $query = 'visibility in (1, 2)'; + $tableAlias = 'visibility__alias'; + + $this->aliasResolver->expects($this->atLeastOnce()) + ->method('getAlias') + ->willReturn($tableAlias); + $this->filter->expects($this->atLeastOnce()) + ->method('getField') + ->will($this->returnValue('visibility')); + $this->filter->expects($this->never()) + ->method('getValue'); + $this->config->expects($this->once()) + ->method('getAttribute') + ->with(\Magento\Catalog\Model\Product::ENTITY, 'visibility') + ->will($this->returnValue($this->attribute)); + + $actualResult = $this->target->process($this->filter, false, $query); + $this->assertSame( + "$tableAlias.$query", + $this->removeWhitespaces($actualResult) + ); + } + public function testProcessStaticAttribute() { $expectedResult = 'attr_table_alias.static_attribute LIKE %name%'; @@ -246,7 +276,7 @@ public function testProcessStaticAttribute() ->willReturn('static_attribute'); $this->aliasResolver->expects($this->once())->method('getAlias') ->willReturn('attr_table_alias'); - $this->filter->expects($this->exactly(3)) + $this->filter->expects($this->exactly(4)) ->method('getField') ->will($this->returnValue('static_attribute')); $this->config->expects($this->exactly(1)) @@ -285,7 +315,7 @@ public function testProcessTermFilter($frontendInput, $fieldValue, $isNegation, $this->aliasResolver->expects($this->once())->method('getAlias') ->willReturn('termAttrAlias'); - $this->filter->expects($this->exactly(3)) + $this->filter->expects($this->exactly(4)) ->method('getField') ->willReturn('termField'); $this->filter->expects($this->exactly(2)) @@ -360,7 +390,7 @@ public function testProcessNotStaticAttribute() $attributeId = 1234567; $this->scope->expects($this->once())->method('getId')->will($this->returnValue($scopeId)); - $this->filter->expects($this->exactly(4)) + $this->filter->expects($this->exactly(5)) ->method('getField') ->will($this->returnValue('not_static_attribute')); $this->config->expects($this->exactly(1)) diff --git a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php index eac7d409bfda2..f92b5f4303055 100644 --- a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php @@ -34,6 +34,11 @@ public function testCatalogSearch() 'field' => 'price_dynamic_algorithm', 'value' => 'auto', 'condition_type' => 'eq' + ], + [ + 'field' => 'visibility', + 'value' => [3, 4], + 'condition_type' => 'eq' ] ] ] diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php index aa70cfc3cd074..716e26bc2ee09 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/ResourceModel/Fulltext/CollectionTest.php @@ -40,6 +40,7 @@ public function filtersDataProviderSearch() ['catalog_view_container', ['category_ids' => 100001], 0], ['catalog_view_container', ['category_ids' => []], 0], ['catalog_view_container', [], 0], + ['catalog_view_container', ['visibility' => [2, 4]], 5], ]; } } From fcf58a3e1199da860feae2a8e5320fc7af31eb77 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Mon, 11 Dec 2017 17:16:35 +0200 Subject: [PATCH 3/7] MAGETWO-59163: Category product count incorporating products with visibility set to search only --- .../testsuite/Magento/Framework/Api/Search/SearchTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php index f92b5f4303055..27f27542683d4 100644 --- a/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Framework/Api/Search/SearchTest.php @@ -37,7 +37,7 @@ public function testCatalogSearch() ], [ 'field' => 'visibility', - 'value' => [3, 4], + 'value' => 4, 'condition_type' => 'eq' ] ] From d8ccc0f2d423f1dbe66a5360ae29b015822ced8b Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Thu, 21 Dec 2017 18:29:28 +0200 Subject: [PATCH 4/7] MAGETWO-59163: Category product count incorporating products with visibility set to search only --- .../ResourceModel/Fulltext/Collection.php | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index aa518b4a1593b..642e24c670ef5 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -254,7 +254,7 @@ public function setFilterBuilder(\Magento\Framework\Api\FilterBuilder $object) * Apply attribute filter to facet collection * * @param string $field - * @param null $condition + * @param null|string|array $condition * @return $this */ public function addFieldToFilter($field, $condition = null) @@ -265,30 +265,21 @@ public function addFieldToFilter($field, $condition = null) $this->getSearchCriteriaBuilder(); $this->getFilterBuilder(); - if (!is_array($condition) || !in_array(key($condition), ['from', 'to'])) { - $this->filterBuilder->setField($field); - $this->filterBuilder->setValue($condition); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); - } else { - if (empty($condition['from']) && empty($condition['to'])) { - $this->filterBuilder->setField($field); - $this->filterBuilder->setValue($condition); - $this->filterBuilder->setConditionType('in'); - $this->searchCriteriaBuilder->addFilter( - $this->filterBuilder->create() - ); - } + if (is_array($condition) + && in_array(key($condition), ['from', 'to'], true) + ) { if (!empty($condition['from'])) { - $this->filterBuilder->setField("{$field}.from"); - $this->filterBuilder->setValue($condition['from']); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + $this->addFieldToFilter("{$field}.from", $condition['from']); } if (!empty($condition['to'])) { - $this->filterBuilder->setField("{$field}.to"); - $this->filterBuilder->setValue($condition['to']); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + $this->addFieldToFilter("{$field}.to", $condition['to']); } + } else { + $this->filterBuilder->setField($field); + $this->filterBuilder->setValue($condition); + $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); } + return $this; } From 48a4844af41828a3b125d84c153b22a4c02a056c Mon Sep 17 00:00:00 2001 From: Kostyantyn Alexeyev Date: Wed, 27 Dec 2017 14:49:17 +0200 Subject: [PATCH 5/7] MAGETWO-69701: [GitHub] Concurrent checkouts can lead to negative stock #6363 [backport 2.1] --- .../Model/ResourceModel/Stock.php | 12 ++- .../Model/StockManagement.php | 16 +++- .../Unit/Model/ResourceModel/StockTest.php | 37 ++++++++- .../GuestPaymentInformationManagement.php | 49 ++++++++---- .../GuestPaymentInformationManagementTest.php | 78 ++++++++++++++++++- 5 files changed, 169 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php index 95d83e06c9809..5160010108582 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php @@ -126,7 +126,7 @@ public function lockProductsStock($productIds, $websiteId) } $itemTable = $this->getTable('cataloginventory_stock_item'); $select = $this->getConnection()->select()->from(['si' => $itemTable]) - ->where('website_id=?', $websiteId) + ->where('website_id = ?', $websiteId) ->where('product_id IN(?)', $productIds) ->forUpdate(true); @@ -139,9 +139,15 @@ public function lockProductsStock($productIds, $websiteId) 'type_id' => 'type_id' ] ); - $this->getConnection()->query($select); + $items = []; - return $this->getConnection()->fetchAll($selectProducts); + foreach ($this->getConnection()->query($select)->fetchAll() as $si) { + $items[$si['product_id']] = $si; + } + foreach ($this->getConnection()->fetchAll($selectProducts) as $p) { + $items[$p['product_id']]['type_id'] = $p['type_id']; + } + return $items; } /** diff --git a/app/code/Magento/CatalogInventory/Model/StockManagement.php b/app/code/Magento/CatalogInventory/Model/StockManagement.php index 3055c1873b1c1..c4eb9f0fd8afd 100644 --- a/app/code/Magento/CatalogInventory/Model/StockManagement.php +++ b/app/code/Magento/CatalogInventory/Model/StockManagement.php @@ -48,6 +48,11 @@ class StockManagement implements StockManagementInterface */ private $qtyCounter; + /** + * @var StockRegistryStorage + */ + private $stockRegistryStorage; + /** * @param ResourceStock $stockResource * @param StockRegistryProviderInterface $stockRegistryProvider @@ -55,6 +60,7 @@ class StockManagement implements StockManagementInterface * @param StockConfigurationInterface $stockConfiguration * @param ProductRepositoryInterface $productRepository * @param QtyCounterInterface $qtyCounter + * @param StockRegistryStorage|null $stockRegistryStorage */ public function __construct( ResourceStock $stockResource, @@ -62,7 +68,8 @@ public function __construct( StockState $stockState, StockConfigurationInterface $stockConfiguration, ProductRepositoryInterface $productRepository, - QtyCounterInterface $qtyCounter + QtyCounterInterface $qtyCounter, + StockRegistryStorage $stockRegistryStorage = null ) { $this->stockRegistryProvider = $stockRegistryProvider; $this->stockState = $stockState; @@ -70,6 +77,8 @@ public function __construct( $this->productRepository = $productRepository; $this->qtyCounter = $qtyCounter; $this->resource = $stockResource; + $this->stockRegistryStorage = $stockRegistryStorage ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StockRegistryStorage::class); } /** @@ -92,9 +101,12 @@ public function registerProductsSale($items, $websiteId = null) $fullSaveItems = $registeredItems = []; foreach ($lockedItems as $lockedItemRecord) { $productId = $lockedItemRecord['product_id']; + $this->stockRegistryStorage->removeStockItem($productId, $websiteId); + /** @var StockItemInterface $stockItem */ $orderedQty = $items[$productId]; $stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId); + $stockItem->setQty($lockedItemRecord['qty']); // update data from locked item $canSubtractQty = $stockItem->getItemId() && $this->canSubtractQty($stockItem); if (!$canSubtractQty || !$this->stockConfiguration->isQty($lockedItemRecord['type_id'])) { continue; @@ -180,7 +192,7 @@ protected function getProductType($productId) } /** - * @return Stock + * @return ResourceStock */ protected function getResource() { diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php index f704387a685ce..0a7680e612c7e 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php @@ -67,6 +67,11 @@ class StockTest extends \PHPUnit_Framework_TestCase */ protected $selectMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Zend_Db_Statement_Interface + */ + protected $statementMock; + /** * Prepare subjects for tests. * @@ -95,6 +100,7 @@ protected function setUp() $this->connectionMock = $this->getMockBuilder(Mysql::class) ->disableOriginalConstructor() ->getMock(); + $this->statementMock = $this->getMockForAbstractClass(\Zend_Db_Statement_Interface::class); $this->stock = $this->getMockBuilder(Stock::class) ->setMethods(['getTable', 'getConnection']) ->setConstructorArgs( @@ -119,7 +125,21 @@ public function testLockProductsStock() { $websiteId = 0; $productIds = [1, 2, 3]; - $result = ['testResult']; + $result = [ + 1 => [ + 'product_id' => 1, + 'type_id' => 'simple' + ], + 2 => [ + 'product_id' => 2, + 'type_id' => 'simple' + ], + 3 => [ + 'product_id' => 3, + 'type_id' => 'simple' + ] + ]; + $this->selectMock->expects(self::exactly(2)) ->method('from') ->withConsecutive( @@ -130,7 +150,7 @@ public function testLockProductsStock() $this->selectMock->expects(self::exactly(3)) ->method('where') ->withConsecutive( - [self::identicalTo('website_id=?'), self::identicalTo($websiteId)], + [self::identicalTo('website_id = ?'), self::identicalTo($websiteId)], [self::identicalTo('product_id IN(?)'), self::identicalTo($productIds)], [self::identicalTo('entity_id IN (?)'), self::identicalTo($productIds)] ) @@ -149,10 +169,19 @@ public function testLockProductsStock() ->willReturn($this->selectMock); $this->connectionMock->expects(self::once()) ->method('query') - ->with(self::identicalTo($this->selectMock)); + ->with(self::identicalTo($this->selectMock)) + ->willReturn($this->statementMock); + $this->statementMock->expects(self::once()) + ->method('fetchAll') + ->willReturn([ + 1 => ['product_id' => 1], + 2 => ['product_id' => 2], + 3 => ['product_id' => 3] + ]); + $this->connectionMock->expects(self::once()) ->method('fetchAll') - ->with($this->selectMock) + ->with(self::identicalTo($this->selectMock)) ->willReturn($result); $this->stock->expects(self::exactly(2)) diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index fc695987481a0..228b97930931e 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -6,6 +6,8 @@ namespace Magento\Checkout\Model; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; @@ -51,6 +53,11 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa */ private $logger; + /** + * @var ResourceConnection + */ + private $connectionPull; + /** * @param \Magento\Quote\Api\GuestBillingAddressManagementInterface $billingAddressManagement * @param \Magento\Quote\Api\GuestPaymentMethodManagementInterface $paymentMethodManagement @@ -58,6 +65,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa * @param \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository + * @param ResourceConnection|null $connectionPull * @codeCoverageIgnore */ public function __construct( @@ -66,7 +74,8 @@ public function __construct( \Magento\Quote\Api\GuestCartManagementInterface $cartManagement, \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement, \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + ResourceConnection $connectionPull = null ) { $this->billingAddressManagement = $billingAddressManagement; $this->paymentMethodManagement = $paymentMethodManagement; @@ -74,6 +83,7 @@ public function __construct( $this->paymentInformationManagement = $paymentInformationManagement; $this->quoteIdMaskFactory = $quoteIdMaskFactory; $this->cartRepository = $cartRepository; + $this->connectionPull = $connectionPull ?: ObjectManager::getInstance()->get(ResourceConnection::class); } /** @@ -85,20 +95,33 @@ public function savePaymentInformationAndPlaceOrder( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { - $this->savePaymentInformation($cartId, $email, $paymentMethod, $billingAddress); + $salesConnection = $this->connectionPull->getConnection('sales'); + $checkoutConnection = $this->connectionPull->getConnection('checkout'); + $salesConnection->beginTransaction(); + $checkoutConnection->beginTransaction(); + try { - $orderId = $this->cartManagement->placeOrder($cartId); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - throw new CouldNotSaveException( - __($e->getMessage()), - $e - ); + $this->savePaymentInformation($cartId, $email, $paymentMethod, $billingAddress); + try { + $orderId = $this->cartManagement->placeOrder($cartId); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + throw new CouldNotSaveException( + __($e->getMessage()), + $e + ); + } catch (\Exception $e) { + $this->getLogger()->critical($e); + throw new CouldNotSaveException( + __('An error occurred on the server. Please try to place the order again.'), + $e + ); + } + $salesConnection->commit(); + $checkoutConnection->commit(); } catch (\Exception $e) { - $this->getLogger()->critical($e); - throw new CouldNotSaveException( - __('An error occurred on the server. Please try to place the order again.'), - $e - ); + $salesConnection->rollBack(); + $checkoutConnection->rollBack(); + throw $e; } return $orderId; diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index 6e809d089d773..96b1f2bc5a777 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -6,6 +6,9 @@ namespace Magento\Checkout\Test\Unit\Model; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -46,6 +49,11 @@ class GuestPaymentInformationManagementTest extends \PHPUnit_Framework_TestCase */ private $loggerMock; + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resourceConnectionMock; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -66,6 +74,10 @@ protected function setUp() false ); $this->loggerMock = $this->getMock(\Psr\Log\LoggerInterface::class); + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->model = $objectManager->getObject( \Magento\Checkout\Model\GuestPaymentInformationManagement::class, [ @@ -73,7 +85,8 @@ protected function setUp() 'paymentMethodManagement' => $this->paymentMethodManagementMock, 'cartManagement' => $this->cartManagementMock, 'cartRepository' => $this->cartRepositoryMock, - 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, + 'connectionPull' => $this->resourceConnectionMock ] ); $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); @@ -89,6 +102,27 @@ public function testSavePaymentInformationAndPlaceOrder() $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); + $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $adapterMockForCheckout = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resourceConnectionMock->expects($this->at(0)) + ->method('getConnection') + ->with('sales') + ->willReturn($adapterMockForSales); + $adapterMockForSales->expects($this->once())->method('beginTransaction'); + $adapterMockForSales->expects($this->once())->method('commit'); + + $this->resourceConnectionMock->expects($this->at(1)) + ->method('getConnection') + ->with('checkout') + ->willReturn($adapterMockForCheckout); + $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); + $adapterMockForCheckout->expects($this->once())->method('commit'); + $this->billingAddressManagementMock->expects($this->once()) ->method('assign') ->with($cartId, $billingAddressMock); @@ -114,6 +148,27 @@ public function testSavePaymentInformationAndPlaceOrderException() $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); + $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $adapterMockForCheckout = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resourceConnectionMock->expects($this->at(0)) + ->method('getConnection') + ->with('sales') + ->willReturn($adapterMockForSales); + $adapterMockForSales->expects($this->once())->method('beginTransaction'); + $adapterMockForSales->expects($this->once())->method('rollback'); + + $this->resourceConnectionMock->expects($this->at(1)) + ->method('getConnection') + ->with('checkout') + ->willReturn($adapterMockForCheckout); + $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); + $adapterMockForCheckout->expects($this->once())->method('rollback'); + $this->billingAddressManagementMock->expects($this->once()) ->method('assign') ->with($cartId, $billingAddressMock); @@ -176,6 +231,27 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); + $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $adapterMockForCheckout = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resourceConnectionMock->expects($this->at(0)) + ->method('getConnection') + ->with('sales') + ->willReturn($adapterMockForSales); + $adapterMockForSales->expects($this->once())->method('beginTransaction'); + $adapterMockForSales->expects($this->once())->method('rollback'); + + $this->resourceConnectionMock->expects($this->at(1)) + ->method('getConnection') + ->with('checkout') + ->willReturn($adapterMockForCheckout); + $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); + $adapterMockForCheckout->expects($this->once())->method('rollback'); + $this->billingAddressManagementMock->expects($this->once()) ->method('assign') ->with($cartId, $billingAddressMock); From 2e6357dfc410999b33186168cb276bd040e69264 Mon Sep 17 00:00:00 2001 From: Dmytro Voskoboinikov Date: Tue, 2 Jan 2018 13:56:29 +0200 Subject: [PATCH 6/7] MAGETWO-69701: [GitHub] Concurrent checkouts can lead to negative stock #6363 [backport 2.1] --- app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php | 2 +- app/code/Magento/CatalogInventory/Model/StockManagement.php | 2 +- .../Test/Unit/Model/ResourceModel/StockTest.php | 2 +- .../Checkout/Model/GuestPaymentInformationManagement.php | 2 +- .../Test/Unit/Model/GuestPaymentInformationManagementTest.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php index 5160010108582..1f01400468984 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php @@ -1,6 +1,6 @@ Date: Tue, 2 Jan 2018 14:00:49 +0200 Subject: [PATCH 7/7] MAGETWO-59163: Category product count incorporating products with visibility set to search only --- .../CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php | 2 +- .../CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php | 2 +- .../CatalogSearch/Model/ResourceModel/Fulltext/Collection.php | 2 +- .../Model/Search/FilterMapper/ExclusionStrategy.php | 2 +- .../Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php | 2 +- .../Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php | 2 +- .../testsuite/Magento/Framework/Api/Search/SearchTest.php | 2 +- .../Model/ResourceModel/Fulltext/CollectionTest.php | 2 +- lib/internal/Magento/Framework/Search/Search.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php index d649e7e09c009..0a787254ef57b 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php @@ -1,6 +1,6 @@