From 9fc50a8a9a1f7555fca1ae049ab75bbad7623989 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Wed, 26 Apr 2017 11:41:17 +0300 Subject: [PATCH 01/19] MAGETWO-62630: [Backport] - Products are missed and total count is wrong on category page - for 2.1 --- .../Category/Product/AbstractAction.php | 45 +++- .../Framework/DB/Adapter/Pdo/Mysql.php | 1 - .../Framework/DB/Query/BatchIterator.php | 2 +- .../DB/Query/BatchIteratorFactory.php | 4 +- .../DB/Query/BatchIteratorInterface.php | 68 ++++++ .../Framework/DB/Query/BatchRangeIterator.php | 208 ++++++++++++++++++ .../Magento/Framework/DB/Query/Generator.php | 118 +++++++++- .../Unit/DB/Query/BatchRangeIteratorTest.php | 123 +++++++++++ .../Test/Unit/DB/Query/GeneratorTest.php | 48 +++- 9 files changed, 593 insertions(+), 24 deletions(-) create mode 100644 lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php create mode 100644 lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php create mode 100644 lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index feb9f3bf8c343..250ea1d477a34 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Model\Indexer\Category\Product; +use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; @@ -102,20 +103,29 @@ abstract class AbstractAction */ protected $tempTreeIndexTableName; + /** + * @var QueryGenerator + */ + private $queryGenerator; + /** * @param ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Config $config + * @param QueryGenerator $queryGenerator */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config + \Magento\Catalog\Model\Config $config, + QueryGenerator $queryGenerator = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); $this->storeManager = $storeManager; $this->config = $config; + $this->queryGenerator = $queryGenerator ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(QueryGenerator::class); } /** @@ -302,22 +312,35 @@ protected function isRangingNeeded() } /** - * Return selects cut by min and max + * Return selects cut by min and max. * * @param \Magento\Framework\DB\Select $select * @param string $field * @param int $range * @return \Magento\Framework\DB\Select[] */ - protected function prepareSelectsByRange(\Magento\Framework\DB\Select $select, $field, $range = self::RANGE_CATEGORY_STEP) - { - return $this->isRangingNeeded() ? $this->connection->selectsByRange( - $field, - $select, - $range - ) : [ - $select - ]; + protected function prepareSelectsByRange( + \Magento\Framework\DB\Select $select, + $field, + $range = self::RANGE_CATEGORY_STEP + ) { + if ($this->isRangingNeeded()) { + $iterator = $this->queryGenerator->generate( + $field, + $select, + $range, + \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + ); + + $queries = []; + foreach ($iterator as $query) { + $queries[] = $query; + } + + return $queries; + } + + return [$select]; } /** diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 3313bd25f06f8..a43f39a5be1a8 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -1350,7 +1350,6 @@ protected function _removeDuplicateEntry($table, $fields, $ids) */ public function select() { -// return new Select($this); return $this->selectFactory->create($this); } diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php index 4235eeb7a65e9..76e46d72d5c9c 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php @@ -11,7 +11,7 @@ /** * Query batch iterator */ -class BatchIterator implements \Iterator +class BatchIterator implements BatchIteratorInterface { /** * @var int diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php b/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php index 147e65b8df466..8a597512554e9 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIteratorFactory.php @@ -39,10 +39,10 @@ public function __construct( } /** - * Create class instance with specified parameters + * Create class instance with specified parameters. * * @param array $data - * @return \Magento\Framework\DB\Query\BatchIterator + * @return \Magento\Framework\DB\Query\BatchIteratorInterface */ public function create(array $data = []) { diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php new file mode 100644 index 0000000000000..9ab94a4c5ac0a --- /dev/null +++ b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php @@ -0,0 +1,68 @@ +batchSize = $batchSize; + $this->select = $select; + $this->correlationName = $correlationName; + $this->rangeField = $rangeField; + $this->rangeFieldAlias = $rangeFieldAlias; + $this->connection = $select->getConnection(); + } + + /** + * Return the current element. + * + * If we don't have sub-select we should create and remember it. + * + * @return Select + */ + public function current() + { + if (null === $this->currentSelect) { + $this->isValid = ($this->currentBatch + $this->batchSize) < $this->totalItemCount; + $this->currentSelect = $this->initSelectObject(); + } + return $this->currentSelect; + } + + /** + * Return the key of the current element. + * + * Сan return the number of the current sub-select in the iteration. + * + * @return int + */ + public function key() + { + return $this->iteration; + } + + /** + * Move forward to next sub-select + * + * Retrieve the next sub-select and move cursor to the next element. + * Checks that the count of elements more than the sum of limit and offset. + * + * @return Select + */ + public function next() + { + if (null === $this->currentSelect) { + $this->current(); + } + $this->isValid = ($this->batchSize + $this->currentBatch) < $this->totalItemCount; + $select = $this->initSelectObject(); + if ($this->isValid) { + $this->iteration++; + $this->currentSelect = $select; + } else { + $this->currentSelect = null; + } + return $this->currentSelect; + } + + /** + * Rewind the BatchRangeIterator to the first element. + * + * Allows to start iteration from the beginning. + * + * @return void + */ + public function rewind() + { + $this->currentSelect = null; + $this->iteration = 0; + $this->isValid = true; + $this->totalItemCount = 0; + } + + /** + * Checks if current position is valid. + * + * @return bool + */ + public function valid() + { + return $this->isValid; + } + + /** + * Initialize select object. + * + * Return sub-select which is limited by current batch value and return items from n page of SQL request. + * + * @return \Magento\Framework\DB\Select + */ + private function initSelectObject() + { + $object = clone $this->select; + + if (!$this->totalItemCount) { + $wrapperSelect = $this->connection->select(); + $wrapperSelect->from( + $object, + [ + new \Zend_Db_Expr('COUNT(*) as cnt') + ] + ); + $row = $this->connection->fetchRow($wrapperSelect); + + $this->totalItemCount = intval($row['cnt']); + } + + //Reset sort order section from origin select object. + $object->order($this->correlationName . '.' . $this->rangeField . ' ' . \Magento\Framework\DB\Select::SQL_ASC); + $object->limit($this->currentBatch, $this->batchSize); + $this->currentBatch += $this->batchSize; + + return $object; + } +} diff --git a/lib/internal/Magento/Framework/DB/Query/Generator.php b/lib/internal/Magento/Framework/DB/Query/Generator.php index 909f4752397a3..49ec98c317976 100644 --- a/lib/internal/Magento/Framework/DB/Query/Generator.php +++ b/lib/internal/Magento/Framework/DB/Query/Generator.php @@ -17,27 +17,61 @@ class Generator */ private $iteratorFactory; + /** + * @var \Magento\Framework\DB\Query\BatchRangeIteratorFactory + */ + private $rangeIteratorFactory; + /** * Initialize dependencies. * * @param BatchIteratorFactory $iteratorFactory + * @param BatchRangeIteratorFactory $rangeIteratorFactory */ - public function __construct(BatchIteratorFactory $iteratorFactory) - { + public function __construct( + BatchIteratorFactory $iteratorFactory, + BatchRangeIteratorFactory $rangeIteratorFactory = null + ) { $this->iteratorFactory = $iteratorFactory; + $this->rangeIteratorFactory = $rangeIteratorFactory ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\DB\Query\BatchRangeIteratorFactory::class); } /** * Generate select query list with predefined items count in each select item. * - * @param string $rangeField + * Generates select parameters - batchSize, correlationName, rangeField, rangeFieldAlias + * to obtain instance of iterator. The behavior of the iterator will depend on the parameters passed to it. + * For example: by default for $batchStrategy parameter used + * \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR. This parameter is determine, what + * instance of Iterator will be returned. + * + * Other params: + * select - represents the select object, that should be passed into Iterator. + * batchSize - sets the number of items in select. + * correlationName - is the base table involved in the select. + * rangeField - this is the basic field which used to split select. + * rangeFieldAlias - alias of range field. + * + * @see \Magento\Framework\DB\Query\BatchIteratorInterface + * @param string $rangeField - Field which is used for the range mechanism in select * @param \Magento\Framework\DB\Select $select - * @param int $batchSize - * @return BatchIterator - * @throws LocalizedException + * @param int $batchSize - Determines on how many parts will be divided + * the number of values in the select. + * @param string $batchStrategy It determines which strategy is chosen + * @return BatchIteratorInterface + * @throws LocalizedException Throws if incorrect "FROM" part in \Select exists */ - public function generate($rangeField, \Magento\Framework\DB\Select $select, $batchSize = 100) - { + public function generate( + $rangeField, + \Magento\Framework\DB\Select $select, + $batchSize = 100, + $batchStrategy = \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR + ) { + if ($batchStrategy == \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR) { + return $this->generateByRange($rangeField, $select, $batchSize); + } + $fromSelect = $select->getPart(\Magento\Framework\DB\Select::FROM); if (empty($fromSelect)) { throw new LocalizedException( @@ -76,4 +110,72 @@ public function generate($rangeField, \Magento\Framework\DB\Select $select, $bat ] ); } + + /** + * Generate select query list with predefined items count in each select item. + * + * Generates select parameters - batchSize, correlationName, rangeField, rangeFieldAlias + * to obtain instance of BatchRangeIterator. + * + * Other params: + * select - represents the select object, that should be passed into Iterator. + * batchSize - sets the number of items in select. + * correlationName - is the base table involved in the select. + * rangeField - this is the basic field which used to split select. + * rangeFieldAlias - alias of range field. + * + * @see BatchRangeIterator + * @param string $rangeField - Field which is used for the range mechanism in select. + * @param \Magento\Framework\DB\Select $select + * @param int $batchSize + * @return BatchIteratorInterface + * @throws LocalizedException Throws if incorrect "FROM" part in \Select exists + * @see \Magento\Framework\DB\Query\Generator + * @deprecated This is a temporary solution which is made due to the fact that we + * can't change method generate() in version 2.1 due to a backwards incompatibility. + * In 2.2 version need to use original method generate() with additional parameter. + */ + public function generateByRange( + $rangeField, + \Magento\Framework\DB\Select $select, + $batchSize = 100 + ) { + $fromSelect = $select->getPart(\Magento\Framework\DB\Select::FROM); + if (empty($fromSelect)) { + throw new LocalizedException( + new \Magento\Framework\Phrase('Select object must have correct "FROM" part') + ); + } + + $fieldCorrelationName = ''; + foreach ($fromSelect as $correlationName => $fromPart) { + if ($fromPart['joinType'] == \Magento\Framework\DB\Select::FROM) { + $fieldCorrelationName = $correlationName; + break; + } + } + + $columns = $select->getPart(\Magento\Framework\DB\Select::COLUMNS); + /** + * Calculate $rangeField alias + */ + $rangeFieldAlias = $rangeField; + foreach ($columns as $column) { + list($table, $columnName, $alias) = $column; + if ($table == $fieldCorrelationName && $columnName == $rangeField) { + $rangeFieldAlias = $alias ?: $rangeField; + break; + } + } + + return $this->rangeIteratorFactory->create( + [ + 'select' => $select, + 'batchSize' => $batchSize, + 'correlationName' => $fieldCorrelationName, + 'rangeField' => $rangeField, + 'rangeFieldAlias' => $rangeFieldAlias, + ] + ); + } } diff --git a/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php new file mode 100644 index 0000000000000..18e91e0264738 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchRangeIteratorTest.php @@ -0,0 +1,123 @@ +batchSize = 10; + $this->currentBatch = 0; + $this->correlationName = 'correlationName'; + $this->rangeField = 'rangeField'; + $this->rangeFieldAlias = 'rangeFieldAlias'; + + $this->selectMock = $this->getMock(Select::class, [], [], '', false, false); + $this->wrapperSelectMock = $this->getMock(Select::class, [], [], '', false, false); + $this->connectionMock = $this->getMock(AdapterInterface::class); + $this->connectionMock->expects($this->any())->method('select')->willReturn($this->wrapperSelectMock); + $this->selectMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); + $this->connectionMock->expects($this->any())->method('quoteIdentifier')->willReturnArgument(0); + + $this->model = new BatchRangeIterator( + $this->selectMock, + $this->batchSize, + $this->correlationName, + $this->rangeField, + $this->rangeFieldAlias + ); + } + + /** + * Test steps: + * 1. $iterator->current(); + * 2. $iterator->key(); + * + * @return void + */ + public function testCurrent() + { + $this->selectMock->expects($this->once())->method('limit')->with($this->currentBatch, $this->batchSize); + $this->selectMock->expects($this->once())->method('order')->with('correlationName.rangeField' . ' ASC'); + + $this->assertEquals($this->selectMock, $this->model->current()); + $this->assertEquals(0, $this->model->key()); + } + + /** + * Test the separation of batches. + */ + public function testIterations() + { + $iterations = 0; + + $this->connectionMock->expects($this->once()) + ->method('fetchRow') + ->willReturn(['cnt' => 105]); + + foreach ($this->model as $key) { + $this->assertEquals($this->selectMock, $key); + $iterations++; + }; + + $this->assertEquals(10, $iterations); + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php b/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php index fcfaa41ffb4e6..ac658930c47ec 100644 --- a/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/DB/Query/GeneratorTest.php @@ -7,6 +7,7 @@ use Magento\Framework\DB\Query\Generator; use Magento\Framework\DB\Query\BatchIteratorFactory; +use Magento\Framework\DB\Query\BatchRangeIteratorFactory; use Magento\Framework\DB\Select; use Magento\Framework\DB\Query\BatchIterator; @@ -32,15 +33,21 @@ class GeneratorTest extends \PHPUnit_Framework_TestCase */ private $iteratorMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $rangeFactoryMock; + /** * Setup test dependencies. */ protected function setUp() { $this->factoryMock = $this->getMock(BatchIteratorFactory::class, [], [], '', false, false); + $this->rangeFactoryMock = $this->getMock(BatchRangeIteratorFactory::class, ['create'], [], '', false, false); $this->selectMock = $this->getMock(Select::class, [], [], '', false, false); $this->iteratorMock = $this->getMock(BatchIterator::class, [], [], '', false, false); - $this->model = new Generator($this->factoryMock); + $this->model = new Generator($this->factoryMock, $this->rangeFactoryMock); } /** @@ -165,4 +172,43 @@ public function testGenerateWithInvalidWithWildcard() )->willReturn($this->iteratorMock); $this->assertEquals($this->iteratorMock, $this->model->generate('entity_id', $this->selectMock, 100)); } + + /** + * Test success generate with non-unique strategy. + * @return void + */ + public function testGenerateWithNonUniqueStrategy() + { + $map = [ + [ + Select::FROM, + [ + 'cp' => ['joinType' => Select::FROM] + ] + ], + [ + Select::COLUMNS, + [ + ['cp', 'entity_id', 'product_id'] + ] + ] + ]; + + $this->selectMock->expects($this->exactly(2))->method('getPart')->willReturnMap($map); + + $this->factoryMock->expects($this->once())->method('create')->with( + [ + 'select' => $this->selectMock, + 'batchSize' => 100, + 'correlationName' => 'cp', + 'rangeField' => 'entity_id', + 'rangeFieldAlias' => 'product_id' + ] + )->willReturn($this->iteratorMock); + + $this->assertEquals( + $this->iteratorMock, + $this->model->generate('entity_id', $this->selectMock, 100, 'non_unique') + ); + } } From 66240af7e6895c38f031d53f0f3c7b695238ea30 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Wed, 26 Apr 2017 14:42:44 +0300 Subject: [PATCH 02/19] MAGETWO-65249: [Backport] - [Performance] Segmentation fault when doing catalogsearch_fulltext reindex - for 2.1 --- .../Model/Indexer/Fulltext/Action/Full.php | 187 ++++++++++++++++-- .../Indexer/Fulltext/Action/IndexIterator.php | 12 ++ .../Indexer/Fulltext/Action/FullTest.php | 85 +++++++- .../product_configurable_not_available.php | 105 ++++++++++ ...ct_configurable_not_available_rollback.php | 34 ++++ .../_files/products_for_index.php | 77 ++++++++ .../_files/products_for_index_rollback.php | 40 ++++ .../_files/configurable_products.php | 161 +++++++++++++++ .../_files/configurable_products_rollback.php | 35 ++++ .../Search/_files/configurable_attribute.php | 60 ++++++ .../configurable_attribute_rollback.php | 30 +++ .../Search/_files/product_configurable.php | 120 +++++++++++ .../_files/product_configurable_rollback.php | 31 +++ 13 files changed, 949 insertions(+), 28 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index 43a9549c46293..e08954806da29 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -6,9 +6,12 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; /** + * Class provides iterator through number of products suitable for fulltext indexation + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -30,6 +33,8 @@ class Full * Index values separator * * @var string + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$separator */ protected $separator = ' | '; @@ -37,6 +42,7 @@ class Full * Array of \DateTime objects per store * * @var \DateTime[] + * @deprecated Not used anymore */ protected $dates = []; @@ -44,6 +50,8 @@ class Full * Product Type Instances cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productTypes */ protected $productTypes = []; @@ -51,6 +59,8 @@ class Full * Product Emulators cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productEmulators */ protected $productEmulators = []; @@ -77,6 +87,8 @@ class Full * Catalog product type * * @var \Magento\Catalog\Model\Product\Type + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$catalogProductType */ protected $catalogProductType; @@ -91,6 +103,7 @@ class Full * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @deprecated Not used anymore */ protected $scopeConfig; @@ -98,6 +111,8 @@ class Full * Store manager * * @var \Magento\Store\Model\StoreManagerInterface + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$storeManager */ protected $storeManager; @@ -108,21 +123,25 @@ class Full /** * @var \Magento\Framework\Indexer\SaveHandler\IndexerInterface + * @deprecated As part of self::cleanIndex() */ protected $indexHandler; /** * @var \Magento\Framework\Stdlib\DateTime + * @deprecated Not used anymore */ protected $dateTime; /** * @var \Magento\Framework\Locale\ResolverInterface + * @deprecated Not used anymore */ protected $localeResolver; /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @deprecated Not used anymore */ protected $localeDate; @@ -133,16 +152,19 @@ class Full /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext + * @deprecated Not used anymore */ protected $fulltextResource; /** * @var \Magento\Framework\Search\Request\Config + * @deprecated As part of self::reindexAll() */ protected $searchRequestConfig; /** * @var \Magento\Framework\Search\Request\DimensionFactory + * @deprecated As part of self::cleanIndex() */ private $dimensionFactory; @@ -153,6 +175,8 @@ class Full /** * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory + * @deprecated DataProvider used directly without IndexIterator + * @see self::$dataProvider */ private $iteratorFactory; @@ -161,6 +185,11 @@ class Full */ private $metadataPool; + /** + * @var DataProvider + */ + private $dataProvider; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -181,6 +210,7 @@ class Full * @param \Magento\Framework\Indexer\ConfigInterface $indexerConfig * @param \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param DataProvider $dataProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -202,7 +232,8 @@ public function __construct( \Magento\Framework\Search\Request\DimensionFactory $dimensionFactory, \Magento\Framework\Indexer\ConfigInterface $indexerConfig, \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null + \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + DataProvider $dataProvider = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -223,13 +254,16 @@ public function __construct( $this->fulltextResource = $fulltextResource; $this->dimensionFactory = $dimensionFactory; $this->iteratorFactory = $indexIteratorFactory; - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->dataProvider = $dataProvider ?: ObjectManager::getInstance()->get(DataProvider::class); } /** * Rebuild whole fulltext index for all stores * + * @deprecated Please use \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull instead + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull * @return void */ public function reindexAll() @@ -282,12 +316,14 @@ protected function getProductIdsFromParents(array $entityIds) /** * Regenerate search index for specific store * + * To be suitable for indexation product must meet set of requirements: + * - to be visible on frontend + * - to be enabled + * - in case product is composite at least one sub product must be enabled + * * @param int $storeId Store View Id - * @param int|array $productIds Product Entity Id + * @param int[] $productIds Product Entity Id * @return \Generator - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function rebuildStoreIndex($storeId, $productIds = null) { @@ -307,27 +343,129 @@ public function rebuildStoreIndex($storeId, $productIds = null) 'datetime' => array_keys($this->getSearchableAttributes('datetime')), ]; - // status and visibility filter + $lastProductId = 0; + do { + $products = $this->dataProvider + ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId); + + $productsIds = array_column($products, 'entity_id'); + $relatedProducts = $this->getRelatedProducts($products); + $productsIds = array_merge($productsIds, array_values($relatedProducts)); + + $productsAttributes = $this->dataProvider->getProductAttributes($storeId, $productsIds, $dynamicFields); + + foreach ($products as $productData) { + $lastProductId = $productData['entity_id']; + + if (!$this->isProductVisible($productData['entity_id'], $productsAttributes) || + !$this->isProductEnabled($productData['entity_id'], $productsAttributes) + ) { + continue; + } + + $productIndex = [$productData['entity_id'] => $productsAttributes[$productData['entity_id']]]; + if (isset($relatedProducts[$productData['entity_id']])) { + $childProductsIndex = $this->getChildProductsIndex( + $productData['entity_id'], + $relatedProducts, + $productsAttributes + ); + if (empty($childProductsIndex)) { + continue; + } + $productIndex = $productIndex + $childProductsIndex; + } + + $index = $this->dataProvider->prepareProductIndex($productIndex, $productData, $storeId); + yield $productData['entity_id'] => $index; + } + } while (count($products) > 0); + } + + /** + * Get related (child) products ids + * + * Load related (child) products ids for composite product type + * + * @param array $products + * @return array + */ + private function getRelatedProducts($products) + { + $relatedProducts = []; + foreach ($products as $productData) { + $relatedProducts[$productData['entity_id']] = $this->dataProvider->getProductChildIds( + $productData['entity_id'], + $productData['type_id'] + ); + } + return array_filter($relatedProducts); + } + + /** + * Performs check that product is visible on Store Front + * + * Check that product is visible on Store Front using visibility attribute + * and allowed visibility values. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductVisible($productId, array $productsAttributes) + { $visibility = $this->getSearchableAttribute('visibility'); - $status = $this->getSearchableAttribute('status'); - $statusIds = $this->catalogProductStatus->getVisibleStatusIds(); $allowedVisibility = $this->engine->getAllowedVisibility(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$visibility->getId()]) && + in_array($productsAttributes[$productId][$visibility->getId()], $allowedVisibility); + } - return $this->iteratorFactory->create([ - 'storeId' => $storeId, - 'productIds' => $productIds, - 'staticFields' => $staticFields, - 'dynamicFields' => $dynamicFields, - 'visibility' => $visibility, - 'allowedVisibility' => $allowedVisibility, - 'status' => $status, - 'statusIds' => $statusIds - ]); + /** + * Performs check that product is enabled on Store Front + * + * Check that product is enabled on Store Front using status attribute + * and statuses allowed to be visible on Store Front. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductEnabled($productId, array $productsAttributes) + { + $status = $this->getSearchableAttribute('status'); + $allowedStatuses = $this->catalogProductStatus->getVisibleStatusIds(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$status->getId()]) && + in_array($productsAttributes[$productId][$status->getId()], $allowedStatuses); + } + + /** + * Get data for index using related(child) products data + * + * Build data for index using child products(if any). + * Use only enabled child products {@see isProductEnabled}. + * + * @param int $parentId + * @param array $relatedProducts + * @param array $productsAttributes + * @return array + */ + private function getChildProductsIndex($parentId, array $relatedProducts, array $productsAttributes) + { + $productIndex = []; + foreach ($relatedProducts[$parentId] as $productChildId) { + if ($this->isProductEnabled($productChildId, $productsAttributes)) { + $productIndex[$productChildId] = $productsAttributes[$productChildId]; + } + } + return $productIndex; } /** * Clean search index data for store * + * @deprecated As part of self::reindexAll() * @param int $storeId * @return void */ @@ -341,6 +479,7 @@ protected function cleanIndex($storeId) * Retrieve EAV Config Singleton * * @return \Magento\Eav\Model\Config + * @deprecated Use $self::$eavConfig directly */ protected function getEavConfig() { @@ -369,7 +508,7 @@ protected function getSearchableAttributes($backendType = null) ['engine' => $this->engine, 'attributes' => $attributes] ); - $entity = $this->getEavConfig()->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); + $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); @@ -413,12 +552,14 @@ protected function getSearchableAttribute($attribute) } } - return $this->getEavConfig()->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); } /** * Returns expression for field unification * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::unifyField() * @param string $field * @param string $backendType * @return \Zend_Db_Expr @@ -436,6 +577,8 @@ protected function unifyField($field, $backendType = 'varchar') /** * Retrieve Product Type Instance * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductTypeInstance() * @param string $typeId * @return \Magento\Catalog\Model\Product\Type\AbstractType */ @@ -452,6 +595,8 @@ protected function getProductTypeInstance($typeId) /** * Retrieve Product Emulator (Magento Object) * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductEmulator() * @param string $typeId * @return \Magento\Framework\DataObject */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php index ef223a774cb97..7b98257bd9c1f 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php @@ -10,6 +10,8 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @deprecated No more used + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full */ class IndexIterator implements \Iterator { @@ -133,6 +135,8 @@ public function __construct( /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function current() { @@ -141,6 +145,8 @@ public function current() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function next() { @@ -237,6 +243,8 @@ public function next() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function key() { @@ -245,6 +253,8 @@ public function key() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function valid() { @@ -253,6 +263,8 @@ public function valid() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function rewind() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 8ee4f8dd7d61b..174d1f015fc58 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -5,26 +5,97 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogSearch\Model\ResourceModel\Engine; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; -/** - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - */ class FullTest extends \PHPUnit_Framework_TestCase { /** * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full */ - protected $fullAction; + protected $actionFull; protected function setUp() { - $this->fullAction = Bootstrap::getObjectManager()->create( + $this->actionFull = Bootstrap::getObjectManager()->create( \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class ); } + /** + * @magentoDataFixture Magento/CatalogSearch/_files/products_for_index.php + * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_not_available.php + * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php + */ + public function testGetIndexData() + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $allowedStatuses = Bootstrap::getObjectManager()->get(Status::class)->getVisibleStatusIds(); + $allowedVisibility = Bootstrap::getObjectManager()->get(Engine::class)->getAllowedVisibility(); + + $result = iterator_to_array($this->actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); + $this->assertNotEmpty($result); + + $productsIds = array_keys($result); + foreach ($productsIds as $productId) { + $product = $productRepository->getById($productId); + $this->assertContains($product->getVisibility(), $allowedVisibility); + $this->assertContains($product->getStatus(), $allowedStatuses); + } + + $expectedData = $this->getExpectedIndexData(); + foreach ($expectedData as $sku => $expectedIndexData) { + $product = $productRepository->get($sku); + $this->assertEquals($expectedIndexData, $result[$product->getId()]); + } + } + + /** + * @return array + */ + private function getExpectedIndexData() + { + /** @var ProductAttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->get(ProductAttributeRepositoryInterface::class); + $skuId = $attributeRepository->get(ProductInterface::SKU)->getAttributeId(); + $nameId = $attributeRepository->get(ProductInterface::NAME)->getAttributeId(); + /** @see dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php */ + $configurableId = $attributeRepository->get('test_configurable_searchable')->getAttributeId(); + return [ + 'configurable_searchable' => [ + $skuId => 'configurable_searchable', + $configurableId => 'Option 1 | Option 2', + $nameId => 'Configurable Product | Configurable OptionOption 1 | Configurable OptionOption 2', + ], + 'index_enabled' => [ + $skuId => 'index_enabled', + $nameId => 'index enabled', + ], + 'index_visible_search' => [ + $skuId => 'index_visible_search', + $nameId => 'index visible search', + ], + 'index_visible_category' => [ + $skuId => 'index_visible_category', + $nameId => 'index visible category', + ], + 'index_visible_both' => [ + $skuId => 'index_visible_both', + $nameId => 'index visible both', + ] + ]; + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ public function testRebuildStoreIndexConfigurable() { $storeId = 1; @@ -36,8 +107,8 @@ public function testRebuildStoreIndexConfigurable() $simpleProductId, $configProductId ]; - $storeIndexDataSimple = $this->fullAction->rebuildStoreIndex($storeId, [$simpleProductId]); - $storeIndexDataExpected = $this->fullAction->rebuildStoreIndex($storeId, $expected); + $storeIndexDataSimple = $this->actionFull->rebuildStoreIndex($storeId, [$simpleProductId]); + $storeIndexDataExpected = $this->actionFull->rebuildStoreIndex($storeId, $expected); $this->assertEquals($storeIndexDataSimple, $storeIndexDataExpected); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php new file mode 100644 index 0000000000000..a5d5e42e077f8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available.php @@ -0,0 +1,105 @@ +reinitialize(); + +require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productsSku = [1110, 1120]; +array_shift($options); //remove the first option which is empty + +$isFirstOption = true; +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productSku = array_shift($productsSku); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_not_avalilable_' . $productSku) + ->setPrice(11) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_DISABLED); + $product = $productRepository->save($product); + + /** @var StockItemInterface $stockItem */ + $stockItem = $product->getExtensionAttributes()->getStockItem(); + $stockItem->setUseConfigManageStock(1)->setIsInStock(true)->setQty(100)->setIsQtyDecimal(0); + + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $isFirstOption = false; +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($attributeSetId) + ->setName('Configurable Product Disable Child Products') + ->setSku('configurable_not_available') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); +$product = $productRepository->save($product); + +/** @var StockItemInterface $stockItem */ +$stockItem = $product->getExtensionAttributes()->getStockItem(); +$stockItem->setUseConfigManageStock(1)->setIsInStock(1); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php new file mode 100644 index 0000000000000..3cca01c289362 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_not_available_rollback.php @@ -0,0 +1,34 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$productSkus = ['simple_not_avalilable_1110', 'simple_not_avalilable_1120', 'configurable_not_available']; +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, $productSkus, 'in'); +$result = $productRepository->getList($searchCriteriaBuilder->create()); +foreach ($result->getItems() as $product) { + $productRepository->delete($product); +} + +require __DIR__ . '/../../../Magento/Framework/Search/_files/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php new file mode 100644 index 0000000000000..6d6c658c1390a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index.php @@ -0,0 +1,77 @@ +get(ProductRepositoryInterface::class); +$products = [ + [ + 'name' => 'index enabled', + 'sku' => 'index_enabled', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + ], + [ + 'name' => 'index disabled', + 'sku' => 'index_disabled', + 'status' => Status::STATUS_DISABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + ], + [ + 'name' => 'index visible search', + 'sku' => 'index_visible_search', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_IN_SEARCH, + ], + [ + 'name' => 'index visible category', + 'sku' => 'index_visible_category', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_IN_CATALOG, + ], + [ + 'name' => 'index visible both', + 'sku' => 'index_visible_both', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, + ], + [ + + 'name' => 'index not visible', + 'sku' => 'index_not_visible', + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_NOT_VISIBLE, + + ] +]; + +/** @var $productFactory ProductInterfaceFactory */ +$productFactory = Bootstrap::getObjectManager()->create(ProductInterfaceFactory::class); +foreach ($products as $data) { + /** @var ProductInterface $product */ + $product = $productFactory->create(); + $product + ->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName($data['name']) + ->setSku($data['sku']) + ->setPrice(10) + ->setVisibility($data['visibility']) + ->setStatus($data['status']); + $product = $productRepository->save($product); + + /** @var StockItemInterface $stockItem */ + $stockItem = $product->getExtensionAttributes()->getStockItem(); + $stockItem->setUseConfigManageStock(0); + $productRepository->save($product); +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php new file mode 100644 index 0000000000000..057d2e63aacc6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_index_rollback.php @@ -0,0 +1,40 @@ +getInstance()->reinitialize(); + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + +$productSkus = [ + 'index_enabled', + 'index_disabled', + 'index_visible_search', + 'index_visible_category', + 'index_visible_both', + 'index_not_visible' +]; +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, $productSkus, 'in'); +$result = $productRepository->getList($searchCriteriaBuilder->create()); +foreach ($result->getItems() as $product) { + $productRepository->delete($product); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php new file mode 100644 index 0000000000000..bcb5de33dea25 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php @@ -0,0 +1,161 @@ +get(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [30, 40]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(11) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product 12345') + ->setSku('configurable_12345') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php new file mode 100644 index 0000000000000..685e5f3a2e883 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_rollback.php @@ -0,0 +1,35 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable', 'simple_30', 'simple_40', 'configurable_12345'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php new file mode 100644 index 0000000000000..fab31aa9d79a8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php @@ -0,0 +1,60 @@ +get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_searchable'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable_searchable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] + ); + + $attributeRepository->save($attribute); +} + +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php new file mode 100644 index 0000000000000..70dc0352b938b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php @@ -0,0 +1,30 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_searchable'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php new file mode 100644 index 0000000000000..0a1d018a8ea0d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php @@ -0,0 +1,120 @@ +reinitialize(); + +require __DIR__ . '/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10010, 10020]; +array_shift($options); //remove the first option which is empty + +$isFirstOption = true; +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurableSearchable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => (int)!$isFirstOption, + ] + ); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock((int)!$isFirstOption); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $isFirstOption = false; +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(10001) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable_searchable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php new file mode 100644 index 0000000000000..3262b6c9da35c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php @@ -0,0 +1,31 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10010', 'simple_10020', 'configurable_searchable'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 5b98a0d10d57154139f60e344e7401082cf95f9e Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Wed, 26 Apr 2017 15:15:00 +0300 Subject: [PATCH 03/19] MAGETWO-65249: [Backport] - [Performance] Segmentation fault when doing catalogsearch_fulltext reindex - for 2.1 --- .../Model/Indexer/Fulltext/Action/IndexIterator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php index 7b98257bd9c1f..7f4b5f517ac9d 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php @@ -132,7 +132,6 @@ public function __construct( $this->statusIds = $statusIds; } - /** * {@inheritDoc} * From 999bdd934ee53843f46444f189671accbd885488 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Wed, 26 Apr 2017 18:06:13 +0300 Subject: [PATCH 04/19] MAGETWO-62630: [Backport] - Products are missed and total count is wrong on category page - for 2.1 --- .../Magento/Framework/DB/Query/BatchIterator.php | 10 +++++----- .../Framework/DB/Query/BatchIteratorInterface.php | 8 ++++---- .../Magento/Framework/DB/Query/BatchRangeIterator.php | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php index 76e46d72d5c9c..f6caf63b84d91 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php @@ -88,7 +88,7 @@ public function __construct( } /** - * @return Select + * {@inheritdoc} */ public function current() { @@ -101,7 +101,7 @@ public function current() } /** - * @return Select + * {@inheritdoc} */ public function next() { @@ -121,7 +121,7 @@ public function next() } /** - * @return int + * {@inheritdoc} */ public function key() { @@ -129,7 +129,7 @@ public function key() } /** - * @return bool + * {@inheritdoc} */ public function valid() { @@ -137,7 +137,7 @@ public function valid() } /** - * @return void + * {@inheritdoc} */ public function rewind() { diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php index 9ab94a4c5ac0a..a78cfebf3659d 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIteratorInterface.php @@ -11,13 +11,13 @@ interface BatchIteratorInterface extends \Iterator { /** - * Constant which determine strategy to create iterator which will to process + * Constant which determines strategy to create iterator which will process * range field eg. entity_id with unique values. */ const UNIQUE_FIELD_ITERATOR = "unique"; /** - * Constant which determine strategy to create iterator which will to process + * Constant which determines strategy to create iterator which will process * range field with non-unique values. */ const NON_UNIQUE_FIELD_ITERATOR = "non_unqiue"; @@ -25,7 +25,7 @@ interface BatchIteratorInterface extends \Iterator /** * Return the current element. * - * If we don't have sub-select we should create and remember it. + * If we don't have sub-select, we should create and remember it. * * @return \Magento\Framework\DB\Select */ @@ -34,7 +34,7 @@ public function current(); /** * Return the key of the current element. * - * Сan return the number of the current sub-select in the iteration. + * Сan return the number of the current sub-selects in the iteration. * * @return int */ diff --git a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php index 3a9546e61f72e..bd61f2528301f 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php @@ -12,8 +12,8 @@ /** * Query batch range iterator. * - * It is uses to processing selects which will obtain values from $rangeField with relation one-to-many. - * This iterator make chunks with operator LIMIT...OFFSET, + * It is used for processing selects which will obtain values from $rangeField with relation one-to-many. + * This iterator makes chunks with operator LIMIT...OFFSET, * starting with zero offset and finishing on OFFSET + LIMIT = TOTAL_COUNT. * * @see \Magento\Framework\DB\Query\Generator @@ -116,7 +116,7 @@ public function current() /** * Return the key of the current element. * - * Сan return the number of the current sub-select in the iteration. + * Сan return the number of the current sub-selects in the iteration. * * @return int */ @@ -126,10 +126,10 @@ public function key() } /** - * Move forward to next sub-select + * Move forward to next sub-select. * * Retrieve the next sub-select and move cursor to the next element. - * Checks that the count of elements more than the sum of limit and offset. + * Check that the count of elements more than the sum of limit and offset. * * @return Select */ From a3e30d1deaac033abbc644c2693c903305e3d424 Mon Sep 17 00:00:00 2001 From: RomanKis Date: Thu, 27 Apr 2017 10:24:44 +0300 Subject: [PATCH 05/19] MAGETWO-62630: [Backport] - Products are missed and total count is wrong on category page - for 2.1 --- .../Framework/DB/Query/BatchRangeIterator.php | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php index bd61f2528301f..8bab558e3d92c 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php @@ -98,11 +98,7 @@ public function __construct( } /** - * Return the current element. - * - * If we don't have sub-select we should create and remember it. - * - * @return Select + * {@inheritdoc} */ public function current() { @@ -114,11 +110,7 @@ public function current() } /** - * Return the key of the current element. - * - * Сan return the number of the current sub-selects in the iteration. - * - * @return int + * {@inheritdoc} */ public function key() { @@ -126,12 +118,7 @@ public function key() } /** - * Move forward to next sub-select. - * - * Retrieve the next sub-select and move cursor to the next element. - * Check that the count of elements more than the sum of limit and offset. - * - * @return Select + * {@inheritdoc} */ public function next() { @@ -150,11 +137,7 @@ public function next() } /** - * Rewind the BatchRangeIterator to the first element. - * - * Allows to start iteration from the beginning. - * - * @return void + * {@inheritdoc} */ public function rewind() { @@ -165,9 +148,7 @@ public function rewind() } /** - * Checks if current position is valid. - * - * @return bool + * {@inheritdoc} */ public function valid() { From 385330ff83f8081824162fcea5873342331b4bb5 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 28 Apr 2017 14:03:44 +0300 Subject: [PATCH 06/19] MAGETWO-53675: [Backport] [GitHub] Sorting configurable products by price doesn't work when simple product has special_price #4778 - for 2.1 --- .../Product/Indexer/Price/DefaultPrice.php | 28 +++- .../Product/Indexer/Price/Configurable.php | 118 ++++------------- .../_files/enable_price_index_schedule.php | 10 ++ .../enable_price_index_schedule_rollback.php | 10 ++ .../Pricing/Price/SpecialPriceIndexerTest.php | 101 +++++++++++++++ .../Pricing/Price/SpecialPriceTest.php | 121 ++++++++++++++++++ 6 files changed, 287 insertions(+), 101 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 41456945191fa..8a97d43a18d7e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -235,11 +235,30 @@ protected function _prepareFinalPriceData($entityIds = null) * @param string|null $type product type, all if null * @return $this * @throws \Magento\Framework\Exception\LocalizedException - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function prepareFinalPriceDataForType($entityIds, $type) { $this->_prepareDefaultFinalPriceTable(); + + $select = $this->getSelect($entityIds, $type); + $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); + $this->getConnection()->query($query); + return $this; + } + + /** + * Forms Select for collecting price related data for final price index table + * Next types of prices took into account: default, special, tier price + * Moved to protected for possible reusing + * + * @param int|array $entityIds Ids for filtering output result + * @param string|null $type Type for filtering output result by specified product type (all if null) + * @return \Magento\Framework\DB\Select + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function getSelect($entityIds = null, $type = null) + { $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); $connection = $this->getConnection(); $select = $connection->select()->from( @@ -368,13 +387,10 @@ protected function prepareFinalPriceDataForType($entityIds, $type) 'select' => $select, 'entity_field' => new \Zend_Db_Expr('e.entity_id'), 'website_field' => new \Zend_Db_Expr('cw.website_id'), - 'store_field' => new \Zend_Db_Expr('cs.store_id') + 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); - - $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); - $connection->query($query); - return $this; + return $select; } /** diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index f6fbccbab4cea..7f4f0f9e9376f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -8,11 +8,11 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; -use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Store\Api\StoreResolverInterface; -use Magento\Store\Model\Store; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice { /** @@ -29,6 +29,7 @@ class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\ * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Module\Manager $moduleManager * @param string $connectionName + * @param StoreResolverInterface $storeResolver */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -83,63 +84,14 @@ public function reindexEntity($entityIds) protected function reindex($entityIds = null) { if ($this->hasEntity() || !empty($entityIds)) { - if (!empty($entityIds)) { - $allEntityIds = $this->getRelatedProducts($entityIds); - $this->prepareFinalPriceDataForType($allEntityIds, null); - } else { - $this->_prepareFinalPriceData($entityIds); - } + $this->prepareFinalPriceDataForType($entityIds, $this->getTypeId()); $this->_applyCustomOption(); - $this->_applyConfigurableOption($entityIds); + $this->_applyConfigurableOption(); $this->_movePriceDataToIndexTable($entityIds); } return $this; } - /** - * Get related product - * - * @param int[] $entityIds - * @return int[] - */ - private function getRelatedProducts($entityIds) - { - $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); - $select = $this->getConnection()->select()->union( - [ - $this->getConnection()->select() - ->from( - ['e' => $this->getTable('catalog_product_entity')], - 'e.entity_id' - )->join( - ['cpsl' => $this->getTable('catalog_product_super_link')], - 'cpsl.parent_id = e.' . $metadata->getLinkField(), - [] - )->where( - 'e.entity_id IN (?)', - $entityIds - ), - $this->getConnection()->select() - ->from( - ['cpsl' => $this->getTable('catalog_product_super_link')], - 'cpsl.product_id' - )->join( - ['e' => $this->getTable('catalog_product_entity')], - 'cpsl.parent_id = e.' . $metadata->getLinkField(), - [] - )->where( - 'e.entity_id IN (?)', - $entityIds - ), - $this->getConnection()->select() - ->from($this->getTable('catalog_product_super_link'), 'product_id') - ->where('product_id in (?)', $entityIds), - ] - ); - - return array_map('intval', $this->getConnection()->fetchCol($select)); - } - /** * Retrieve table name for custom option temporary aggregation data * @@ -195,56 +147,32 @@ protected function _applyConfigurableOption() $connection = $this->getConnection(); $coaTable = $this->_getConfigurableOptionAggregateTable(); $copTable = $this->_getConfigurableOptionPriceTable(); + $linkField = $metadata->getLinkField(); $this->_prepareConfigurableOptionAggregateTable(); $this->_prepareConfigurableOptionPriceTable(); - $statusAttribute = $this->_getAttribute(ProductInterface::STATUS); - $linkField = $metadata->getLinkField(); - - $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], - [] - )->join( - ['e' => $this->getTable('catalog_product_entity')], - 'e.entity_id = i.entity_id', - ['parent_id' => 'e.entity_id'] - )->join( + $subSelect = $this->getSelect(); + $subSelect->join( ['l' => $this->getTable('catalog_product_super_link')], - 'l.parent_id = e.' . $linkField, - ['product_id'] - )->columns( - ['customer_group_id', 'website_id'], - 'i' + 'l.product_id = e.entity_id', + [] )->join( ['le' => $this->getTable('catalog_product_entity')], - 'le.entity_id = l.product_id', - [] - )->where( - 'le.required_options=0' - )->joinLeft( - ['status_global_attr' => $statusAttribute->getBackendTable()], - "status_global_attr.{$linkField} = le.{$linkField}" - . ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() - . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID, - [] - )->joinLeft( - ['status_attr' => $statusAttribute->getBackendTable()], - "status_attr.{$linkField} = le.{$linkField}" - . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() - . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(), - [] - )->where( - 'IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED - )->group( - ['e.entity_id', 'i.customer_group_id', 'i.website_id', 'l.product_id'] + 'le.' . $linkField . ' = l.parent_id', + ['parent_id' => 'entity_id'] ); - $priceColumn = $this->_addAttributeToSelect($select, 'price', 'le.' . $linkField, 0, null, true); - $tierPriceColumn = $connection->getIfNullSql('MIN(i.tier_price)', 'NULL'); - $select->columns( - ['price' => $priceColumn, 'tier_price' => $tierPriceColumn] - ); + $select = $connection->select(); + $select->from(['sub' => new \Zend_Db_Expr('(' . (string)$subSelect . ')')], '') + ->columns([ + 'sub.parent_id', + 'sub.entity_id', + 'sub.customer_group_id', + 'sub.website_id', + 'sub.price', + 'sub.tier_price', + ]); $query = $select->insertFromSelect($coaTable); $connection->query($query); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php new file mode 100644 index 0000000000000..ba0e9bba43ab9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule.php @@ -0,0 +1,10 @@ +get(Processor::class); +$indexerProcessor->getIndexer()->setScheduled(true); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php new file mode 100644 index 0000000000000..864226ea6c5d4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_price_index_schedule_rollback.php @@ -0,0 +1,10 @@ +get(Processor::class); +$indexerProcessor->getIndexer()->setScheduled(false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php new file mode 100644 index 0000000000000..e4e9daa7f2a88 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceIndexerTest.php @@ -0,0 +1,101 @@ +productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + $this->indexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); + } + + /** + * Use collection to check data in index. + * Do not use magentoDbIsolation because index statement changing "tears" transaction (triggers creating). + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDataFixture Magento/Catalog/_files/enable_price_index_schedule.php + */ + public function testFullReindexIfChildHasSpecialPrice() + { + $specialPrice = 2; + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', $specialPrice); + $this->productRepository->save($childProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->addPriceData()->addFieldToFilter(ProductInterface::SKU, 'configurable'); + + /** @var Product[] $items */ + $items = array_values($collection->getItems()); + self::assertEquals(10, $items[0]->getData('min_price')); + + $this->indexerProcessor->reindexAll(); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->addPriceData()->addFieldToFilter(ProductInterface::SKU, 'configurable'); + + /** @var Product $item */ + $item = $collection->getFirstItem(); + self::assertEquals($specialPrice, $item->getData('min_price')); + } + + /** + * Use collection to check data in index. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ + public function testOnSaveIndexationIfChildHasSpecialPrice() + { + $specialPrice = 2; + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', $specialPrice); + $this->productRepository->save($childProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->addPriceData()->addFieldToFilter(ProductInterface::SKU, 'configurable'); + + /** @var Product $item */ + $item = $collection->getFirstItem(); + self::assertEquals($specialPrice, $item->getData('min_price')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php new file mode 100644 index 0000000000000..60fd7e6403abc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/SpecialPriceTest.php @@ -0,0 +1,121 @@ +productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + /** + * Check final price in configurable with special price in his child. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ + public function testPriceInfoIfChildHasSpecialPrice() + { + $specialPrice = 2; + + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', $specialPrice); + $this->productRepository->save($childProduct); + + /** @var Product $configurableProduct */ + $configurableProduct = $this->productRepository->get('configurable', true); + $priceInfo = $configurableProduct->getPriceInfo(); + /** @var FinalPrice $finalPrice */ + $finalPrice = $priceInfo->getPrice(FinalPrice::PRICE_CODE); + + self::assertEquals($specialPrice, $finalPrice->getMinimalPrice()->getValue()); + } + + /** + * Check sorting configurable product without special price in his children. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_simple_77.php + */ + public function testSortingOfProductsIfChildHasNotSpecialPrice() + { + /** @var Product $simpleProduct */ + $simpleProduct = $this->productRepository->get('simple_77', true); + $simpleProduct + ->setOptions([]) + ->setTierPrice([]) + ->setPrice(5); + $this->productRepository->save($simpleProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->setVisibility([Visibility::VISIBILITY_IN_CATALOG, Visibility::VISIBILITY_BOTH]) + ->setOrder(ProductInterface::PRICE, Collection::SORT_ORDER_DESC); + + /** @var Product[] $items */ + $items = array_values($collection->getItems()); + self::assertEquals('configurable', $items[0]->getSku()); + self::assertEquals('simple_77', $items[1]->getSku()); + } + + /** + * Check sorting configurable product with special price in his child. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_simple_77.php + */ + public function testSortingOfProductsIfChildHasSpecialPrice() + { + /** @var Product $simpleProduct */ + $simpleProduct = $this->productRepository->get('simple_77', true); + $simpleProduct->setOptions([]) + ->setTierPrice([]) + ->setPrice(5); + $this->productRepository->save($simpleProduct); + + /** @var Product $childProduct */ + $childProduct = $this->productRepository->get('simple_10', true); + $childProduct->setData('special_price', 2); + $this->productRepository->save($childProduct); + + /** @var ProductCollection $collection */ + $collection = $this->productCollectionFactory->create(); + $collection->setVisibility([Visibility::VISIBILITY_IN_CATALOG, Visibility::VISIBILITY_BOTH]) + ->setOrder(ProductInterface::PRICE, Collection::SORT_ORDER_DESC); + + /** @var Product[] $items */ + $items = array_values($collection->getItems()); + self::assertEquals('simple_77', $items[0]->getSku()); + self::assertEquals('configurable', $items[1]->getSku()); + } +} From d0ed6ed907e1309cc1c1a1d81e25766de09e35e2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 8 May 2017 16:32:43 +0300 Subject: [PATCH 07/19] MAGETWO-61027: [Backport] - "Product export duplicate rows for product with html special chars in data" for 2.1.x --- .../Model/Export/Product.php | 4 +- .../products_with_multiselect_attribute.php | 94 +++++++++---------- .../AbstractProductExportImportTestCase.php | 1 + .../Model/Export/ProductTest.php | 17 ++++ .../product_export_data_special_chars.php | 38 ++++++++ ...uct_export_data_special_chars_rollback.php | 10 ++ 6 files changed, 112 insertions(+), 52 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 57a3e56153d26..b3f266728e952 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -944,7 +944,7 @@ protected function collectRawData() if ($storeId != Store::DEFAULT_STORE_ID && isset($data[$itemId][Store::DEFAULT_STORE_ID][$fieldName]) - && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == $attrValue + && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == htmlspecialchars_decode($attrValue) ) { continue; } @@ -983,7 +983,7 @@ protected function collectRawData() $data[$itemId][$storeId][self::COL_ATTR_SET] = $this->_attrSetIdToName[$attrSetId]; $data[$itemId][$storeId][self::COL_TYPE] = $item->getTypeId(); } - $data[$itemId][$storeId][self::COL_SKU] = $item->getSku(); + $data[$itemId][$storeId][self::COL_SKU] = htmlspecialchars_decode($item->getSku()); $data[$itemId][$storeId]['store_id'] = $storeId; $data[$itemId][$storeId]['product_id'] = $itemId; $data[$itemId][$storeId]['product_link_id'] = $productLinkId; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php index cadeadf255276..550a432f11f8f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute.php @@ -12,62 +12,56 @@ /** Create product with options and multiselect attribute */ /** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Setup\CategorySetup'); +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Setup\CategorySetup::class); /** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ $options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection' + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class ); $options->setAttributeFilter($attribute->getId()); $optionIds = $options->getAllIds(); /** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); -$product->setTypeId( - \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE -)->setId( - $optionIds[0] * 10 -)->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') -)->setWebsiteIds( - [1] -)->setName( - 'With Multiselect 1' -)->setSku( - 'simple_ms_1' -)->setPrice( - 10 -)->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH -)->setMultiselectAttribute( - [$optionIds[0]] -)->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -)->setStockData( - ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] -)->save(); +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[0] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 1') + ->setSku('simple_ms_1') + ->setPrice(10) + ->setDescription('Hello " &" Bring the water bottle when you can!') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[0]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1 + ] + ) + ->save(); -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create('Magento\Catalog\Model\Product'); -$product->setTypeId( - \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE -)->setId( - $optionIds[1] * 10 -)->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') -)->setWebsiteIds( - [1] -)->setName( - 'With Multiselect 2' -)->setSku( - 'simple_ms_2' -)->setPrice( - 10 -)->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH -)->setMultiselectAttribute( - [$optionIds[1], $optionIds[2], $optionIds[3]] -)->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED -)->setStockData( - ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] -)->save(); +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[1] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect 2') + ->setSku('simple_ms_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttribute([$optionIds[1], $optionIds[2], $optionIds[3]]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1 + ] + ) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index d2cceb1087471..f9b15df553e4f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -56,6 +56,7 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit_Framework_Te 'custom_design_from', 'updated_in', 'tax_class_id', + 'description' ]; protected function setUp() diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 76381c7cdc401..1471d7b8ad75e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -87,6 +87,23 @@ public function testExport() $this->assertContains('test_option_code_2', $exportData); $this->assertContains('max_characters=10', $exportData); $this->assertContains('text_attribute=!@#$%^&*()_+1234567890-=|\\:;""\'<,>.?/', $exportData); + $occurrencesCount = substr_count($exportData, 'Hello "" &"" Bring the water bottle when you can!'); + $this->assertEquals(1, $occurrencesCount); + } + + /** + * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data_special_chars.php + * @magentoDbIsolationEnabled + */ + public function testExportSpecialChars() + { + $this->model->setWriter( + $this->objectManager->create( + \Magento\ImportExport\Model\Export\Adapter\Csv::class + ) + ); + $exportData = $this->model->export(); + $this->assertContains('simple ""1""', $exportData); } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php new file mode 100644 index 0000000000000..a7c7aff597c2b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php @@ -0,0 +1,38 @@ +create(\Magento\Catalog\Model\Product::class); + +$productModel->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setName('New Product') + ->setSku('simple "1"') + ->setPrice(10) + ->addData(['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/']) + ->setTierPrice([0 => ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 8]]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCateroryIds([]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1]) + ->setCanSaveCustomOptions(true) + ->setCategoryIds([333]) + ->setUpSellLinkData([$product->getId() => ['position' => 1]]); + +$productModel->setOptions([])->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php new file mode 100644 index 0000000000000..a4769078a7713 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php @@ -0,0 +1,10 @@ + Date: Thu, 11 May 2017 13:41:01 +0300 Subject: [PATCH 08/19] MAGETWO-63514: [Backport] - Free Shipping promo still applies for UPS after removing free ship item - for 2.1 --- .../Model/Quote/Address/Total/Shipping.php | 15 +-- .../Test/Repository/CatalogProductSimple.xml | 28 +++++ .../Checkout/Test/Block/Cart/Totals.php | 1 + .../SalesRule/Test/Handler/SalesRule/Curl.php | 28 +++-- .../SalesRule/Test/Repository/SalesRule.xml | 20 ++++ .../ShoppingCartWithFreeShippingTest.php | 79 ++++++++++++ .../ShoppingCartWithFreeShippingTest.xml | 25 ++++ .../Quote/Address/Total/ShippingTest.php | 113 ++++++++++++++++++ .../_files/cart_rule_free_shipping.php | 74 ++++++++++++ .../cart_rule_free_shipping_rollback.php | 17 +++ .../rule_free_shipping_by_product_weight.php | 36 ++++++ ...ee_shipping_by_product_weight_rollback.php | 7 ++ 12 files changed, 424 insertions(+), 19 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php create mode 100644 dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php create mode 100644 dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index 289dc05c365ba..4b91466195eb9 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -59,11 +59,8 @@ public function collect( $addressWeight = $address->getWeight(); $freeMethodWeight = $address->getFreeMethodWeight(); + $addressFreeShipping = $address->getFreeShipping(); - $isAllFree = $this->freeShipping->isFreeShipping($quote, $shippingAssignment->getItems()); - if ($isAllFree && !$address->getFreeShipping()) { - $address->setFreeShipping(true); - } $total->setTotalAmount($this->getCode(), 0); $total->setBaseTotalAmount($this->getCode(), 0); @@ -99,7 +96,7 @@ public function collect( $itemQty = $child->getTotalQty(); $rowWeight = $itemWeight * $itemQty; $addressWeight += $rowWeight; - if ($address->getFreeShipping() || $child->getFreeShipping() === true) { + if ($addressFreeShipping || $child->getFreeShipping() === true) { $rowWeight = 0; } elseif (is_numeric($child->getFreeShipping())) { $freeQty = $child->getFreeShipping(); @@ -117,7 +114,7 @@ public function collect( $itemWeight = $item->getWeight(); $rowWeight = $itemWeight * $item->getQty(); $addressWeight += $rowWeight; - if ($address->getFreeShipping() || $item->getFreeShipping() === true) { + if ($addressFreeShipping || $item->getFreeShipping() === true) { $rowWeight = 0; } elseif (is_numeric($item->getFreeShipping())) { $freeQty = $item->getFreeShipping(); @@ -137,7 +134,7 @@ public function collect( $itemWeight = $item->getWeight(); $rowWeight = $itemWeight * $item->getQty(); $addressWeight += $rowWeight; - if ($address->getFreeShipping() || $item->getFreeShipping() === true) { + if ($addressFreeShipping || $item->getFreeShipping() === true) { $rowWeight = 0; } elseif (is_numeric($item->getFreeShipping())) { $freeQty = $item->getFreeShipping(); @@ -158,6 +155,10 @@ public function collect( $address->setWeight($addressWeight); $address->setFreeMethodWeight($freeMethodWeight); + $address->setFreeShipping( + $this->freeShipping->isFreeShipping($quote, $shippingAssignment->getItems()) + ); + $address->collectShippingRates(); if ($method) { diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 7a8a629ca765b..e441da8d2b1ed 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -1103,5 +1103,33 @@ overnight-duffle + + + default + + Simple Product %isolation% + sku_simple_product_%isolation% + This item has weight + 10 + + 25 + In Stock + + + 560 + + + taxable_goods + + + Main Website + + Catalog, Search + simple-product-%isolation% + + simple_order_default + + + diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index e29a19a732409..7e1c3ac5ff599 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -105,6 +105,7 @@ class Totals extends Block */ public function getGrandTotal() { + $this->waitForUpdatedTotals(); $grandTotal = $this->_rootElement->find($this->grandTotal, Locator::SELECTOR_CSS)->getText(); return $this->escapeCurrency($grandTotal); } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php index 9b8aa37304e04..404961d085d43 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Handler/SalesRule/Curl.php @@ -38,52 +38,56 @@ class Curl extends Conditions implements SalesRuleInterface */ protected $mapTypeParams = [ 'Subtotal' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'base_subtotal', ], 'Total Items Quantity' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'total_qty', ], 'Conditions combination' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Combine', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, 'aggregator' => 'all', 'value' => '1', ], 'Products subselection' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product\Subselect', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Subselect::class, 'attribute' => 'qty', 'operator' => '==', 'value' => '1', 'aggregator' => 'all', ], 'Product attribute combination' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product\Found', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, 'value' => '1', 'aggregator' => 'all', ], 'Shipping Country' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'country_id', ], 'Shipping Postcode' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Address', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, 'attribute' => 'postcode', ], + 'Total Weight' => [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'weight', + ], 'Category' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'category_ids', ], 'Price in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_price', ], 'Quantity in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_qty', ], 'Row total in cart' => [ - 'type' => 'Magento\SalesRule\Model\Rule\Condition\Product', + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, 'attribute' => 'quote_item_row_total', ] ]; @@ -207,7 +211,7 @@ public function prepareData(FixtureInterface $fixture) if (isset($this->data['actions_serialized'])) { $this->mapTypeParams['Conditions combination']['type'] = - 'Magento\SalesRule\Model\Rule\Condition\Product\Combine'; + \Magento\SalesRule\Model\Rule\Condition\Product\Combine::class; $this->data['rule']['actions'] = $this->prepareCondition($this->data['actions_serialized']); unset($this->data['actions_serialized']); } diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml index 13479dbfc19e4..98c130ef0d37e 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml @@ -279,5 +279,25 @@ No No + + + Cart price rule with free shipping by weight + Yes + + Main Website + + + NOT LOGGED IN + + No Coupon + 1 + Yes + [Total Weight|is|1] + Percent of product price discount + 0 + No + No + For matching items only + diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php new file mode 100644 index 0000000000000..02a7d90db5e5e --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.php @@ -0,0 +1,79 @@ +testStepFactory = $testStepFactory; + } + + /** + * Test sales rule with free shipping applied by product weight. + * + * @param \Magento\SalesRule\Test\Fixture\SalesRule $salesRule + * @param \Magento\Catalog\Test\Fixture\CatalogProductSimple $product + * @param \Magento\Checkout\Test\Fixture\Cart $cart + * @return void + */ + public function testRuleWithFreeShippingByWeight( + \Magento\SalesRule\Test\Fixture\SalesRule $salesRule, + \Magento\Catalog\Test\Fixture\CatalogProductSimple $product, + \Magento\Checkout\Test\Fixture\Cart $cart + ) { + $salesRule->persist(); + $product->persist(); + + $this->testStepFactory->create( + \Magento\Checkout\Test\TestStep\AddProductsToTheCartStep::class, + ['products' => [$product]] + )->run(); + + $this->testStepFactory->create( + \Magento\Checkout\Test\TestStep\EstimateShippingAndTaxStep::class, + ['products' => [$product], 'cart' => $cart] + )->run(); + } + + /** + * Clear data after test. + * + * @return void + */ + public function tearDown() + { + $this->testStepFactory->create(\Magento\SalesRule\Test\TestStep\DeleteAllSalesRuleStep::class)->run(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml new file mode 100644 index 0000000000000..b9c611a08cd14 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ShoppingCartWithFreeShippingTest.xml @@ -0,0 +1,25 @@ + + + + + + rule_with_freeshipping_by_weight + default + 560.00 + 0.00 + 560.00 + + + rule_with_freeshipping_by_weight + simple_with_weight_10_for_salesrule + 560.00 + 5.00 + 565.00 + + + diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php new file mode 100644 index 0000000000000..8e8466ab13717 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php @@ -0,0 +1,113 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->cartManagement = $this->objectManager->get(\Magento\Quote\Api\GuestCartManagementInterface::class); + $this->itemRepository = $this->objectManager->get(\Magento\Quote\Api\GuestCartItemRepositoryInterface::class); + } + + /** + * Estimate shipment for product that match salesrule with free shipping. + * + * @magentoDataFixture Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testRuleByProductWeightWithFreeShipping() + { + $cartId = $this->prepareQuote(1); + $methods = $this->estimateShipping($cartId); + + $this->assertTrue(count($methods) > 0); + $this->assertEquals('flatrate', $methods[0]->getMethodCode()); + $this->assertEquals(0, $methods[0]->getAmount()); + + } + + /** + * Estimate shipment for product that doesn't match salesrule with free shipping. + * + * @magentoDataFixture Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testRuleByProductWeightWithoutFreeShipping() + { + $cartId = $this->prepareQuote(5); + $methods = $this->estimateShipping($cartId); + + $this->assertTrue(count($methods) > 0); + $this->assertEquals('flatrate', $methods[0]->getMethodCode()); + $this->assertEquals(25, $methods[0]->getAmount()); + + } + + /** + * Estimate shipment for guest cart. + * + * @param int $cartId + * @return \Magento\Quote\Api\Data\ShippingMethodInterface[] + */ + private function estimateShipping($cartId) + { + $addressFactory = $this->objectManager->get(\Magento\Quote\Api\Data\AddressInterfaceFactory::class); + /** @var \Magento\Quote\Api\Data\AddressInterface $address */ + $address = $addressFactory->create(); + $address->setCountryId('US'); + $address->setRegionId(2); + + /** @var \Magento\Quote\Api\GuestShipmentEstimationInterface $estimation */ + $estimation = $this->objectManager->get(\Magento\Quote\Api\GuestShipmentEstimationInterface::class); + return $estimation->estimateByExtendedAddress($cartId, $address); + } + + /** + * Create guest quote with products. + * + * @param int $itemQty + * @return int + */ + private function prepareQuote($itemQty) + { + $cartId = $this->cartManagement->createEmptyCart(); + + /** @var \Magento\Quote\Api\Data\CartItemInterfaceFactory $cartItemFactory */ + $cartItemFactory = $this->objectManager->get(\Magento\Quote\Api\Data\CartItemInterfaceFactory::class); + + /** @var \Magento\Quote\Api\Data\CartItemInterface $cartItem */ + $cartItem = $cartItemFactory->create(); + $cartItem->setQuoteId($cartId); + $cartItem->setQty($itemQty); + $cartItem->setSku('simple'); + $cartItem->setProductType(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE); + + $this->itemRepository->save($cartItem); + + return $cartId; + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php new file mode 100644 index 0000000000000..dc0b6e6f834c3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php @@ -0,0 +1,74 @@ +create(\Magento\SalesRule\Model\RuleFactory::class); +/** @var \Magento\SalesRule\Model\Rule $salesRule */ +$salesRule = $salesRuleFactory->create(); +$row = + [ + 'name' => 'Free shipping if item price >10', + 'is_active' => 1, + 'customer_group_ids' => [\Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, + 'conditions' => [ + 1 => + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + ] + + ], + 'actions' => [ + 1 => [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product::class, + 'attribute' => 'quote_item_price', + 'operator' => '==', + 'value' => '7', + 'is_value_processed' => false, + ] + ] + ] + ], + 'is_advanced' => 1, + 'simple_action' => 'by_percent', + 'discount_amount' => 0, + 'stop_rules_processing' => 0, + 'discount_qty' => 0, + 'discount_step' => 0, + 'apply_to_shipping' => 1, + 'times_used' => 0, + 'is_rss' => 1, + 'use_auto_generation' => 0, + 'uses_per_coupon' => 0, + 'simple_free_shipping' => 1, + + 'website_ids' => [ + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getWebsite()->getId() + ] + ]; +$salesRule->loadPost($row); +$salesRule->save(); +/** @var Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('cart_rule_free_shipping'); +$registry->register('cart_rule_free_shipping', $salesRule); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php new file mode 100644 index 0000000000000..48471841b79ba --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping_rollback.php @@ -0,0 +1,17 @@ +get(\Magento\Framework\Registry::class); + +/** @var \Magento\SalesRule\Model\Rule $salesRule */ +$salesRule = $registry->registry('cart_rule_free_shipping'); +if ($salesRule !== null) { + $salesRule->delete(); + $registry->unregister('cart_rule_free_shipping'); +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php new file mode 100644 index 0000000000000..8dfef696b4fec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php @@ -0,0 +1,36 @@ + 'Free shipping if item weight <= 1', + 'conditions' => [ + 1 => + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'weight', + 'operator' => '<=', + 'value' => '1', + 'is_value_processed' => false, + ] + ] + ] + + ], + 'actions' => [], + ]; +$salesRule->loadPost($row); +$salesRule->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php new file mode 100644 index 0000000000000..c387fd35650c3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight_rollback.php @@ -0,0 +1,7 @@ + Date: Fri, 12 May 2017 13:02:59 +0300 Subject: [PATCH 09/19] MAGETWO-63514: [Backport] - Free Shipping promo still applies for UPS after removing free ship item - for 2.1 --- .../Model/Quote/Address/Total/Shipping.php | 1 + .../Checkout/Test/Block/Cart/Totals.php | 11 ++ .../Quote/Address/Total/ShippingTest.php | 3 +- .../_files/cart_rule_free_shipping.php | 101 +++++++++--------- .../rule_free_shipping_by_product_weight.php | 43 ++++---- 5 files changed, 82 insertions(+), 77 deletions(-) diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index 4b91466195eb9..bf44378b3a543 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -180,6 +180,7 @@ public function collect( } } } + return $this; } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index 7e1c3ac5ff599..dc3354c3f0a21 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -107,6 +107,7 @@ public function getGrandTotal() { $this->waitForUpdatedTotals(); $grandTotal = $this->_rootElement->find($this->grandTotal, Locator::SELECTOR_CSS)->getText(); + return $this->escapeCurrency($grandTotal); } @@ -118,6 +119,7 @@ public function getGrandTotal() public function getGrandTotalIncludingTax() { $priceElement = $this->_rootElement->find($this->grandTotalInclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -129,6 +131,7 @@ public function getGrandTotalIncludingTax() public function getGrandTotalExcludingTax() { $priceElement = $this->_rootElement->find($this->grandTotalExclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -140,6 +143,7 @@ public function getGrandTotalExcludingTax() public function getTax() { $priceElement = $this->_rootElement->find($this->tax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -161,6 +165,7 @@ public function isTaxVisible() public function getSubtotal() { $subTotal = $this->_rootElement->find($this->subtotal, Locator::SELECTOR_CSS)->getText(); + return $this->escapeCurrency($subTotal); } @@ -172,6 +177,7 @@ public function getSubtotal() public function getSubtotalIncludingTax() { $priceElement = $this->_rootElement->find($this->subtotalInclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -183,6 +189,7 @@ public function getSubtotalIncludingTax() public function getSubtotalExcludingTax() { $priceElement = $this->_rootElement->find($this->subtotalExclTax, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -195,6 +202,7 @@ public function getSubtotalExcludingTax() protected function escapeCurrency($price) { preg_match("/^\\D*\\s*([\\d,\\.]+)\\s*\\D*$/", $price, $matches); + return (isset($matches[1])) ? $matches[1] : null; } @@ -206,6 +214,7 @@ protected function escapeCurrency($price) public function getDiscount() { $priceElement = $this->_rootElement->find($this->discount, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -217,6 +226,7 @@ public function getDiscount() public function getShippingPrice() { $priceElement = $this->_rootElement->find($this->shippingPriceSelector, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -228,6 +238,7 @@ public function getShippingPrice() public function getShippingPriceInclTax() { $priceElement = $this->_rootElement->find($this->shippingPriceInclTaxSelector, Locator::SELECTOR_CSS); + return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php index 8e8466ab13717..dde854febc80d 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/Address/Total/ShippingTest.php @@ -47,7 +47,6 @@ public function testRuleByProductWeightWithFreeShipping() $this->assertTrue(count($methods) > 0); $this->assertEquals('flatrate', $methods[0]->getMethodCode()); $this->assertEquals(0, $methods[0]->getAmount()); - } /** @@ -64,7 +63,6 @@ public function testRuleByProductWeightWithoutFreeShipping() $this->assertTrue(count($methods) > 0); $this->assertEquals('flatrate', $methods[0]->getMethodCode()); $this->assertEquals(25, $methods[0]->getAmount()); - } /** @@ -83,6 +81,7 @@ private function estimateShipping($cartId) /** @var \Magento\Quote\Api\GuestShipmentEstimationInterface $estimation */ $estimation = $this->objectManager->get(\Magento\Quote\Api\GuestShipmentEstimationInterface::class); + return $estimation->estimateByExtendedAddress($cartId, $address); } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php index dc0b6e6f834c3..a2977132ad880 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_free_shipping.php @@ -9,62 +9,59 @@ $salesRuleFactory = $objectManager->create(\Magento\SalesRule\Model\RuleFactory::class); /** @var \Magento\SalesRule\Model\Rule $salesRule */ $salesRule = $salesRuleFactory->create(); -$row = - [ - 'name' => 'Free shipping if item price >10', - 'is_active' => 1, - 'customer_group_ids' => [\Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID], - 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, - 'conditions' => [ - 1 => +$row = [ + 'name' => 'Free shipping if item price >10', + 'is_active' => 1, + 'customer_group_ids' => [\Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, + 'conditions' => [ + 1 => [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + ] + ], + 'actions' => [ + 1 => [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, - 'attribute' => null, - 'operator' => null, - 'value' => '1', - 'is_value_processed' => null, - 'aggregator' => 'all', - ] - - ], - 'actions' => [ - 1 => [ - 'type' => Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, - 'attribute' => null, - 'operator' => null, - 'value' => '1', - 'is_value_processed' => null, - 'aggregator' => 'all', - 'conditions' => [ - [ - 'type' => Magento\SalesRule\Model\Rule\Condition\Product::class, - 'attribute' => 'quote_item_price', - 'operator' => '==', - 'value' => '7', - 'is_value_processed' => false, - ] + 'type' => Magento\SalesRule\Model\Rule\Condition\Product::class, + 'attribute' => 'quote_item_price', + 'operator' => '==', + 'value' => '7', + 'is_value_processed' => false, ] ] - ], - 'is_advanced' => 1, - 'simple_action' => 'by_percent', - 'discount_amount' => 0, - 'stop_rules_processing' => 0, - 'discount_qty' => 0, - 'discount_step' => 0, - 'apply_to_shipping' => 1, - 'times_used' => 0, - 'is_rss' => 1, - 'use_auto_generation' => 0, - 'uses_per_coupon' => 0, - 'simple_free_shipping' => 1, - - 'website_ids' => [ - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getWebsite()->getId() ] - ]; + ], + 'is_advanced' => 1, + 'simple_action' => 'by_percent', + 'discount_amount' => 0, + 'stop_rules_processing' => 0, + 'discount_qty' => 0, + 'discount_step' => 0, + 'apply_to_shipping' => 1, + 'times_used' => 0, + 'is_rss' => 1, + 'use_auto_generation' => 0, + 'uses_per_coupon' => 0, + 'simple_free_shipping' => 1, + + 'website_ids' => [ + \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getWebsite()->getId() + ] +]; $salesRule->loadPost($row); $salesRule->save(); /** @var Magento\Framework\Registry $registry */ diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php index 8dfef696b4fec..c02cc33423cd3 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/rule_free_shipping_by_product_weight.php @@ -6,31 +6,28 @@ require 'cart_rule_free_shipping.php'; -$row = - [ - 'name' => 'Free shipping if item weight <= 1', - 'conditions' => [ - 1 => +$row = [ + 'name' => 'Free shipping if item weight <= 1', + 'conditions' => [ + 1 => [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, - 'attribute' => null, - 'operator' => null, + 'type' => Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'weight', + 'operator' => '<=', 'value' => '1', - 'is_value_processed' => null, - 'aggregator' => 'all', - 'conditions' => [ - [ - 'type' => Magento\SalesRule\Model\Rule\Condition\Address::class, - 'attribute' => 'weight', - 'operator' => '<=', - 'value' => '1', - 'is_value_processed' => false, - ] - ] + 'is_value_processed' => false, ] - - ], - 'actions' => [], - ]; + ] + ] + ], + 'actions' => [], +]; $salesRule->loadPost($row); $salesRule->save(); From 35059e45fa36e81112f22ca693265d972e1550cc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 12 May 2017 15:38:02 +0300 Subject: [PATCH 10/19] MAGETWO-63656: [Backport] - category_ids attribute scope dropdown - for 2.1 --- app/code/Magento/Catalog/etc/eav_attributes.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Catalog/etc/eav_attributes.xml b/app/code/Magento/Catalog/etc/eav_attributes.xml index 133849a28e048..005402937232f 100644 --- a/app/code/Magento/Catalog/etc/eav_attributes.xml +++ b/app/code/Magento/Catalog/etc/eav_attributes.xml @@ -30,6 +30,7 @@ + From 3a4fb597e380a66b561059b3d5f21509f46a9d30 Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Thu, 18 May 2017 12:29:58 +0300 Subject: [PATCH 11/19] MAGETWO-63819: [Backport] - Order grid displays wrong Purchase Date for order - for 2.1 --- .../Sales/view/adminhtml/ui_component/sales_order_grid.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index ec0ba345bc216..cf202e51578bc 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -186,7 +186,7 @@ Magento_Ui/js/grid/columns/date date Purchase Date - MMM dd, YYYY, H:MM:SS A + MMM dd, YYYY, H:mm:ss A From 884f09d87b0a9d4bdc9951dbb0a6847d86a593fc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Thu, 18 May 2017 14:09:10 +0300 Subject: [PATCH 12/19] MAGETWO-62628: [Backport] Fix functionality for functional tests --- .../view/frontend/web/js/model/gift-message.js | 2 +- .../tests/app/Magento/Checkout/Test/Block/Cart.php | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js b/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js index bd9da994f9639..c64c4c0012381 100644 --- a/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js +++ b/app/code/Magento/GiftMessage/view/frontend/web/js/model/gift-message.js @@ -78,7 +78,7 @@ define(['uiElement', 'underscore', 'mage/url'], }, afterSubmit: function() { window.location.href = url.build('checkout/cart/updatePost') - + '?form_key=' + window.giftOptionsConfig.giftMessage.formKey + + '?form_key=' + window.checkoutConfig.formKey + '&cart[]'; }, getSubmitParams: function(remove) { diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php index afa9f4c460074..4936bb7922716 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php @@ -99,6 +99,13 @@ class Cart extends Block */ protected $preloaderSpinner = '#preloaderSpinner'; + /** + * Cart item class name. + * + * @var string + */ + protected $cartItemClass = \Magento\Checkout\Test\Block\Cart\CartItem::class; + /** * Wait for PayPal page is loaded. * @@ -129,7 +136,7 @@ public function getCartItem(FixtureInterface $product) Locator::SELECTOR_XPATH ); $cartItem = $this->blockFactory->create( - 'Magento\Checkout\Test\Block\Cart\CartItem', + $this->cartItemClass, ['element' => $cartItemBlock] ); } From 0ccc53fec29870ddb193f71a1c5e09758d9eba2b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 08:48:41 +0300 Subject: [PATCH 13/19] MAGETWO-64068: [Backport] - Not possible to update or delete products in cart or checkout after deleting address - for 2.1 --- .../ShippingAssignmentProcessor.php | 44 ++- .../Model/QuoteRepository/SaveHandler.php | 39 ++- .../ShippingAssignmentProcessorTest.php | 210 ++++++++++---- .../Model/QuoteRepository/SaveHandlerTest.php | 258 ++++++++++++------ .../Quote/Model/QuoteRepositoryTest.php | 132 +++++++-- 5 files changed, 513 insertions(+), 170 deletions(-) diff --git a/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php b/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php index fdf819c8d864c..45d55a4be794f 100644 --- a/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php +++ b/app/code/Magento/Quote/Model/Quote/ShippingAssignment/ShippingAssignmentProcessor.php @@ -9,7 +9,10 @@ use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Framework\Exception\InputException; use Magento\Quote\Model\ShippingAssignmentFactory; -use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\ObjectManager; class ShippingAssignmentProcessor { @@ -24,45 +27,65 @@ class ShippingAssignmentProcessor protected $shippingProcessor; /** - * @var CartItemPersister + * @var \Magento\Quote\Model\Quote\Item\CartItemPersister */ protected $cartItemPersister; + /** + * Customer address CRUD interface. + * + * @var AddressRepositoryInterface + */ + private $addressRepository; + /** * @param ShippingAssignmentFactory $shippingAssignmentFactory * @param ShippingProcessor $shippingProcessor - * @param CartItemPersister $cartItemPersister + * @param \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister + * @param AddressRepositoryInterface $addressRepository */ public function __construct( ShippingAssignmentFactory $shippingAssignmentFactory, ShippingProcessor $shippingProcessor, - CartItemPersister $cartItemPersister + \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister, + AddressRepositoryInterface $addressRepository = null ) { $this->shippingAssignmentFactory = $shippingAssignmentFactory; $this->shippingProcessor = $shippingProcessor; $this->cartItemPersister = $cartItemPersister; + $this->addressRepository = $addressRepository + ?: ObjectManager::getInstance()->get(AddressRepositoryInterface::class); } /** + * Create shipping assignment. + * * @param CartInterface $quote + * * @return \Magento\Quote\Api\Data\ShippingAssignmentInterface */ public function create(CartInterface $quote) { /** @var \Magento\Quote\Model\Quote $quote */ $shippingAddress = $quote->getShippingAddress(); + /** @var \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment */ $shippingAssignment = $this->shippingAssignmentFactory->create(); $shippingAssignment->setItems($quote->getItems()); $shippingAssignment->setShipping($this->shippingProcessor->create($shippingAddress)); + return $shippingAssignment; } /** + * Save shipping assignment. + * * @param ShippingAssignmentInterface $shippingAssignment * @param CartInterface $quote + * * @return void - * @throws InputException + * + * @throws InputException|LocalizedException */ public function save(CartInterface $quote, ShippingAssignmentInterface $shippingAssignment) { @@ -73,6 +96,17 @@ public function save(CartInterface $quote, ShippingAssignmentInterface $shipping $this->cartItemPersister->save($quote, $item); } } + + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + + if ($shippingAddress->getCustomerAddressId()) { + try { + $this->addressRepository->getById($shippingAddress->getCustomerAddressId()); + } catch (NoSuchEntityException $e) { + $shippingAddress->setCustomerAddressId(null); + } + } + $this->shippingProcessor->save($shippingAssignment->getShipping(), $quote); } } diff --git a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php index 647946e141a63..e91becaf3cf88 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php +++ b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php @@ -7,6 +7,8 @@ use Magento\Quote\Api\Data\CartInterface; use Magento\Framework\Exception\InputException; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; class SaveHandler { @@ -30,26 +32,40 @@ class SaveHandler */ private $shippingAssignmentPersister; + /** + * Customer address CRUD interface. + * + * @var \Magento\Customer\Api\AddressRepositoryInterface + */ + private $addressRepository; + /** * @param \Magento\Quote\Model\ResourceModel\Quote $quoteResource * @param \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister * @param \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister * @param \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister + * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository */ public function __construct( \Magento\Quote\Model\ResourceModel\Quote $quoteResource, \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister, \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister, - \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister + \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister, + \Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null ) { $this->quoteResourceModel = $quoteResource; $this->cartItemPersister = $cartItemPersister; $this->billingAddressPersister = $billingAddressPersister; $this->shippingAssignmentPersister = $shippingAssignmentPersister; + $this->addressRepository = $addressRepository + ?: ObjectManager::getInstance()->get(\Magento\Customer\Api\AddressRepositoryInterface::class); } /** + * Process and save quote data. + * * @param CartInterface $quote + * * @return CartInterface * * @throws InputException @@ -62,6 +78,7 @@ public function save(CartInterface $quote) /** @var \Magento\Quote\Model\Quote $quote */ // Quote Item processing $items = $quote->getItems(); + if ($items) { foreach ($items as $item) { /** @var \Magento\Quote\Model\Quote\Item $item */ @@ -73,30 +90,46 @@ public function save(CartInterface $quote) // Billing Address processing $billingAddress = $quote->getBillingAddress(); + if ($billingAddress) { + if ($billingAddress->getCustomerAddressId()) { + try { + $this->addressRepository->getById($billingAddress->getCustomerAddressId()); + } catch (NoSuchEntityException $e) { + $billingAddress->setCustomerAddressId(null); + } + } + $this->billingAddressPersister->save($quote, $billingAddress); } $this->processShippingAssignment($quote); - $this->quoteResourceModel->save($quote->collectTotals()); + return $quote; } /** + * Process shipping assignment. + * * @param \Magento\Quote\Model\Quote $quote + * * @return void + * * @throws InputException */ private function processShippingAssignment($quote) { // Shipping Assignments processing $extensionAttributes = $quote->getExtensionAttributes(); + if (!$quote->isVirtual() && $extensionAttributes && $extensionAttributes->getShippingAssignments()) { $shippingAssignments = $extensionAttributes->getShippingAssignments(); + if (count($shippingAssignments) > 1) { - throw new InputException(__("Only 1 shipping assignment can be set")); + throw new InputException(__('Only 1 shipping assignment can be set')); } + $this->shippingAssignmentPersister->save($quote, $shippingAssignments[0]); } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php index e74070c3eaee7..2f1b1c093604e 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/ShippingAssignment/ShippingAssignmentProcessorTest.php @@ -5,107 +5,199 @@ */ namespace Magento\Quote\Test\Unit\Model\Quote\ShippingAssignment; -use Magento\Quote\Api\Data\ShippingAssignmentInterface; -use Magento\Quote\Api\Data\ShippingInterface; -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\ShippingAssignmentFactory; -use Magento\Quote\Model\Quote\Item\CartItemPersister; use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Quote\Model\ShippingAssignmentFactory; use Magento\Quote\Model\Quote\ShippingAssignment\ShippingProcessor; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Api\Data\ShippingInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Framework\Exception\NoSuchEntityException; /** - * Class ShippingAssignmentProcessorTest + * ShippingAssignmentProcessor test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShippingAssignmentProcessorTest extends \PHPUnit_Framework_TestCase { /** - * @var ShippingAssignmentFactory|MockObject + * @var ShippingAssignmentProcessor */ - private $shippingAssignmentFactory; + private $shippingAssignmentProcessor; /** - * @var ShippingProcessor|MockObject + * @var ObjectManagerHelper */ - private $shippingProcessor; + private $objectManagerHelper; /** - * @var CartItemPersister|MockObject + * @var ShippingAssignmentFactory|\PHPUnit_Framework_MockObject_MockObject */ - private $cartItemPersister; + private $shippingAssignmentFactoryMock; /** - * @var ShippingAssignmentProcessor + * @var ShippingProcessor|\PHPUnit_Framework_MockObject_MockObject */ - private $shippingAssignmentProcessor; + private $shippingProcessorMock; + + /** + * @var CartItemPersister|\PHPUnit_Framework_MockObject_MockObject + */ + private $cartItemPersisterMock; + + /** + * @var AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressRepositoryMock; + + /** + * @var Quote|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var ShippingAssignmentInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingAssignmentMock; + + /** + * @var QuoteAddress|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingAddressMock; + + /** + * @var ShippingInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingMock; protected function setUp() { - $this->shippingAssignmentFactory = $this->getMockBuilder(ShippingAssignmentFactory::class) + $this->shippingAssignmentFactoryMock = $this->getMockBuilder(ShippingAssignmentFactory::class) ->disableOriginalConstructor() ->getMock(); - - $this->shippingProcessor = $this->getMockBuilder(ShippingProcessor::class) + $this->shippingProcessorMock = $this->getMockBuilder(ShippingProcessor::class) ->disableOriginalConstructor() ->getMock(); - - $this->cartItemPersister = $this->getMockBuilder(CartItemPersister::class) + $this->cartItemPersisterMock = $this->getMockBuilder(CartItemPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->addressRepositoryMock = $this->getMockBuilder(AddressRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->shippingAssignmentMock = $this->getMockBuilder(ShippingAssignmentInterface::class) + ->getMockForAbstractClass(); + $this->shippingAddressMock = $this->getMockBuilder(QuoteAddress::class) ->disableOriginalConstructor() ->getMock(); + $this->shippingMock = $this->getMockBuilder(ShippingInterface::class) + ->getMockForAbstractClass(); + + $this->quoteMock->expects(static::any()) + ->method('getShippingAddress') + ->willReturn($this->shippingAddressMock); + $this->shippingAssignmentMock->expects(static::any()) + ->method('getShipping') + ->willReturn($this->shippingMock); + $this->shippingMock->expects(static::any()) + ->method('getAddress') + ->willReturn($this->shippingAddressMock); - $this->shippingAssignmentProcessor = new ShippingAssignmentProcessor( - $this->shippingAssignmentFactory, - $this->shippingProcessor, - $this->cartItemPersister + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->shippingAssignmentProcessor = $this->objectManagerHelper->getObject( + ShippingAssignmentProcessor::class, + [ + 'shippingAssignmentFactory' => $this->shippingAssignmentFactoryMock, + 'shippingProcessor' => $this->shippingProcessorMock, + 'cartItemPersister' => $this->cartItemPersisterMock, + 'addressRepository' => $this->addressRepositoryMock + ] ); } /** - * Test saving shipping assignments with deleted cart items - * - * @covers \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor::save + * Tests save() method with deleted cart items. */ public function testSaveWithDeletedCartItems() { - $shippingAssignment = $this->getMockForAbstractClass(ShippingAssignmentInterface::class); - $shipping = $this->getMockForAbstractClass(ShippingInterface::class); - $quoteId = 1; + $quoteItemId = 1; - $quote = $this->getMockBuilder(Quote::class) - ->disableOriginalConstructor() - ->getMock(); - $quoteItem = $this->getMockBuilder(\Magento\Quote\Model\Quote\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $quoteItem->expects(static::once()) - ->method('isDeleted') - ->willReturn(true); - $quoteItem->expects(static::once()) - ->method('getItemId') - ->willReturn($quoteId); - - $quote->expects(static::once()) + $this->shippingAssignmentMock->expects(static::once()) + ->method('getItems') + ->willReturn([$this->createQuoteItemMock($quoteItemId, true)]); + $this->quoteMock->expects(static::atLeastOnce()) ->method('getItemById') - ->with($quoteId) + ->with($quoteItemId) ->willReturn(null); + $this->cartItemPersisterMock->expects(static::never()) + ->method('save'); + $this->shippingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn(null); + $this->addressRepositoryMock->expects(static::never()) + ->method('getById'); + $this->shippingProcessorMock->expects(static::once()) + ->method('save') + ->with($this->shippingMock, $this->quoteMock); - $shippingAssignment->expects(static::once()) - ->method('getItems') - ->willReturn([$quoteItem]); - $shippingAssignment->expects(static::once()) - ->method('getShipping') - ->willReturn($shipping); + $this->shippingAssignmentProcessor->save($this->quoteMock, $this->shippingAssignmentMock); + } - $this->cartItemPersister->expects(static::never()) - ->method('save'); + /** + * Tests save() method with not existing customer address. + */ + public function testSaveWithNotExistingCustomerAddress() + { + $customerAddressId = 11; - $this->shippingProcessor->expects(static::once()) + $this->shippingAssignmentMock->expects(static::atLeastOnce()) + ->method('getItems') + ->willReturn([]); + $this->shippingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn($customerAddressId); + $this->addressRepositoryMock->expects(static::once()) + ->method('getById') + ->with($customerAddressId) + ->willThrowException(new NoSuchEntityException()); + $this->shippingAddressMock->expects(static::once()) + ->method('setCustomerAddressId') + ->with(null) + ->willReturn($this->shippingAddressMock); + $this->shippingProcessorMock->expects(static::once()) ->method('save') - ->with($shipping, $quote); + ->with($this->shippingMock, $this->quoteMock); - $this->shippingAssignmentProcessor->save( - $quote, - $shippingAssignment - ); + $this->shippingAssignmentProcessor->save($this->quoteMock, $this->shippingAssignmentMock); + } + + /** + * Create quote item mock. + * + * @param int|string $id + * @param bool $isDeleted + * + * @return QuoteItem|\PHPUnit_Framework_MockObject_MockObject + */ + private function createQuoteItemMock($id, $isDeleted) + { + $quoteItemMock = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteItemMock->expects(static::any()) + ->method('getItemId') + ->willReturn($id); + $quoteItemMock->expects(static::any()) + ->method('isDeleted') + ->willReturn($isDeleted); + + return $quoteItemMock; } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php index 633beb6c1853a..86bb3ddcf3386 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepository/SaveHandlerTest.php @@ -3,11 +3,26 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Quote\Test\Unit\Model\QuoteRepository; use Magento\Quote\Model\QuoteRepository\SaveHandler; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResourceModel; +use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Quote\Model\Quote\Address\BillingAddressPersister; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Framework\Exception\NoSuchEntityException; +/** + * SaveHandler test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveHandlerTest extends \PHPUnit_Framework_TestCase { /** @@ -16,124 +31,201 @@ class SaveHandlerTest extends \PHPUnit_Framework_TestCase private $saveHandler; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ObjectManagerHelper */ - private $cartItemPersister; + private $objectManagerHelper; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var QuoteResourceModel|\PHPUnit_Framework_MockObject_MockObject */ - private $billingAddressPersister; + private $quoteResourceModelMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CartItemPersister|\PHPUnit_Framework_MockObject_MockObject */ - private $quoteResourceModel; + private $cartItemPersisterMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var BillingAddressPersister|\PHPUnit_Framework_MockObject_MockObject */ - private $shippingAssignmentPersister; + private $billingAddressPersisterMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var ShippingAssignmentPersister|\PHPUnit_Framework_MockObject_MockObject */ - private $quoteMock; + private $shippingAssignmentPersisterMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $itemMock; + private $addressRepositoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var Quote|\PHPUnit_Framework_MockObject_MockObject */ - private $billingAddressMock; + private $quoteMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var QuoteAddress|\PHPUnit_Framework_MockObject_MockObject */ - private $extensionAttributeMock; + private $billingAddressMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var CartExtensionInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $shippingAssignmentMock; + private $extensionAttributesMock; protected function setUp() { - $this->quoteResourceModel = $this->getMock(\Magento\Quote\Model\ResourceModel\Quote::class, [], [], '', false); - $this->cartItemPersister = $this->getMock( - \Magento\Quote\Model\Quote\Item\CartItemPersister::class, - [], - [], - '', - false - ); - $this->billingAddressPersister = $this->getMock( - \Magento\Quote\Model\Quote\Address\BillingAddressPersister::class, - [], - [], - '', - false - ); - $this->shippingAssignmentPersister = $this->getMock( - \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister::class, - [], - [], - '', - false - ); - $methods = [ - 'getItems', 'setLastAddedItem', 'getBillingAddress', 'getIsActive', - 'getExtensionAttributes', 'isVirtual', 'collectTotals' - ]; - $this->quoteMock = $this->getMock(\Magento\Quote\Model\Quote::class, $methods, [], '', false); - $this->itemMock = $this->getMock(\Magento\Quote\Model\Quote\Item::class, [], [], '', false); - $this->billingAddressMock = $this->getMock(\Magento\Quote\Model\Quote\Address::class, [], [], '', false); - $this->extensionAttributeMock = $this->getMock(\Magento\Quote\Api\Data\CartExtensionInterface::class); - $this->shippingAssignmentMock = - $this->getMock( - \Magento\Quote\Api\Data\CartExtension::class, - ['getShippingAssignments', 'setShippingAssignments'], - [], - '', - false - ); - $this->saveHandler = new SaveHandler( - $this->quoteResourceModel, - $this->cartItemPersister, - $this->billingAddressPersister, - $this->shippingAssignmentPersister - ); + $this->quoteResourceModelMock = $this->getMockBuilder(QuoteResourceModel::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cartItemPersisterMock = $this->getMockBuilder(CartItemPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->billingAddressPersisterMock = $this->getMockBuilder(BillingAddressPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->shippingAssignmentPersisterMock = $this->getMockBuilder(ShippingAssignmentPersister::class) + ->disableOriginalConstructor() + ->getMock(); + $this->addressRepositoryMock = $this->getMockBuilder(AddressRepositoryInterface::class) + ->getMockForAbstractClass(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getItems', 'setLastAddedItem', 'getBillingAddress', 'getExtensionAttributes', 'isVirtual', + 'collectTotals' + ] + ) + ->getMock(); + $this->billingAddressMock = $this->getMockBuilder(QuoteAddress::class) + ->disableOriginalConstructor() + ->getMock(); + $this->extensionAttributesMock = $this->getMockBuilder(CartExtensionInterface::class) + ->getMockForAbstractClass(); + + $this->quoteMock->expects(static::any()) + ->method('getBillingAddress') + ->willReturn($this->billingAddressMock); + $this->quoteMock->expects(static::any()) + ->method('getExtensionAttributes') + ->willReturn($this->extensionAttributesMock); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->saveHandler = $this->objectManagerHelper->getObject( + SaveHandler::class, + [ + 'quoteResource' => $this->quoteResourceModelMock, + 'cartItemPersister' => $this->cartItemPersisterMock, + 'billingAddressPersister' => $this->billingAddressPersisterMock, + 'shippingAssignmentPersister' => $this->shippingAssignmentPersisterMock, + 'addressRepository' => $this->addressRepositoryMock + ] + ); } + /** + * Tests save() method for virtual quote. + */ public function testSaveForVirtualQuote() { - $this->quoteMock->expects($this->once())->method('getItems')->willReturn([$this->itemMock]); - $this->itemMock->expects($this->once())->method('isDeleted')->willReturn(false); - $this->cartItemPersister - ->expects($this->once()) + $quoteItemMock = $this->createQuoteItemMock(false); + + $this->quoteMock->expects(static::atLeastOnce()) + ->method('getItems') + ->willReturn([$quoteItemMock]); + $this->cartItemPersisterMock->expects(static::once()) ->method('save') - ->with($this->quoteMock, $this->itemMock) - ->willReturn($this->itemMock); - $this->quoteMock->expects($this->once())->method('setLastAddedItem')->with($this->itemMock); - $this->quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($this->billingAddressMock); - $this->billingAddressPersister - ->expects($this->once()) + ->with($this->quoteMock, $quoteItemMock) + ->willReturn($quoteItemMock); + $this->quoteMock->expects(static::once()) + ->method('setLastAddedItem') + ->with($quoteItemMock) + ->willReturnSelf(); + $this->billingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn(null); + $this->billingAddressMock->expects(static::never()) + ->method('getCustomerAddress'); + $this->billingAddressPersisterMock->expects(static::once()) ->method('save') ->with($this->quoteMock, $this->billingAddressMock); - $this->quoteMock - ->expects($this->once()) - ->method('getExtensionAttributes') - ->willReturn($this->extensionAttributeMock); - $this->extensionAttributeMock - ->expects($this->never()) + $this->quoteMock->expects(static::atLeastOnce()) + ->method('isVirtual') + ->willReturn(true); + $this->extensionAttributesMock->expects(static::never()) + ->method('getShippingAssignments'); + $this->quoteMock->expects(static::atLeastOnce()) + ->method('collectTotals') + ->willReturnSelf(); + $this->quoteResourceModelMock->expects(static::once()) + ->method('save') + ->with($this->quoteMock) + ->willReturnSelf(); + + $this->assertSame($this->quoteMock, $this->saveHandler->save($this->quoteMock)); + } + + /** + * Tests save() method with not existing customer address. + */ + public function testSaveWithNotExistingCustomerAddress() + { + $customerAddressId = 5; + + $this->quoteMock->expects(static::atLeastOnce()) + ->method('getItems') + ->willReturn([]); + $this->quoteMock->expects(static::never()) + ->method('setLastAddedItem'); + $this->billingAddressMock->expects(static::atLeastOnce()) + ->method('getCustomerAddressId') + ->willReturn($customerAddressId); + $this->addressRepositoryMock->expects(static::once()) + ->method('getById') + ->with($customerAddressId) + ->willThrowException(new NoSuchEntityException()); + $this->billingAddressMock->expects(static::once()) + ->method('setCustomerAddressId') + ->willReturn(null); + $this->billingAddressPersisterMock->expects(static::once()) + ->method('save') + ->with($this->quoteMock, $this->billingAddressMock); + $this->quoteMock->expects(static::atLeastOnce()) + ->method('isVirtual') + ->willReturn(true); + $this->extensionAttributesMock->expects(static::never()) ->method('getShippingAssignments'); - $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(true); - $this->quoteMock->expects($this->once())->method('collectTotals')->willReturn($this->quoteMock); - $this->quoteResourceModel->expects($this->once())->method('save')->with($this->quoteMock); - $this->assertEquals($this->quoteMock, $this->saveHandler->save($this->quoteMock)); + $this->quoteMock->expects(static::atLeastOnce()) + ->method('collectTotals') + ->willReturnSelf(); + $this->quoteResourceModelMock->expects(static::once()) + ->method('save') + ->with($this->quoteMock) + ->willReturnSelf(); + + $this->assertSame($this->quoteMock, $this->saveHandler->save($this->quoteMock)); + } + + /** + * Create quote item mock. + * + * @param bool $isDeleted + * + * @return QuoteItem|\PHPUnit_Framework_MockObject_MockObject + */ + private function createQuoteItemMock($isDeleted) + { + $quoteItemMock = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteItemMock->expects(static::any()) + ->method('isDeleted') + ->willReturn($isDeleted); + + return $quoteItemMock; } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php index 3a25f419e38ba..103c72bc237c4 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php @@ -7,25 +7,62 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Api\FilterBuilder; -use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Quote\Api\Data\CartInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\User\Api\Data\UserInterface; +use Magento\Framework\Api\SearchResults; +use Magento\Quote\Api\Data\CartSearchResultsInterface; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Api\Data\CartExtension; +/** + * QuoteRepository test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class QuoteRepositoryTest extends \PHPUnit_Framework_TestCase { /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->quoteRepository = $this->objectManager->create(QuoteRepository::class); + $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); + } + /** + * Tests getting list of quotes according to search criteria. * @magentoDataFixture Magento/Sales/_files/quote.php */ public function testGetList() { $searchCriteria = $this->getSearchCriteria('test01'); - /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ - $quoteRepository = Bootstrap::getObjectManager()->create(CartRepositoryInterface::class); - $searchResult = $quoteRepository->getList($searchCriteria); + $searchResult = $this->quoteRepository->getList($searchCriteria); $this->performAssertions($searchResult); } /** + * Tests getting list of quotes according to different search criterias. * @magentoDataFixture Magento/Sales/_files/quote.php */ public function testGetListDoubleCall() @@ -33,37 +70,91 @@ public function testGetListDoubleCall() $searchCriteria1 = $this->getSearchCriteria('test01'); $searchCriteria2 = $this->getSearchCriteria('test02'); - /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ - $quoteRepository = Bootstrap::getObjectManager()->create(CartRepositoryInterface::class); - $searchResult = $quoteRepository->getList($searchCriteria1); + $searchResult = $this->quoteRepository->getList($searchCriteria1); $this->performAssertions($searchResult); - $searchResult = $quoteRepository->getList($searchCriteria2); - $items = $searchResult->getItems(); - $this->assertEmpty($items); + + $searchResult = $this->quoteRepository->getList($searchCriteria2); + $this->assertEmpty($searchResult->getItems()); + } + + /** + * Save quote test. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testSaveWithNotExistingCustomerAddress() + { + $addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; + + /** @var QuoteAddress $billingAddress */ + $billingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_BILLING) + ->setCustomerAddressId('not_existing'); + + /** @var QuoteAddress $shippingAddress */ + $shippingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); + $shippingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_SHIPPING) + ->setCustomerAddressId('not_existing'); + + /** @var Shipping $shipping */ + $shipping = $this->objectManager->create(Shipping::class); + $shipping->setAddress($shippingAddress); + + /** @var ShippingAssignment $shippingAssignment */ + $shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $shippingAssignment->setItems([]); + $shippingAssignment->setShipping($shipping); + + /** @var CartExtension $extensionAttributes */ + $extensionAttributes = $this->objectManager->create(CartExtension::class); + $extensionAttributes->setShippingAssignments([$shippingAssignment]); + + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->setStoreId(1) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setExtensionAttributes($extensionAttributes) + ->save(); + $this->quoteRepository->save($quote); + + $this->assertNull($quote->getBillingAddress()->getCustomerAddressId()); + $this->assertNull( + $quote->getExtensionAttributes() + ->getShippingAssignments()[0] + ->getShipping() + ->getAddress() + ->getCustomerAddressId() + ); } /** + * Get search criteria. + * * @param string $filterValue + * * @return \Magento\Framework\Api\SearchCriteria */ private function getSearchCriteria($filterValue) { - /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = Bootstrap::getObjectManager()->create(SearchCriteriaBuilder::class); - $filterBuilder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $filters = []; - $filters[] = $filterBuilder + $filters[] = $this->filterBuilder ->setField('reserved_order_id') ->setConditionType('=') ->setValue($filterValue) ->create(); - $searchCriteriaBuilder->addFilters($filters); + $this->searchCriteriaBuilder->addFilters($filters); - return $searchCriteriaBuilder->create(); + return $this->searchCriteriaBuilder->create(); } /** - * @param object $searchResult + * Perform assertions. + * + * @param SearchResults|CartSearchResultsInterface $searchResult */ protected function performAssertions($searchResult) { @@ -74,12 +165,13 @@ protected function performAssertions($searchResult) ]; $items = $searchResult->getItems(); - /** @var \Magento\Quote\Api\Data\CartInterface $actualQuote */ + /** @var CartInterface $actualQuote */ $actualQuote = array_pop($items); + /** @var UserInterface $testAttribute */ + $testAttribute = $actualQuote->getExtensionAttributes()->getQuoteTestAttribute(); + $this->assertInstanceOf(CartInterface::class, $actualQuote); $this->assertEquals('test01', $actualQuote->getReservedOrderId()); - /** @var \Magento\User\Api\Data\UserInterface $testAttribute */ - $testAttribute = $actualQuote->getExtensionAttributes()->getQuoteTestAttribute(); $this->assertEquals($expectedExtensionAttributes['firstname'], $testAttribute->getFirstName()); $this->assertEquals($expectedExtensionAttributes['lastname'], $testAttribute->getLastName()); $this->assertEquals($expectedExtensionAttributes['email'], $testAttribute->getEmail()); From ea25325b0b9f23ea63c9b3351f8f89bbb482bc5b Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 09:13:16 +0300 Subject: [PATCH 14/19] MAGETWO-64068: [Backport] - Not possible to update or delete products in cart or checkout after deleting address - for 2.1 --- .../testsuite/Magento/Quote/Model/QuoteRepositoryTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php index 103c72bc237c4..104b0162121b9 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php @@ -50,6 +50,7 @@ protected function setUp() $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); } + /** * Tests getting list of quotes according to search criteria. * @magentoDataFixture Magento/Sales/_files/quote.php From 9ff16b00276e442b59a007dce3ffad9bd4f1188d Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 11:58:43 +0300 Subject: [PATCH 15/19] MAGETWO-64730: [Backport] - Cannot place order with PayPal Express (last product in stock) - for 2.1 --- .../Magento/Paypal/Model/Express/Checkout.php | 38 ++++++- .../Paypal/Model/Express/CheckoutTest.php | 98 ++++++++++++++++--- .../Magento/Paypal/_files/quote_express.php | 90 +++++++++++++++++ .../_files/quote_express_with_customer.php | 77 +++++++++++++++ .../Paypal/_files/quote_payment_express.php | 92 +---------------- .../quote_payment_express_with_customer.php | 74 +------------- 6 files changed, 291 insertions(+), 178 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php create mode 100644 dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index be185ce7b0ac0..a35a5eea96a6b 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -12,6 +12,8 @@ use Magento\Quote\Model\Quote\Address; use Magento\Framework\DataObject; use Magento\Paypal\Model\Cart as PaypalCart; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Api\OrderRepositoryInterface; /** * Wrapper that performs Paypal Express and Checkout communication @@ -70,7 +72,7 @@ class Checkout * * @var string */ - protected $_apiType = 'Magento\Paypal\Model\Api\Nvp'; + protected $_apiType = \Magento\Paypal\Model\Api\Nvp::class; /** * Payment method type @@ -268,6 +270,13 @@ class Checkout */ protected $totalsCollector; + /** + * Order repository interface. + * + * @var OrderRepositoryInterface + */ + private $orderRepository; + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Customer\Model\Url $customerUrl @@ -538,6 +547,12 @@ public function start($returnUrl, $cancelUrl, $button = null) } $this->_api->setSuppressShipping(true); } else { + $billingAddress = $this->_quote->getBillingAddress(); + + if ($billingAddress) { + $this->_api->setBillingAddress($billingAddress); + } + $address = $this->_quote->getShippingAddress(); $isOverridden = 0; if (true === $address->validate()) { @@ -668,6 +683,7 @@ public function returnFromPaypal($token) $this->_setExportedAddressData($billingAddress, $exportedBillingAddress); $billingAddress->setCustomerNote($exportedBillingAddress->getData('note')); $quote->setBillingAddress($billingAddress); + $quote->setCheckoutMethod($this->getCheckoutMethod()); // import payment info $payment = $quote->getPayment(); @@ -789,7 +805,8 @@ public function place($token, $shippingMethodCode = null) $this->ignoreAddressValidation(); $this->_quote->collectTotals(); - $order = $this->quoteManagement->submit($this->_quote); + $orderId = $this->quoteManagement->placeOrder($this->_quote->getId()); + $order = $this->getOrderRepository()->get($orderId); if (!$order) { return; @@ -1157,4 +1174,21 @@ protected function prepareGuestQuote() ->setCustomerGroupId(\Magento\Customer\Model\Group::NOT_LOGGED_IN_ID); return $this; } + + /** + * Returns order repository instance. + * + * @return OrderRepositoryInterface + * + * @deprecated + */ + private function getOrderRepository() + { + if ($this->orderRepository === null) { + $this->orderRepository = ObjectManager::getInstance() + ->get(OrderRepositoryInterface::class); + } + + return $this->orderRepository; + } } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index bf1fed29857c6..0fe79ad7bd2db 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -8,6 +8,10 @@ use Magento\Quote\Model\Quote; use Magento\Checkout\Model\Type\Onepage; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Paypal\Model\Config; +use Magento\Paypal\Model\Api\Type\Factory; +use Magento\Paypal\Model\Info; +use Magento\Paypal\Model\Api\Nvp; /** * Class CheckoutTest @@ -32,7 +36,7 @@ protected function setUp() /** * Verify that an order placed with an existing customer can re-use the customer addresses. * - * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php + * @magentoDataFixture Magento/Paypal/_files/quote_express_with_customer.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ @@ -43,19 +47,19 @@ public function testPrepareCustomerQuote() $quote->setCheckoutMethod(Onepage::METHOD_CUSTOMER); // to dive into _prepareCustomerQuote() on switch $quote->getShippingAddress()->setSameAsBilling(0); $quote->setReservedOrderId(null); - $customer = $this->_objectManager->create('Magento\Customer\Model\Customer')->load(1); + $customer = $this->_objectManager->create(\Magento\Customer\Model\Customer::class)->load(1); $customer->setDefaultBilling(false) ->setDefaultShipping(false) ->save(); /** @var \Magento\Customer\Model\Session $customerSession */ - $customerSession = $this->_objectManager->get('Magento\Customer\Model\Session'); + $customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); $customerSession->loginById(1); $checkout = $this->_getCheckout($quote); $checkout->place('token'); /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerService */ - $customerService = $this->_objectManager->get('Magento\Customer\Api\CustomerRepositoryInterface'); + $customerService = $this->_objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); $customer = $customerService->getById($quote->getCustomerId()); $this->assertEquals(1, $quote->getCustomerId()); @@ -72,7 +76,7 @@ public function testPrepareCustomerQuote() /** * Verify that after placing the order, addresses are associated with the order and the quote is a guest quote. * - * @magentoDataFixture Magento/Paypal/_files/quote_payment_express.php + * @magentoDataFixture Magento/Paypal/_files/quote_express.php * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ @@ -109,10 +113,10 @@ public function testPlaceGuestQuote() protected function _getCheckout(Quote $quote) { return $this->_objectManager->create( - 'Magento\Paypal\Model\Express\Checkout', + \Magento\Paypal\Model\Express\Checkout::class, [ 'params' => [ - 'config' => $this->getMock('Magento\Paypal\Model\Config', [], [], '', false), + 'config' => $this->getMock(\Magento\Paypal\Model\Config::class, [], [], '', false), 'quote' => $quote, ] ] @@ -129,11 +133,11 @@ protected function _getCheckout(Quote $quote) public function testReturnFromPaypal() { $quote = $this->_getFixtureQuote(); - $paypalConfigMock = $this->getMock('Magento\Paypal\Model\Config', [], [], '', false); - $apiTypeFactory = $this->getMock('Magento\Paypal\Model\Api\Type\Factory', [], [], '', false); - $paypalInfo = $this->getMock('Magento\Paypal\Model\Info', [], [], '', false); + $paypalConfigMock = $this->getMock(\Magento\Paypal\Model\Config::class, [], [], '', false); + $apiTypeFactory = $this->getMock(\Magento\Paypal\Model\Api\Type\Factory::class, [], [], '', false); + $paypalInfo = $this->getMock(\Magento\Paypal\Model\Info::class, [], [], '', false); $checkoutModel = $this->_objectManager->create( - 'Magento\Paypal\Model\Express\Checkout', + \Magento\Paypal\Model\Express\Checkout::class, [ 'params' => ['quote' => $quote, 'config' => $paypalConfigMock], 'apiTypeFactory' => $apiTypeFactory, @@ -142,7 +146,7 @@ public function testReturnFromPaypal() ); $api = $this->getMock( - 'Magento\Paypal\Model\Api\Nvp', + \Magento\Paypal\Model\Api\Nvp::class, ['call', 'getExportedShippingAddress', 'getExportedBillingAddress'], [], '', @@ -190,7 +194,73 @@ public function testReturnFromPaypal() } /** - * Prepare fixture for exported address + * Verify that guest customer quote has set type of checkout. + * + * @magentoDataFixture Magento/Paypal/_files/quote_payment_express.php + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testGuestReturnFromPaypal() + { + $quote = $this->_getFixtureQuote(); + $paypalConfig = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + + $apiTypeFactory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $paypalInfo = $this->getMockBuilder(Info::class) + ->disableOriginalConstructor() + ->setMethods(['importToPayment']) + ->getMock(); + + $checkoutModel = $this->_objectManager->create( + Checkout::class, + [ + 'params' => ['quote' => $quote, 'config' => $paypalConfig], + 'apiTypeFactory' => $apiTypeFactory, + 'paypalInfo' => $paypalInfo + ] + ); + + $api = $this->getMockBuilder(Nvp::class) + ->disableOriginalConstructor() + ->setMethods(['call', 'getExportedShippingAddress', 'getExportedBillingAddress']) + ->getMock(); + + $api->expects($this->any()) + ->method('call') + ->will($this->returnValue([])); + + $apiTypeFactory->expects($this->any()) + ->method('create') + ->will($this->returnValue($api)); + + $exportedBillingAddress = $this->_getExportedAddressFixture($quote->getBillingAddress()->getData()); + $api->expects($this->any()) + ->method('getExportedBillingAddress') + ->will($this->returnValue($exportedBillingAddress)); + + $exportedShippingAddress = $this->_getExportedAddressFixture($quote->getShippingAddress()->getData()); + $api->expects($this->any()) + ->method('getExportedShippingAddress') + ->will($this->returnValue($exportedShippingAddress)); + + $paypalInfo->expects($this->once()) + ->method('importToPayment') + ->with($api, $quote->getPayment()); + + $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 1); + + $checkoutModel->returnFromPaypal('token'); + $this->assertEquals(Onepage::METHOD_GUEST, $quote->getCheckoutMethod()); + } + + /** + * Prepare fixture for exported address. * * @param array $addressData * @return \Magento\Framework\DataObject @@ -216,7 +286,7 @@ protected function _getExportedAddressFixture(array $addressData) protected function _getFixtureQuote() { /** @var \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection */ - $quoteCollection = $this->_objectManager->create('Magento\Quote\Model\ResourceModel\Quote\Collection'); + $quoteCollection = $this->_objectManager->create(\Magento\Quote\Model\ResourceModel\Quote\Collection::class); return $quoteCollection->getLastItem(); } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php new file mode 100644 index 0000000000000..b541f3e7cb9f0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express.php @@ -0,0 +1,90 @@ +loadArea('adminhtml'); +$objectManager->get( + \Magento\Framework\App\Config\MutableScopeConfigInterface::class +)->setValue( + 'carriers/flatrate/active', + 1, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE +); +$objectManager->get( + \Magento\Framework\App\Config\MutableScopeConfigInterface::class +)->setValue( + 'payment/paypal_express/active', + 1, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE +); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setId(1) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + ] + )->save(); +$product->load(1); + +$billingData = [ + 'firstname' => 'testname', + 'lastname' => 'lastname', + 'company' => '', + 'email' => 'test@com.com', + 'street' => [ + 0 => 'test1', + 1 => '', + ], + 'city' => 'Test', + 'region_id' => '1', + 'region' => '', + 'postcode' => '9001', + 'country_id' => 'US', + 'telephone' => '11111111', + 'fax' => '', + 'confirm_password' => '', + 'save_in_address_book' => '1', + 'use_for_shipping' => '1', +]; + +$billingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class, ['data' => $billingData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); +$shippingAddress->setShippingMethod('flatrate_flatrate'); +$shippingAddress->setCollectShippingRates(true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(true) + ->setStoreId( + $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId() + ) + ->setReservedOrderId('100000002') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product, 10); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); + +$quoteRepository = $objectManager->get(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); +$quote = $quoteRepository->get($quote->getId()); +$quote->setCustomerEmail('admin@example.com'); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php new file mode 100644 index 0000000000000..897e3f379d9c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_express_with_customer.php @@ -0,0 +1,77 @@ +loadArea('adminhtml'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('carriers/flatrate/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); +$objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('payment/paypal_express/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + +/** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +$customer = $customerRepository->getById(1); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setId(1) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 100, + ]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); +$product->load(1); + +$customerBillingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); +$customerBillingAddress->load(1); +$billingAddressDataObject = $customerBillingAddress->getDataModel(); +$billingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); +$billingAddress->importCustomerAddressData($billingAddressDataObject); +$billingAddress->setAddressType('billing'); + +/** @var \Magento\Customer\Model\Address $customerShippingAddress */ +$customerShippingAddress = $objectManager->create(\Magento\Customer\Model\Address::class); +$customerShippingAddress->load(2); +$shippingAddressDataObject = $customerShippingAddress->getDataModel(); +$shippingAddress = $objectManager->create(\Magento\Quote\Model\Quote\Address::class); +$shippingAddress->importCustomerAddressData($shippingAddressDataObject); +$shippingAddress->setAddressType('shipping'); + +$shippingAddress->setShippingMethod('flatrate_flatrate'); +$shippingAddress->setCollectShippingRates(true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(false) + ->setCustomerId($customer->getId()) + ->setCustomer($customer) + ->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()) + ->setReservedOrderId('test02') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product, 10); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); + +/** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); +$quote = $quoteRepository->get($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php index b868e64a8af0f..2bc22ff98339a 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express.php @@ -3,98 +3,10 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Framework\App\Config\MutableScopeConfigInterface' -)->setValue( - 'carriers/flatrate/active', - 1, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -); -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Framework\App\Config\MutableScopeConfigInterface' -)->setValue( - 'payment/paypal_express/active', - 1, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE -); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create('Magento\Catalog\Model\Product'); -$product->setTypeId('simple') - ->setId(1) - ->setAttributeSetId(4) - ->setName('Simple Product') - ->setSku('simple') - ->setPrice(10) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setStockData( - [ - 'qty' => 100, - 'is_in_stock' => 1, - ] - )->save(); -$product->load(1); -$billingData = [ - 'firstname' => 'testname', - 'lastname' => 'lastname', - 'company' => '', - 'email' => 'test@com.com', - 'street' => [ - 0 => 'test1', - 1 => '', - ], - 'city' => 'Test', - 'region_id' => '1', - 'region' => '', - 'postcode' => '9001', - 'country_id' => 'US', - 'telephone' => '11111111', - 'fax' => '', - 'confirm_password' => '', - 'save_in_address_book' => '1', - 'use_for_shipping' => '1', -]; - -$billingAddress = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Quote\Model\Quote\Address', ['data' => $billingData]); -$billingAddress->setAddressType('billing'); - -$shippingAddress = clone $billingAddress; -$shippingAddress->setId(null)->setAddressType('shipping'); -$shippingAddress->setShippingMethod('flatrate_flatrate'); -$shippingAddress->setCollectShippingRates(true); - -/** @var $quote \Magento\Quote\Model\Quote */ -$quote = $objectManager->create('Magento\Quote\Model\Quote'); -$quote->setCustomerIsGuest( - true -)->setStoreId( - $objectManager->get( - 'Magento\Store\Model\StoreManagerInterface' - )->getStore()->getId() -)->setReservedOrderId( - '100000002' -)->setBillingAddress( - $billingAddress -)->setShippingAddress( - $shippingAddress -)->addProduct( - $product, - 10 -); -$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); -$quote->getShippingAddress()->setCollectShippingRates(true); -$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); - -$quoteRepository = $objectManager->get(\Magento\Quote\Api\CartRepositoryInterface::class); -$quoteRepository->save($quote); -$quote = $quoteRepository->get($quote->getId()); -$quote->setCustomerEmail('admin@example.com'); +require __DIR__ . '/quote_express.php'; /** @var $service \Magento\Quote\Api\CartManagementInterface */ $service = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('\Magento\Quote\Api\CartManagementInterface'); + ->create(\Magento\Quote\Api\CartManagementInterface::class); $order = $service->submit($quote, ['increment_id' => '100000002']); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php index 2570bfdeb8220..8879c9a863c63 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/_files/quote_payment_express_with_customer.php @@ -4,78 +4,8 @@ * See COPYING.txt for license details. */ -require __DIR__ . '/../../Customer/_files/customer.php'; -require __DIR__ . '/../../Customer/_files/customer_two_addresses.php'; - -\Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); - -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - -$objectManager->get('Magento\Framework\App\Config\MutableScopeConfigInterface') - ->setValue('carriers/flatrate/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); -$objectManager->get('Magento\Framework\App\Config\MutableScopeConfigInterface') - ->setValue('payment/paypal_express/active', 1, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); - -/** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ -$customerRepository = $objectManager->create('Magento\Customer\Api\CustomerRepositoryInterface'); -$customer = $customerRepository->getById(1); - -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create('Magento\Catalog\Model\Product'); -$product->setTypeId('simple') - ->setId(1) - ->setAttributeSetId(4) - ->setName('Simple Product') - ->setSku('simple') - ->setPrice(10) - ->setStockData([ - 'use_config_manage_stock' => 1, - 'qty' => 100, - 'is_qty_decimal' => 0, - 'is_in_stock' => 100, -]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); -$product->load(1); - -$customerBillingAddress = $objectManager->create('Magento\Customer\Model\Address'); -$customerBillingAddress->load(1); -$billingAddressDataObject = $customerBillingAddress->getDataModel(); -$billingAddress = $objectManager->create('Magento\Quote\Model\Quote\Address'); -$billingAddress->importCustomerAddressData($billingAddressDataObject); -$billingAddress->setAddressType('billing'); - -/** @var \Magento\Customer\Model\Address $customerShippingAddress */ -$customerShippingAddress = $objectManager->create('Magento\Customer\Model\Address'); -$customerShippingAddress->load(2); -$shippingAddressDataObject = $customerShippingAddress->getDataModel(); -$shippingAddress = $objectManager->create('Magento\Quote\Model\Quote\Address'); -$shippingAddress->importCustomerAddressData($shippingAddressDataObject); -$shippingAddress->setAddressType('shipping'); - -$shippingAddress->setShippingMethod('flatrate_flatrate'); -$shippingAddress->setCollectShippingRates(true); - -/** @var $quote \Magento\Quote\Model\Quote */ -$quote = $objectManager->create('Magento\Quote\Model\Quote'); -$quote->setCustomerIsGuest(false) - ->setCustomerId($customer->getId()) - ->setCustomer($customer) - ->setStoreId($objectManager->get('Magento\Store\Model\StoreManagerInterface')->getStore()->getId()) - ->setReservedOrderId('test02') - ->setBillingAddress($billingAddress) - ->setShippingAddress($shippingAddress) - ->addProduct($product, 10); -$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); -$quote->getShippingAddress()->setCollectShippingRates(true); -$quote->getPayment()->setMethod(\Magento\Paypal\Model\Config::METHOD_WPS_EXPRESS); - -/** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ -$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); -$quoteRepository->save($quote); -$quote = $quoteRepository->get($quote->getId()); +require __DIR__ . '/quote_express_with_customer.php'; /** @var $service \Magento\Quote\Api\CartManagementInterface */ -$service = $objectManager->create('\Magento\Quote\Api\CartManagementInterface'); +$service = $objectManager->create(\Magento\Quote\Api\CartManagementInterface::class); $order = $service->submit($quote, ['increment_id' => '100000002']); From 205fb60e2258bc005c1c0fa6f629e7b6c81d9fe2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 12:56:56 +0300 Subject: [PATCH 16/19] MAGETWO-64730: [Backport] - Cannot place order with PayPal Express (last product in stock) - for 2.1 --- .../testsuite/Magento/Paypal/Model/Express/CheckoutTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index 0fe79ad7bd2db..0d908e4d458ca 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php @@ -14,7 +14,9 @@ use Magento\Paypal\Model\Api\Nvp; /** - * Class CheckoutTest + * Checkout test. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CheckoutTest extends \PHPUnit_Framework_TestCase { From aa325ad5b12d5caaaddab049f2ad24ef0be8796d Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 19 May 2017 16:09:29 +0300 Subject: [PATCH 17/19] MAGETWO-67628: [Backport] - 503 error when trying to make tax_class_id attribute Not Searchable - for 2.1 --- .../Adminhtml/Product/Attribute/Validate.php | 14 ++-- .../Product/Attribute/ValidateTest.php | 18 +++-- .../Adminhtml/Product/AttributeTest.php | 71 +++++++++++++++---- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index cb4d370cf51e8..94fad64f5ed96 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -68,7 +68,7 @@ public function execute() $attributeCode = $attributeCode ?: $this->generateCode($frontendLabel[0]); $attributeId = $this->getRequest()->getParam('attribute_id'); $attribute = $this->_objectManager->create( - 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class )->loadByCode( $this->_entityTypeId, $attributeCode @@ -87,10 +87,10 @@ public function execute() if ($this->getRequest()->has('new_attribute_set_name')) { $setName = $this->getRequest()->getParam('new_attribute_set_name'); /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ - $attributeSet = $this->_objectManager->create('Magento\Eav\Model\Entity\Attribute\Set'); + $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); $attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name'); if ($attributeSet->getId()) { - $setName = $this->_objectManager->get('Magento\Framework\Escaper')->escapeHtml($setName); + $setName = $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($setName); $this->messageManager->addError(__('An attribute set named \'%1\' already exists.', $setName)); $layout = $this->layoutFactory->create(); @@ -149,10 +149,16 @@ private function setMessageToResponse($response, $messages) /** * @param DataObject $response * @param array|null $options + * + * @return void */ private function checkUniqueOption(DataObject $response, array $options = null) { - if (is_array($options) && !$this->isUniqueAdminValues($options['value'], $options['delete'])) { + if (is_array($options) + && !empty($options['value']) + && !empty($options['delete']) + && !$this->isUniqueAdminValues($options['value'], $options['delete']) + ) { $this->setMessageToResponse($response, [__('The value of Admin must be unique.')]); $response->setError(true); } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index eb0149544323a..64cab9578c654 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -198,8 +198,16 @@ public function testUniqueValidation(array $options, $isError) public function provideUniqueData() { return [ - // valid options - [ + 'no values' => [ + [ + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ] + ], false + ], + 'valid options' => [ [ 'value' => [ "option_0" => [1, 0], @@ -213,8 +221,7 @@ public function provideUniqueData() ] ], false ], - //with duplicate - [ + 'duplicate options' => [ [ 'value' => [ "option_0" => [1, 0], @@ -228,8 +235,7 @@ public function provideUniqueData() ] ], true ], - //with duplicate but deleted - [ + 'duplicate and deleted' => [ [ 'value' => [ "option_0" => [1, 0], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php index ca67bbbaa2769..991b71f6af03a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -28,7 +28,7 @@ public function testWrongFrontendInput() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); $message = $messages->getItemsByType('error')[0]; $this->assertEquals('Input type "some_input" not found in the input types list.', $message->getText()); @@ -53,7 +53,7 @@ public function testWithPopup() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('success')); $message = $messages->getItemsByType('success')[0]; $this->assertEquals('You saved the product attribute.', $message->getText()); @@ -73,7 +73,7 @@ public function testWithExceptionWhenSaveAttribute() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); } @@ -91,7 +91,7 @@ public function testWrongAttributeId() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); /** @var \Magento\Framework\Message\Error $message */ $message = $messages->getItemsByType('error')[0]; @@ -116,7 +116,7 @@ public function testAttributeWithoutId() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('success')); /** @var \Magento\Framework\Message\Success $message */ $message = $messages->getItemsByType('success')[0]; @@ -137,7 +137,7 @@ public function testWrongAttributeCode() $this->getResponse()->getHeader('Location')->getFieldValue() ); /** @var \Magento\Framework\Message\Collection $messages */ - $messages = $this->_objectManager->create('Magento\Framework\Message\ManagerInterface')->getMessages(); + $messages = $this->_objectManager->create(\Magento\Framework\Message\ManagerInterface::class)->getMessages(); $this->assertEquals(1, $messages->getCountByType('error')); /** @var \Magento\Framework\Message\Error $message */ $message = $messages->getItemsByType('error')[0]; @@ -171,7 +171,7 @@ public function testSaveActionApplyToDataSystemAttribute() $postData = $this->_getAttributeData() + ['attribute_id' => '2']; $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); - $model = $this->_objectManager->create('Magento\Catalog\Model\ResourceModel\Eav\Attribute'); + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $model->load($postData['attribute_id']); $this->assertNull($model->getData('apply_to')); } @@ -185,7 +185,7 @@ public function testSaveActionApplyToDataUserDefinedAttribute() $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $model */ - $model = $this->_objectManager->create('Magento\Catalog\Model\ResourceModel\Eav\Attribute'); + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $model->load($postData['attribute_id']); $this->assertEquals('simple', $model->getData('apply_to')); } @@ -199,7 +199,7 @@ public function testSaveActionApplyToData() unset($postData['apply_to']); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); - $model = $this->_objectManager->create('Magento\Catalog\Model\ResourceModel\Eav\Attribute'); + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $model->load($postData['attribute_id']); $this->assertEquals(['simple'], $model->getApplyTo()); } @@ -212,7 +212,7 @@ public function testSaveActionApplyToData() public function testSaveActionCleanAttributeLabelCache() { /** @var \Magento\Translation\Model\ResourceModel\StringUtils $string */ - $string = $this->_objectManager->create('Magento\Translation\Model\ResourceModel\StringUtils'); + $string = $this->_objectManager->create(\Magento\Translation\Model\ResourceModel\StringUtils::class); $this->assertEquals('predefined string translation', $this->_translate('string to translate')); $string->saveTranslate('string to translate', 'new string translation'); $postData = $this->_getAttributeData() + ['attribute_id' => 1]; @@ -231,12 +231,12 @@ protected function _translate($string) { // emulate admin store and design \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - 'Magento\Framework\View\DesignInterface' + \Magento\Framework\View\DesignInterface::class )->setDesignTheme( 1 ); /** @var \Magento\Framework\TranslateInterface $translate */ - $translate = $this->_objectManager->get('Magento\Framework\TranslateInterface'); + $translate = $this->_objectManager->get(\Magento\Framework\TranslateInterface::class); $translate->loadData(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, true); return __($string); } @@ -270,4 +270,51 @@ protected function _getAttributeData() 'frontend_label' => [\Magento\Store\Model\Store::DEFAULT_STORE_ID => 'string to translate'] ]; } + + /** + * Tests \Magento\Catalog\Controller\Adminhtml\Product\Attribute\Validate. + * + * @dataProvider dataProviderForTestValidate + */ + public function testValidateAttribute($postData) + { + $expectedResult = ['error' => false]; + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $model */ + $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); + $model->load($postData['attribute_code'], 'attribute_code'); + $attributeId = $model->getId(); + + $postData['attribute_id'] = $attributeId; + + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/product_attribute/validate'); + $response = $this->getResponse()->getBody(); + $this->assertJson($response, 'Validate controller didn\'t return expected result.'); + $result = \Zend_Json::decode($response); + $this->assertEquals($expectedResult, $result, 'Attribute validation didn\'t pass.'); + } + + /** + * Returns data for testValidateAttribute. + * + * @return array + */ + public function dataProviderForTestValidate() + { + return [ + 'tax_class_id' => [ + 'postData' => [ + 'attribute_code' => 'tax_class_id', + 'frontend_label' => 'Tax Class', + 'option' => [ + 'delete' => [ + 0 => '', + 2 => '' + ] + ] + ] + ] + + ]; + } } From c29ae7ba19ef956be7d082744a7927b5ef82271b Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 19 May 2017 18:11:36 +0300 Subject: [PATCH 18/19] MAGETWO-64675: Functional test changes. --- .../Checkout/Test/Block/Onepage/Review.php | 22 +++++++++- .../Test/TestStep/ViewAndEditCartStep.php | 41 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php index 5c667e30ead4d..470feddf3252a 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php @@ -6,12 +6,30 @@ namespace Magento\Checkout\Test\Block\Onepage; -use Magento\Checkout\Test\Block\Onepage\AbstractReview; +use Magento\Mtf\Client\Locator; /** * One page checkout status review block. */ class Review extends AbstractReview { - // + /** + * Review gift card line locator. + * + * @var string + */ + private $giftCardTotalSelector = '//div[contains(@class, "opc-block-summary")]//tr[contains(@class, "giftcard")]'; + + + /** + * Return if gift card is applied. + * + * @return bool + */ + public function isGiftCardApplied() + { + $this->waitForElementNotVisible($this->waitElement); + + return $this->_rootElement->find($this->giftCardTotalSelector, Locator::SELECTOR_XPATH)->isVisible(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php new file mode 100644 index 0000000000000..0d73bed319f7c --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/ViewAndEditCartStep.php @@ -0,0 +1,41 @@ +checkoutCart = $checkoutCart; + } + + /** + * Proceed to checkout cart page. + * + * @return void + */ + public function run() + { + $this->checkoutCart->open(); + } +} From 4191e0821b953119e963644bf3c90adb832ad78a Mon Sep 17 00:00:00 2001 From: Andrii Meysar Date: Fri, 19 May 2017 18:39:00 +0300 Subject: [PATCH 19/19] MAGETWO-64675: Functional test changes. --- .../tests/app/Magento/Checkout/Test/Block/Onepage/Review.php | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php index 470feddf3252a..8f47c0f5716ca 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Review.php @@ -20,7 +20,6 @@ class Review extends AbstractReview */ private $giftCardTotalSelector = '//div[contains(@class, "opc-block-summary")]//tr[contains(@class, "giftcard")]'; - /** * Return if gift card is applied. *