= $block->escapeHtml($_option->getTitle()) ?>
- showSingle()) : ?>
+ showSingle()): ?>
= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?>
= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?>
-
-
+
+
+ getSelectionId() ?>
- getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"'; } ?>
- name="bundle_option[= $block->escapeHtmlAttr($_option->getId()) ?>][= $block->escapeHtmlAttr($_selection->getId()) ?>]"
- data-selector="bundle_option[= $block->escapeHtmlAttr($_option->getId()) ?>][= $block->escapeHtmlAttr($_selection->getId()) ?>]"
- isSelected($_selection)) { echo ' checked="checked"'; } ?>
- isSaleable()) { echo ' disabled="disabled"'; } ?>
- value="= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/>
+ getRequired()): ?>
+ = /* @noEscape */ $dataValidation ?>
+
+ name="=/* @noEscape */ $inputName .'['. $block->escapeHtmlAttr($sectionId)?>]"
+ data-selector="= /* @noEscape */ $inputName.'['.$block->escapeHtmlAttr($sectionId)?>]"
+ isSelected($selection)): ?>
+ = ' checked="checked"' ?>
+
+ isSaleable()): ?>
+ = ' disabled="disabled"' ?>
+
+ value="= $block->escapeHtmlAttr($sectionId) ?>"
+ data-errors-message-box="#validation-message-box"/>
- = /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?>
+ for="= /* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId) ?>">
+ = /* @noEscape */ $block->getSelectionQtyTitlePrice($selection) ?>
- = /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?>
+ = /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($selection) ?>
+
diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
index 9103c4191544c..030c9f5efcf50 100644
--- a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
+++ b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
@@ -14,7 +14,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php
index 76f5dbd1bea88..523efe08c6a4e 100644
--- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php
+++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php
@@ -149,7 +149,7 @@ public function getItems()
$this->_compareProduct->setAllowUsedFlat(false);
$this->_items = $this->_itemCollectionFactory->create();
- $this->_items->useProductItem(true)->setStoreId($this->_storeManager->getStore()->getId());
+ $this->_items->useProductItem()->setStoreId($this->_storeManager->getStore()->getId());
if ($this->httpContext->getValue(Context::CONTEXT_AUTH)) {
$this->_items->setCustomerId($this->currentCustomer->getCustomerId());
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
index cf12e332be86d..8da2614eb2ced 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
@@ -261,6 +261,10 @@ public function execute()
unset($data['apply_to']);
}
+ if ($model->getBackendType() == 'static' && !$model->getIsUserDefined()) {
+ $data['frontend_class'] = $model->getFrontendClass();
+ }
+
$model->addData($data);
if (!$attributeId) {
diff --git a/app/code/Magento/Catalog/Helper/Product/Compare.php b/app/code/Magento/Catalog/Helper/Product/Compare.php
index 49a90c590a440..4e476fe8d1568 100644
--- a/app/code/Magento/Catalog/Helper/Product/Compare.php
+++ b/app/code/Magento/Catalog/Helper/Product/Compare.php
@@ -279,7 +279,7 @@ public function getItemCollection()
// cannot be placed in constructor because of the cyclic dependency which cannot be fixed with proxy class
// collection uses this helper in constructor when calling isEnabledFlat() method
$this->_itemCollection = $this->_itemCollectionFactory->create();
- $this->_itemCollection->useProductItem(true)->setStoreId($this->_storeManager->getStore()->getId());
+ $this->_itemCollection->useProductItem()->setStoreId($this->_storeManager->getStore()->getId());
if ($this->_customerSession->isLoggedIn()) {
$this->_itemCollection->setCustomerId($this->_customerSession->getCustomerId());
@@ -313,7 +313,7 @@ public function calculate($logout = false)
{
/** @var $collection Collection */
$collection = $this->_itemCollectionFactory->create()
- ->useProductItem(true);
+ ->useProductItem();
if (!$logout && $this->_customerSession->isLoggedIn()) {
$collection->setCustomerId($this->_customerSession->getCustomerId());
} elseif ($this->_customerId) {
diff --git a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php
index 497ed2fd49953..a928ddea03a70 100644
--- a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php
+++ b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php
@@ -8,21 +8,21 @@
namespace Magento\Catalog\Model;
/**
- * Filter custom attributes for product using the blacklist
+ * Filter custom attributes for product using the excluded list
*/
class FilterProductCustomAttribute
{
/**
* @var array
*/
- private $blackList;
+ private $excludedList;
/**
- * @param array $blackList
+ * @param array $excludedList
*/
- public function __construct(array $blackList = [])
+ public function __construct(array $excludedList = [])
{
- $this->blackList = $blackList;
+ $this->excludedList = $excludedList;
}
/**
@@ -33,6 +33,6 @@ public function __construct(array $blackList = [])
*/
public function execute(array $attributes): array
{
- return array_diff_key($attributes, array_flip($this->blackList));
+ return array_diff_key($attributes, array_flip($this->excludedList));
}
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
index e9a907f0b5097..f010536f06ee5 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php
@@ -173,7 +173,7 @@ protected function _syncData(array $processIds = [])
}
}
- $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTable($dimensions));
+ $query = $insertSelect->insertFromSelect($this->tableMaintainer->getMainTableByDimensions($dimensions));
$this->getConnection()->query($query);
}
return $this;
@@ -385,7 +385,7 @@ protected function _reindexRows($changedIds = [])
// copy to index
$this->_insertFromTable(
$temporaryTable,
- $this->tableMaintainer->getMainTable($dimensions)
+ $this->tableMaintainer->getMainTableByDimensions($dimensions)
);
}
} else {
@@ -401,6 +401,8 @@ protected function _reindexRows($changedIds = [])
}
/**
+ * Delete Index data
+ *
* @param array $entityIds
* @return void
*/
@@ -408,7 +410,7 @@ private function deleteIndexData(array $entityIds)
{
foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
$select = $this->getConnection()->select()->from(
- ['index_price' => $this->tableMaintainer->getMainTable($dimensions)],
+ ['index_price' => $this->tableMaintainer->getMainTableByDimensions($dimensions)],
null
)->where('index_price.entity_id IN (?)', $entityIds);
$query = $select->deleteFromSelect('index_price');
@@ -476,7 +478,7 @@ private function getIndexTargetTableByDimension(array $dimensions)
{
$indexTargetTable = $this->getIndexTargetTable();
if ($indexTargetTable === self::getIndexTargetTable()) {
- $indexTargetTable = $this->tableMaintainer->getMainTable($dimensions);
+ $indexTargetTable = $this->tableMaintainer->getMainTableByDimensions($dimensions);
}
if ($indexTargetTable === self::getIndexTargetTable() . '_replica') {
$indexTargetTable = $this->tableMaintainer->getMainReplicaTable($dimensions);
@@ -497,6 +499,8 @@ protected function getIndexTargetTable()
}
/**
+ * Get product Id field name
+ *
* @return string
*/
protected function getProductIdFieldName()
@@ -533,6 +537,7 @@ private function getProductsTypes(array $changedIds = [])
/**
* Get parent products types
+ *
* Used for add composite products to reindex if we have only simple products in changed ids set
*
* @param array $productsIds
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
index b30c85cfc52f0..390c9784b50de 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php
@@ -425,7 +425,7 @@ private function switchTables(): void
$mainTablesByDimension = [];
foreach ($this->dimensionCollectionFactory->create() as $dimensions) {
- $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTable($dimensions);
+ $mainTablesByDimension[] = $this->dimensionTableMaintainer->getMainTableByDimensions($dimensions);
//Move data from indexers with old realisation
$this->moveDataFromReplicaTableToReplicaTables($dimensions);
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php
index c418f2e1f253b..974bd0e8f7860 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/ModeSwitcher.php
@@ -15,6 +15,8 @@
/**
* Class to prepare new tables for new indexer mode
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class ModeSwitcher implements \Magento\Indexer\Model\ModeSwitcherInterface
{
@@ -98,7 +100,6 @@ public function switchMode(string $currentMode, string $previousMode)
* Create new tables
*
* @param string $currentMode
- *
* @return void
* @throws \Zend_Db_Exception
*/
@@ -116,7 +117,6 @@ public function createTables(string $currentMode)
*
* @param string $currentMode
* @param string $previousMode
- *
* @return void
*/
public function moveData(string $currentMode, string $previousMode)
@@ -125,17 +125,17 @@ public function moveData(string $currentMode, string $previousMode)
$dimensionsArrayForPreviousMode = $this->getDimensionsArray($previousMode);
foreach ($dimensionsArrayForCurrentMode as $dimensionsForCurrentMode) {
- $newTable = $this->tableMaintainer->getMainTable($dimensionsForCurrentMode);
+ $newTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForCurrentMode);
if (empty($dimensionsForCurrentMode)) {
// new mode is 'none'
foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) {
- $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode);
+ $oldTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForPreviousMode);
$this->insertFromOldTablesToNew($newTable, $oldTable);
}
} else {
// new mode is not 'none'
foreach ($dimensionsArrayForPreviousMode as $dimensionsForPreviousMode) {
- $oldTable = $this->tableMaintainer->getMainTable($dimensionsForPreviousMode);
+ $oldTable = $this->tableMaintainer->getMainTableByDimensions($dimensionsForPreviousMode);
$this->insertFromOldTablesToNew($newTable, $oldTable, $dimensionsForCurrentMode);
}
}
@@ -146,7 +146,6 @@ public function moveData(string $currentMode, string $previousMode)
* Drop old tables
*
* @param string $previousMode
- *
* @return void
*/
public function dropTables(string $previousMode)
@@ -164,7 +163,6 @@ public function dropTables(string $previousMode)
* Get dimensions array
*
* @param string $mode
- *
* @return \Magento\Framework\Indexer\MultiDimensionProvider
*/
private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\MultiDimensionProvider
@@ -184,7 +182,6 @@ private function getDimensionsArray(string $mode): \Magento\Framework\Indexer\Mu
* @param string $newTable
* @param string $oldTable
* @param Dimension[] $dimensions
- *
* @return void
*/
private function insertFromOldTablesToNew(string $newTable, string $oldTable, array $dimensions = [])
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php
index e3077baaeb7a6..5eed262352c76 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/TableMaintainer.php
@@ -7,36 +7,27 @@
namespace Magento\Catalog\Model\Indexer\Product\Price;
-use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\Indexer\Table\StrategyInterface;
+use Magento\Framework\Model\ResourceModel\Db\Context as DbContext;
use Magento\Framework\Search\Request\Dimension;
-use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver;
+use Magento\Indexer\Model\ResourceModel\AbstractResource as AbstractIndexerResource;
/**
* Class encapsulate logic of work with tables per store in Product Price indexer
*/
-class TableMaintainer
+class TableMaintainer extends AbstractIndexerResource
{
/**
* Catalog product price index table name
*/
const MAIN_INDEX_TABLE = 'catalog_product_index_price';
- /**
- * @var ResourceConnection
- */
- private $resource;
-
/**
* @var TableResolver
*/
private $tableResolver;
- /**
- * @var AdapterInterface
- */
- private $connection;
-
/**
* Catalog tmp category index table name
*/
@@ -53,47 +44,27 @@ class TableMaintainer
private $mainTmpTable;
/**
- * @var null|string
- */
- private $connectionName;
-
- /**
- * @param ResourceConnection $resource
+ * @param DbContext $context
+ * @param StrategyInterface $tableStrategy
* @param TableResolver $tableResolver
- * @param null $connectionName
+ * @param string|null $connectionName
*/
public function __construct(
- ResourceConnection $resource,
+ DbContext $context,
+ StrategyInterface $tableStrategy,
TableResolver $tableResolver,
$connectionName = null
) {
- $this->resource = $resource;
+ parent::__construct($context, $tableStrategy, $connectionName);
$this->tableResolver = $tableResolver;
- $this->connectionName = $connectionName;
- }
-
- /**
- * Get connection for work with price indexer
- *
- * @return AdapterInterface
- */
- public function getConnection(): AdapterInterface
- {
- if (null === $this->connection) {
- $this->connection = $this->resource->getConnection($this->connectionName);
- }
- return $this->connection;
}
/**
- * Return validated table name
- *
- * @param string $table
- * @return string
+ * @inheritDoc
*/
- private function getTable(string $table): string
+ protected function _construct()
{
- return $this->resource->getTableName($table);
+ $this->_init(self::MAIN_INDEX_TABLE, 'entity_id');
}
/**
@@ -101,9 +72,7 @@ private function getTable(string $table): string
*
* @param string $mainTableName
* @param string $newTableName
- *
* @return void
- *
* @throws \Zend_Db_Exception
*/
private function createTable(string $mainTableName, string $newTableName)
@@ -119,7 +88,6 @@ private function createTable(string $mainTableName, string $newTableName)
* Drop table
*
* @param string $tableName
- *
* @return void
*/
private function dropTable(string $tableName)
@@ -133,7 +101,6 @@ private function dropTable(string $tableName)
* Truncate table
*
* @param string $tableName
- *
* @return void
*/
private function truncateTable(string $tableName)
@@ -147,7 +114,6 @@ private function truncateTable(string $tableName)
* Get array key for tmp table
*
* @param Dimension[] $dimensions
- *
* @return string
*/
private function getArrayKeyForTmpTable(array $dimensions): string
@@ -160,13 +126,12 @@ private function getArrayKeyForTmpTable(array $dimensions): string
}
/**
- * Return main index table name
+ * Return main index table name using dimensions
*
* @param Dimension[] $dimensions
- *
* @return string
*/
- public function getMainTable(array $dimensions): string
+ public function getMainTableByDimensions(array $dimensions): string
{
return $this->tableResolver->resolve(self::MAIN_INDEX_TABLE, $dimensions);
}
@@ -175,14 +140,12 @@ public function getMainTable(array $dimensions): string
* Create main and replica index tables for dimensions
*
* @param Dimension[] $dimensions
- *
* @return void
- *
* @throws \Zend_Db_Exception
*/
public function createTablesForDimensions(array $dimensions)
{
- $mainTableName = $this->getMainTable($dimensions);
+ $mainTableName = $this->getMainTableByDimensions($dimensions);
//Create index table for dimensions based on main replica table
//Using main replica table is necessary for backward capability and TableResolver plugin work
$this->createTable(
@@ -190,7 +153,7 @@ public function createTablesForDimensions(array $dimensions)
$mainTableName
);
- $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix;
//Create replica table for dimensions based on main replica table
$this->createTable(
$this->getTable(self::MAIN_INDEX_TABLE . $this->additionalTableSuffix),
@@ -202,15 +165,14 @@ public function createTablesForDimensions(array $dimensions)
* Drop main and replica index tables for dimensions
*
* @param Dimension[] $dimensions
- *
* @return void
*/
public function dropTablesForDimensions(array $dimensions)
{
- $mainTableName = $this->getMainTable($dimensions);
+ $mainTableName = $this->getMainTableByDimensions($dimensions);
$this->dropTable($mainTableName);
- $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix;
$this->dropTable($mainReplicaTableName);
}
@@ -218,15 +180,14 @@ public function dropTablesForDimensions(array $dimensions)
* Truncate main and replica index tables for dimensions
*
* @param Dimension[] $dimensions
- *
* @return void
*/
public function truncateTablesForDimensions(array $dimensions)
{
- $mainTableName = $this->getMainTable($dimensions);
+ $mainTableName = $this->getMainTableByDimensions($dimensions);
$this->truncateTable($mainTableName);
- $mainReplicaTableName = $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ $mainReplicaTableName = $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix;
$this->truncateTable($mainReplicaTableName);
}
@@ -234,26 +195,24 @@ public function truncateTablesForDimensions(array $dimensions)
* Return replica index table name
*
* @param Dimension[] $dimensions
- *
* @return string
*/
public function getMainReplicaTable(array $dimensions): string
{
- return $this->getMainTable($dimensions) . $this->additionalTableSuffix;
+ return $this->getMainTableByDimensions($dimensions) . $this->additionalTableSuffix;
}
/**
* Create temporary index table for dimensions
*
* @param Dimension[] $dimensions
- *
* @return void
*/
public function createMainTmpTable(array $dimensions)
{
// Create temporary table based on template table catalog_product_index_price_tmp without indexes
- $templateTableName = $this->resource->getTableName(self::MAIN_INDEX_TABLE . '_tmp');
- $temporaryTableName = $this->getMainTable($dimensions) . $this->tmpTableSuffix;
+ $templateTableName = $this->_resources->getTableName(self::MAIN_INDEX_TABLE . '_tmp');
+ $temporaryTableName = $this->getMainTableByDimensions($dimensions) . $this->tmpTableSuffix;
$this->getConnection()->createTemporaryTableLike($temporaryTableName, $templateTableName, true);
$this->mainTmpTable[$this->getArrayKeyForTmpTable($dimensions)] = $temporaryTableName;
}
@@ -262,9 +221,7 @@ public function createMainTmpTable(array $dimensions)
* Return temporary index table name
*
* @param Dimension[] $dimensions
- *
* @return string
- *
* @throws \LogicException
*/
public function getMainTmpTable(array $dimensions): string
diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php
index 77dedb9eb0121..3494fd00a8b6c 100644
--- a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php
+++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php
@@ -72,6 +72,8 @@ public function renderRangeLabel($fromPrice, $toPrice)
}
/**
+ * Prepare range data
+ *
* @param int $range
* @param int[] $dbRanges
* @return array
@@ -81,12 +83,10 @@ public function renderRangeData($range, $dbRanges)
if (empty($dbRanges)) {
return [];
}
- $lastIndex = array_keys($dbRanges);
- $lastIndex = $lastIndex[count($lastIndex) - 1];
foreach ($dbRanges as $index => $count) {
- $fromPrice = $index == 1 ? '' : ($index - 1) * $range;
- $toPrice = $index == $lastIndex ? '' : $index * $range;
+ $fromPrice = $index == 1 ? 0 : ($index - 1) * $range;
+ $toPrice = $index * $range;
$this->itemDataBuilder->addItemData(
$this->renderRangeLabel($fromPrice, $toPrice),
$fromPrice . '-' . $toPrice,
diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php
index 3d4d9f607da48..40fe6a01e260c 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php
@@ -83,6 +83,12 @@ public function getFailedItems()
}
}
+ /**
+ * Clear validation messages to prevent wrong validation for subsequent price update.
+ * Work around for backward compatible changes.
+ */
+ $this->failedItems = [];
+
return $failedItems;
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php
deleted file mode 100644
index 404760a51eff5..0000000000000
--- a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php
+++ /dev/null
@@ -1,57 +0,0 @@
-productRepository = $productRepository;
- $this->storeManager = $storeManager;
- }
-
- /**
- * Check if product is salable.
- *
- * @param int|string $productId
- * @param int|null $storeId
- * @return bool
- */
- public function isSalable($productId, $storeId = null): bool
- {
- if ($storeId === null) {
- $storeId = $this->storeManager->getStore()->getId();
- }
- /** @var \Magento\Catalog\Model\Product $product */
- $product = $this->productRepository->getById($productId, false, $storeId);
-
- return $product->isSalable();
- }
-}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php
index fab2441db26c9..939f9d354af85 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php
@@ -8,11 +8,15 @@
use Magento\Catalog\Model\Category;
/**
+ * Aggregate count for parent category after deleting child category
+ *
* Class AggregateCount
*/
class AggregateCount
{
/**
+ * Reduces children count for parent categories
+ *
* @param Category $category
* @return void
*/
@@ -25,9 +29,7 @@ public function processDelete(Category $category)
*/
$parentIds = $category->getParentIds();
if ($parentIds) {
- $childDecrease = $category->getChildrenCount() + 1;
- // +1 is itself
- $data = ['children_count' => new \Zend_Db_Expr('children_count - ' . $childDecrease)];
+ $data = ['children_count' => new \Zend_Db_Expr('children_count - 1')];
$where = ['entity_id IN(?)' => $parentIds];
$resourceModel->getConnection()->update($resourceModel->getEntityTable(), $data, $where);
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
index cf5760b0c33a9..8d03eb3ccafc9 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
@@ -147,7 +147,7 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi
* @param bool $insert
* @return array
*/
- private function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false)
+ public function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false)
{
if (empty($insertLinks)) {
return [];
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php
index 5a055e5ed9603..47365929e159e 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/SimpleProductPrice.php
@@ -62,7 +62,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritDoc
*/
public function executeByDimensions(array $dimensions, \Traversable $entityIds)
{
@@ -81,8 +81,7 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds)
'tierPriceField' => 'tier_price',
]);
$select = $this->baseFinalPrice->getQuery($dimensions, $this->productType, iterator_to_array($entityIds));
- $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false);
- $this->tableMaintainer->getConnection()->query($query);
+ $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []);
$this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds));
}
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml
new file mode 100644
index 0000000000000..020fb27063be7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Switch the Storefront to the provided Store.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml
new file mode 100644
index 0000000000000..14a7967422332
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Updates the Category Name for proper Store View.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml
new file mode 100644
index 0000000000000..bd7eb664819dd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Enable the category
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.xml
new file mode 100644
index 0000000000000..c9ad309dcadc1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryExistInCategoryListActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Check Category exist in Category list for Assign to Product.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml
new file mode 100644
index 0000000000000..fb0717fe173af
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormCategoryNotExistInCategoryListActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Check Category not exist in Category list for Assign to Product.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml
new file mode 100644
index 0000000000000..8ecef0df400be
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Set "Manage Stock" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml
new file mode 100644
index 0000000000000..0f6a8df1ebf8c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fills in the "Maximum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml
new file mode 100644
index 0000000000000..abbfdacc15395
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fills in the "Minimum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml
new file mode 100644
index 0000000000000..4ecfa0762db9f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Fills in the "Notify for Quantity Below" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml
new file mode 100644
index 0000000000000..7846689a8d643
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Set "Qty Uses Decimals" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml
new file mode 100644
index 0000000000000..98156eb1ad9b1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Set "Stock status" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml
new file mode 100644
index 0000000000000..8905643658cd8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Clicks the "Done" button on the Search Categories popup.
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml
new file mode 100644
index 0000000000000..3a75b0a3cd361
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml
new file mode 100644
index 0000000000000..e0a98a8932d4d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml
new file mode 100644
index 0000000000000..84e14269d24c2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+ {{expectedPage}}
+ currentPageText
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml
index 22209b61d5316..43732c137c35c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml
@@ -20,7 +20,9 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml
new file mode 100644
index 0000000000000..cead98091d268
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validate that the Category is not present in menu on Frontend.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml
new file mode 100644
index 0000000000000..c56a18b4895a4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validate that the Category is present in menu on Frontend.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml
new file mode 100644
index 0000000000000..65858be673dfa
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml
new file mode 100644
index 0000000000000..5b7dd3026a905
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Click on the product item from the sidebar comparing list.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml
new file mode 100644
index 0000000000000..4776c9d32a34d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Navigates storefront category next page from toolbar
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
new file mode 100644
index 0000000000000..4a403364a91e3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Switch the Storefront to the provided Store.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml
index c9b67e0db4398..1d6bb970ea4d3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml
@@ -30,4 +30,9 @@
No
0
+
+
+ cataloginventory/options/stock_threshold_qty
+ 0
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/QueueConsumerData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/QueueConsumerData.xml
new file mode 100644
index 0000000000000..cd53fede3ab58
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/QueueConsumerData.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ product_action_attribute.update
+ 100
+
+
+ product_action_attribute.website.update
+ 100
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml
index 26946692ce050..7a829a5475758 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml
index 544bdf85681c9..b919cdff2bb92 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml
@@ -8,12 +8,12 @@
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml
index 09eb4ad954274..c27a6107e5e35 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml
@@ -12,6 +12,6 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
index 78818dd37a5d4..7be02126e3a0f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
@@ -25,5 +25,6 @@
+
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
index a94610abf0918..6dcdde75bb2b7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
@@ -34,42 +34,46 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
index e64707a895fd4..cb6ae9244e958 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
@@ -37,41 +37,48 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
index 192bab7c6d126..2cdec1405e9f9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
@@ -31,21 +31,19 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
index b8e58eae8a98a..83404391abca9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
@@ -23,16 +23,13 @@
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
index 4b0774d2307dd..e66984dda4427 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
@@ -21,10 +21,8 @@
-
-
-
+
@@ -37,39 +35,32 @@
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
index 1c12b048e96d0..f8346f5a9dd5c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
@@ -38,8 +38,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
index ff30c46a51c3a..0aa89bdfd45b6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
@@ -38,8 +38,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
index 2b4437aed1bb2..171d15fe6ed4f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
@@ -38,8 +38,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
index 40bd3bdcfea20..4979b06a1051e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
@@ -27,16 +27,19 @@
-
-
-
+
+
+
+
-
-
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
index fe07360d6b9ca..4310c6f06219a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
@@ -33,59 +33,48 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
index e8e0d449aee4e..845ce340451d1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
@@ -56,11 +56,13 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml
index 31b5961edaaaa..cd34741b6a68c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml
@@ -30,6 +30,7 @@
+
@@ -39,6 +40,7 @@
+
@@ -59,22 +61,23 @@
-
+
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
@@ -84,9 +87,9 @@
-
-
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
index fe31456aca334..055f4e23cd9e7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
@@ -116,7 +116,7 @@
-
+
@@ -188,7 +188,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
index 0ca8e74c4e59e..f4d464455491b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
@@ -32,16 +32,12 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -50,32 +46,40 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml
index 6ee1fd6a58e42..1214ba879f211 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml
@@ -29,21 +29,17 @@
-
-
-
-
-
-
+
+
+
+
+
-
-
-
@@ -52,6 +48,9 @@
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml
index dd79dd6824bbb..490f8dbdc4f81 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml
@@ -30,16 +30,17 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
similarity index 100%
rename from app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHint.xml
rename to app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml
index 9b5fa25085e1a..48e6245b011ba 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml
@@ -37,23 +37,26 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml
new file mode 100644
index 0000000000000..507e4ae14e83c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml
new file mode 100644
index 0000000000000..dc608a7f12dd3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml
new file mode 100644
index 0000000000000..914ac3444db22
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php
index ca35d49113f41..681cef8489796 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php
@@ -7,10 +7,12 @@
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute;
+use Magento\Backend\Model\Session;
use Magento\Backend\Model\View\Result\Redirect as ResultRedirect;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save;
use Magento\Catalog\Helper\Product as ProductHelper;
+use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation;
use Magento\Catalog\Model\Product\AttributeSet\Build;
use Magento\Catalog\Model\Product\AttributeSet\BuildFactory;
use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory;
@@ -31,63 +33,64 @@
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.TooManyFields)
*/
class SaveTest extends AttributeTest
{
/**
* @var BuildFactory|MockObject
*/
- protected $buildFactoryMock;
+ private $buildFactoryMock;
/**
* @var FilterManager|MockObject
*/
- protected $filterManagerMock;
+ private $filterManagerMock;
/**
* @var ProductHelper|MockObject
*/
- protected $productHelperMock;
+ private $productHelperMock;
/**
* @var AttributeFactory|MockObject
*/
- protected $attributeFactoryMock;
+ private $attributeFactoryMock;
/**
* @var ValidatorFactory|MockObject
*/
- protected $validatorFactoryMock;
+ private $validatorFactoryMock;
/**
* @var CollectionFactory|MockObject
*/
- protected $groupCollectionFactoryMock;
+ private $groupCollectionFactoryMock;
/**
* @var LayoutFactory|MockObject
*/
- protected $layoutFactoryMock;
+ private $layoutFactoryMock;
/**
* @var ResultRedirect|MockObject
*/
- protected $redirectMock;
+ private $redirectMock;
/**
- * @var AttributeSet|MockObject
+ * @var AttributeSetInterface|MockObject
*/
- protected $attributeSetMock;
+ private $attributeSetMock;
/**
* @var Build|MockObject
*/
- protected $builderMock;
+ private $builderMock;
/**
* @var InputTypeValidator|MockObject
*/
- protected $inputTypeValidatorMock;
+ private $inputTypeValidatorMock;
/**
* @var FormData|MockObject
@@ -104,19 +107,34 @@ class SaveTest extends AttributeTest
*/
private $attributeCodeValidatorMock;
+ /**
+ * @var Presentation|MockObject
+ */
+ private $presentationMock;
+
+ /**
+ * @var Session|MockObject
+ */
+
+ private $sessionMock;
+
protected function setUp(): void
{
parent::setUp();
+ $this->filterManagerMock = $this->createMock(FilterManager::class);
+ $this->productHelperMock = $this->createMock(ProductHelper::class);
+ $this->attributeSetMock = $this->createMock(AttributeSetInterface::class);
+ $this->builderMock = $this->createMock(Build::class);
+ $this->inputTypeValidatorMock = $this->createMock(InputTypeValidator::class);
+ $this->formDataSerializerMock = $this->createMock(FormData::class);
+ $this->attributeCodeValidatorMock = $this->createMock(AttributeCodeValidator::class);
+ $this->presentationMock = $this->createMock(Presentation::class);
+ $this->sessionMock = $this->createMock(Session::class);
+ $this->layoutFactoryMock = $this->createMock(LayoutFactory::class);
$this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class)
->setMethods(['create'])
->disableOriginalConstructor()
->getMock();
- $this->filterManagerMock = $this->getMockBuilder(FilterManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->productHelperMock = $this->getMockBuilder(ProductHelper::class)
- ->disableOriginalConstructor()
- ->getMock();
$this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class)
->setMethods(['create'])
->disableOriginalConstructor()
@@ -129,32 +147,23 @@ protected function setUp(): void
->setMethods(['create'])
->disableOriginalConstructor()
->getMock();
- $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class)
- ->disableOriginalConstructor()
- ->getMock();
$this->redirectMock = $this->getMockBuilder(ResultRedirect::class)
->setMethods(['setData', 'setPath'])
->disableOriginalConstructor()
->getMock();
- $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
- $this->builderMock = $this->getMockBuilder(Build::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->formDataSerializerMock = $this->getMockBuilder(FormData::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class)
- ->disableOriginalConstructor()
- ->getMock();
$this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class)
- ->setMethods(['getId', 'get'])
- ->getMockForAbstractClass();
-
+ ->setMethods(
+ [
+ 'getId',
+ 'get',
+ 'getBackendTypeByInput',
+ 'getDefaultValueByInput',
+ 'getBackendType',
+ 'getFrontendClass',
+ 'addData',
+ 'save'
+ ]
+ )->getMockForAbstractClass();
$this->buildFactoryMock->expects($this->any())
->method('create')
->willReturn($this->builderMock);
@@ -167,7 +176,7 @@ protected function setUp(): void
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function getModel()
{
@@ -184,7 +193,9 @@ protected function getModel()
'groupCollectionFactory' => $this->groupCollectionFactoryMock,
'layoutFactory' => $this->layoutFactoryMock,
'formDataSerializer' => $this->formDataSerializerMock,
- 'attributeCodeValidator' => $this->attributeCodeValidatorMock
+ 'attributeCodeValidator' => $this->attributeCodeValidatorMock,
+ 'presentation' => $this->presentationMock,
+ '_session' => $this->sessionMock
]);
}
@@ -214,6 +225,67 @@ public function testExecuteWithEmptyData()
$this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute());
}
+ public function testExecuteSaveFrontendClass()
+ {
+ $data = [
+ 'frontend_input' => 'test_frontend_input',
+ ];
+
+ $this->requestMock->expects($this->any())
+ ->method('getParam')
+ ->willReturnMap([
+ ['isAjax', null, null],
+ ['serialized_options', '[]', ''],
+ ['set', null, 1],
+ ['attribute_code', null, 'test_attribute_code'],
+ ]);
+ $this->formDataSerializerMock
+ ->expects($this->once())
+ ->method('unserialize')
+ ->with('')
+ ->willReturn([]);
+ $this->requestMock->expects($this->once())
+ ->method('getPostValue')
+ ->willReturn($data);
+ $this->inputTypeValidatorMock->expects($this->any())
+ ->method('isValid')
+ ->with($data['frontend_input'])
+ ->willReturn(true);
+ $this->presentationMock->expects($this->once())
+ ->method('convertPresentationDataToInputType')
+ ->willReturn($data);
+ $this->productHelperMock->expects($this->once())
+ ->method('getAttributeSourceModelByInputType')
+ ->with($data['frontend_input'])
+ ->willReturn(null);
+ $this->productHelperMock->expects($this->once())
+ ->method('getAttributeBackendModelByInputType')
+ ->with($data['frontend_input'])
+ ->willReturn(null);
+ $this->productAttributeMock->expects($this->once())
+ ->method('getBackendTypeByInput')
+ ->with($data['frontend_input'])
+ ->willReturnSelf('test_backend_type');
+ $this->productAttributeMock->expects($this->once())
+ ->method('getDefaultValueByInput')
+ ->with($data['frontend_input'])
+ ->willReturn(null);
+ $this->productAttributeMock->expects($this->once())
+ ->method('getBackendType')
+ ->willReturn('static');
+ $this->productAttributeMock->expects($this->once())
+ ->method('getFrontendClass')
+ ->willReturn('static');
+ $this->resultFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->redirectMock);
+ $this->redirectMock->expects($this->any())
+ ->method('setPath')
+ ->willReturnSelf();
+
+ $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute());
+ }
+
public function testExecute()
{
$data = [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php
new file mode 100644
index 0000000000000..c73e02fb7ecbf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php
@@ -0,0 +1,91 @@
+categoryMock = $this->createMock(Category::class);
+ $this->resourceCategoryMock = $this->createMock(ResourceCategory::class);
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->aggregateCount = $this->objectManagerHelper->getObject(AggregateCount::class);
+ }
+
+ /**
+ * @return void
+ */
+ public function testProcessDelete(): void
+ {
+ $parentIds = 3;
+ $table = 'catalog_category_entity';
+
+ $this->categoryMock->expects($this->once())
+ ->method('getResource')
+ ->willReturn($this->resourceCategoryMock);
+ $this->categoryMock->expects($this->once())
+ ->method('getParentIds')
+ ->willReturn($parentIds);
+ $this->resourceCategoryMock->expects($this->any())
+ ->method('getEntityTable')
+ ->willReturn($table);
+ $this->resourceCategoryMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('update')
+ ->with(
+ $table,
+ ['children_count' => new \Zend_Db_Expr('children_count - 1')],
+ ['entity_id IN(?)' => $parentIds]
+ );
+ $this->aggregateCount->processDelete($this->categoryMock);
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
index 254d893d24584..17318d4207841 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
@@ -7,13 +7,16 @@
namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier;
-use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
-use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories;
+use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
+use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
use Magento\Framework\AuthorizationInterface;
use Magento\Framework\DB\Helper as DbHelper;
use Magento\Framework\UrlInterface;
use Magento\Store\Model\Store;
+use Magento\Backend\Model\Auth\Session;
+use Magento\Authorization\Model\Role;
+use Magento\User\Model\User;
use PHPUnit\Framework\MockObject\MockObject;
/**
@@ -51,6 +54,11 @@ class CategoriesTest extends AbstractModifierTest
*/
private $authorizationMock;
+ /**
+ * @var Session|MockObject
+ */
+ private $sessionMock;
+
protected function setUp(): void
{
parent::setUp();
@@ -72,7 +80,10 @@ protected function setUp(): void
$this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
-
+ $this->sessionMock = $this->getMockBuilder(Session::class)
+ ->setMethods(['getUser'])
+ ->disableOriginalConstructor()
+ ->getMock();
$this->categoryCollectionFactoryMock->expects($this->any())
->method('create')
->willReturn($this->categoryCollectionMock);
@@ -88,6 +99,26 @@ protected function setUp(): void
$this->categoryCollectionMock->expects($this->any())
->method('getIterator')
->willReturn(new \ArrayIterator([]));
+
+ $roleAdmin = $this->getMockBuilder(Role::class)
+ ->setMethods(['getId'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $roleAdmin->expects($this->any())
+ ->method('getId')
+ ->willReturn(0);
+
+ $userAdmin = $this->getMockBuilder(User::class)
+ ->setMethods(['getRole'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $userAdmin->expects($this->any())
+ ->method('getRole')
+ ->willReturn($roleAdmin);
+
+ $this->sessionMock->expects($this->any())
+ ->method('getUser')
+ ->willReturn($userAdmin);
}
/**
@@ -101,11 +132,28 @@ protected function createModel()
'locator' => $this->locatorMock,
'categoryCollectionFactory' => $this->categoryCollectionFactoryMock,
'arrayManager' => $this->arrayManagerMock,
- 'authorization' => $this->authorizationMock
+ 'authorization' => $this->authorizationMock,
+ 'session' => $this->sessionMock
]
);
}
+ /**
+ * @param object $object
+ * @param string $method
+ * @param array $args
+ * @return mixed
+ * @throws \ReflectionException
+ */
+ private function invokeMethod($object, $method, $args = [])
+ {
+ $class = new \ReflectionClass(Categories::class);
+ $method = $class->getMethod($method);
+ $method->setAccessible(true);
+
+ return $method->invokeArgs($object, $args);
+ }
+
public function testModifyData()
{
$this->assertSame([], $this->getModel()->modifyData([]));
@@ -176,4 +224,44 @@ public function modifyMetaLockedDataProvider()
{
return [[true], [false]];
}
+
+ /**
+ * Asserts that a user with an ACL role ID of 0 and a user with an ACL role ID of 1 do not have the same cache IDs
+ * Assumes a store ID of 0
+ *
+ * @throws \ReflectionException
+ */
+ public function testAclCacheIds()
+ {
+ $categoriesAdmin = $this->createModel();
+ $cacheIdAdmin = $this->invokeMethod($categoriesAdmin, 'getCategoriesTreeCacheId', [0]);
+
+ $roleAclUser = $this->getMockBuilder(Role::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $roleAclUser->expects($this->any())
+ ->method('getId')
+ ->willReturn(1);
+
+ $userAclUser = $this->getMockBuilder(User::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $userAclUser->expects($this->any())
+ ->method('getRole')
+ ->will($this->returnValue($roleAclUser));
+
+ $this->sessionMock = $this->getMockBuilder(Session::class)
+ ->setMethods(['getUser'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->sessionMock->expects($this->any())
+ ->method('getUser')
+ ->will($this->returnValue($userAclUser));
+
+ $categoriesAclUser = $this->createModel();
+ $cacheIdAclUser = $this->invokeMethod($categoriesAclUser, 'getCategoriesTreeCacheId', [0]);
+
+ $this->assertNotEquals($cacheIdAdmin, $cacheIdAclUser);
+ }
}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
index 7608173c8edfc..c0d5f0a1af3b8 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
@@ -18,12 +18,14 @@
use Magento\Framework\UrlInterface;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Framework\AuthorizationInterface;
+use Magento\Backend\Model\Auth\Session;
/**
* Data provider for categories field of product page
*
* @api
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
* @since 101.0.0
*/
class Categories extends AbstractModifier
@@ -86,6 +88,11 @@ class Categories extends AbstractModifier
*/
private $authorization;
+ /**
+ * @var Session
+ */
+ private $session;
+
/**
* @param LocatorInterface $locator
* @param CategoryCollectionFactory $categoryCollectionFactory
@@ -94,6 +101,7 @@ class Categories extends AbstractModifier
* @param ArrayManager $arrayManager
* @param SerializerInterface $serializer
* @param AuthorizationInterface $authorization
+ * @param Session $session
*/
public function __construct(
LocatorInterface $locator,
@@ -102,7 +110,8 @@ public function __construct(
UrlInterface $urlBuilder,
ArrayManager $arrayManager,
SerializerInterface $serializer = null,
- AuthorizationInterface $authorization = null
+ AuthorizationInterface $authorization = null,
+ Session $session = null
) {
$this->locator = $locator;
$this->categoryCollectionFactory = $categoryCollectionFactory;
@@ -111,6 +120,7 @@ public function __construct(
$this->arrayManager = $arrayManager;
$this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class);
$this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class);
+ $this->session = $session ?: ObjectManager::getInstance()->get(Session::class);
}
/**
@@ -370,10 +380,16 @@ protected function getCategoriesTree($filter = null)
* @param string $filter
* @return string
*/
- private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string
+ private function getCategoriesTreeCacheId(int $storeId, string $filter = ''): string
{
+ if ($this->session->getUser() !== null) {
+ return self::CATEGORY_TREE_ID
+ . '_' . (string)$storeId
+ . '_' . $this->session->getUser()->getAclRole()
+ . '_' . $filter;
+ }
return self::CATEGORY_TREE_ID
- . '_' . (string) $storeId
+ . '_' . (string)$storeId
. '_' . $filter;
}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
index 0295e778f2b9b..dd757841410e2 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
@@ -40,7 +40,7 @@
use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory;
/**
- * Data provider for eav attributes on product page
+ * Class Eav data provider for product editing form
*
* @api
*
@@ -791,7 +791,9 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute)
\Magento\Store\Model\ScopeInterface::SCOPE_STORE,
$this->storeManager->getStore()
);
- $attribute->setDefaultValue($defaultValue);
+ if ($defaultValue !== null) {
+ $attribute->setDefaultValue($defaultValue);
+ }
}
return $attribute->getDefaultValue();
}
diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml
index 1c97c920266df..a0aa48fb76b13 100644
--- a/app/code/Magento/Catalog/etc/db_schema.xml
+++ b/app/code/Magento/Catalog/etc/db_schema.xml
@@ -138,6 +138,11 @@
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json
index d4bd6927d4345..f4cda73c371d0 100644
--- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json
@@ -69,7 +69,8 @@
},
"index": {
"CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID": true,
- "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true
+ "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true,
+ "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE": true
},
"constraint": {
"PRIMARY": true,
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
index 320e0adc29b9f..140659abfbfe6 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
@@ -8,6 +8,7 @@
namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation;
use Magento\Framework\App\ResourceConnection;
+use Magento\Store\Model\Store;
/**
* Fetch product attribute option data including attribute info
@@ -41,16 +42,18 @@ public function __construct(ResourceConnection $resourceConnection)
* Get option data. Return list of attributes with option data
*
* @param array $optionIds
+ * @param int|null $storeId
* @param array $attributeCodes
* @return array
* @throws \Zend_Db_Statement_Exception
*/
- public function getOptions(array $optionIds, array $attributeCodes = []): array
+ public function getOptions(array $optionIds, ?int $storeId, array $attributeCodes = []): array
{
if (!$optionIds) {
return [];
}
+ $storeId = $storeId ?: Store::DEFAULT_STORE_ID;
$connection = $this->resourceConnection->getConnection();
$select = $connection->select()
->from(
@@ -70,9 +73,21 @@ public function getOptions(array $optionIds, array $attributeCodes = []): array
['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')],
'options.option_id = option_value.option_id',
[
- 'option_label' => 'option_value.value',
'option_id' => 'option_value.option_id',
]
+ )->joinLeft(
+ ['option_value_store' => $this->resourceConnection->getTableName('eav_attribute_option_value')],
+ "options.option_id = option_value_store.option_id AND option_value_store.store_id = {$storeId}",
+ [
+ 'option_label' => $connection->getCheckSql(
+ 'option_value_store.value_id > 0',
+ 'option_value_store.value',
+ 'option_value.value'
+ )
+ ]
+ )->where(
+ 'a.attribute_id = options.attribute_id AND option_value.store_id = ?',
+ Store::DEFAULT_STORE_ID
);
$select->where('option_value.option_id IN (?)', $optionIds);
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
index 0ec65c88024f2..105e91320de49 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
@@ -71,7 +71,7 @@ public function __construct(
*/
public function build(AggregationInterface $aggregation, ?int $storeId): array
{
- $attributeOptions = $this->getAttributeOptions($aggregation);
+ $attributeOptions = $this->getAttributeOptions($aggregation, $storeId);
// build layer per attribute
$result = [];
@@ -133,10 +133,11 @@ private function isBucketEmpty(?BucketInterface $bucket): bool
* Get list of attributes with options
*
* @param AggregationInterface $aggregation
+ * @param int|null $storeId
* @return array
* @throws \Zend_Db_Statement_Exception
*/
- private function getAttributeOptions(AggregationInterface $aggregation): array
+ private function getAttributeOptions(AggregationInterface $aggregation, ?int $storeId): array
{
$attributeOptionIds = [];
$attributes = [];
@@ -154,6 +155,6 @@ function (AggregationValueInterface $value) {
return [];
}
- return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $attributes);
+ return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
index 69592657241a0..0bfd9d58ec969 100644
--- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
+++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
@@ -8,7 +8,10 @@
namespace Magento\CatalogGraphQl\Model;
use GraphQL\Language\AST\FieldNode;
+use GraphQL\Language\AST\InlineFragmentNode;
+use GraphQL\Language\AST\NodeKind;
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Joins attributes for provided field node field names.
@@ -43,11 +46,12 @@ public function __construct(array $fieldToAttributeMap = [])
*
* @param FieldNode $fieldNode
* @param AbstractCollection $collection
+ * @param ResolveInfo $resolveInfo
* @return void
*/
- public function join(FieldNode $fieldNode, AbstractCollection $collection): void
+ public function join(FieldNode $fieldNode, AbstractCollection $collection, ResolveInfo $resolveInfo): void
{
- foreach ($this->getQueryFields($fieldNode) as $field) {
+ foreach ($this->getQueryFields($fieldNode, $resolveInfo) as $field) {
$this->addFieldToCollection($collection, $field);
}
}
@@ -56,26 +60,70 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection): void
* Get an array of queried fields.
*
* @param FieldNode $fieldNode
+ * @param ResolveInfo $resolveInfo
* @return string[]
*/
- public function getQueryFields(FieldNode $fieldNode): array
+ public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array
{
if (null === $this->getFieldNodeSelections($fieldNode)) {
$query = $fieldNode->selectionSet->selections;
$selectedFields = [];
+ $fragmentFields = [];
/** @var FieldNode $field */
foreach ($query as $field) {
- if ($field->kind === 'InlineFragment') {
- continue;
+ if ($field->kind === NodeKind::INLINE_FRAGMENT) {
+ $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field);
+ } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD &&
+ ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) {
+
+ foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) {
+ if (isset($spreadNode->selectionSet->selections)) {
+ $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo);
+ } else {
+ $selectedFields[] = $spreadNode->name->value;
+ }
+ }
+ } else {
+ $selectedFields[] = $field->name->value;
}
- $selectedFields[] = $field->name->value;
}
- $this->setSelectionsForFieldNode($fieldNode, $selectedFields);
+ if ($fragmentFields) {
+ $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields));
+ }
+ $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields));
}
return $this->getFieldNodeSelections($fieldNode);
}
+ /**
+ * Add fields from inline fragment nodes
+ *
+ * @param ResolveInfo $resolveInfo
+ * @param InlineFragmentNode $inlineFragmentField
+ * @param array $inlineFragmentFields
+ * @return string[]
+ */
+ private function addInlineFragmentFields(
+ ResolveInfo $resolveInfo,
+ InlineFragmentNode $inlineFragmentField,
+ $inlineFragmentFields = []
+ ): array {
+ $query = $inlineFragmentField->selectionSet->selections;
+ /** @var FieldNode $field */
+ foreach ($query as $field) {
+ if ($field->kind === NodeKind::INLINE_FRAGMENT) {
+ $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields);
+ } elseif (isset($field->selectionSet->selections)) {
+ continue;
+ } else {
+ $inlineFragmentFields[] = $field->name->value;
+ }
+ }
+
+ return array_unique($inlineFragmentFields);
+ }
+
/**
* Add field to collection select
*
diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
index b5d02511da4e7..ab100c7272ba0 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
@@ -8,6 +8,9 @@
namespace Magento\CatalogGraphQl\Model\Category;
use GraphQL\Language\AST\FieldNode;
+use GraphQL\Language\AST\InlineFragmentNode;
+use GraphQL\Language\AST\NodeKind;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Used for determining the depth information for a requested category tree in a GraphQL request
@@ -17,22 +20,57 @@ class DepthCalculator
/**
* Calculate the total depth of a category tree inside a GraphQL request
*
+ * @param ResolveInfo $resolveInfo
* @param FieldNode $fieldNode
* @return int
*/
- public function calculate(FieldNode $fieldNode) : int
+ public function calculate(ResolveInfo $resolveInfo, FieldNode $fieldNode) : int
{
$selections = $fieldNode->selectionSet->selections ?? [];
$depth = count($selections) ? 1 : 0;
$childrenDepth = [0];
foreach ($selections as $node) {
- if ($node->kind === 'InlineFragment' || null !== $node->alias) {
+ if (isset($node->alias) && null !== $node->alias) {
continue;
}
- $childrenDepth[] = $this->calculate($node);
+ if ($node->kind === NodeKind::INLINE_FRAGMENT) {
+ $childrenDepth[] = $this->addInlineFragmentDepth($resolveInfo, $node);
+ } elseif ($node->kind === NodeKind::FRAGMENT_SPREAD && isset($resolveInfo->fragments[$node->name->value])) {
+ foreach ($resolveInfo->fragments[$node->name->value]->selectionSet->selections as $spreadNode) {
+ $childrenDepth[] = $this->calculate($resolveInfo, $spreadNode);
+ }
+ } else {
+ $childrenDepth[] = $this->calculate($resolveInfo, $node);
+ }
}
return $depth + max($childrenDepth);
}
+
+ /**
+ * Add inline fragment fields into calculating of category depth
+ *
+ * @param ResolveInfo $resolveInfo
+ * @param InlineFragmentNode $inlineFragmentField
+ * @param array $depth
+ * @return int
+ */
+ private function addInlineFragmentDepth(
+ ResolveInfo $resolveInfo,
+ InlineFragmentNode $inlineFragmentField,
+ $depth = []
+ ): int {
+ $selections = $inlineFragmentField->selectionSet->selections;
+ /** @var FieldNode $field */
+ foreach ($selections as $field) {
+ if ($field->kind === NodeKind::INLINE_FRAGMENT) {
+ $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth);
+ } elseif ($field->selectionSet && $field->selectionSet->selections) {
+ $depth[] = $this->calculate($resolveInfo, $field);
+ }
+ }
+
+ return $depth ? max($depth) : 0;
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php
index 5a230ceed0ca4..c6de07bdedd19 100644
--- a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php
+++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php
@@ -10,7 +10,7 @@
use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface;
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
class ProductLinksTypeResolver implements TypeResolverInterface
{
@@ -20,9 +20,9 @@ class ProductLinksTypeResolver implements TypeResolverInterface
private $linkTypes = ['related', 'upsell', 'crosssell'];
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
- public function resolveType(array $data) : string
+ public function resolveType(array $data): string
{
if (isset($data['link_type'])) {
$linkType = $data['link_type'];
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
index 535fe3a80cd25..d7118d71db89b 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
@@ -7,18 +7,18 @@
namespace Magento\CatalogGraphQl\Model\Resolver;
-use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\CatalogGraphQl\Model\AttributesJoiner;
+use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator;
+use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
-use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
-use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator;
+use Magento\Framework\GraphQl\Query\ResolverInterface;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -121,7 +121,7 @@ function () use ($that, $categoryIds, $info) {
}
if (!$this->collection->isLoaded()) {
- $that->attributesJoiner->join($info->fieldNodes[0], $this->collection);
+ $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info);
$this->collection->addIdFilter($this->categoryIds);
}
/** @var CategoryInterface | \Magento\Catalog\Model\Category $item */
@@ -130,7 +130,7 @@ function () use ($that, $categoryIds, $info) {
// Try to extract all requested fields from the loaded collection data
$categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true);
$categories[$item->getId()]['model'] = $item;
- $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]);
+ $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info);
$extractedFields = array_keys($categories[$item->getId()]);
$foundFields = array_intersect($requestedFields, $extractedFields);
if (count($requestedFields) === count($foundFields)) {
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
index 14732ecf37c63..187fd05c1001e 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
@@ -22,7 +22,15 @@ class BatchProductLinks implements BatchServiceContractResolverInterface
/**
* @var string[]
*/
- private static $linkTypes = ['related', 'upsell', 'crosssell'];
+ private $linkTypes;
+
+ /**
+ * @param array $linkTypes
+ */
+ public function __construct(array $linkTypes)
+ {
+ $this->linkTypes = $linkTypes;
+ }
/**
* @inheritDoc
@@ -44,7 +52,7 @@ public function convertToServiceArgument(ResolveRequestInterface $request)
/** @var \Magento\Catalog\Model\Product $product */
$product = $value['model'];
- return new ListCriteria((string)$product->getId(), self::$linkTypes, $product);
+ return new ListCriteria((string)$product->getId(), $this->linkTypes, $product);
}
/**
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
index 9ddad4e6451fa..3139c35774008 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Product;
+use GraphQL\Language\AST\NodeKind;
use Magento\Framework\GraphQl\Query\FieldTranslator;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
@@ -43,9 +44,9 @@ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeN
continue;
}
foreach ($node->selectionSet->selections as $selectionNode) {
- if ($selectionNode->kind === 'InlineFragment') {
+ if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) {
foreach ($selectionNode->selectionSet->selections as $inlineSelection) {
- if ($inlineSelection->kind === 'InlineFragment') {
+ if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) {
continue;
}
$fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value);
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
index fc5a563c82b4e..c553d4486f9e9 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
@@ -8,15 +8,16 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider;
use GraphQL\Language\AST\FieldNode;
-use Magento\CatalogGraphQl\Model\Category\DepthCalculator;
-use Magento\CatalogGraphQl\Model\Category\LevelCalculator;
-use Magento\Framework\EntityManager\MetadataPool;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use GraphQL\Language\AST\NodeKind;
use Magento\Catalog\Api\Data\CategoryInterface;
+use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\CatalogGraphQl\Model\AttributesJoiner;
-use Magento\Catalog\Model\Category;
+use Magento\CatalogGraphQl\Model\Category\DepthCalculator;
+use Magento\CatalogGraphQl\Model\Category\LevelCalculator;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Category tree data provider
@@ -85,8 +86,8 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato
{
$categoryQuery = $resolveInfo->fieldNodes[0];
$collection = $this->collectionFactory->create();
- $this->joinAttributesRecursively($collection, $categoryQuery);
- $depth = $this->depthCalculator->calculate($categoryQuery);
+ $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo);
+ $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery);
$level = $this->levelCalculator->calculate($rootCategoryId);
// If root category is being filter, we've to remove first slash
@@ -124,24 +125,27 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato
*
* @param Collection $collection
* @param FieldNode $fieldNode
+ * @param ResolveInfo $resolveInfo
* @return void
*/
- private function joinAttributesRecursively(Collection $collection, FieldNode $fieldNode) : void
- {
+ private function joinAttributesRecursively(
+ Collection $collection,
+ FieldNode $fieldNode,
+ ResolveInfo $resolveInfo
+ ): void {
if (!isset($fieldNode->selectionSet->selections)) {
return;
}
$subSelection = $fieldNode->selectionSet->selections;
- $this->attributesJoiner->join($fieldNode, $collection);
+ $this->attributesJoiner->join($fieldNode, $collection, $resolveInfo);
/** @var FieldNode $node */
foreach ($subSelection as $node) {
- if ($node->kind === 'InlineFragment') {
+ if ($node->kind === NodeKind::INLINE_FRAGMENT || $node->kind === NodeKind::FRAGMENT_SPREAD) {
continue;
}
-
- $this->joinAttributesRecursively($collection, $node);
+ $this->joinAttributesRecursively($collection, $node, $resolveInfo);
}
}
}
diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml
index 5fec7bfd4fda7..03f9d7ad03f04 100644
--- a/app/code/Magento/CatalogGraphQl/etc/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/di.xml
@@ -74,4 +74,14 @@
+
+
+
+
+ - related
+ - upsell
+ - crosssell
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
index c5fcac99767bd..189bfa61f2c42 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
@@ -1595,6 +1595,7 @@ protected function _saveProducts()
}
$rowSku = $rowData[self::COL_SKU];
+ $rowSkuNormalized = mb_strtolower($rowSku);
if (null === $rowSku) {
$this->getErrorAggregator()->addRowToSkip($rowNum);
@@ -1604,9 +1605,9 @@ protected function _saveProducts()
$storeId = !empty($rowData[self::COL_STORE])
? $this->getStoreIdByCode($rowData[self::COL_STORE])
: Store::DEFAULT_STORE_ID;
- $rowExistingImages = $existingImages[$storeId][$rowSku] ?? [];
+ $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? [];
$rowStoreMediaGalleryValues = $rowExistingImages;
- $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSku] ?? [];
+ $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? [];
if (self::SCOPE_STORE == $rowScope) {
// set necessary data from SCOPE_DEFAULT row
@@ -1762,10 +1763,11 @@ protected function _saveProducts()
continue;
}
- if (isset($rowExistingImages[$uploadedFile])) {
- $currentFileData = $rowExistingImages[$uploadedFile];
+ $uploadedFileNormalized = ltrim($uploadedFile, '/\\');
+ if (isset($rowExistingImages[$uploadedFileNormalized])) {
+ $currentFileData = $rowExistingImages[$uploadedFileNormalized];
$currentFileData['store_id'] = $storeId;
- $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]);
+ $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]);
if (array_key_exists($uploadedFile, $imageHiddenStates)
&& $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile]
) {
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
index a45338c391a58..78ff26675930e 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
@@ -89,7 +89,6 @@ public function saveLinks(
$resource = $this->linkFactory->create();
$mainTable = $resource->getMainTable();
$positionAttrId = [];
- $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable);
// pre-load 'position' attributes ID for each link type once
foreach ($this->linkNameToId as $linkId) {
@@ -103,6 +102,7 @@ public function saveLinks(
$positionAttrId[$linkId] = $importEntity->getConnection()->fetchOne($select, $bind);
}
while ($bunch = $dataSourceModel->getNextBunch()) {
+ $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable);
$this->processLinkBunches($importEntity, $linkField, $bunch, $resource, $nextLinkId, $positionAttrId);
}
}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php
index a94a87a44b32a..d4694b72ba64f 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php
@@ -384,7 +384,9 @@ public function getExistingImages(array $bunch)
foreach ($this->connection->fetchAll($select) as $image) {
$storeId = $image['store_id'];
unset($image['store_id']);
- $result[$storeId][$image['sku']][$image['value']] = $image;
+ $sku = mb_strtolower($image['sku']);
+ $value = ltrim($image['value'], '/\\');
+ $result[$storeId][$sku][$value] = $image;
}
return $result;
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml
index 677752be02eb5..785d19c000af0 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml
@@ -79,8 +79,6 @@
-
-
@@ -94,9 +92,8 @@
-
-
+
@@ -105,7 +102,12 @@
-
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml
index 5078fa5c571db..9f8d65968d741 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml
@@ -48,7 +48,6 @@
-
@@ -56,7 +55,7 @@
-
+
@@ -68,8 +67,13 @@
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
index 8eba6a39f6199..d2517fa28cdd1 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
@@ -150,9 +150,9 @@
+
-
@@ -164,10 +164,13 @@
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml
index 44f7b91324025..94478e63aa92a 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml
@@ -83,9 +83,8 @@
-
-
+
@@ -98,8 +97,13 @@
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml
index 744e51bfe8896..95cfe2c87bffb 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml
@@ -89,7 +89,7 @@
-
+
@@ -99,9 +99,8 @@
-
-
+
@@ -113,8 +112,13 @@
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml
index 09f37a10fb14d..2f57d94113d38 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml
@@ -85,9 +85,8 @@
-
-
+
@@ -97,8 +96,13 @@
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml
index d345b0b0de116..dac97a61a967b 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml
@@ -28,7 +28,8 @@
-
+
+
@@ -36,7 +37,7 @@
-
+
@@ -48,12 +49,13 @@
-
-
-
+
+
+
+
+
-
-
+
diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php b/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php
deleted file mode 100644
index c061c459bfb49..0000000000000
--- a/app/code/Magento/CatalogInventory/Model/Plugin/PriceIndexUpdater.php
+++ /dev/null
@@ -1,79 +0,0 @@
-priceIndexProcessor = $priceIndexProcessor;
- }
-
- /**
- * @param Item $subject
- * @param Item $result
- * @param AbstractModel $model
- * @return Item
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
- */
- public function afterSave(Item $subject, Item $result, AbstractModel $model): Item
- {
- $fields = [
- 'is_in_stock',
- 'use_config_manage_stock',
- 'manage_stock',
- ];
- foreach ($fields as $field) {
- if ($model->dataHasChangedFor($field)) {
- $this->priceIndexProcessor->reindexRow($model->getProductId());
- break;
- }
- }
-
- return $result;
- }
-
- /**
- * @param Item $subject
- * @param mixed $result
- * @param int $websiteId
- * @return void
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
- */
- public function afterUpdateSetOutOfStock(Item $subject, $result, int $websiteId)
- {
- $this->priceIndexProcessor->markIndexerAsInvalid();
- }
-
- /**
- * @param Item $subject
- * @param mixed $result
- * @param int $websiteId
- * @return void
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
- */
- public function afterUpdateSetInStock(Item $subject, $result, int $websiteId)
- {
- $this->priceIndexProcessor->markIndexerAsInvalid();
- }
-}
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php
index edccad60231ec..dc9233e77d3a9 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php
@@ -5,14 +5,13 @@
*/
namespace Magento\CatalogInventory\Model\ResourceModel\Stock;
-use Magento\CatalogInventory\Api\Data\StockItemInterface;
+use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexProcessor;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\CatalogInventory\Model\Stock;
use Magento\CatalogInventory\Model\Indexer\Stock\Processor;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Model\ResourceModel\Db\Context;
use Magento\Framework\DB\Select;
-use Magento\Framework\App\ObjectManager;
use Magento\Framework\Stdlib\DateTime\DateTime;
/**
@@ -42,27 +41,33 @@ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
*/
private $dateTime;
+ /**
+ * @var PriceIndexProcessor
+ */
+ private $priceIndexProcessor;
+
/**
* @param Context $context
* @param Processor $processor
- * @param string $connectionName
* @param StockConfigurationInterface $stockConfiguration
* @param DateTime $dateTime
+ * @param PriceIndexProcessor $priceIndexProcessor
+ * @param string $connectionName
*/
public function __construct(
Context $context,
Processor $processor,
- $connectionName = null,
- StockConfigurationInterface $stockConfiguration = null,
- DateTime $dateTime = null
+ StockConfigurationInterface $stockConfiguration,
+ DateTime $dateTime,
+ PriceIndexProcessor $priceIndexProcessor,
+ $connectionName = null
) {
$this->stockIndexerProcessor = $processor;
parent::__construct($context, $connectionName);
- $this->stockConfiguration = $stockConfiguration ??
- ObjectManager::getInstance()->get(StockConfigurationInterface::class);
- $this->dateTime = $dateTime ??
- ObjectManager::getInstance()->get(DateTime::class);
+ $this->stockConfiguration = $stockConfiguration;
+ $this->dateTime = $dateTime;
+ $this->priceIndexProcessor = $priceIndexProcessor;
}
/**
@@ -144,10 +149,25 @@ protected function _prepareDataForTable(\Magento\Framework\DataObject $object, $
protected function _afterSave(AbstractModel $object)
{
parent::_afterSave($object);
- /** @var StockItemInterface $object */
+
+ $productId = $object->getProductId();
if ($this->processIndexEvents) {
- $this->stockIndexerProcessor->reindexRow($object->getProductId());
+ $this->stockIndexerProcessor->reindexRow($productId);
}
+ $fields = [
+ 'is_in_stock',
+ 'use_config_manage_stock',
+ 'manage_stock',
+ ];
+ foreach ($fields as $field) {
+ if ($object->dataHasChangedFor($field)) {
+ $this->addCommitCallback(function () use ($productId) {
+ $this->priceIndexProcessor->reindexRow($productId);
+ });
+ break;
+ }
+ }
+
return $this;
}
@@ -196,6 +216,7 @@ public function updateSetOutOfStock(int $websiteId)
$connection->update($this->getMainTable(), $values, $where);
$this->stockIndexerProcessor->markIndexerAsInvalid();
+ $this->priceIndexProcessor->markIndexerAsInvalid();
}
/**
@@ -228,6 +249,7 @@ public function updateSetInStock(int $websiteId)
$connection->update($this->getMainTable(), $values, $where);
$this->stockIndexerProcessor->markIndexerAsInvalid();
+ $this->priceIndexProcessor->markIndexerAsInvalid();
}
/**
diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml
index 78a0c2b734315..751fa465bdb17 100644
--- a/app/code/Magento/CatalogInventory/etc/di.xml
+++ b/app/code/Magento/CatalogInventory/etc/di.xml
@@ -37,7 +37,7 @@
-
+
- quantity_and_stock_status
@@ -129,9 +129,6 @@
-
-
-
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
index b9318f72bee9e..c3132e5c46cc9 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
@@ -17,6 +17,7 @@
+
@@ -25,33 +26,32 @@
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
@@ -67,7 +67,9 @@
-
+
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml
index 6c436fee808a7..9d7607d7521c9 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml
@@ -44,7 +44,7 @@
website
-
+
@@ -64,7 +64,7 @@
-
+
@@ -80,7 +80,6 @@
-
@@ -129,7 +128,6 @@
-
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php
index 28624c667e42b..360df8f4edc66 100644
--- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php
@@ -572,11 +572,11 @@ public function prepareProductIndex($indexData, $productData, $storeId)
foreach ($indexData as $entityId => $attributeData) {
foreach ($attributeData as $attributeId => $attributeValues) {
$value = $this->getAttributeValue($attributeId, $attributeValues, $storeId);
- if (!empty($value)) {
+ if ($value !== null && $value !== false && $value != '') {
if (!isset($index[$attributeId])) {
$index[$attributeId] = [];
}
- $index[$attributeId][$entityId] = $value;
+ $index[$attributeId][$entityId] = $value;
}
}
}
diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
index 332bb991bf29f..b2aaa054ebc34 100644
--- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
@@ -176,15 +176,16 @@ public function getCurrencyRate()
*
* @param float|string $fromPrice
* @param float|string $toPrice
+ * @param boolean $isLast
* @return float|\Magento\Framework\Phrase
*/
- protected function _renderRangeLabel($fromPrice, $toPrice)
+ protected function _renderRangeLabel($fromPrice, $toPrice, $isLast = false)
{
$fromPrice = empty($fromPrice) ? 0 : $fromPrice * $this->getCurrencyRate();
$toPrice = empty($toPrice) ? $toPrice : $toPrice * $this->getCurrencyRate();
$formattedFromPrice = $this->priceCurrency->format($fromPrice);
- if ($toPrice === '') {
+ if ($isLast) {
return __('%1 and above', $formattedFromPrice);
} elseif ($fromPrice == $toPrice && $this->dataProvider->getOnePriceIntervalValue()) {
return $formattedFromPrice;
@@ -215,12 +216,15 @@ protected function _getItemsData()
$data = [];
if (count($facets) > 1) { // two range minimum
+ $lastFacet = array_key_last($facets);
foreach ($facets as $key => $aggregation) {
$count = $aggregation['count'];
if (strpos($key, '_') === false) {
continue;
}
- $data[] = $this->prepareData($key, $count, $data);
+
+ $isLast = $lastFacet === $key;
+ $data[] = $this->prepareData($key, $count, $isLast);
}
}
@@ -264,18 +268,13 @@ protected function getFrom($from)
*
* @param string $key
* @param int $count
+ * @param boolean $isLast
* @return array
*/
- private function prepareData($key, $count)
+ private function prepareData($key, $count, $isLast = false)
{
- list($from, $to) = explode('_', $key);
- if ($from == '*') {
- $from = $this->getFrom($to);
- }
- if ($to == '*') {
- $to = $this->getTo($to);
- }
- $label = $this->_renderRangeLabel($from, $to);
+ [$from, $to] = explode('_', $key);
+ $label = $this->_renderRangeLabel($from, $to, $isLast);
$value = $from . '-' . $to . $this->dataProvider->getAdditionalRequestData();
$data = [
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml
index 67e9fdd43f5fe..cceac0475aa78 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml
@@ -13,6 +13,7 @@
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php
index f8bf00b16cf37..31bb630718e55 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/View.php
@@ -3,14 +3,18 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\CatalogUrlRewrite\Model\Category\Plugin\Store;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\CategoryFactory;
+use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ProductFactory;
use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator;
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\Framework\Model\AbstractModel;
+use Magento\Store\Model\ResourceModel\Store;
use Magento\UrlRewrite\Model\UrlPersistInterface;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
@@ -19,32 +23,31 @@
*
* @see \Magento\Store\Model\ResourceModel\Store
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
- * @package Magento\CatalogUrlRewrite\Model\Category\Plugin\Store
*/
class View
{
/**
- * @var \Magento\UrlRewrite\Model\UrlPersistInterface
+ * @var UrlPersistInterface
*/
protected $urlPersist;
/**
- * @var \Magento\Catalog\Model\CategoryFactory
+ * @var CategoryFactory
*/
protected $categoryFactory;
/**
- * @var \Magento\Catalog\Model\ProductFactory
+ * @var ProductFactory
*/
protected $productFactory;
/**
- * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator
+ * @var CategoryUrlRewriteGenerator
*/
protected $categoryUrlRewriteGenerator;
/**
- * @var \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator
+ * @var ProductUrlRewriteGenerator
*/
protected $productUrlRewriteGenerator;
@@ -75,114 +78,108 @@ public function __construct(
}
/**
- * @param \Magento\Store\Model\ResourceModel\Store $object
+ * Setter for Orig Store data
+ *
+ * @param Store $object
* @param AbstractModel $store
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function beforeSave(
- \Magento\Store\Model\ResourceModel\Store $object,
+ Store $object,
AbstractModel $store
- ) {
+ ): void {
$this->origStore = $store;
}
/**
* Regenerate urls on store after save
*
- * @param \Magento\Store\Model\ResourceModel\Store $object
- * @param \Magento\Store\Model\ResourceModel\Store $store
- * @return \Magento\Store\Model\ResourceModel\Store
+ * @param Store $object
+ * @param Store $store
+ * @return Store
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterSave(
- \Magento\Store\Model\ResourceModel\Store $object,
- \Magento\Store\Model\ResourceModel\Store $store
- ) {
+ Store $object,
+ Store $store
+ ): Store {
if ($this->origStore->isObjectNew() || $this->origStore->dataHasChangedFor('group_id')) {
- if (!$this->origStore->isObjectNew()) {
- $this->urlPersist->deleteByData([UrlRewrite::STORE_ID => $this->origStore->getId()]);
- }
-
- $this->urlPersist->replace(
- $this->generateCategoryUrls($this->origStore->getRootCategoryId(), $this->origStore->getId())
+ $categoryRewriteUrls = $this->generateCategoryUrls(
+ (int)$this->origStore->getRootCategoryId(),
+ (int)$this->origStore->getId()
);
+ $this->urlPersist->replace($categoryRewriteUrls);
+
$this->urlPersist->replace(
- $this->generateProductUrls(
- $this->origStore->getWebsiteId(),
- $this->origStore->getOrigData('website_id'),
- $this->origStore->getId()
- )
+ $this->generateProductUrls((int)$this->origStore->getId())
);
}
+
return $store;
}
/**
- * Generate url rewrites for products assigned to website
+ * Generate url rewrites for products assigned to store
*
- * @param int $websiteId
- * @param int $originWebsiteId
* @param int $storeId
* @return array
*/
- protected function generateProductUrls($websiteId, $originWebsiteId, $storeId)
+ protected function generateProductUrls(int $storeId): array
{
$urls = [];
- $websiteIds = $websiteId != $originWebsiteId && $originWebsiteId !== null
- ? [$websiteId, $originWebsiteId]
- : [$websiteId];
$collection = $this->productFactory->create()
->getCollection()
->addCategoryIds()
->addAttributeToSelect(['name', 'url_path', 'url_key', 'visibility'])
- ->addWebsiteFilter($websiteIds);
+ ->addStoreFilter($storeId);
foreach ($collection as $product) {
+ /** @var Product $product */
$product->setStoreId($storeId);
- /** @var \Magento\Catalog\Model\Product $product */
- $urls = array_merge(
- $urls,
- $this->productUrlRewriteGenerator->generate($product)
- );
+ $urls[] = $this->productUrlRewriteGenerator->generate($product);
}
+ $urls = array_merge([], ...$urls);
+
return $urls;
}
/**
+ * Generate url rewrites for categories assigned to store
+ *
* @param int $rootCategoryId
* @param int $storeId
* @return array
*/
- protected function generateCategoryUrls($rootCategoryId, $storeId)
+ protected function generateCategoryUrls(int $rootCategoryId, int $storeId): array
{
$urls = [];
- $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true);
+ $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true, false);
+ $categories->setStoreId($storeId);
foreach ($categories as $category) {
- /** @var \Magento\Catalog\Model\Category $category */
+ /** @var Category $category */
$category->setStoreId($storeId);
- $urls = array_merge(
- $urls,
- $this->categoryUrlRewriteGenerator->generate($category)
- );
+ $urls[] = $this->categoryUrlRewriteGenerator->generate($category);
}
+ $urls = array_merge([], ...$urls);
+
return $urls;
}
/**
* Delete unused url rewrites
*
- * @param \Magento\Store\Model\ResourceModel\Store $subject
- * @param \Magento\Store\Model\ResourceModel\Store $result
+ * @param Store $subject
+ * @param Store $result
* @param AbstractModel $store
- * @return \Magento\Store\Model\ResourceModel\Store
+ * @return Store
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterDelete(
- \Magento\Store\Model\ResourceModel\Store $subject,
- \Magento\Store\Model\ResourceModel\Store $result,
+ Store $subject,
+ Store $result,
AbstractModel $store
- ) {
+ ): Store {
$this->urlPersist->deleteByData([UrlRewrite::STORE_ID => $store->getId()]);
return $result;
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml
index 75ae9d821c356..329f5e8cae3f6 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml
@@ -72,11 +72,13 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php
index 4471ea0e59887..61557db883aa1 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/ViewTest.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category\Plugin\Store;
+use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\CategoryFactory;
use Magento\Catalog\Model\Product;
@@ -87,6 +88,9 @@ class ViewTest extends TestCase
*/
private $productMock;
+ /**
+ * @inheritdoc
+ */
protected function setUp(): void
{
$this->objectManager = new ObjectManager($this);
@@ -121,7 +125,7 @@ protected function setUp(): void
->getMock();
$this->productCollectionMock = $this->getMockBuilder(ProductCollection::class)
->disableOriginalConstructor()
- ->setMethods(['addCategoryIds', 'addAttributeToSelect', 'addWebsiteFilter', 'getIterator'])
+ ->setMethods(['addCategoryIds', 'addAttributeToSelect', 'getIterator', 'addStoreFilter'])
->getMock();
$this->productMock = $this->getMockBuilder(Product::class)
->disableOriginalConstructor()
@@ -139,7 +143,12 @@ protected function setUp(): void
);
}
- public function testAfterSave()
+ /**
+ * Test after save
+ *
+ * @return void
+ */
+ public function testAfterSave(): void
{
$origStoreMock = $this->getMockBuilder(\Magento\Store\Model\Store::class)
->disableOriginalConstructor()
@@ -155,9 +164,16 @@ public function testAfterSave()
$this->abstractModelMock->expects($this->any())
->method('isObjectNew')
->willReturn(true);
+ $categoryCollection = $this->getMockBuilder(CategoryCollection::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getIterator'])
+ ->getMock();
+ $categoryCollection->expects($this->any())
+ ->method('getIterator')
+ ->willReturn(new \ArrayIterator([]));
$this->categoryMock->expects($this->once())
->method('getCategories')
- ->willReturn([]);
+ ->willReturn($categoryCollection);
$this->categoryFactoryMock->expects($this->once())
->method('create')
->willReturn($this->categoryMock);
@@ -174,7 +190,7 @@ public function testAfterSave()
->method('addAttributeToSelect')
->willReturn($this->productCollectionMock);
$this->productCollectionMock->expects($this->once())
- ->method('addWebsiteFilter')
+ ->method('addStoreFilter')
->willReturn($this->productCollectionMock);
$iterator = new \ArrayIterator([$this->productMock]);
$this->productCollectionMock->expects($this->once())
@@ -191,7 +207,12 @@ public function testAfterSave()
);
}
- public function testAfterDelete()
+ /**
+ * Test after delete
+ *
+ * @return void
+ */
+ public function testAfterDelete(): void
{
$this->urlPersistMock->expects($this->once())
->method('deleteByData');
diff --git a/app/code/Magento/Checkout/Controller/Cart/Add.php b/app/code/Magento/Checkout/Controller/Cart/Add.php
index 739f71caeb804..c4294e0fbade1 100644
--- a/app/code/Magento/Checkout/Controller/Cart/Add.php
+++ b/app/code/Magento/Checkout/Controller/Cart/Add.php
@@ -9,6 +9,8 @@
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Checkout\Model\Cart as CustomerCart;
+use Magento\Framework\App\ResponseInterface;
+use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\Exception\NoSuchEntityException;
/**
@@ -77,7 +79,7 @@ protected function _initProduct()
/**
* Add product to shopping cart action
*
- * @return \Magento\Framework\Controller\Result\Redirect
+ * @return ResponseInterface|ResultInterface
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function execute()
@@ -90,7 +92,6 @@ public function execute()
}
$params = $this->getRequest()->getParams();
-
try {
if (isset($params['qty'])) {
$filter = new \Zend_Filter_LocalizedToNormalized(
@@ -104,9 +105,7 @@ public function execute()
$product = $this->_initProduct();
$related = $this->getRequest()->getParam('related_product');
- /**
- * Check product availability
- */
+ /** Check product availability */
if (!$product) {
return $this->goBack();
}
@@ -115,7 +114,6 @@ public function execute()
if (!empty($related)) {
$this->cart->addProductsByIds(explode(',', $related));
}
-
$this->cart->save();
/**
@@ -127,21 +125,25 @@ public function execute()
);
if (!$this->_checkoutSession->getNoCartRedirect(true)) {
- if (!$this->cart->getQuote()->getHasError()) {
- if ($this->shouldRedirectToCart()) {
- $message = __(
- 'You added %1 to your shopping cart.',
- $product->getName()
- );
- $this->messageManager->addSuccessMessage($message);
- } else {
- $this->messageManager->addComplexSuccessMessage(
- 'addCartSuccessMessage',
- [
- 'product_name' => $product->getName(),
- 'cart_url' => $this->getCartUrl(),
- ]
- );
+ if ($this->shouldRedirectToCart()) {
+ $message = __(
+ 'You added %1 to your shopping cart.',
+ $product->getName()
+ );
+ $this->messageManager->addSuccessMessage($message);
+ } else {
+ $this->messageManager->addComplexSuccessMessage(
+ 'addCartSuccessMessage',
+ [
+ 'product_name' => $product->getName(),
+ 'cart_url' => $this->getCartUrl(),
+ ]
+ );
+ }
+ if ($this->cart->getQuote()->getHasError()) {
+ $errors = $this->cart->getQuote()->getErrors();
+ foreach ($errors as $error) {
+ $this->messageManager->addErrorMessage($error->getText());
}
}
return $this->goBack(null, $product);
@@ -161,7 +163,6 @@ public function execute()
}
$url = $this->_checkoutSession->getRedirectUrl(true);
-
if (!$url) {
$url = $this->_redirect->getRedirectUrl($this->getCartUrl());
}
@@ -175,6 +176,8 @@ public function execute()
$this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e);
return $this->goBack();
}
+
+ return $this->getResponse();
}
/**
@@ -182,7 +185,7 @@ public function execute()
*
* @param string $backUrl
* @param \Magento\Catalog\Model\Product $product
- * @return $this|\Magento\Framework\Controller\Result\Redirect
+ * @return ResponseInterface|ResultInterface
*/
protected function goBack($backUrl = null, $product = null)
{
@@ -205,6 +208,8 @@ protected function goBack($backUrl = null, $product = null)
$this->getResponse()->representJson(
$this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($result)
);
+
+ return $this->getResponse();
}
/**
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml
index c3f3865ef4549..c81540382c86f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml
@@ -16,7 +16,8 @@
-
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml
index e2d4fd2e89c2f..daa27b9918e47 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
index e82f3c0588835..4185261993ffd 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
@@ -9,6 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
+
@@ -18,6 +19,7 @@
+
@@ -46,19 +48,23 @@
-
-
+
+
+
+
+
+
-
+
@@ -93,8 +99,8 @@
-
-
+
+
@@ -107,13 +113,13 @@
-
+
-
+
diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml
index c33b784fcd20c..192f20653f8c3 100644
--- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml
+++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml
@@ -105,7 +105,7 @@
- opc-new-shipping-address
-
-
-
- Ship here
+ - Ship Here
- action primary action-save-address
-
diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js
index c570bda51a80e..9adfb549a5b1c 100644
--- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js
+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js
@@ -192,10 +192,6 @@ define([
* @returns {Boolean} - initial visibility state.
*/
resolveInitialPasswordVisibility: function () {
- if (checkoutData.getInputFieldEmailValue() !== '' && checkoutData.getCheckedEmailValue() === '') {
- return true;
- }
-
if (checkoutData.getInputFieldEmailValue() !== '') {
return checkoutData.getInputFieldEmailValue() === checkoutData.getCheckedEmailValue();
}
diff --git a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html
index 8e71d5845cec8..b7dfaffa558f3 100644
--- a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html
+++ b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html
@@ -21,6 +21,7 @@
data-bind="
textInput: email,
hasFocus: emailFocused,
+ afterRender: emailHasChanged,
mageInit: {'mage/trim-input':{}}"
name="username"
data-validate="{required:true, 'validate-email':true}"
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml
index 6f16fa54a6ebf..ebf024490cce6 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml
index 112335e726270..a6f4e7780d096 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
-
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
index 1869a6544c3d3..5be91f61e1e1e 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
@@ -38,6 +38,8 @@
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml
index c46410dce919e..3687bb4fe5743 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsPageTest.xml
@@ -14,6 +14,7 @@
+
diff --git a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php
index e56225cbe2548..9e360481e8eb3 100644
--- a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php
+++ b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php
@@ -7,6 +7,7 @@
namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store;
+use Magento\Cms\Api\Data\PageInterface;
use Magento\Cms\Api\PageRepositoryInterface;
use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator;
use Magento\Framework\Api\SearchCriteriaBuilder;
@@ -21,6 +22,8 @@
*/
class View
{
+ private const ALL_STORE_VIEWS = '0';
+
/**
* @var UrlPersistInterface
*/
@@ -72,7 +75,7 @@ public function __construct(
*/
public function afterSave(ResourceStore $object, ResourceStore $result, AbstractModel $store): void
{
- if ($store->isObjectNew() || $store->dataHasChangedFor('group_id')) {
+ if ($store->isObjectNew()) {
$this->urlPersist->replace(
$this->generateCmsPagesUrls((int)$store->getId())
);
@@ -89,9 +92,8 @@ private function generateCmsPagesUrls(int $storeId): array
{
$rewrites = [];
$urls = [];
- $searchCriteria = $this->searchCriteriaBuilder->create();
- $cmsPagesCollection = $this->pageRepository->getList($searchCriteria)->getItems();
- foreach ($cmsPagesCollection as $page) {
+
+ foreach ($this->getCmsPageItems() as $page) {
$page->setStoreId($storeId);
$rewrites[] = $this->cmsPageUrlRewriteGenerator->generate($page);
}
@@ -99,4 +101,18 @@ private function generateCmsPagesUrls(int $storeId): array
return $urls;
}
+
+ /**
+ * Return cms page items for all store view
+ *
+ * @return PageInterface[]
+ */
+ private function getCmsPageItems(): array
+ {
+ $searchCriteria = $this->searchCriteriaBuilder->addFilter('store_id', self::ALL_STORE_VIEWS)
+ ->create();
+ $list = $this->pageRepository->getList($searchCriteria);
+
+ return $list->getItems();
+ }
}
diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php
index 356c6ca17da18..f61e99529c3cc 100644
--- a/app/code/Magento/Config/Model/Config.php
+++ b/app/code/Magento/Config/Model/Config.php
@@ -208,6 +208,7 @@ public function save()
);
$groupChangedPaths = $this->getChangedPaths($sectionId, $groupId, $groupData, $oldConfig, $extraOldGroups);
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$changedPaths = \array_merge($changedPaths, $groupChangedPaths);
}
@@ -370,6 +371,7 @@ private function getChangedPaths(
$oldConfig,
$extraOldGroups
);
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$changedPaths = \array_merge($changedPaths, $subGroupChangedPaths);
}
}
@@ -435,11 +437,11 @@ protected function _processGroup(
if (!isset($fieldData['value'])) {
$fieldData['value'] = null;
}
-
+
if ($field->getType() == 'multiline' && is_array($fieldData['value'])) {
$fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value']));
}
-
+
$data = [
'field' => $fieldId,
'groups' => $groups,
@@ -453,7 +455,7 @@ protected function _processGroup(
$backendModel->addData($data);
$this->_checkSingleStoreMode($field, $backendModel);
- $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig);
+ $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups);
$backendModel->setPath($path)->setValue($fieldData['value']);
$inherit = !empty($fieldData['inherit']);
diff --git a/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php
new file mode 100644
index 0000000000000..fbc45a9cfc791
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php
@@ -0,0 +1,42 @@
+authorization = $authorization;
+ }
+
+ /**
+ * Check that user is allowed to manage attributes
+ *
+ * @return bool
+ */
+ public function isAllowedToManageAttributes(): bool
+ {
+ return $this->authorization->isAllowed('Magento_Catalog::attributes_attributes');
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php
deleted file mode 100644
index 92b7ab0d88ea8..0000000000000
--- a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php
+++ /dev/null
@@ -1,48 +0,0 @@
-configurableType = $configurableType;
- }
-
- /**
- * Add child identities to product identities
- *
- * @param Product $subject
- * @param array $identities
- * @return array
- */
- public function afterGetIdentities(Product $subject, array $identities): array
- {
- foreach ($this->configurableType->getChildrenIds($subject->getId()) as $childIds) {
- foreach ($childIds as $childId) {
- $identities[] = Product::CACHE_TAG . '_' . $childId;
- }
- }
-
- return array_unique($identities);
- }
-}
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 b7bbf7aa1871c..9080314126ee1 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
@@ -134,8 +134,7 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds)
\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE,
iterator_to_array($entityIds)
);
- $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false);
- $this->tableMaintainer->getConnection()->query($query);
+ $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []);
$this->basePriceModifier->modifyPrice($temporaryPriceTable, iterator_to_array($entityIds));
$this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds));
@@ -224,8 +223,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar
if ($entityIds !== null) {
$select->where('le.entity_id IN (?)', $entityIds);
}
- $query = $select->insertFromSelect($temporaryOptionsTableName);
- $this->getConnection()->query($query);
+ $this->tableMaintainer->insertFromSelect($select, $temporaryOptionsTableName, []);
}
/**
@@ -269,7 +267,7 @@ private function getMainTable($dimensions)
if ($this->fullReindexAction) {
return $this->tableMaintainer->getMainReplicaTable($dimensions);
}
- return $this->tableMaintainer->getMainTable($dimensions);
+ return $this->tableMaintainer->getMainTableByDimensions($dimensions);
}
/**
diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
index 1555e88700a45..2f333e7ca6f6e 100644
--- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
+++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
@@ -4,11 +4,21 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel;
+use Magento\Catalog\Api\Data\ProductAttributeInterface;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\ConfigurableProduct\Api\Data\OptionInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+use Magento\Framework\Api\FilterBuilder;
+use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Indexer\ActionInterface;
+/**
+ * Plugin product resource model
+ */
class Product
{
/**
@@ -21,18 +31,45 @@ class Product
*/
private $productIndexer;
+ /**
+ * @var ProductAttributeRepositoryInterface
+ */
+ private $productAttributeRepository;
+
+ /**
+ * @var SearchCriteriaBuilder
+ */
+ private $searchCriteriaBuilder;
+
+ /**
+ * @var FilterBuilder
+ */
+ private $filterBuilder;
+
/**
* Initialize Product dependencies.
*
* @param Configurable $configurable
* @param ActionInterface $productIndexer
+ * @param ProductAttributeRepositoryInterface $productAttributeRepository
+ * @param SearchCriteriaBuilder $searchCriteriaBuilder
+ * @param FilterBuilder $filterBuilder
*/
public function __construct(
Configurable $configurable,
- ActionInterface $productIndexer
+ ActionInterface $productIndexer,
+ ProductAttributeRepositoryInterface $productAttributeRepository = null,
+ SearchCriteriaBuilder $searchCriteriaBuilder = null,
+ FilterBuilder $filterBuilder = null
) {
$this->configurable = $configurable;
$this->productIndexer = $productIndexer;
+ $this->productAttributeRepository = $productAttributeRepository ?: ObjectManager::getInstance()
+ ->get(ProductAttributeRepositoryInterface::class);
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance()
+ ->get(SearchCriteriaBuilder::class);
+ $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance()
+ ->get(FilterBuilder::class);
}
/**
@@ -41,6 +78,7 @@ public function __construct(
* @param \Magento\Catalog\Model\ResourceModel\Product $subject
* @param \Magento\Framework\DataObject $object
* @return void
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -51,6 +89,39 @@ public function beforeSave(
/** @var \Magento\Catalog\Model\Product $object */
if ($object->getTypeId() == Configurable::TYPE_CODE) {
$object->getTypeInstance()->getSetAttributes($object);
+ $this->resetConfigurableOptionsData($object);
+ }
+ }
+
+ /**
+ * Set null for configurable options attribute of configurable product
+ *
+ * @param \Magento\Catalog\Model\Product $object
+ * @return void
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function resetConfigurableOptionsData($object)
+ {
+ $extensionAttribute = $object->getExtensionAttributes();
+ if ($extensionAttribute && $extensionAttribute->getConfigurableProductOptions()) {
+ $attributeIds = [];
+ /** @var OptionInterface $option */
+ foreach ($extensionAttribute->getConfigurableProductOptions() as $option) {
+ $attributeIds[] = $option->getAttributeId();
+ }
+
+ $filter = $this->filterBuilder
+ ->setField(ProductAttributeInterface::ATTRIBUTE_ID)
+ ->setConditionType('in')
+ ->setValue($attributeIds)
+ ->create();
+ $this->searchCriteriaBuilder->addFilters([$filter]);
+ $searchCriteria = $this->searchCriteriaBuilder->create();
+ $optionAttributes = $this->productAttributeRepository->getList($searchCriteria)->getItems();
+
+ foreach ($optionAttributes as $optionAttribute) {
+ $object->setData($optionAttribute->getAttributeCode(), null);
+ }
}
}
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml
new file mode 100644
index 0000000000000..c48f22a3656d5
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Adds 3 provided Options to a new Attribute on the Configurable Product creation/edit page. Selected default first option. Set "Use in Layered Navigation" to "Yes".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml
new file mode 100644
index 0000000000000..cc709b80efebb
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Fill basic value for Configurable Product using the default Product Options.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml
new file mode 100644
index 0000000000000..969a41e27d459
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Goes to the select values page from each attribute to include in the product.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml
new file mode 100644
index 0000000000000..cc2ff9a63ae40
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Click to check option.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml
new file mode 100644
index 0000000000000..3cca319d9569c
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Set quantity 1 to all child skus for configurable product. Save a configurable product and confirm.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml
index bd409d0e4bfde..186799bf4626b 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml
@@ -65,10 +65,11 @@
-
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php
deleted file mode 100644
index f96da3a7967bf..0000000000000
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php
+++ /dev/null
@@ -1,75 +0,0 @@
-product = $this->getMockBuilder(Product::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId'])
- ->getMock();
-
- $this->configurableTypeMock = $this->getMockBuilder(Configurable::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock);
- }
-
- public function testAfterGetIdentities()
- {
- $identities = [
- 'SomeCacheId',
- 'AnotherCacheId',
- ];
- $productId = 12345;
- $childIdentities = [
- 0 => [1, 2, 5, 100500]
- ];
- $expectedIdentities = [
- 'SomeCacheId',
- 'AnotherCacheId',
- Product::CACHE_TAG . '_' . 1,
- Product::CACHE_TAG . '_' . 2,
- Product::CACHE_TAG . '_' . 5,
- Product::CACHE_TAG . '_' . 100500,
- ];
-
- $this->product->expects($this->once())
- ->method('getId')
- ->willReturn($productId);
-
- $this->configurableTypeMock->expects($this->once())
- ->method('getChildrenIds')
- ->with($productId)
- ->willReturn($childIdentities);
-
- $productIdentities = $this->plugin->afterGetIdentities($this->product, $identities);
- $this->assertEquals($expectedIdentities, $productIdentities);
- }
-}
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
index abab103fa6d37..3d5a0d1cc6a3f 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
@@ -7,20 +7,38 @@
namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\ResourceModel;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\Catalog\Model\Product as ModelProduct;
use Magento\Catalog\Model\Product\Type;
+use Magento\Catalog\Model\ProductAttributeSearchResults;
+use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute;
+use Magento\Catalog\Model\ResourceModel\Product as ResourceModelProduct;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
-use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute;
+use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product as PluginResourceModelProduct;
+use Magento\Framework\Api\ExtensionAttributesInterface;
+use Magento\Framework\Api\FilterBuilder;
+use Magento\Framework\Api\SearchCriteria;
+use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Indexer\ActionInterface;
-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class ProductTest extends TestCase
{
/**
- * @var ObjectManager
+ * @var PluginResourceModelProduct
*/
- private $objectManager;
+ private $model;
+
+ /**
+ * @var ObjectManagerHelper
+ */
+ private $objectManagerHelper;
/**
* @var Configurable|MockObject
@@ -33,39 +51,128 @@ class ProductTest extends TestCase
private $actionMock;
/**
- * @var Product
+ * @var ProductAttributeRepositoryInterface|MockObject
*/
- private $model;
+ private $productAttributeRepositoryMock;
+
+ /**
+ * @var SearchCriteriaBuilder|MockObject
+ */
+ private $searchCriteriaBuilderMock;
+
+ /**
+ * @var FilterBuilder|MockObject
+ */
+ private $filterBuilderMock;
protected function setUp(): void
{
- $this->objectManager = new ObjectManager($this);
$this->configurableMock = $this->createMock(Configurable::class);
$this->actionMock = $this->getMockForAbstractClass(ActionInterface::class);
-
- $this->model = $this->objectManager->getObject(
- Product::class,
+ $this->productAttributeRepositoryMock = $this->getMockBuilder(ProductAttributeRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getList'])
+ ->getMockForAbstractClass();
+ $this->searchCriteriaBuilderMock = $this->createPartialMock(
+ SearchCriteriaBuilder::class,
+ ['addFilters', 'create']
+ );
+ $this->filterBuilderMock = $this->createPartialMock(
+ FilterBuilder::class,
+ ['setField', 'setConditionType', 'setValue', 'create']
+ );
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->model = $this->objectManagerHelper->getObject(
+ PluginResourceModelProduct::class,
[
'configurable' => $this->configurableMock,
'productIndexer' => $this->actionMock,
+ 'productAttributeRepository' => $this->productAttributeRepositoryMock,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
+ 'filterBuilder' => $this->filterBuilderMock
]
);
}
- public function testBeforeSaveConfigurable()
+ public function testBeforeSaveConfigurable(): void
{
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $object */
- $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']);
+ /** @var ResourceModelProduct|MockObject $subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $object */
+ $object = $this->createPartialMock(
+ ModelProduct::class,
+ [
+ 'getTypeId',
+ 'getTypeInstance',
+ 'getExtensionAttributes',
+ 'setData'
+ ]
+ );
$type = $this->createPartialMock(
Configurable::class,
['getSetAttributes']
);
- $type->expects($this->once())->method('getSetAttributes')->with($object);
-
- $object->expects($this->once())->method('getTypeId')->willReturn(Configurable::TYPE_CODE);
- $object->expects($this->once())->method('getTypeInstance')->willReturn($type);
+ $extensionAttributes = $this->getMockBuilder(ExtensionAttributesInterface::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getConfigurableProductOptions'])
+ ->getMock();
+ $option = $this->createPartialMock(
+ ConfigurableAttribute::class,
+ ['getAttributeId']
+ );
+ $extensionAttributes->expects($this->exactly(2))
+ ->method('getConfigurableProductOptions')
+ ->willReturn([$option]);
+ $object->expects($this->once())
+ ->method('getExtensionAttributes')
+ ->willReturn($extensionAttributes);
+
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setField')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setValue')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setConditionType')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('create')
+ ->willReturnSelf();
+ $searchCriteria = $this->createMock(SearchCriteria::class);
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('create')
+ ->willReturn($searchCriteria);
+ $searchResultMockClass = $this->createPartialMock(
+ ProductAttributeSearchResults::class,
+ ['getItems']
+ );
+ $this->productAttributeRepositoryMock->expects($this->once())
+ ->method('getList')
+ ->with($searchCriteria)
+ ->willReturn($searchResultMockClass);
+ $optionAttribute = $this->createPartialMock(
+ EavAttribute::class,
+ ['getAttributeCode']
+ );
+ $searchResultMockClass->expects($this->once())
+ ->method('getItems')
+ ->willReturn([$optionAttribute]);
+ $type->expects($this->once())
+ ->method('getSetAttributes')
+ ->with($object);
+ $object->expects($this->once())
+ ->method('getTypeId')
+ ->will($this->returnValue(Configurable::TYPE_CODE));
+ $object->expects($this->once())
+ ->method('getTypeInstance')
+ ->will($this->returnValue($type));
+ $object->expects($this->once())
+ ->method('setData');
+ $option->expects($this->once())
+ ->method('getAttributeId');
+ $optionAttribute->expects($this->once())
+ ->method('getAttributeCode');
$this->model->beforeSave(
$subject,
@@ -73,14 +180,23 @@ public function testBeforeSaveConfigurable()
);
}
- public function testBeforeSaveSimple()
+ public function testBeforeSaveSimple(): void
{
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $object */
- $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']);
- $object->expects($this->once())->method('getTypeId')->willReturn(Type::TYPE_SIMPLE);
- $object->expects($this->never())->method('getTypeInstance');
+ /** @var ResourceModelProduct|MockObject$subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $object */
+ $object = $this->createPartialMock(
+ ModelProduct::class,
+ [
+ 'getTypeId',
+ 'getTypeInstance'
+ ]
+ );
+ $object->expects($this->once())
+ ->method('getTypeId')
+ ->will($this->returnValue(Type::TYPE_SIMPLE));
+ $object->expects($this->never())
+ ->method('getTypeInstance');
$this->model->beforeSave(
$subject,
@@ -88,29 +204,35 @@ public function testBeforeSaveSimple()
);
}
- public function testAroundDelete()
+ public function testAroundDelete(): void
{
$productId = '1';
$parentConfigId = ['2'];
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $product */
+ /** @var ResourceModelProduct|MockObject $subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $product */
$product = $this->createPartialMock(
- \Magento\Catalog\Model\Product::class,
+ ModelProduct::class,
['getId', 'delete']
);
- $product->expects($this->once())->method('getId')->willReturn($productId);
- $product->expects($this->once())->method('delete')->willReturn(true);
+ $product->expects($this->once())
+ ->method('getId')
+ ->willReturn($productId);
+ $product->expects($this->once())
+ ->method('delete')
+ ->willReturn(true);
$this->configurableMock->expects($this->once())
->method('getParentIdsByChild')
->with($productId)
->willReturn($parentConfigId);
- $this->actionMock->expects($this->once())->method('executeList')->with($parentConfigId);
+ $this->actionMock->expects($this->once())
+ ->method('executeList')
+ ->with($parentConfigId);
$return = $this->model->aroundDelete(
$subject,
- /** @var \Magento\Catalog\Model\Product|MockObject $prod */
- function (\Magento\Catalog\Model\Product $prod) use ($subject) {
+ /** @var ModelProduct|MockObject $prod */
+ function (ModelProduct $prod) use ($subject) {
$prod->delete();
return $subject;
},
diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
index b2d50f54f5334..f60234453dc60 100644
--- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
+++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
@@ -10,9 +10,6 @@
-
-
-
diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml
index a084abfc31eaa..ffd17a8bf4734 100644
--- a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml
+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml
@@ -48,6 +48,7 @@
- configurableModal
- productFormConfigurable
+ Magento\ConfigurableProduct\Block\DataProviders\PermissionsData
diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml
index e996df8260719..e94d94e0ded55 100644
--- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml
+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml
@@ -5,6 +5,9 @@
*/
/* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */
+$isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes();
+$attributesUrl = $block->getUrl('catalog/product_attribute/getAttributes');
+$optionsUrl = $block->getUrl('catalog/product_attribute/createOptions');
?>
= $block->escapeHtml(
@@ -12,7 +15,8 @@
); ?>
= $block->escapeHtml(
- __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.')
+ __('Select values from each attribute to include in this product. ' .
+ 'Each unique combination of values creates a unique product SKU.')
);?>
@@ -72,7 +76,8 @@
-
@@ -120,8 +125,8 @@
"= /* @noEscape */ $block->getComponentName() ?>": {
"component": "Magento_ConfigurableProduct/js/variations/steps/attributes_values",
"appendTo": "= /* @noEscape */ $block->getParentComponentName() ?>",
- "optionsUrl": "= /* @noEscape */ $block->getUrl('catalog/product_attribute/getAttributes') ?>",
- "createOptionsUrl": "= /* @noEscape */ $block->getUrl('catalog/product_attribute/createOptions') ?>"
+ "optionsUrl": "= /* @noEscape */ $attributesUrl ?>",
+ "createOptionsUrl": "= /* @noEscape */ $optionsUrl ?>"
}
}
}
diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml
index d218e650657ac..eee9f742a59a4 100644
--- a/app/code/Magento/Contact/view/frontend/templates/form.phtml
+++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml
@@ -4,6 +4,9 @@
* See COPYING.txt for license details.
*/
+// phpcs:disable Magento2.Templates.ThisInTemplate
+// phpcs:disable Generic.Files.LineLength.TooLong
+
/** @var \Magento\Contact\Block\ContactForm $block */
/** @var \Magento\Contact\ViewModel\UserDataProvider $viewModel */
@@ -23,35 +26,35 @@ $viewModel = $block->getViewModel();
@@ -60,12 +63,12 @@ $viewModel = $block->getViewModel();
= $block->escapeHtml(__('What’s on your mind?')) ?>
-
@@ -81,3 +84,12 @@ $viewModel = $block->getViewModel();
+
diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php
index 2be340c8ccca4..b4c737f6600bf 100644
--- a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php
+++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php
@@ -52,7 +52,7 @@ public function getFrontendLabel(string $attributeCode): string
{
try {
$attribute = $this->addressMetadata->getAttributeMetadata($attributeCode);
- $frontendLabel = $attribute->getFrontendLabel();
+ $frontendLabel = $attribute->getStoreLabel() ?: $attribute->getFrontendLabel();
} catch (NoSuchEntityException $e) {
$frontendLabel = '';
}
diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php
index d68904f6d1645..f2868132790cf 100644
--- a/app/code/Magento/Customer/Model/CustomerRegistry.php
+++ b/app/code/Magento/Customer/Model/CustomerRegistry.php
@@ -101,8 +101,10 @@ public function retrieve($customerId)
public function retrieveByEmail($customerEmail, $websiteId = null)
{
if ($websiteId === null) {
- $websiteId = $this->storeManager->getStore()->getWebsiteId();
+ $websiteId = $this->storeManager->getStore()->getWebsiteId()
+ ?: $this->storeManager->getDefaultStoreView()->getWebsiteId();
}
+
$emailKey = $this->getEmailKey($customerEmail, $websiteId);
if (isset($this->customerRegistryByEmail[$emailKey])) {
return $this->customerRegistryByEmail[$emailKey];
diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php
index 9eb9ffb806c9f..b877b2cca67a5 100644
--- a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php
+++ b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php
@@ -7,7 +7,10 @@
namespace Magento\Customer\Model\Plugin;
use Magento\Authorization\Model\UserContextInterface;
+use Magento\Customer\Model\CustomerFactory;
+use Magento\Customer\Model\ResourceModel\Customer as CustomerResource;
use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService;
+use Magento\Store\Model\StoreManagerInterface;
/**
* Plugin around \Magento\Framework\Authorization::isAllowed
@@ -19,16 +22,41 @@ class CustomerAuthorization
/**
* @var UserContextInterface
*/
- protected $userContext;
+ private $userContext;
+
+ /**
+ * @var CustomerFactory
+ */
+ private $customerFactory;
+
+ /**
+ * @var CustomerResource
+ */
+ private $customerResource;
+
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
/**
* Inject dependencies.
*
* @param UserContextInterface $userContext
+ * @param CustomerFactory $customerFactory
+ * @param CustomerResource $customerResource
+ * @param StoreManagerInterface $storeManager
*/
- public function __construct(UserContextInterface $userContext)
- {
+ public function __construct(
+ UserContextInterface $userContext,
+ CustomerFactory $customerFactory,
+ CustomerResource $customerResource,
+ StoreManagerInterface $storeManager
+ ) {
$this->userContext = $userContext;
+ $this->customerFactory = $customerFactory;
+ $this->customerResource = $customerResource;
+ $this->storeManager = $storeManager;
}
/**
@@ -53,9 +81,15 @@ public function aroundIsAllowed(
&& $this->userContext->getUserId()
&& $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER
) {
- return true;
- } else {
- return $proceed($resource, $privilege);
+ $customer = $this->customerFactory->create();
+ $this->customerResource->load($customer, $this->userContext->getUserId());
+ $currentStoreId = $this->storeManager->getStore()->getId();
+ $sharedStoreIds = $customer->getSharedStoreIds();
+ if (in_array($currentStoreId, $sharedStoreIds)) {
+ return true;
+ }
}
+
+ return $proceed($resource, $privilege);
}
}
diff --git a/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php
new file mode 100644
index 0000000000000..fdde31e05fb2e
--- /dev/null
+++ b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php
@@ -0,0 +1,81 @@
+request = $request;
+ }
+
+ /**
+ * Update customer by id from request if exist
+ *
+ * @param CustomerRepositoryInterface $customerRepository
+ * @param CustomerInterface $customer
+ * @param string|null $passwordHash
+ * @return array
+ */
+ public function beforeSave(
+ CustomerRepositoryInterface $customerRepository,
+ CustomerInterface $customer,
+ ?string $passwordHash = null
+ ): array {
+ $customerId = $this->request->getParam('customerId');
+
+ if ($customerId) {
+ $customer = $this->getUpdatedCustomer($customerRepository->getById($customerId), $customer);
+ }
+
+ return [$customer, $passwordHash];
+ }
+
+ /**
+ * Return updated customer
+ *
+ * @param CustomerInterface $originCustomer
+ * @param CustomerInterface $customer
+ * @return CustomerInterface
+ */
+ private function getUpdatedCustomer(
+ CustomerInterface $originCustomer,
+ CustomerInterface $customer
+ ): CustomerInterface {
+ $newCustomer = clone $originCustomer;
+ foreach ($customer->__toArray() as $name => $value) {
+ if ($name === CustomerInterface::CUSTOM_ATTRIBUTES) {
+ $value = $customer->getCustomAttributes();
+ } elseif ($name === CustomerInterface::EXTENSION_ATTRIBUTES_KEY) {
+ $value = $customer->getExtensionAttributes();
+ } elseif ($name === CustomerInterface::KEY_ADDRESSES) {
+ $value = $customer->getAddresses();
+ }
+
+ $newCustomer->setData($name, $value);
+ }
+
+ return $newCustomer;
+ }
+}
diff --git a/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php
new file mode 100644
index 0000000000000..c2b7189b808a3
--- /dev/null
+++ b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php
@@ -0,0 +1,78 @@
+orderRepository = $orderRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ }
+
+ /**
+ * Upgrade order customer email when customer has changed email
+ *
+ * @param Observer $observer
+ * @return void
+ */
+ public function execute(Observer $observer): void
+ {
+ /** @var Customer $originalCustomer */
+ $originalCustomer = $observer->getEvent()->getOrigCustomerDataObject();
+ if (!$originalCustomer) {
+ return;
+ }
+
+ /** @var Customer $customer */
+ $customer = $observer->getEvent()->getCustomerDataObject();
+ $customerEmail = $customer->getEmail();
+
+ if ($customerEmail === $originalCustomer->getEmail()) {
+ return;
+ }
+ $searchCriteria = $this->searchCriteriaBuilder
+ ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId())
+ ->create();
+
+ /**
+ * @var Collection $orders
+ */
+ $orders = $this->orderRepository->getList($searchCriteria);
+ $orders->setDataToAll(OrderInterface::CUSTOMER_EMAIL, $customerEmail);
+ $orders->save();
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml
new file mode 100644
index 0000000000000..b827cba8490b8
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml
new file mode 100644
index 0000000000000..bbdc4de330840
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml
new file mode 100644
index 0000000000000..66b464006aa0f
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml
new file mode 100644
index 0000000000000..16688be61171e
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml
new file mode 100644
index 0000000000000..f287c728bb28d
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Create Order via API assigned to Customer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml
new file mode 100644
index 0000000000000..5dafe59bf3c48
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml b/app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimpleData.xml
similarity index 100%
rename from app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimple.xml
rename to app/code/Magento/Customer/Test/Mftf/Data/ExtensionAttributeSimpleData.xml
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml
new file mode 100644
index 0000000000000..39a67968c66e4
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
index ec5141d84b1bd..61ce050aa3ef2 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
@@ -17,5 +17,9 @@
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
new file mode 100644
index 0000000000000..ba113c739d706
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php
new file mode 100644
index 0000000000000..d05c10c00e6c3
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php
@@ -0,0 +1,222 @@
+orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class)
+ ->getMock();
+
+ $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->eventMock = $this->getMockBuilder(Event::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getCustomerDataObject', 'getOrigCustomerDataObject'])
+ ->getMock();
+
+ $this->observerMock = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->observerMock->expects($this->any())->method('getEvent')->willReturn($this->eventMock);
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->orderCustomerEmailObserver = $this->objectManagerHelper->getObject(
+ UpgradeOrderCustomerEmailObserver::class,
+ [
+ 'orderRepository' => $this->orderRepositoryMock,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
+ ]
+ );
+ }
+
+ /**
+ * Verifying that the order email is not updated when the customer email is not updated
+ *
+ */
+ public function testUpgradeOrderCustomerEmailWhenMailIsNotChanged(): void
+ {
+ $customer = $this->createCustomerMock();
+ $originalCustomer = $this->createCustomerMock();
+
+ $this->setCustomerToEventMock($customer);
+ $this->setOriginalCustomerToEventMock($originalCustomer);
+
+ $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL);
+ $this->setCustomerEmail($customer, self::ORIGINAL_CUSTOMER_EMAIL);
+
+ $this->whenOrderRepositoryGetListIsNotCalled();
+
+ $this->orderCustomerEmailObserver->execute($this->observerMock);
+ }
+
+ /**
+ * Verifying that the order email is updated after the customer updates their email
+ *
+ */
+ public function testUpgradeOrderCustomerEmail(): void
+ {
+ $customer = $this->createCustomerMock();
+ $originalCustomer = $this->createCustomerMock();
+ $orderCollectionMock = $this->createOrderMock();
+
+ $this->setCustomerToEventMock($customer);
+ $this->setOriginalCustomerToEventMock($originalCustomer);
+
+ $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL);
+ $this->setCustomerEmail($customer, self::NEW_CUSTOMER_EMAIL);
+
+ $this->whenOrderRepositoryGetListIsCalled($orderCollectionMock);
+
+ $this->whenOrderCollectionSetDataToAllIsCalled($orderCollectionMock);
+
+ $this->whenOrderCollectionSaveIsCalled($orderCollectionMock);
+
+ $this->orderCustomerEmailObserver->execute($this->observerMock);
+ }
+
+ private function createCustomerMock(): MockObject
+ {
+ $customer = $this->getMockBuilder(CustomerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $customer;
+ }
+
+ private function createOrderMock(): MockObject
+ {
+ $orderCollectionMock = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $orderCollectionMock;
+ }
+
+ private function setCustomerToEventMock(MockObject $customer): void
+ {
+ $this->eventMock->expects($this->once())
+ ->method('getCustomerDataObject')
+ ->willReturn($customer);
+ }
+
+ private function setOriginalCustomerToEventMock(MockObject $originalCustomer): void
+ {
+ $this->eventMock->expects($this->once())
+ ->method('getOrigCustomerDataObject')
+ ->willReturn($originalCustomer);
+ }
+
+ private function setCustomerEmail(MockObject $originalCustomer, string $email): void
+ {
+ $originalCustomer->expects($this->once())
+ ->method('getEmail')
+ ->willReturn($email);
+ }
+
+ private function whenOrderRepositoryGetListIsCalled(MockObject $orderCollectionMock): void
+ {
+ $searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('create')
+ ->willReturn($searchCriteriaMock);
+
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('addFilter')
+ ->willReturn($this->searchCriteriaBuilderMock);
+
+ $this->orderRepositoryMock->expects($this->once())
+ ->method('getList')
+ ->with($searchCriteriaMock)
+ ->willReturn($orderCollectionMock);
+ }
+
+ private function whenOrderCollectionSetDataToAllIsCalled(MockObject $orderCollectionMock): void
+ {
+ $orderCollectionMock->expects($this->once())
+ ->method('setDataToAll')
+ ->with(OrderInterface::CUSTOMER_EMAIL, self::NEW_CUSTOMER_EMAIL);
+ }
+
+ private function whenOrderCollectionSaveIsCalled(MockObject $orderCollectionMock): void
+ {
+ $orderCollectionMock->expects($this->once())
+ ->method('save');
+ }
+
+ private function whenOrderRepositoryGetListIsNotCalled(): void
+ {
+ $this->searchCriteriaBuilderMock->expects($this->never())
+ ->method('addFilter');
+ $this->searchCriteriaBuilderMock->expects($this->never())
+ ->method('create');
+
+ $this->orderRepositoryMock->expects($this->never())
+ ->method('getList');
+ }
+}
diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml
index 2a724498a0359..0194f91c591f5 100644
--- a/app/code/Magento/Customer/etc/events.xml
+++ b/app/code/Magento/Customer/etc/events.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml
index 5f3ca2fdb7453..a349d07a5e222 100644
--- a/app/code/Magento/Customer/etc/webapi_rest/di.xml
+++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml
@@ -19,4 +19,7 @@
+
+
+
diff --git a/app/code/Magento/Customer/view/frontend/email/change_email.html b/app/code/Magento/Customer/view/frontend/email/change_email.html
index bd961ad99ec40..5341a2dc67ad5 100644
--- a/app/code/Magento/Customer/view/frontend/email/change_email.html
+++ b/app/code/Magento/Customer/view/frontend/email/change_email.html
@@ -18,8 +18,5 @@
{{trans "We have received a request to change the following information associated with your account at %store_name: email." store_name=$store.frontend_name}}
{{trans 'If you have not authorized this action, please contact us immediately at %store_email ' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at %store_phone ' store_phone=$store_phone |raw}}{{/depend}}.
-
-
-{{trans "Thanks, %store_name" store_name=$store.frontend_name |raw}}
{{template config_path="design/email/footer_template"}}
diff --git a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html
index 4f5c85b2381f3..ed2af7ada669e 100644
--- a/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html
+++ b/app/code/Magento/Customer/view/frontend/email/change_email_and_password.html
@@ -18,8 +18,5 @@
{{trans "We have received a request to change the following information associated with your account at %store_name: email, password." store_name=$store.frontend_name}}
{{trans 'If you have not authorized this action, please contact us immediately at %store_email ' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at %store_phone ' store_phone=$store_phone |raw}}{{/depend}}.
-
-
-{{trans "Thanks, %store_name" store_name=$store.frontend_name |raw}}
{{template config_path="design/email/footer_template"}}
diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset.html b/app/code/Magento/Customer/view/frontend/email/password_reset.html
index cab05a89227b6..a6c54842a1573 100644
--- a/app/code/Magento/Customer/view/frontend/email/password_reset.html
+++ b/app/code/Magento/Customer/view/frontend/email/password_reset.html
@@ -19,8 +19,5 @@
{{trans "We have received a request to change the following information associated with your account at %store_name: password." store_name=$store.frontend_name}}
{{trans 'If you have not authorized this action, please contact us immediately at %store_email ' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at %store_phone ' store_phone=$store_phone |raw}}{{/depend}}.
-
-
-{{trans "Thanks, %store_name" store_name=$store.frontend_name |raw}}
{{template config_path="design/email/footer_template"}}
diff --git a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml
index be201afa8f66c..caa501d48da83 100644
--- a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml
@@ -6,6 +6,8 @@
* @var $block \Magento\Customer\Block\Account\Forgotpassword
*/
+// phpcs:disable Generic.Files.LineLength.TooLong
+
/** @var \Magento\Customer\Block\Account\Forgotpassword $block */
?>
+
diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml
index ef74b0062c023..a1d1a0260672a 100644
--- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml
@@ -4,6 +4,8 @@
* See COPYING.txt for license details.
*/
+// phpcs:disable Generic.Files.LineLength.TooLong
+
/** @var \Magento\Customer\Block\Form\Login $block */
?>
@@ -22,13 +24,22 @@
= $block->getChildHtml('form_additional_info') ?>
@@ -41,3 +52,12 @@
+
diff --git a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml
index f7d10f6df1728..e84861b9b5cf6 100644
--- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml
@@ -301,13 +301,6 @@ require([
ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden'
}).find('input:text').attr('autocomplete', 'off');
- dataForm.submit(function () {
- $(this).find(':submit').attr('disabled', 'disabled');
- });
- dataForm.bind("invalid-form.validate", function () {
- $(this).find(':submit').prop('disabled', false);
- });
-
});
getShowAddressFields()): ?>
@@ -337,6 +330,11 @@ require([
"passwordStrengthIndicator": {
"formSelector": "form.form-create-account"
}
+ },
+ "*": {
+ "Magento_Customer/js/block-submit-on-send": {
+ "formId": "form-validate"
+ }
}
}
diff --git a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js
new file mode 100644
index 0000000000000..b941ec7a254d8
--- /dev/null
+++ b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'mage/mage'
+], function ($) {
+ 'use strict';
+
+ return function (config) {
+ var dataForm = $('#' + config.formId);
+
+ dataForm.submit(function () {
+ $(this).find(':submit').attr('disabled', 'disabled');
+ });
+ dataForm.bind('invalid-form.validate', function () {
+ $(this).find(':submit').prop('disabled', false);
+ });
+ };
+});
diff --git a/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php
new file mode 100644
index 0000000000000..ef3e86788c43f
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php
@@ -0,0 +1,24 @@
+getAllowedCustomerAttributes = $getAllowedCustomerAttributes;
$this->emailAddressValidator = $emailAddressValidator;
+ $this->validators = $validators;
}
/**
* Validate customer data
*
* @param array $customerData
- *
- * @return void
- *
* @throws GraphQlInputException
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
*/
- public function execute(array $customerData): void
+ public function execute(array $customerData)
{
- $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData));
- $errorInput = [];
-
- foreach ($attributes as $attributeInfo) {
- if ($attributeInfo->getIsRequired()
- && (!isset($customerData[$attributeInfo->getAttributeCode()])
- || $customerData[$attributeInfo->getAttributeCode()] == '')
- ) {
- $errorInput[] = $attributeInfo->getDefaultFrontendLabel();
- }
- }
-
- if ($errorInput) {
- throw new GraphQlInputException(
- __('Required parameters are missing: %1', [implode(', ', $errorInput)])
- );
- }
-
- if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) {
- throw new GraphQlInputException(
- __('"%1" is not a valid email address.', $customerData['email'])
- );
+ /** @var ValidateCustomerDataInterface $validator */
+ foreach ($this->validators as $validator) {
+ $validator->execute($customerData);
}
}
}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php
new file mode 100644
index 0000000000000..87f8831550f04
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php
@@ -0,0 +1,45 @@
+emailAddressValidator = $emailAddressValidator;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $customerData): void
+ {
+ if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) {
+ throw new GraphQlInputException(
+ __('"%1" is not a valid email address.', $customerData['email'])
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php
new file mode 100644
index 0000000000000..463372a63d8d5
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php
@@ -0,0 +1,58 @@
+customerMetadata = $customerMetadata;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $customerData): void
+ {
+ if (isset($customerData['gender']) && $customerData['gender']) {
+ /** @var AttributeMetadata $genderData */
+ $options = $this->customerMetadata->getAttributeMetadata('gender')->getOptions();
+
+ $isValid = false;
+ foreach ($options as $optionData) {
+ if ($optionData->getValue() && $optionData->getValue() == $customerData['gender']) {
+ $isValid = true;
+ }
+ }
+
+ if (!$isValid) {
+ throw new GraphQlInputException(
+ __('"%1" is not a valid gender value.', $customerData['gender'])
+ );
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php
new file mode 100644
index 0000000000000..fdf4fa1c824f2
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php
@@ -0,0 +1,59 @@
+getAllowedCustomerAttributes = $getAllowedCustomerAttributes;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $customerData): void
+ {
+ $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData));
+ $errorInput = [];
+
+ foreach ($attributes as $attributeInfo) {
+ if ($attributeInfo->getIsRequired()
+ && (!isset($customerData[$attributeInfo->getAttributeCode()])
+ || $customerData[$attributeInfo->getAttributeCode()] == '')
+ ) {
+ $errorInput[] = $attributeInfo->getDefaultFrontendLabel();
+ }
+ }
+
+ if ($errorInput) {
+ throw new GraphQlInputException(
+ __('Required parameters are missing: %1', [implode(', ', $errorInput)])
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
index 1ba0e457430e0..3ed77a2ad563c 100644
--- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
@@ -29,4 +29,14 @@
+
+
+
+
+ - Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateRequiredArguments
+ - Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail
+ - Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateGender
+
+
+
diff --git a/app/code/Magento/Developer/Console/Command/patch_template.php.dist b/app/code/Magento/Developer/Console/Command/patch_template.php.dist
index f4fc25abcb29a..8e14b24bdc933 100644
--- a/app/code/Magento/Developer/Console/Command/patch_template.php.dist
+++ b/app/code/Magento/Developer/Console/Command/patch_template.php.dist
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace %moduleName%\Setup\Patch\%patchType%;
@@ -36,7 +37,7 @@ class %class% implements %implementsInterfaces%
}
%revertFunction%
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getAliases()
{
@@ -44,12 +45,10 @@ class %class% implements %implementsInterfaces%
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public static function getDependencies()
{
- return [
-
- ];
+ return [];
}
}
diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php
index ba98524bb665e..fc659c773c0af 100644
--- a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php
+++ b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php
@@ -21,11 +21,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug
*/
private $state;
- /**
- * @var ScopeConfigInterface
- */
- private $scopeConfig;
-
/**
* @var DeploymentConfig
*/
@@ -34,7 +29,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug
/**
* @param DriverInterface $filesystem
* @param State $state
- * @param ScopeConfigInterface $scopeConfig
* @param DeploymentConfig $deploymentConfig
* @param string $filePath
* @throws \Exception
@@ -42,14 +36,12 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug
public function __construct(
DriverInterface $filesystem,
State $state,
- ScopeConfigInterface $scopeConfig,
DeploymentConfig $deploymentConfig,
$filePath = null
) {
parent::__construct($filesystem, $filePath);
$this->state = $state;
- $this->scopeConfig = $scopeConfig;
$this->deploymentConfig = $deploymentConfig;
}
diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php
index 3f5ff58640313..c6ee70fb9ce40 100644
--- a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php
+++ b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php
@@ -29,13 +29,10 @@ class Syslog extends \Magento\Framework\Logger\Handler\Syslog
private $deploymentConfig;
/**
- * @param ScopeConfigInterface $scopeConfig Scope config
* @param DeploymentConfig $deploymentConfig Deployment config
* @param string $ident The string ident to be added to each message
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
- ScopeConfigInterface $scopeConfig,
DeploymentConfig $deploymentConfig,
string $ident
) {
diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php
index 8bb0b1f176313..5e824e43764de 100644
--- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php
+++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php
@@ -44,7 +44,6 @@ protected function setUp(): void
$this->deploymentConfigMock = $this->createMock(DeploymentConfig::class);
$this->model = new Syslog(
- $this->scopeConfigMock,
$this->deploymentConfigMock,
'Magento'
);
diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
index c0bc825a8285b..c449f8f54872f 100644
--- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
+++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
@@ -7,8 +7,9 @@
namespace Magento\Downloadable\Controller\Download;
-use Magento\Catalog\Model\Product\SalabilityChecker;
use Magento\Downloadable\Helper\Download as DownloadHelper;
+use Magento\Downloadable\Model\Link as LinkModel;
+use Magento\Downloadable\Model\RelatedProductRetriever;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
@@ -20,20 +21,21 @@
class LinkSample extends \Magento\Downloadable\Controller\Download
{
/**
- * @var SalabilityChecker
+ * @var RelatedProductRetriever
*/
- private $salabilityChecker;
+ private $relatedProductRetriever;
/**
* @param Context $context
- * @param SalabilityChecker|null $salabilityChecker
+ * @param RelatedProductRetriever $relatedProductRetriever
*/
public function __construct(
Context $context,
- SalabilityChecker $salabilityChecker = null
+ RelatedProductRetriever $relatedProductRetriever
) {
parent::__construct($context);
- $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class);
+
+ $this->relatedProductRetriever = $relatedProductRetriever;
}
/**
@@ -44,9 +46,10 @@ public function __construct(
public function execute()
{
$linkId = $this->getRequest()->getParam('link_id', 0);
- /** @var \Magento\Downloadable\Model\Link $link */
- $link = $this->_objectManager->create(\Magento\Downloadable\Model\Link::class)->load($linkId);
- if ($link->getId() && $this->salabilityChecker->isSalable($link->getProductId())) {
+ /** @var LinkModel $link */
+ $link = $this->_objectManager->create(LinkModel::class);
+ $link->load($linkId);
+ if ($link->getId() && $this->isProductSalable($link)) {
$resource = '';
$resourceType = '';
if ($link->getSampleType() == DownloadHelper::LINK_TYPE_URL) {
@@ -74,4 +77,16 @@ public function execute()
return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl());
}
+
+ /**
+ * Check is related product salable.
+ *
+ * @param LinkModel $link
+ * @return bool
+ */
+ private function isProductSalable(LinkModel $link): bool
+ {
+ $product = $this->relatedProductRetriever->getProduct((int) $link->getProductId());
+ return $product ? $product->isSalable() : false;
+ }
}
diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php
index b95ec510fdd9b..e2561092a7592 100644
--- a/app/code/Magento/Downloadable/Controller/Download/Sample.php
+++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php
@@ -7,8 +7,9 @@
namespace Magento\Downloadable\Controller\Download;
-use Magento\Catalog\Model\Product\SalabilityChecker;
use Magento\Downloadable\Helper\Download as DownloadHelper;
+use Magento\Downloadable\Model\RelatedProductRetriever;
+use Magento\Downloadable\Model\Sample as SampleModel;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
@@ -20,20 +21,21 @@
class Sample extends \Magento\Downloadable\Controller\Download
{
/**
- * @var SalabilityChecker
+ * @var RelatedProductRetriever
*/
- private $salabilityChecker;
+ private $relatedProductRetriever;
/**
* @param Context $context
- * @param SalabilityChecker|null $salabilityChecker
+ * @param RelatedProductRetriever $relatedProductRetriever
*/
public function __construct(
Context $context,
- SalabilityChecker $salabilityChecker = null
+ RelatedProductRetriever $relatedProductRetriever
) {
parent::__construct($context);
- $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class);
+
+ $this->relatedProductRetriever = $relatedProductRetriever;
}
/**
@@ -44,9 +46,10 @@ public function __construct(
public function execute()
{
$sampleId = $this->getRequest()->getParam('sample_id', 0);
- /** @var \Magento\Downloadable\Model\Sample $sample */
- $sample = $this->_objectManager->create(\Magento\Downloadable\Model\Sample::class)->load($sampleId);
- if ($sample->getId() && $this->salabilityChecker->isSalable($sample->getProductId())) {
+ /** @var SampleModel $sample */
+ $sample = $this->_objectManager->create(SampleModel::class);
+ $sample->load($sampleId);
+ if ($sample->getId() && $this->isProductSalable($sample)) {
$resource = '';
$resourceType = '';
if ($sample->getSampleType() == DownloadHelper::LINK_TYPE_URL) {
@@ -71,4 +74,16 @@ public function execute()
return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl());
}
+
+ /**
+ * Check is related product salable.
+ *
+ * @param SampleModel $sample
+ * @return bool
+ */
+ private function isProductSalable(SampleModel $sample): bool
+ {
+ $product = $this->relatedProductRetriever->getProduct((int) $sample->getProductId());
+ return $product ? $product->isSalable() : false;
+ }
}
diff --git a/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php
new file mode 100644
index 0000000000000..f701f96b910e7
--- /dev/null
+++ b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php
@@ -0,0 +1,68 @@
+productRepository = $productRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->metadataPool = $metadataPool;
+ }
+
+ /**
+ * Get related product.
+ *
+ * @param int $productId
+ * @return ProductInterface|null
+ */
+ public function getProduct(int $productId): ?ProductInterface
+ {
+ $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class);
+
+ $searchCriteria = $this->searchCriteriaBuilder->addFilter($productMetadata->getLinkField(), $productId)
+ ->create();
+ $items = $this->productRepository->getList($searchCriteria)
+ ->getItems();
+ $product = $items ? array_shift($items) : null;
+
+ return $product;
+ }
+}
diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php
index 90b458ff6348e..e50213f139ba1 100644
--- a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php
+++ b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php
@@ -100,10 +100,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
- * @param array $dimensions
- * @param \Traversable $entityIds
- * @throws \Exception
+ * @inheritDoc
*/
public function executeByDimensions(array $dimensions, \Traversable $entityIds)
{
@@ -205,8 +202,7 @@ private function fillTemporaryTable(string $temporaryDownloadableTableName, arra
'max_price' => new \Zend_Db_Expr('SUM(' . $ifPrice . ')'),
]
);
- $query = $select->insertFromSelect($temporaryDownloadableTableName);
- $this->getConnection()->query($query);
+ $this->tableMaintainer->insertFromSelect($select, $temporaryDownloadableTableName, []);
}
/**
@@ -259,14 +255,13 @@ private function fillFinalPrice(
IndexTableStructure $temporaryPriceTable
) {
$select = $this->baseFinalPrice->getQuery($dimensions, Type::TYPE_DOWNLOADABLE, iterator_to_array($entityIds));
- $query = $select->insertFromSelect($temporaryPriceTable->getTableName(), [], false);
- $this->tableMaintainer->getConnection()->query($query);
+ $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []);
}
/**
* Get connection
*
- * return \Magento\Framework\DB\Adapter\AdapterInterface
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
* @throws \DomainException
*/
private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface
diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
index 8d30322745b8d..b7b079d208d97 100644
--- a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
+++ b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
@@ -24,7 +24,7 @@ class Sample extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
/**
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
* @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
- * @param null $connectionName
+ * @param string|null $connectionName
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -126,7 +126,7 @@ public function getSearchableData($productId, $storeId)
)->join(
['cpe' => $this->getTable('catalog_product_entity')],
sprintf(
- 'cpe.entity_id = m.product_id',
+ 'cpe.%s = m.product_id',
$this->metadataPool->getMetadata(ProductInterface::class)->getLinkField()
),
[]
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml
similarity index 100%
rename from app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml
rename to app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml
index 9ad20385519d1..34b9701f2dca5 100644
--- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml
@@ -37,7 +37,7 @@
-
+
diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php
deleted file mode 100644
index 725c06004f117..0000000000000
--- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php
+++ /dev/null
@@ -1,237 +0,0 @@
-objectManagerHelper = new ObjectManagerHelper($this);
-
- $this->request = $this->getMockForAbstractClass(RequestInterface::class);
- $this->response = $this->getMockBuilder(ResponseInterface::class)
- ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect'])
- ->onlyMethods(['sendResponse'])
- ->getMockForAbstractClass();
-
- $this->helperData = $this->createPartialMock(
- Data::class,
- ['getIsShareable']
- );
- $this->downloadHelper = $this->createPartialMock(
- Download::class,
- [
- 'setResource',
- 'getFilename',
- 'getContentType',
- 'getFileSize',
- 'getContentDisposition',
- 'output'
- ]
- );
- $this->product = $this->getMockBuilder(Product::class)
- ->addMethods(['_wakeup'])
- ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class);
- $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class);
- $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class);
- $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class);
- $this->objectManager = $this->createPartialMock(
- \Magento\Framework\ObjectManager\ObjectManager::class,
- ['create', 'get']
- );
- $this->linkSample = $this->objectManagerHelper->getObject(
- LinkSample::class,
- [
- 'objectManager' => $this->objectManager,
- 'request' => $this->request,
- 'response' => $this->response,
- 'messageManager' => $this->messageManager,
- 'redirect' => $this->redirect,
- 'salabilityChecker' => $this->salabilityCheckerMock,
- ]
- );
- }
-
- /**
- * Execute Download link's sample action with Url link.
- *
- * @return void
- */
- public function testExecuteLinkTypeUrl()
- {
- $linkMock = $this->getMockBuilder(Link::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id');
- $this->objectManager->expects($this->once())
- ->method('create')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf();
- $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $linkMock->expects($this->once())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_URL
- );
- $linkMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url');
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->linkSample->execute());
- }
-
- /**
- * Execute Download link's sample action with File link.
- *
- * @return void
- */
- public function testExecuteLinkTypeFile()
- {
- $linkMock = $this->getMockBuilder(Link::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath'])
- ->getMock();
- $fileMock = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->setMethods(['getFilePath', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id');
- $this->objectManager->expects($this->at(0))
- ->method('create')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf();
- $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $linkMock->expects($this->any())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_FILE
- );
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(File::class)
- ->willReturn($fileMock);
- $this->objectManager->expects($this->at(2))
- ->method('get')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('getBaseSamplePath')->willReturn('downloadable/files/link_samples');
- $this->objectManager->expects($this->at(3))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->linkSample->execute());
- }
-}
diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php
deleted file mode 100644
index 6dcd09a91dd2e..0000000000000
--- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php
+++ /dev/null
@@ -1,232 +0,0 @@
-objectManagerHelper = new ObjectManagerHelper($this);
-
- $this->request = $this->getMockForAbstractClass(RequestInterface::class);
- $this->response = $this->getMockBuilder(ResponseInterface::class)
- ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect'])
- ->onlyMethods(['sendResponse'])
- ->getMockForAbstractClass();
-
- $this->helperData = $this->createPartialMock(
- Data::class,
- ['getIsShareable']
- );
- $this->downloadHelper = $this->createPartialMock(
- Download::class,
- [
- 'setResource',
- 'getFilename',
- 'getContentType',
- 'getFileSize',
- 'getContentDisposition',
- 'output'
- ]
- );
- $this->product = $this->getMockBuilder(Product::class)
- ->addMethods(['_wakeup'])
- ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class);
- $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class);
- $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class);
- $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class);
- $this->objectManager = $this->createPartialMock(
- \Magento\Framework\ObjectManager\ObjectManager::class,
- ['create', 'get']
- );
- $this->sample = $this->objectManagerHelper->getObject(
- Sample::class,
- [
- 'objectManager' => $this->objectManager,
- 'request' => $this->request,
- 'response' => $this->response,
- 'messageManager' => $this->messageManager,
- 'redirect' => $this->redirect,
- 'salabilityChecker' => $this->salabilityCheckerMock,
- ]
- );
- }
-
- /**
- * Execute Download sample action with Sample Url.
- *
- * @return void
- */
- public function testExecuteSampleWithUrlType()
- {
- $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id');
- $this->objectManager->expects($this->once())
- ->method('create')
- ->with(\Magento\Downloadable\Model\Sample::class)
- ->willReturn($sampleMock);
- $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf();
- $sampleMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $sampleMock->expects($this->once())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_URL
- );
- $sampleMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url');
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->sample->execute());
- }
-
- /**
- * Execute Download sample action with Sample File.
- *
- * @return void
- */
- public function testExecuteSampleWithFileType()
- {
- $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath'])
- ->getMock();
- $fileHelperMock = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->setMethods(['getFilePath'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id');
- $this->objectManager->expects($this->at(0))
- ->method('create')
- ->with(\Magento\Downloadable\Model\Sample::class)
- ->willReturn($sampleMock);
- $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf();
- $sampleMock->expects($this->once())->method('getId')->willReturn('some_sample_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $sampleMock->expects($this->any())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_FILE
- );
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(File::class)
- ->willReturn($fileHelperMock);
- $fileHelperMock->expects($this->once())->method('getFilePath')->willReturn('file_path');
- $this->objectManager->expects($this->at(2))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->sample->execute());
- }
-}
diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
index 15dcea077c887..7e434166a15be 100644
--- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
+++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
@@ -23,12 +23,12 @@ class Data extends \Magento\Framework\Validator\AbstractValidator
/**
* @var array
*/
- protected $_attributesWhiteList = [];
+ protected $allowedAttributesList = [];
/**
* @var array
*/
- protected $_attributesBlackList = [];
+ protected $deniedAttributesList = [];
/**
* @var array
@@ -68,9 +68,9 @@ public function setAttributes(array $attributes)
* @param array $attributesCodes
* @return $this
*/
- public function setAttributesWhiteList(array $attributesCodes)
+ public function setAllowedAttributesList(array $attributesCodes)
{
- $this->_attributesWhiteList = $attributesCodes;
+ $this->allowedAttributesList = $attributesCodes;
return $this;
}
@@ -82,9 +82,9 @@ public function setAttributesWhiteList(array $attributesCodes)
* @param array $attributesCodes
* @return $this
*/
- public function setAttributesBlackList(array $attributesCodes)
+ public function setDeniedAttributesList(array $attributesCodes)
{
- $this->_attributesBlackList = $attributesCodes;
+ $this->deniedAttributesList = $attributesCodes;
return $this;
}
@@ -171,11 +171,11 @@ protected function _getAttributes($entity)
$attributesCodes[] = $attributeCode;
}
- $ignoreAttributes = $this->_attributesBlackList;
- if ($this->_attributesWhiteList) {
+ $ignoreAttributes = $this->deniedAttributesList;
+ if ($this->allowedAttributesList) {
$ignoreAttributes = array_merge(
$ignoreAttributes,
- array_diff($attributesCodes, $this->_attributesWhiteList)
+ array_diff($attributesCodes, $this->allowedAttributesList)
);
}
diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
index 774b968f1b697..a8ecbb8371ac9 100644
--- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
+++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
@@ -249,10 +249,10 @@ public function testIsValidAttributesFromCollection()
}
/**
- * @dataProvider whiteBlackListProvider
+ * @dataProvider allowDenyListProvider
* @param callable $callback
*/
- public function testIsValidBlackListWhiteListChecks($callback)
+ public function testIsValidExclusionInclusionListChecks($callback)
{
$attribute = $this->_getAttributeMock(
[
@@ -302,19 +302,19 @@ public function testIsValidBlackListWhiteListChecks($callback)
/**
* @return array
*/
- public function whiteBlackListProvider()
+ public function allowDenyListProvider()
{
- $whiteCallback = function ($validator) {
- $validator->setAttributesWhiteList(['attribute']);
+ $allowedCallbackList = function ($validator) {
+ $validator->setAllowedAttributesList(['attribute']);
};
- $blackCallback = function ($validator) {
- $validator->setAttributesBlackList(['attribute2']);
+ $deniedCallbackList = function ($validator) {
+ $validator->setDeniedAttributesList(['attribute2']);
};
- return ['white_list' => [$whiteCallback], 'black_list' => [$blackCallback]];
+ return ['allowed' => [$allowedCallbackList], 'denied' => [$deniedCallbackList]];
}
- public function testSetAttributesWhiteList()
+ public function testSetAttributesAllowedList()
{
$this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties');
@@ -328,12 +328,14 @@ public function testSetAttributesWhiteList()
)
->getMock();
$validator = new Data($attrDataFactory);
- $result = $validator->setAttributesWhiteList($attributes);
- $this->assertAttributeEquals($attributes, '_attributesWhiteList', $validator);
+ $result = $validator->setIncludedAttributesList($attributes);
+
+ // phpstan:ignore
+ $this->assertAttributeEquals($attributes, '_attributesAllowed', $validator);
$this->assertEquals($validator, $result);
}
- public function testSetAttributesBlackList()
+ public function testSetAttributesDeniedList()
{
$this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties');
@@ -347,8 +349,9 @@ public function testSetAttributesBlackList()
)
->getMock();
$validator = new Data($attrDataFactory);
- $result = $validator->setAttributesBlackList($attributes);
- $this->assertAttributeEquals($attributes, '_attributesBlackList', $validator);
+ $result = $validator->setDeniedAttributesList($attributes);
+ // phpstan:ignore
+ $this->assertAttributeEquals($attributes, '_attributesDenied', $validator);
$this->assertEquals($validator, $result);
}
diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
index 1f6e05c9e02fc..8576d8df0cc95 100644
--- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
+++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
@@ -19,7 +19,7 @@ class Converter implements ConverterInterface
*/
private const ES_DATA_TYPE_TEXT = 'text';
private const ES_DATA_TYPE_KEYWORD = 'keyword';
- private const ES_DATA_TYPE_FLOAT = 'float';
+ private const ES_DATA_TYPE_DOUBLE = 'double';
private const ES_DATA_TYPE_INT = 'integer';
private const ES_DATA_TYPE_DATE = 'date';
/**#@-*/
@@ -32,7 +32,7 @@ class Converter implements ConverterInterface
private $mapping = [
self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT,
self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD,
- self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT,
+ self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE,
self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT,
self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE,
];
diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php
index bd9a380230652..8d8787a5eff72 100644
--- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php
+++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php
@@ -276,7 +276,7 @@ public function addFieldsMapping(array $fields, $index, $entityType)
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php
index abdb064506641..245e4d494afe1 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php
@@ -190,7 +190,7 @@ private function convertToProductData(int $productId, array $indexData, int $sto
$attributeValues = [$productId => $attributeValues];
}
$attributeValues = $this->prepareAttributeValues($productId, $attribute, $attributeValues, $storeId);
- $productAttributes += $this->convertAttribute($attribute, $attributeValues);
+ $productAttributes += $this->convertAttribute($attribute, $attributeValues, $storeId);
}
return $productAttributes;
@@ -201,18 +201,19 @@ private function convertToProductData(int $productId, array $indexData, int $sto
*
* @param Attribute $attribute
* @param array $attributeValues
+ * @param int $storeId
* @return array
*/
- private function convertAttribute(Attribute $attribute, array $attributeValues): array
+ private function convertAttribute(Attribute $attribute, array $attributeValues, int $storeId): array
{
$productAttributes = [];
$retrievedValue = $this->retrieveFieldValue($attributeValues);
- if ($retrievedValue) {
+ if ($retrievedValue !== null) {
$productAttributes[$attribute->getAttributeCode()] = $retrievedValue;
if ($attribute->getIsSearchable()) {
- $attributeLabels = $this->getValuesLabels($attribute, $attributeValues);
+ $attributeLabels = $this->getValuesLabels($attribute, $attributeValues, $storeId);
$retrievedLabel = $this->retrieveFieldValue($attributeLabels);
if ($retrievedLabel) {
$productAttributes[$attribute->getAttributeCode() . '_value'] = $retrievedLabel;
@@ -299,20 +300,21 @@ private function isAttributeDate(Attribute $attribute): bool
*
* @param Attribute $attribute
* @param array $attributeValues
+ * @param int $storeId
* @return array
*/
- private function getValuesLabels(Attribute $attribute, array $attributeValues): array
+ private function getValuesLabels(Attribute $attribute, array $attributeValues, int $storeId): array
{
$attributeLabels = [];
- $options = $this->getAttributeOptions($attribute);
+ $options = $this->getAttributeOptions($attribute, $storeId);
if (empty($options)) {
return $attributeLabels;
}
- foreach ($attributeValues as $attributeValue) {
- if (isset($options[$attributeValue])) {
- $attributeLabels[] = $options[$attributeValue]->getLabel();
+ foreach ($options as $option) {
+ if (\in_array($option['value'], $attributeValues)) {
+ $attributeLabels[] = $option['label'];
}
}
@@ -323,20 +325,23 @@ private function getValuesLabels(Attribute $attribute, array $attributeValues):
* Retrieve options for attribute
*
* @param Attribute $attribute
+ * @param int $storeId
* @return array
*/
- private function getAttributeOptions(Attribute $attribute): array
+ private function getAttributeOptions(Attribute $attribute, int $storeId): array
{
- if (!isset($this->attributeOptionsCache[$attribute->getId()])) {
- $options = $attribute->getOptions() ?? [];
- $optionsByValue = [];
- foreach ($options as $option) {
- $optionsByValue[$option->getValue()] = $option;
- }
- $this->attributeOptionsCache[$attribute->getId()] = $optionsByValue;
+ if (!isset($this->attributeOptionsCache[$storeId][$attribute->getId()])) {
+ $attributeStoreId = $attribute->getStoreId();
+ /**
+ * Load array format of options.
+ * $attribute->getOptions() loads options into data objects which can be costly.
+ */
+ $options = $attribute->usesSource() ? $attribute->setStoreId($storeId)->getSource()->getAllOptions() : [];
+ $this->attributeOptionsCache[$storeId][$attribute->getId()] = $options;
+ $attribute->setStoreId($attributeStoreId);
}
- return $this->attributeOptionsCache[$attribute->getId()];
+ return $this->attributeOptionsCache[$storeId][$attribute->getId()];
}
/**
@@ -349,7 +354,7 @@ private function getAttributeOptions(Attribute $attribute): array
*/
private function retrieveFieldValue(array $values)
{
- $values = \array_filter(\array_unique($values));
+ $values = \array_unique($values);
return count($values) === 1 ? \array_shift($values) : \array_values($values);
}
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
index 88dab83698794..2067dcdc7fe9f 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
@@ -16,7 +16,7 @@ class Converter implements ConverterInterface
* Text flags for Elasticsearch field types
*/
private const ES_DATA_TYPE_STRING = 'string';
- private const ES_DATA_TYPE_FLOAT = 'float';
+ private const ES_DATA_TYPE_DOUBLE = 'double';
private const ES_DATA_TYPE_INT = 'integer';
private const ES_DATA_TYPE_DATE = 'date';
/**#@-*/
@@ -29,7 +29,7 @@ class Converter implements ConverterInterface
private $mapping = [
self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_STRING,
self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_STRING,
- self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT,
+ self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE,
self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT,
self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE,
];
diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php
index 1e106023ea00d..548a57e55f3e2 100644
--- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php
+++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php
@@ -35,7 +35,7 @@ public function __construct(Repository $algorithmRepository, EntityStorageFactor
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function build(
RequestBucketInterface $bucket,
@@ -46,9 +46,7 @@ public function build(
/** @var DynamicBucket $bucket */
$algorithm = $this->algorithmRepository->get($bucket->getMethod(), ['dataProvider' => $dataProvider]);
$data = $algorithm->getItems($bucket, $dimensions, $this->getEntityStorage($queryResult));
- $resultData = $this->prepareData($data);
-
- return $resultData;
+ return $this->prepareData($data);
}
/**
@@ -77,12 +75,9 @@ private function prepareData($data)
{
$resultData = [];
foreach ($data as $value) {
- $from = is_numeric($value['from']) ? $value['from'] : '*';
- $to = is_numeric($value['to']) ? $value['to'] : '*';
- unset($value['from'], $value['to']);
-
- $rangeName = "{$from}_{$to}";
- $resultData[$rangeName] = array_merge(['value' => $rangeName], $value);
+ $rangeName = "{$value['from']}_{$value['to']}";
+ $value['value'] = $rangeName;
+ $resultData[$rangeName] = $value;
}
return $resultData;
diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php
index 496a77e4c5ac3..7bc64b59ffe78 100644
--- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php
+++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php
@@ -235,11 +235,9 @@ public function prepareData($range, array $dbRanges)
{
$data = [];
if (!empty($dbRanges)) {
- $lastIndex = array_keys($dbRanges);
- $lastIndex = $lastIndex[count($lastIndex) - 1];
foreach ($dbRanges as $index => $count) {
- $fromPrice = $index == 1 ? '' : ($index - 1) * $range;
- $toPrice = $index == $lastIndex ? '' : $index * $range;
+ $fromPrice = $index == 1 ? 0 : ($index - 1) * $range;
+ $toPrice = $index * $range;
$data[] = [
'from' => $fromPrice,
'to' => $toPrice,
diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php
index 76a2f00f44fe2..ce79f433460d9 100644
--- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php
+++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php
@@ -68,7 +68,7 @@ public function buildFilter(RequestFilterInterface $filter)
$fieldName .= '.' . $suffix;
}
- if ($filter->getValue()) {
+ if ($filter->getValue() !== false) {
$operator = is_array($filter->getValue()) ? 'terms' : 'term';
$filterQuery []= [
$operator => [
diff --git a/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php
new file mode 100644
index 0000000000000..7cd72c322d647
--- /dev/null
+++ b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php
@@ -0,0 +1,66 @@
+moduleDataSetup = $moduleDataSetup;
+ $this->indexerRegistry = $indexerRegistry;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function apply(): PatchInterface
+ {
+ $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate();
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getDependencies(): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAliases(): array
+ {
+ return [];
+ }
+}
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php
index 49a894f1295c7..575a64dc43abd 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php
@@ -329,7 +329,7 @@ public function testAddFieldsMapping()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
@@ -400,7 +400,7 @@ public function testAddFieldsMappingFailure()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php
index 85be064d40b3a..2c87549da6075 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php
@@ -11,6 +11,7 @@
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
use Magento\Eav\Api\Data\AttributeOptionInterface;
+use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface;
use Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper;
use Magento\Elasticsearch\Model\Adapter\Document\Builder;
use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface;
@@ -199,11 +200,17 @@ public function testGetMapWithOptions()
*/
private function getAttribute(array $attributeData): MockObject
{
- $attributeMock = $this->createMock(Attribute::class);
- $attributeMock->method('getAttributeCode')->willReturn($attributeData['code']);
- $attributeMock->method('getBackendType')->willReturn($attributeData['backendType']);
- $attributeMock->method('getFrontendInput')->willReturn($attributeData['frontendInput']);
- $attributeMock->method('getIsSearchable')->willReturn($attributeData['is_searchable']);
+ $attributeMock = $this->createPartialMock(
+ Attribute::class,
+ [
+ 'getSource',
+ 'getOptions',
+ ]
+ );
+
+ $sourceMock = $this->createMock(SourceInterface::class);
+ $attributeMock->method('getSource')->willReturn($sourceMock);
+ $sourceMock->method('getAllOptions')->willReturn($attributeData['options'] ?? []);
$options = [];
foreach ($attributeData['options'] as $option) {
$optionMock = $this->getMockForAbstractClass(AttributeOptionInterface::class);
@@ -212,6 +219,8 @@ private function getAttribute(array $attributeData): MockObject
$options[] = $optionMock;
}
$attributeMock->method('getOptions')->willReturn($options);
+ unset($attributeData['options']);
+ $attributeMock->setData($attributeData);
return $attributeMock;
}
@@ -226,9 +235,9 @@ public static function mapProvider(): array
'text' => [
10,
[
- 'code' => 'description',
- 'backendType' => 'text',
- 'frontendInput' => 'text',
+ 'attribute_code' => 'description',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'text',
'is_searchable' => false,
'options' => [],
],
@@ -238,9 +247,9 @@ public static function mapProvider(): array
'datetime' => [
10,
[
- 'code' => 'created_at',
- 'backendType' => 'datetime',
- 'frontendInput' => 'date',
+ 'attribute_code' => 'created_at',
+ 'backend_type' => 'datetime',
+ 'frontend_input' => 'date',
'is_searchable' => false,
'options' => [],
],
@@ -251,9 +260,9 @@ public static function mapProvider(): array
'array single value' => [
10,
[
- 'code' => 'attribute_array',
- 'backendType' => 'text',
- 'frontendInput' => 'text',
+ 'attribute_code' => 'attribute_array',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'text',
'is_searchable' => false,
'options' => [],
],
@@ -263,9 +272,9 @@ public static function mapProvider(): array
'array multiple value' => [
10,
[
- 'code' => 'attribute_array',
- 'backendType' => 'text',
- 'frontendInput' => 'text',
+ 'attribute_code' => 'attribute_array',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'text',
'is_searchable' => false,
'options' => [],
],
@@ -275,9 +284,9 @@ public static function mapProvider(): array
'array multiple decimal value' => [
10,
[
- 'code' => 'decimal_array',
- 'backendType' => 'decimal',
- 'frontendInput' => 'text',
+ 'attribute_code' => 'decimal_array',
+ 'backend_type' => 'decimal',
+ 'frontend_input' => 'text',
'is_searchable' => false,
'options' => [],
],
@@ -287,9 +296,9 @@ public static function mapProvider(): array
'array excluded from merge' => [
10,
[
- 'code' => 'status',
- 'backendType' => 'int',
- 'frontendInput' => 'select',
+ 'attribute_code' => 'status',
+ 'backend_type' => 'int',
+ 'frontend_input' => 'select',
'is_searchable' => false,
'options' => [
['value' => '1', 'label' => 'Enabled'],
@@ -302,9 +311,9 @@ public static function mapProvider(): array
'select without options' => [
10,
[
- 'code' => 'color',
- 'backendType' => 'text',
- 'frontendInput' => 'select',
+ 'attribute_code' => 'color',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'select',
'is_searchable' => false,
'options' => [],
],
@@ -314,9 +323,9 @@ public static function mapProvider(): array
'unsearchable select with options' => [
10,
[
- 'code' => 'color',
- 'backendType' => 'text',
- 'frontendInput' => 'select',
+ 'attribute_code' => 'color',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'select',
'is_searchable' => false,
'options' => [
['value' => '44', 'label' => 'red'],
@@ -329,9 +338,9 @@ public static function mapProvider(): array
'searchable select with options' => [
10,
[
- 'code' => 'color',
- 'backendType' => 'text',
- 'frontendInput' => 'select',
+ 'attribute_code' => 'color',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'select',
'is_searchable' => true,
'options' => [
['value' => '44', 'label' => 'red'],
@@ -344,9 +353,9 @@ public static function mapProvider(): array
'composite select with options' => [
10,
[
- 'code' => 'color',
- 'backendType' => 'text',
- 'frontendInput' => 'select',
+ 'attribute_code' => 'color',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'select',
'is_searchable' => true,
'options' => [
['value' => '44', 'label' => 'red'],
@@ -359,9 +368,9 @@ public static function mapProvider(): array
'multiselect without options' => [
10,
[
- 'code' => 'multicolor',
- 'backendType' => 'text',
- 'frontendInput' => 'multiselect',
+ 'attribute_code' => 'multicolor',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'multiselect',
'is_searchable' => false,
'options' => [],
],
@@ -371,9 +380,9 @@ public static function mapProvider(): array
'unsearchable multiselect with options' => [
10,
[
- 'code' => 'multicolor',
- 'backendType' => 'text',
- 'frontendInput' => 'multiselect',
+ 'attribute_code' => 'multicolor',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'multiselect',
'is_searchable' => false,
'options' => [
['value' => '44', 'label' => 'red'],
@@ -386,9 +395,9 @@ public static function mapProvider(): array
'searchable multiselect with options' => [
10,
[
- 'code' => 'multicolor',
- 'backendType' => 'text',
- 'frontendInput' => 'multiselect',
+ 'attribute_code' => 'multicolor',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'multiselect',
'is_searchable' => true,
'options' => [
['value' => '44', 'label' => 'red'],
@@ -401,9 +410,9 @@ public static function mapProvider(): array
'composite multiselect with options' => [
10,
[
- 'code' => 'multicolor',
- 'backendType' => 'text',
- 'frontendInput' => 'multiselect',
+ 'attribute_code' => 'multicolor',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'multiselect',
'is_searchable' => true,
'options' => [
['value' => '44', 'label' => 'red'],
@@ -417,9 +426,9 @@ public static function mapProvider(): array
'excluded attribute' => [
10,
[
- 'code' => 'price',
- 'backendType' => 'int',
- 'frontendInput' => 'int',
+ 'attribute_code' => 'price',
+ 'backend_type' => 'int',
+ 'frontend_input' => 'int',
'is_searchable' => false,
'options' => []
],
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
index 87f072836544e..a9bcd1a20a1b2 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
@@ -246,7 +246,7 @@ function ($type) use ($complexType) {
if ($type === 'string') {
return 'string';
} elseif ($type === 'float') {
- return 'float';
+ return 'double';
} elseif ($type === 'integer') {
return 'integer';
} else {
@@ -281,7 +281,7 @@ public function attributeProvider()
'index' => 'no_index'
],
'price_1_1' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true
]
]
@@ -300,7 +300,7 @@ public function attributeProvider()
'index' => 'no_index'
],
'price_1_1' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true
]
],
@@ -319,7 +319,7 @@ public function attributeProvider()
'index' => 'no_index'
],
'price_1_1' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true
]
]
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php
index 75b79bc43e805..718adf255254f 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php
@@ -56,7 +56,7 @@ public function convertProvider()
{
return [
['string', 'string'],
- ['float', 'float'],
+ ['float', 'double'],
['integer', 'integer'],
];
}
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php
index c5b9089acd91c..0595b667f4ee8 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php
@@ -390,13 +390,13 @@ public function testPrepareData()
{
$expectedResult = [
[
- 'from' => '',
+ 'from' => 0,
'to' => 10,
'count' => 1,
],
[
'from' => 10,
- 'to' => '',
+ 'to' => 20,
'count' => 1,
],
];
diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml
index 633889e70591b..633e67dfe698e 100644
--- a/app/code/Magento/Elasticsearch/etc/di.xml
+++ b/app/code/Magento/Elasticsearch/etc/di.xml
@@ -537,7 +537,7 @@
-
+
- Elasticsearch 2
diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php
index 2c1c283c5b24d..0571b075aff28 100644
--- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php
+++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php
@@ -281,7 +281,7 @@ public function addFieldsMapping(array $fields, $index, $entityType)
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php
index aa0b49400c517..2a7fa2ce8114a 100644
--- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php
@@ -439,7 +439,7 @@ public function testAddFieldsMapping()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
@@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php
index feacca8d62804..4b318f987abfe 100644
--- a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php
+++ b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php
@@ -289,7 +289,7 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php
index 593bbd7792f46..091387f844d55 100644
--- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php
@@ -438,7 +438,7 @@ public function testAddFieldsMapping()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
@@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl
index 62795f07239a6..3629bb424f207 100644
--- a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl
@@ -472,7 +472,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -983,7 +983,7 @@
- The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions
+ The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions
@@ -1005,7 +1005,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -4867,4 +4867,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl
index 17a6f74cc09b8..2f3feecb58084 100644
--- a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl
@@ -471,7 +471,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment commitment more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -983,7 +983,7 @@
- The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions
+ The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions
@@ -1005,7 +1005,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl
index 54bb57d490c76..439d032a61fd0 100644
--- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl
@@ -497,7 +497,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -724,7 +724,7 @@
- The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
+ The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl
index d8dc0fdfed4ab..a449bf41dbd68 100644
--- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl
@@ -497,7 +497,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -724,7 +724,7 @@
- The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
+ The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
new file mode 100644
index 0000000000000..2ce51c8bbf19d
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
@@ -0,0 +1,94 @@
+cartRepository = $cartRepository;
+ $this->giftMessageHelper = $giftMessageHelper;
+ }
+
+ /**
+ * Return information about Gift message of cart
+ *
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ *
+ * @return array|Value|mixed
+ *
+ * @throws GraphQlInputException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new GraphQlInputException(__('"model" value must be specified'));
+ }
+
+ $cart = $value['model'];
+
+ if (!$this->giftMessageHelper->isMessagesAllowed('order', $cart)) {
+ return null;
+ }
+
+ try {
+ $giftCartMessage = $this->cartRepository->get($cart->getId());
+ } catch (LocalizedException $e) {
+ throw new GraphQlInputException(__('Can\'t load cart.'));
+ }
+
+ if (!isset($giftCartMessage)) {
+ return null;
+ }
+
+ return [
+ 'to' => $giftCartMessage->getRecipient() ?? '',
+ 'from' => $giftCartMessage->getSender() ?? '',
+ 'message'=> $giftCartMessage->getMessage() ?? ''
+ ];
+ }
+}
diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php
new file mode 100644
index 0000000000000..a9a8e682612cc
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php
@@ -0,0 +1,97 @@
+itemRepository = $itemRepository;
+ $this->giftMessageHelper = $giftMessageHelper;
+ }
+
+ /**
+ * Return information about Gift message for item cart
+ *
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ *
+ * @return array|Value|mixed
+ * @throws GraphQlInputException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new GraphQlInputException(__('"model" value must be specified'));
+ }
+
+ $quoteItem = $value['model'];
+
+ if (!$this->giftMessageHelper->isMessagesAllowed('items', $quoteItem)) {
+ return null;
+ }
+
+ if (!$this->giftMessageHelper->isMessagesAllowed('item', $quoteItem)) {
+ return null;
+ }
+
+ try {
+ $giftItemMessage = $this->itemRepository->get($quoteItem->getQuoteId(), $quoteItem->getItemId());
+ } catch (LocalizedException $e) {
+ throw new GraphQlInputException(__('Can\'t load cart item'));
+ }
+
+ if (!isset($giftItemMessage)) {
+ return null;
+ }
+
+ return [
+ 'to' => $giftItemMessage->getRecipient() ?? '',
+ 'from' => $giftItemMessage->getSender() ?? '',
+ 'message'=> $giftItemMessage->getMessage() ?? ''
+ ];
+ }
+}
diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php
new file mode 100644
index 0000000000000..aae0e3709d87f
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php
@@ -0,0 +1,78 @@
+orderRepository = $orderRepository;
+ }
+
+ /**
+ * Return information about gift message for order
+ *
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ *
+ * @return array|Value|mixed
+ * @throws GraphQlInputException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['id'])) {
+ throw new GraphQlInputException(__('"id" value should be specified'));
+ }
+
+ try {
+ $orderGiftMessage = $this->orderRepository->get($value['id']);
+ } catch (LocalizedException $e) {
+ throw new GraphQlInputException(__('Can\'t load gift message for order'));
+ }
+
+ if (!isset($orderGiftMessage)) {
+ return null;
+ }
+
+ return [
+ 'to' => $orderGiftMessage->getRecipient() ?? '',
+ 'from' => $orderGiftMessage->getSender() ?? '',
+ 'message'=> $orderGiftMessage->getMessage() ?? ''
+ ];
+ }
+}
diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md
new file mode 100644
index 0000000000000..d73a058e0db9c
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/README.md
@@ -0,0 +1,3 @@
+# GiftMessageGraphQl
+
+**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items.
diff --git a/app/code/Magento/GiftMessageGraphQl/composer.json b/app/code/Magento/GiftMessageGraphQl/composer.json
new file mode 100644
index 0000000000000..48088f2a48a32
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "magento/module-gift-message-graph-ql",
+ "description": "N/A",
+ "type": "magento2-module",
+ "require": {
+ "php": "~7.3.0||~7.4.0",
+ "magento/framework": "*",
+ "magento/module-gift-message": "*"
+ },
+ "suggest": {
+ "magento/module-graph-ql": "*"
+ },
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\GiftMessageGraphQl\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml
new file mode 100644
index 0000000000000..bce5b7063e6b9
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ - sales/gift_options/allow_order
+ - sales/gift_options/allow_items
+
+
+
+
diff --git a/app/code/Magento/GiftMessageGraphQl/etc/module.xml b/app/code/Magento/GiftMessageGraphQl/etc/module.xml
new file mode 100644
index 0000000000000..5eaaae0b0b988
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
new file mode 100644
index 0000000000000..ad18054abca13
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
@@ -0,0 +1,47 @@
+# Copyright © Magento, Inc. All rights reserved.
+# See COPYING.txt for license details.
+
+type StoreConfig {
+ allow_order : String @doc(description: "The value of the Allow Gift Messages on Order Level option")
+ allow_items : String @doc(description: "The value of the Allow Gift Messages for Order Items option")
+}
+
+type Cart {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\GiftMessage") @doc(description: "The entered gift message for the cart")
+}
+
+type SimpleCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
+}
+
+type ConfigurableCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
+}
+
+type BundleCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
+}
+
+type GiftMessage @doc(description: "Contains the text of a gift message, its sender, and recipient") {
+ to: String! @doc(description: "Recipient name")
+ from: String! @doc(description: "Sender name")
+ message: String! @doc(description: "Gift message text")
+}
+
+input CartItemUpdateInput {
+ gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item")
+}
+
+input GiftMessageInput @doc(description: "Contains the text of a gift message, its sender, and recipient") {
+ to: String! @doc(description: "Recipient name")
+ from: String! @doc(description: "Sender name")
+ message: String! @doc(description: "Gift message text")
+}
+
+type SalesItemInterface {
+ gift_message: GiftMessage @doc(description: "The entered gift message for the order item")
+}
+
+type CustomerOrder {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order")
+}
diff --git a/app/code/Magento/GiftMessageGraphQl/registration.php b/app/code/Magento/GiftMessageGraphQl/registration.php
new file mode 100644
index 0000000000000..bb260c23b0177
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/registration.php
@@ -0,0 +1,13 @@
+_initRequestParams();
if ($this->_isNewCode()) {
- $this->_saveCode();
+ if (!$this->_isEmptyCode()) {
+ $this->_saveCode();
+ }
} else {
$this->_loadCode();
if ($this->_isEmptyCode()) {
@@ -185,6 +191,8 @@ protected function _deleteCode()
}
/**
+ * Check data availability
+ *
* @return bool
*/
private function isDataAvailable()
@@ -194,6 +202,8 @@ private function isDataAvailable()
}
/**
+ * Get request data
+ *
* @return mixed
*/
private function getRequestData()
diff --git a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php
index 8a5c247369657..c6d02957c4be9 100644
--- a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php
+++ b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php
@@ -127,6 +127,39 @@ public function testCreatingCodeIfRequestIsValid()
$this->_modelObserver->execute($this->_eventObserverMock);
}
+ /**
+ * Test that code is not saving when request is empty
+ *
+ * @return void
+ */
+ public function testCreatingCodeIfRequestIsEmpty(): void
+ {
+ $this->_helperMock->expects(
+ $this->once()
+ )->method(
+ 'isGoogleExperimentActive'
+ )->with(
+ $this->_storeId
+ )->willReturn(
+ true
+ );
+
+ $this->_requestMock->expects(
+ $this->exactly(3)
+ )->method(
+ 'getParam'
+ )->with(
+ 'google_experiment'
+ )->willReturn(
+ ['code_id' => '', 'experiment_script' => '']
+ );
+
+ $this->_codeMock->expects($this->never())->method('addData');
+ $this->_codeMock->expects($this->never())->method('save');
+
+ $this->_modelObserver->execute($this->_eventObserverMock);
+ }
+
/**
* @param array $params
* @dataProvider dataProviderWrongRequestForCreating
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php
new file mode 100644
index 0000000000000..ba2e995d4f704
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return "1";
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php
new file mode 100644
index 0000000000000..68760de543daa
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedHeaders();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php
new file mode 100644
index 0000000000000..233839b9deb74
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedMethods();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php
new file mode 100644
index 0000000000000..21850f18db1f2
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedOrigins();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php
new file mode 100644
index 0000000000000..e30209ae25e68
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return (string) $this->corsConfiguration->getMaxAge();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php
new file mode 100644
index 0000000000000..dd5a0b426e22d
--- /dev/null
+++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php
@@ -0,0 +1,96 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * Are CORS headers enabled
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED);
+ }
+
+ /**
+ * Get allowed origins or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedOrigins(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS);
+ }
+
+ /**
+ * Get allowed headers or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedHeaders(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS);
+ }
+
+ /**
+ * Get allowed methods or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedMethods(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS);
+ }
+
+ /**
+ * Get max age header value
+ *
+ * @return int
+ */
+ public function getMaxAge(): int
+ {
+ return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE);
+ }
+
+ /**
+ * Are credentials allowed
+ *
+ * @return bool
+ */
+ public function isCredentialsAllowed(): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS);
+ }
+}
diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php
new file mode 100644
index 0000000000000..b40b64f48e51f
--- /dev/null
+++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php
@@ -0,0 +1,56 @@
+
+
+
+
+
+ GraphQL
+ service
+ Magento_Integration::config_oauth
+
+ CORS Settings
+
+ CORS Headers Enabled
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Allowed origins
+ The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins.
+
+ 1
+
+
+
+
+ Allowed methods
+ The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST)
+
+ 1
+
+
+
+
+
+
+ Max Age
+ validate-digits
+ The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.
+
+ 1
+
+
+
+
+ Credentials Allowed
+ Magento\Config\Model\Config\Source\Yesno
+ The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include.
+
+ 1
+
+
+
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml
new file mode 100644
index 0000000000000..39caacbec42d2
--- /dev/null
+++ b/app/code/Magento/GraphQl/etc/config.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ 0
+
+
+
+ 86400
+ 0
+
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml
index b356f33c4f4bf..fca6c425e2507 100644
--- a/app/code/Magento/GraphQl/etc/di.xml
+++ b/app/code/Magento/GraphQl/etc/di.xml
@@ -98,4 +98,31 @@
300
+
+
+
+
+ Access-Control-Max-Age
+
+
+
+
+ Access-Control-Allow-Credentials
+
+
+
+
+ Access-Control-Allow-Headers
+
+
+
+
+ Access-Control-Allow-Methods
+
+
+
+
+ Access-Control-Allow-Origin
+
+
diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml
index 2bcd44e9ae410..23d49124d1a02 100644
--- a/app/code/Magento/GraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/GraphQl/etc/graphql/di.xml
@@ -15,10 +15,6 @@
- Magento\Webapi\Model\Authorization\TokenUserContext
- 10
- -
-
- Magento\Webapi\Model\Authorization\OauthUserContext
- - 40
-
-
- Magento\Webapi\Model\Authorization\GuestUserContext
- 100
@@ -34,4 +30,15 @@
+
+
+
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls
index fccde015c3388..0212d32db0f2f 100644
--- a/app/code/Magento/GraphQl/etc/schema.graphqls
+++ b/app/code/Magento/GraphQl/etc/schema.graphqls
@@ -79,6 +79,12 @@ input FilterMatchTypeInput @doc(description: "Defines a filter that performs a f
match: String @doc(description: "One or more words to filter on")
}
+input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") {
+ in: [String] @doc(description: "Filters items that are exactly the same as entries specified in an array of strings.")
+ eq: String @doc(description: "Filters items that are exactly the same as the specified string.")
+ match: String @doc(description: "Defines a filter that performs a fuzzy search using the specified string.")
+}
+
type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") {
page_size: Int @doc(description: "Specifies the maximum number of items to return")
current_page: Int @doc(description: "Specifies which page of results to return")
diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php
index e1599dc772c2c..c5f0316feb126 100644
--- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php
+++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php
@@ -20,6 +20,7 @@
/**
* Calculate minimal and maximal prices for Grouped products
+ *
* Use calculated price for relation products
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
@@ -85,10 +86,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
- * @param array $dimensions
- * @param \Traversable $entityIds
- * @throws \Exception
+ * @inheritDoc
*/
public function executeByDimensions(array $dimensions, \Traversable $entityIds)
{
@@ -105,9 +103,8 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds)
'maxPriceField' => 'max_price',
'tierPriceField' => 'tier_price',
]);
- $query = $this->prepareGroupedProductPriceDataSelect($dimensions, iterator_to_array($entityIds))
- ->insertFromSelect($temporaryPriceTable->getTableName());
- $this->getConnection()->query($query);
+ $select = $this->prepareGroupedProductPriceDataSelect($dimensions, iterator_to_array($entityIds));
+ $this->tableMaintainer->insertFromSelect($select, $temporaryPriceTable->getTableName(), []);
}
/**
@@ -186,13 +183,13 @@ private function getMainTable($dimensions)
if ($this->fullReindexAction) {
return $this->tableMaintainer->getMainReplicaTable($dimensions);
}
- return $this->tableMaintainer->getMainTable($dimensions);
+ return $this->tableMaintainer->getMainTableByDimensions($dimensions);
}
/**
* Get connection
*
- * return \Magento\Framework\DB\Adapter\AdapterInterface
+ * @return \Magento\Framework\DB\Adapter\AdapterInterface
* @throws \DomainException
*/
private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml
index aaa9cf5b2f925..e966ccf82648f 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml
@@ -21,7 +21,6 @@
-
diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php
index 92cfb375fea41..29fa2bffabb3b 100644
--- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php
+++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php
@@ -10,7 +10,7 @@
use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface;
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
class GroupedProductLinksTypeResolver implements TypeResolverInterface
{
@@ -20,14 +20,14 @@ class GroupedProductLinksTypeResolver implements TypeResolverInterface
private $linkTypes = ['associated'];
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
- public function resolveType(array $data) : string
+ public function resolveType(array $data): string
{
if (isset($data['link_type'])) {
$linkType = $data['link_type'];
if (in_array($linkType, $this->linkTypes)) {
- return 'GroupedProductLinks';
+ return 'ProductLinks';
}
}
return '';
diff --git a/app/code/Magento/GroupedProductGraphQl/etc/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/di.xml
index 35b63370baf2f..717bc14826f70 100644
--- a/app/code/Magento/GroupedProductGraphQl/etc/di.xml
+++ b/app/code/Magento/GroupedProductGraphQl/etc/di.xml
@@ -13,4 +13,11 @@
+
+
+
+ - associated
+
+
+
diff --git a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php
index 09b17371ae4e8..f5b62df9aea2c 100644
--- a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php
+++ b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php
@@ -54,6 +54,19 @@ public function destruct()
{
if (is_object($this->_fileHandler)) {
$this->_fileHandler->close();
+ $this->resolveDestination();
+ }
+ }
+
+ /**
+ * Remove temporary destination
+ *
+ * @return void
+ */
+ private function resolveDestination(): void
+ {
+ // only temporary file located directly in var folder
+ if (strpos($this->_destination, '/') === false) {
$this->_directoryHandle->delete($this->_destination);
}
}
diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php
index 5bd956c1bc322..9bf5b945c8fbd 100644
--- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php
+++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php
@@ -15,6 +15,7 @@
/**
* Import entity abstract model
*
+ * phpcs:disable Magento2.Classes.AbstractApi
* @api
*
* @SuppressWarnings(PHPMD.TooManyFields)
@@ -335,6 +336,8 @@ public function __construct(
}
/**
+ * Returns Error aggregator
+ *
* @return ProcessingErrorAggregatorInterface
*/
public function getErrorAggregator()
@@ -413,7 +416,7 @@ protected function _saveValidatedBunches()
$source->rewind();
$this->_dataSourceModel->cleanBunches();
- $masterAttributeCode = $this->getMasterAttributeCode();
+ $mainAttributeCode = $this->getMasterAttributeCode();
while ($source->valid() || count($bunchRows) || isset($entityGroup)) {
if ($startNewBunch || !$source->valid()) {
@@ -453,7 +456,7 @@ protected function _saveValidatedBunches()
continue;
}
- if (isset($rowData[$masterAttributeCode]) && trim($rowData[$masterAttributeCode])) {
+ if (isset($rowData[$mainAttributeCode]) && trim($rowData[$mainAttributeCode])) {
/* Add entity group that passed validation to bunch */
if (isset($entityGroup)) {
foreach ($entityGroup as $key => $value) {
@@ -590,6 +593,7 @@ public function getBehavior(array $rowData = null)
* Get default import behavior
*
* @return string
+ * phpcs:disable Magento2.Functions.StaticFunction
*/
public static function getDefaultBehavior()
{
@@ -652,7 +656,9 @@ public function isAttributeParticular($attributeCode)
}
/**
- * @return string the master attribute code to use in an import
+ * Returns the master attribute code to use in an import
+ *
+ * @return string
*/
public function getMasterAttributeCode()
{
diff --git a/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php
index 7032c9258cae6..d86b15134283b 100644
--- a/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php
+++ b/app/code/Magento/Indexer/Model/ResourceModel/AbstractResource.php
@@ -11,6 +11,7 @@
/**
* Abstract resource model. Can be used as base for indexer resources
*
+ * phpcs:disable Magento2.Classes.AbstractApi
* @api
* @since 100.0.2
*/
@@ -120,8 +121,7 @@ public function insertFromTable($sourceTable, $destTable, $readToIndex = true)
}
/**
- * Insert data from select statement of read adapter to
- * destination table related with index adapter
+ * Insert data from select statement of read adapter to destination table related with index adapter
*
* @param Select $select
* @param string $destTable
@@ -141,6 +141,9 @@ public function insertFromSelect($select, $destTable, array $columns, $readToInd
if ($from === $to) {
$query = $select->insertFromSelect($destTable, $columns);
+ if ($to->getTransactionLevel() === 0) {
+ $to->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED;');
+ }
$to->query($query);
} else {
$stmt = $from->query($select);
diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerReindexActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerReindexActionGroup.xml
new file mode 100644
index 0000000000000..5556707a4d494
--- /dev/null
+++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerReindexActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Run reindex by CLI with specified indexers (space separated).
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php
new file mode 100644
index 0000000000000..1565d455cc43f
--- /dev/null
+++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php
@@ -0,0 +1,120 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted category and remove relation betwen category and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $category = $observer->getEvent()->getData('category');
+ $contentAssetLinks = [];
+
+ if ($category instanceof CatalogCategory) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $category->getEntityId(),
+ ]
+ );
+ $content = implode(PHP_EOL, $this->getContent->execute($contentIdentity));
+ $assets = $this->extractAssetsFromContent->execute($content);
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php
new file mode 100644
index 0000000000000..421bb5a33fa1d
--- /dev/null
+++ b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php
@@ -0,0 +1,120 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted product and remove relation betwen product and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $product = $observer->getEvent()->getData('product');
+ $contentAssetLinks = [];
+
+ if ($product instanceof CatalogProduct) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $product->getEntityId(),
+ ]
+ );
+ $productContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity));
+ $assets = $this->extractAssetsFromContent->execute($productContent);
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCatalog/etc/di.xml b/app/code/Magento/MediaContentCatalog/etc/di.xml
index a2d300a2bb208..6b0ee83b30788 100644
--- a/app/code/Magento/MediaContentCatalog/etc/di.xml
+++ b/app/code/Magento/MediaContentCatalog/etc/di.xml
@@ -14,6 +14,22 @@
+
+
+
+ - description
+ - short_description
+
+
+
+
+
+
+ - image
+ - description
+
+
+
diff --git a/app/code/Magento/MediaContentCatalog/etc/events.xml b/app/code/Magento/MediaContentCatalog/etc/events.xml
index f68d66eb3cc40..8ec7a30b961ba 100644
--- a/app/code/Magento/MediaContentCatalog/etc/events.xml
+++ b/app/code/Magento/MediaContentCatalog/etc/events.xml
@@ -9,6 +9,12 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php
new file mode 100644
index 0000000000000..582f0a9ec6701
--- /dev/null
+++ b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php
@@ -0,0 +1,119 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted category and remove relation betwen category and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $block = $observer->getEvent()->getData('object');
+ $contentAssetLinks = [];
+
+ if ($block instanceof CmsBlock) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $block->getId(),
+ ]
+ );
+ $assets = $this->extractAssetsFromContent->execute((string) $block->getData($field));
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCms/Observer/PageDelete.php b/app/code/Magento/MediaContentCms/Observer/PageDelete.php
new file mode 100644
index 0000000000000..96d2bf89873bd
--- /dev/null
+++ b/app/code/Magento/MediaContentCms/Observer/PageDelete.php
@@ -0,0 +1,120 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted category and remove relation betwen category and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $page = $observer->getEvent()->getData('object');
+ $contentAssetLinks = [];
+
+ if ($page instanceof CmsPage) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $page->getId(),
+ ]
+ );
+
+ $assets = $this->extractAssetsFromContent->execute((string) $page->getData($field));
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCms/etc/di.xml b/app/code/Magento/MediaContentCms/etc/di.xml
index f980936465faf..6f196889540af 100644
--- a/app/code/Magento/MediaContentCms/etc/di.xml
+++ b/app/code/Magento/MediaContentCms/etc/di.xml
@@ -20,4 +20,18 @@
+
+
+
+ - content
+
+
+
+
+
+
+ - content
+
+
+
diff --git a/app/code/Magento/MediaContentCms/etc/events.xml b/app/code/Magento/MediaContentCms/etc/events.xml
index 7e9abe3bf19c4..94f963f40be15 100644
--- a/app/code/Magento/MediaContentCms/etc/events.xml
+++ b/app/code/Magento/MediaContentCms/etc/events.xml
@@ -6,8 +6,14 @@
*/
-->
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGallery/Model/Asset.php b/app/code/Magento/MediaGallery/Model/Asset.php
index 78b9477a70b08..7a4e51709dc0a 100644
--- a/app/code/Magento/MediaGallery/Model/Asset.php
+++ b/app/code/Magento/MediaGallery/Model/Asset.php
@@ -32,11 +32,21 @@ class Asset implements AssetInterface
*/
private $title;
+ /**
+ * @var string|null
+ */
+ private $description;
+
/**
* @var string|null
*/
private $source;
+ /**
+ * @var string|null
+ */
+ private $hash;
+
/**
* @var string
*/
@@ -80,7 +90,9 @@ class Asset implements AssetInterface
* @param int $size
* @param int|null $id
* @param string|null $title
+ * @param string|null $description
* @param string|null $source
+ * @param string|null $hash
* @param string|null $createdAt
* @param string|null $updatedAt
* @param AssetExtensionInterface|null $extensionAttributes
@@ -93,7 +105,9 @@ public function __construct(
int $size,
?int $id = null,
?string $title = null,
+ ?string $description = null,
?string $source = null,
+ ?string $hash = null,
?string $createdAt = null,
?string $updatedAt = null,
?AssetExtensionInterface $extensionAttributes = null
@@ -105,7 +119,9 @@ public function __construct(
$this->size = $size;
$this->id = $id;
$this->title = $title;
+ $this->description = $description;
$this->source = $source;
+ $this->hash = $hash;
$this->createdAt = $createdAt;
$this->updatedAt = $updatedAt;
$this->extensionAttributes = $extensionAttributes;
@@ -135,6 +151,14 @@ public function getTitle(): ?string
return $this->title;
}
+ /**
+ * @inheritdoc
+ */
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
/**
* @inheritdoc
*/
@@ -143,6 +167,14 @@ public function getSource(): ?string
return $this->source;
}
+ /**
+ * @inheritdoc
+ */
+ public function getHash(): ?string
+ {
+ return $this->hash;
+ }
+
/**
* @inheritdoc
*/
diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php
index b2f900233e46a..71e2cb70663f3 100644
--- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php
+++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php
@@ -94,7 +94,9 @@ public function execute(int $mediaAssetId): AssetInterface
'id' => $mediaAssetData['id'],
'path' => $mediaAssetData['path'],
'title' => $mediaAssetData['title'],
+ 'description' => $mediaAssetData['description'],
'source' => $mediaAssetData['source'],
+ 'hash' => $mediaAssetData['hash'],
'contentType' => $mediaAssetData['content_type'],
'width' => $mediaAssetData['width'],
'height' => $mediaAssetData['height'],
diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php
index d9faad62b2cd1..02512a12f9d07 100644
--- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php
+++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php
@@ -86,7 +86,9 @@ public function execute(string $path): AssetInterface
'id' => $data['id'],
'path' => $data['path'],
'title' => $data['title'],
+ 'description' => $data['description'],
'source' => $data['source'],
+ 'hash' => $data['hash'],
'contentType' => $data['content_type'],
'width' => $data['width'],
'height' => $data['height'],
diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php
index 4d87c1aa95285..f33c22a18b4b8 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php
@@ -10,7 +10,7 @@
use Magento\Cms\Model\Wysiwyg\Images\Storage;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface;
-use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface;
+use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;
use Psr\Log\LoggerInterface;
/**
@@ -29,23 +29,23 @@ class CreateByPaths implements CreateDirectoriesByPathsInterface
private $storage;
/**
- * @var IsPathBlacklistedInterface
+ * @var IsPathExcludedInterface
*/
- private $isPathBlacklisted;
+ private $isPathExcluded;
/**
* @param LoggerInterface $logger
* @param Storage $storage
- * @param IsPathBlacklistedInterface $isPathBlacklisted
+ * @param IsPathExcludedInterface $isPathExcluded
*/
public function __construct(
LoggerInterface $logger,
Storage $storage,
- IsPathBlacklistedInterface $isPathBlacklisted
+ IsPathExcludedInterface $isPathExcluded
) {
$this->logger = $logger;
$this->storage = $storage;
- $this->isPathBlacklisted = $isPathBlacklisted;
+ $this->isPathExcluded = $isPathExcluded;
}
/**
@@ -55,7 +55,7 @@ public function execute(array $paths): void
{
$failedPaths = [];
foreach ($paths as $path) {
- if ($this->isPathBlacklisted->execute($path)) {
+ if ($this->isPathExcluded->execute($path)) {
$failedPaths[] = $path;
continue;
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php
index d46fb854fff22..2e45000c07225 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php
@@ -10,7 +10,7 @@
use Magento\Cms\Model\Wysiwyg\Images\Storage;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface;
-use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface;
+use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;
use Psr\Log\LoggerInterface;
/**
@@ -29,23 +29,23 @@ class DeleteByPaths implements DeleteDirectoriesByPathsInterface
private $storage;
/**
- * @var IsPathBlacklistedInterface
+ * @var IsPathExcludedInterface
*/
- private $isPathBlacklisted;
+ private $isPathExcluded;
/**
* @param LoggerInterface $logger
* @param Storage $storage
- * @param IsPathBlacklistedInterface $isPathBlacklisted
+ * @param IsPathExcludedInterface $isPathExcluded
*/
public function __construct(
LoggerInterface $logger,
Storage $storage,
- IsPathBlacklistedInterface $isPathBlacklisted
+ IsPathExcludedInterface $isPathExcluded
) {
$this->logger = $logger;
$this->storage = $storage;
- $this->isPathBlacklisted = $isPathBlacklisted;
+ $this->isPathExcluded = $isPathExcluded;
}
/**
@@ -55,7 +55,7 @@ public function execute(array $paths): void
{
$failedPaths = [];
foreach ($paths as $path) {
- if ($this->isPathBlacklisted->execute($path)) {
+ if ($this->isPathExcluded->execute($path)) {
$failedPaths[] = $path;
continue;
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php
index 91f16d246f636..3d9911c805efb 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php
@@ -15,9 +15,9 @@
class Converter implements ConverterInterface
{
/**
- * Blacklist tag name
+ * Excluded list tag name
*/
- private const BLACKLIST_TAG_NAME = 'blacklist';
+ private const EXCLUDED_LIST_TAG_NAME = 'exclude';
/**
* Patterns tag name
@@ -43,12 +43,12 @@ public function convert($source): array
throw new \InvalidArgumentException('The source should be instance of DOMDocument');
}
- foreach ($source->getElementsByTagName(self::BLACKLIST_TAG_NAME) as $blacklist) {
- $result[self::BLACKLIST_TAG_NAME] = [];
- foreach ($blacklist->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) {
- $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] = [];
+ foreach ($source->getElementsByTagName(self::EXCLUDED_LIST_TAG_NAME) as $excludedList) {
+ $result[self::EXCLUDED_LIST_TAG_NAME] = [];
+ foreach ($excludedList->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) {
+ $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] = [];
foreach ($patterns->getElementsByTagName(self::PATTERN_TAG_NAME) as $pattern) {
- $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME]
+ $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME]
[$pattern->attributes->getNamedItem('name')->nodeValue] = $pattern->nodeValue;
}
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php
similarity index 68%
rename from app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php
rename to app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php
index 8fdd4f70d5060..29ed5fbf04ecd 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php
@@ -8,14 +8,14 @@
namespace Magento\MediaGallery\Model\Directory;
use Magento\Framework\Config\DataInterface;
-use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface;
+use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface;
/**
* Media gallery directory config
*/
-class BlacklistPatternsConfig implements BlacklistPatternsConfigInterface
+class ExcludedPatternsConfig implements ExcludedPatternsConfigInterface
{
- private const XML_PATH_BLACKLIST_PATTERNS = 'blacklist/patterns';
+ private const XML_PATH_EXCLUDED_PATTERNS = 'exclude/patterns';
/**
* @var DataInterface
@@ -37,6 +37,6 @@ public function __construct(DataInterface $data)
*/
public function get() : array
{
- return $this->data->get(self::XML_PATH_BLACKLIST_PATTERNS);
+ return $this->data->get(self::XML_PATH_EXCLUDED_PATTERNS);
}
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php
similarity index 61%
rename from app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php
rename to app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php
index 0191b357aaefa..8fb0e03b76548 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php
@@ -7,23 +7,23 @@
namespace Magento\MediaGallery\Model\Directory;
-use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface;
-use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface;
+use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;
+use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface;
/**
- * Check if the path is blacklisted for media gallery. Directory path may be blacklisted if it's reserved by the system
+ * Check if the path is excluded for media gallery. Directory path may be blacklisted if it's reserved by the system
*/
-class IsBlacklisted implements IsPathBlacklistedInterface
+class IsExcluded implements IsPathExcludedInterface
{
/**
- * @var BlacklistPatternsConfigInterface
+ * @var ExcludedPatternsConfigInterface
*/
private $config;
/**
- * @param BlacklistPatternsConfigInterface $config
+ * @param ExcludedPatternsConfigInterface $config
*/
- public function __construct(BlacklistPatternsConfigInterface $config)
+ public function __construct(ExcludedPatternsConfigInterface $config)
{
$this->config = $config;
}
diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php
index 53185939b2283..f73162b775683 100644
--- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php
+++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php
@@ -65,7 +65,9 @@ public function execute(array $ids): array
'id' => $assetData['id'],
'path' => $assetData['path'],
'title' => $assetData['title'],
+ 'description' => $assetData['description'],
'source' => $assetData['source'],
+ 'hash' => $assetData['hash'],
'contentType' => $assetData['content_type'],
'width' => $assetData['width'],
'height' => $assetData['height'],
diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php
index 5593083d9673a..b25d2e22aabd4 100644
--- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php
+++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php
@@ -66,7 +66,9 @@ public function execute(array $paths): array
'id' => $assetData['id'],
'path' => $assetData['path'],
'title' => $assetData['title'],
+ 'description' => $assetData['description'],
'source' => $assetData['source'],
+ 'hash' => $assetData['hash'],
'contentType' => $assetData['content_type'],
'width' => $assetData['width'],
'height' => $assetData['height'],
diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php
index ec08addf93462..801279aa7fd7d 100644
--- a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php
+++ b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php
@@ -60,7 +60,9 @@ public function execute(array $assets): void
'id' => $asset->getId(),
'path' => $asset->getPath(),
'title' => $asset->getTitle(),
+ 'description' => $asset->getDescription(),
'source' => $asset->getSource(),
+ 'hash' => $asset->getHash(),
'content_type' => $asset->getContentType(),
'width' => $asset->getWidth(),
'height' => $asset->getHeight(),
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php
index 09ce7ffe8ff20..5f99163db8f12 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php
@@ -28,7 +28,9 @@ class GetByIdExceptionDuringMediaAssetInitializationTest extends TestCase
'id' => 45,
'path' => 'img.jpg',
'title' => 'Img',
+ 'description' => 'Img Description',
'source' => 'Adobe Stock',
+ 'hash' => 'hash',
'content_type' => 'image/jpeg',
'width' => 420,
'height' => 240,
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php
index 89efae07360b4..3b47b0036224b 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php
@@ -29,7 +29,9 @@ class GetByIdExceptionOnGetDataTest extends TestCase
'id' => 45,
'path' => 'img.jpg',
'title' => 'Img',
+ 'description' => 'Img Description',
'source' => 'Adobe Stock',
+ 'hash' => 'hash',
'content_type' => 'image/jpeg',
'width' => 420,
'height' => 240,
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php
index 8b805d0256e37..2c24899746473 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php
@@ -29,7 +29,9 @@ class GetByIdSuccessfulTest extends TestCase
'id' => 45,
'path' => 'img.jpg',
'title' => 'Img',
+ 'description' => 'Img Description',
'source' => 'Adobe Stock',
+ 'hash' => 'hash',
'content_type' => 'image/jpeg',
'width' => 420,
'height' => 240,
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php
similarity index 70%
rename from app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php
rename to app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php
index c96fd2ee54512..cc57b043954d7 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php
@@ -8,45 +8,45 @@
namespace Magento\MediaGallery\Test\Unit\Model\Directory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
-use Magento\MediaGallery\Model\Directory\IsBlacklisted;
-use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface;
+use Magento\MediaGallery\Model\Directory\IsExcluded;
+use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
- * Test for IsBlacklisted
+ * Test for IsExcluded
*/
-class IsBlacklistedTest extends TestCase
+class IsExcludedTest extends TestCase
{
/**
- * @var IsBlacklisted
+ * @var IsExcluded
*/
private $object;
/**
- * @var BlacklistPatternsConfigInterface|MockObject
+ * @var ExcludedPatternsConfigInterface|MockObject
*/
- private $config;
+ private $configMock;
/**
* Initialize basic test class mocks
*/
protected function setUp(): void
{
- $this->config = $this->getMockBuilder(BlacklistPatternsConfigInterface::class)
+ $this->configMock = $this->getMockBuilder(ExcludedPatternsConfigInterface::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
- $this->config->expects($this->at(0))->method('get')->willReturn([
+ $this->configMock->expects($this->at(0))->method('get')->willReturn([
'tmp' => '/pub\/media\/tmp/',
'captcha' => '/pub\/media\/captcha/'
]);
- $this->object = (new ObjectManager($this))->getObject(IsBlacklisted::class, [
- 'config' => $this->config
+ $this->object = (new ObjectManager($this))->getObject(IsExcluded::class, [
+ 'config' => $this->configMock
]);
}
/**
- * Test if the directory path is blacklisted
+ * Test if the directory path is excluded
*
* @param string $path
* @param bool $isExcluded
diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml
index 31a764ef00c4d..1001737daa8a7 100644
--- a/app/code/Magento/MediaGallery/etc/db_schema.xml
+++ b/app/code/Magento/MediaGallery/etc/db_schema.xml
@@ -10,7 +10,9 @@
+
+
diff --git a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json
index 8f5098caa9753..b32dfbf082175 100644
--- a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json
+++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json
@@ -4,7 +4,9 @@
"id": true,
"path": true,
"title": true,
+ "description": true,
"source": true,
+ "hash": true,
"content_type": true,
"width": true,
"height": true,
diff --git a/app/code/Magento/MediaGallery/etc/di.xml b/app/code/Magento/MediaGallery/etc/di.xml
index a85c26e275226..bedb78758786b 100644
--- a/app/code/Magento/MediaGallery/etc/di.xml
+++ b/app/code/Magento/MediaGallery/etc/di.xml
@@ -21,7 +21,7 @@
-
+
@@ -40,7 +40,7 @@
Magento\MediaGallery\Model\Directory\Config\Converter
Magento\MediaGallery\Model\Directory\Config\SchemaLocator
- - name
+ - name
@@ -50,11 +50,10 @@
Media_Gallery_Patterns_CacheId
-
+
Magento\MediaGallery\Model\Directory\Config\Data
-
-
+
diff --git a/app/code/Magento/MediaGallery/etc/directory.xml b/app/code/Magento/MediaGallery/etc/directory.xml
index 92f50b2dd0a30..42094aff72640 100644
--- a/app/code/Magento/MediaGallery/etc/directory.xml
+++ b/app/code/Magento/MediaGallery/etc/directory.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
/^captcha/
/^customer/
@@ -17,5 +17,5 @@
/^tmp/
/^\./
-
+
diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php
index 5df420a274933..a747cb963baab 100644
--- a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php
+++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php
@@ -38,6 +38,13 @@ public function getPath(): string;
*/
public function getTitle(): ?string;
+ /**
+ * Get description
+ *
+ * @return string|null
+ */
+ public function getDescription(): ?string;
+
/**
* Get the name of the channel/stock/integration file was retrieved from. null if not identified.
*
@@ -45,6 +52,13 @@ public function getTitle(): ?string;
*/
public function getSource(): ?string;
+ /**
+ * Get file hash
+ *
+ * @return string|null
+ */
+ public function getHash(): ?string;
+
/**
* Get content type
*
diff --git a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php
similarity index 71%
rename from app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php
rename to app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php
index cbd23ec3fbde7..1e41debb1b1c5 100644
--- a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php
+++ b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php
@@ -8,12 +8,12 @@
namespace Magento\MediaGalleryApi\Api;
/**
- * Check if the path is blacklisted for media gallery.
+ * Check if the path is excluded for media gallery.
*
- * Directory path may be blacklisted if it's reserved by the system.
+ * Directory path may be excluded if it's reserved by the system.
* @api
*/
-interface IsPathBlacklistedInterface
+interface IsPathExcludedInterface
{
/**
* Check if the path is excluded from displaying and processing in the media gallery
diff --git a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php
similarity index 75%
rename from app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php
rename to app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php
index b4710f32e0c46..dd82f87780a49 100644
--- a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php
+++ b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php
@@ -7,9 +7,9 @@
namespace Magento\MediaGalleryApi\Model;
/**
- * Returns list of blacklist regexp patterns
+ * Returns list of excluded regexp patterns
*/
-interface BlacklistPatternsConfigInterface
+interface ExcludedPatternsConfigInterface
{
/**
* Get regexp patterns
diff --git a/app/code/Magento/MediaGalleryApi/etc/directory.xsd b/app/code/Magento/MediaGalleryApi/etc/directory.xsd
index 2ad76c8fcc9f2..2fb4fed028469 100644
--- a/app/code/Magento/MediaGalleryApi/etc/directory.xsd
+++ b/app/code/Magento/MediaGalleryApi/etc/directory.xsd
@@ -11,14 +11,14 @@
-
+
-
+
- Blacklist used for excluding directories from media gallery rendering and operations
+ List used for excluding directories from media gallery rendering and operations
diff --git a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml
index eaced3f642f70..f1ec76a877368 100644
--- a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml
+++ b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml
@@ -6,9 +6,9 @@
*/
-->
-
+
/^catalog\/product/
-
+
diff --git a/app/code/Magento/MessageQueue/Test/Mftf/ActionGroup/CliConsumerStartActionGroup.xml b/app/code/Magento/MessageQueue/Test/Mftf/ActionGroup/CliConsumerStartActionGroup.xml
new file mode 100644
index 0000000000000..204691f1ad5e5
--- /dev/null
+++ b/app/code/Magento/MessageQueue/Test/Mftf/ActionGroup/CliConsumerStartActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Starts message queue for specific consumer by CLI.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MessageQueue/Test/Mftf/Data/QueueConsumerData.xml b/app/code/Magento/MessageQueue/Test/Mftf/Data/QueueConsumerData.xml
new file mode 100644
index 0000000000000..ef989808eea60
--- /dev/null
+++ b/app/code/Magento/MessageQueue/Test/Mftf/Data/QueueConsumerData.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ exportProcessor
+ 100
+
+
+ product_action_attribute.update
+ 100
+
+
+ codegeneratorProcessor
+ 100
+
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml
index 02187658a8781..815d406c68bfa 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml
@@ -19,12 +19,6 @@
-
-
-
-
-
-
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml
index a49a37e475409..8205ab962b9fe 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml
@@ -22,8 +22,6 @@
-
-
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml
index 80407a219a841..2e5c0acc32053 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml
@@ -27,7 +27,6 @@
-
@@ -43,7 +42,6 @@
-
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml
index caf0ce3a51bae..7bb26525b173f 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml
@@ -22,8 +22,6 @@
-
-
diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
index 6391219e23c7e..2519dd3a6fea8 100644
--- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
+++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
@@ -116,6 +116,7 @@ public function setMessagesScope($scope)
* @param string $email
* @param int $websiteId
* @return array
+ * @throws LocalizedException
*/
public function loadBySubscriberEmail(string $email, int $websiteId): array
{
diff --git a/app/code/Magento/Newsletter/i18n/en_US.csv b/app/code/Magento/Newsletter/i18n/en_US.csv
index f390f6792635d..f8706967117fe 100644
--- a/app/code/Magento/Newsletter/i18n/en_US.csv
+++ b/app/code/Magento/Newsletter/i18n/en_US.csv
@@ -153,3 +153,6 @@ Store,Store
"Newsletter Subscriptions","Newsletter Subscriptions"
"We have updated your subscription.","We have updated your subscription."
"Are you sure you want to delete the selected subscriber(s)?","Are you sure you want to delete the selected subscriber(s)?"
+"Cannot create a newsletter subscription.","Cannot create a newsletter subscription."
+"Enter a valid email address.","Enter a valid email address."
+"Guests can not subscribe to the newsletter. You must create an account to subscribe.","Guests can not subscribe to the newsletter. You must create an account to subscribe."
diff --git a/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html b/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html
index d56163c10fdf3..996dff0c973e9 100644
--- a/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html
+++ b/app/code/Magento/Newsletter/view/frontend/email/subscr_success.html
@@ -13,6 +13,6 @@
{{template config_path="design/email/header_template"}}
-{{trans "You have been successfully subscribed to our newsletter."}}
+{{trans "You have been successfully subscribed to our newsletter."}}
{{template config_path="design/email/footer_template"}}
diff --git a/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html b/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html
index d39b5d8a8b8e9..1f222f85abac7 100644
--- a/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html
+++ b/app/code/Magento/Newsletter/view/frontend/email/unsub_success.html
@@ -13,6 +13,6 @@
{{template config_path="design/email/header_template"}}
-{{trans "You have been unsubscribed from the newsletter."}}
+{{trans "You have been unsubscribed from the newsletter."}}
{{template config_path="design/email/footer_template"}}
diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml
index 429482e5795bf..768c97ef316f7 100644
--- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml
+++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml
@@ -40,3 +40,12 @@
+
diff --git a/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php b/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php
new file mode 100644
index 0000000000000..a4b3bc43d0a8b
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/Model/Resolver/SubscribeEmailToNewsletter.php
@@ -0,0 +1,142 @@
+customerRepository = $customerRepository;
+ $this->enumLookup = $enumLookup;
+ $this->logger = $logger;
+ $this->subscriptionManager = $subscriptionManager;
+ $this->validator = $validator;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ $email = trim($args['email']);
+
+ if (empty($email)) {
+ throw new GraphQlInputException(
+ __('You must specify an email address to subscribe to a newsletter.')
+ );
+ }
+
+ $currentUserId = (int)$context->getUserId();
+ $storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
+ $websiteId = (int)$context->getExtensionAttributes()->getStore()->getWebsiteId();
+
+ $this->validator->execute($email, $currentUserId, $websiteId);
+
+ try {
+ $subscriber = $this->isCustomerSubscription($email, $currentUserId)
+ ? $this->subscriptionManager->subscribeCustomer($currentUserId, $storeId)
+ : $this->subscriptionManager->subscribe($email, $storeId);
+
+ $status = $this->enumLookup->getEnumValueFromField(
+ 'SubscriptionStatusesEnum',
+ (string)$subscriber->getSubscriberStatus()
+ );
+ } catch (LocalizedException $e) {
+ $this->logger->error($e->getMessage());
+
+ throw new GraphQlInputException(
+ __('Cannot create a newsletter subscription.')
+ );
+ }
+
+ return [
+ 'status' => $status
+ ];
+ }
+
+ /**
+ * Returns true if a provided email equals to a current customer one
+ *
+ * @param string $email
+ * @param int $currentUserId
+ * @return bool
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ */
+ private function isCustomerSubscription(string $email, int $currentUserId): bool
+ {
+ if ($currentUserId > 0) {
+ $customer = $this->customerRepository->getById($currentUserId);
+
+ if ($customer->getEmail() == $email) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php b/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php
new file mode 100644
index 0000000000000..8b8cac0c58cf2
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/Model/SubscribeEmailToNewsletter/Validation.php
@@ -0,0 +1,195 @@
+customerAccountManagement = $customerAccountManagement;
+ $this->customerRepository = $customerRepository;
+ $this->emailValidator = $emailValidator;
+ $this->logger = $logger;
+ $this->scopeConfig = $scopeConfig;
+ $this->subscriberResource = $subscriberResource;
+ }
+
+ /**
+ * Validate the next cases:
+ * - email format
+ * - email address isn't being used by a different account
+ * - if a guest user can be subscribed to a newsletter
+ * - verify if email is already subscribed
+ *
+ * @param string $email
+ * @param int $currentUserId
+ * @param int $websiteId
+ * @throws GraphQlAlreadyExistsException
+ * @throws GraphQlInputException
+ */
+ public function execute(string $email = '', int $currentUserId = 0, int $websiteId = 1): void
+ {
+ $this->validateEmailFormat($email);
+
+ if ($currentUserId > 0) {
+ $this->validateEmailAvailable($email, $currentUserId, $websiteId);
+ } else {
+ $this->validateGuestSubscription();
+ }
+
+ $this->validateAlreadySubscribed($email, $websiteId);
+ }
+
+ /**
+ * Validate the format of the email address
+ *
+ * @param string $email
+ * @throws GraphQlInputException
+ */
+ private function validateEmailFormat(string $email): void
+ {
+ if (!$this->emailValidator->isValid($email)) {
+ throw new GraphQlInputException(__('Enter a valid email address.'));
+ }
+ }
+
+ /**
+ * Validate that the email address isn't being used by a different account.
+ *
+ * @param string $email
+ * @param int $currentUserId
+ * @param int $websiteId
+ * @throws GraphQlInputException
+ */
+ private function validateEmailAvailable(string $email, int $currentUserId, int $websiteId): void
+ {
+ try {
+ $customer = $this->customerRepository->getById($currentUserId);
+ $customerEmail = $customer->getEmail();
+ } catch (LocalizedException $e) {
+ $customerEmail = '';
+ }
+
+ try {
+ $emailAvailable = $this->customerAccountManagement->isEmailAvailable($email, $websiteId);
+ } catch (LocalizedException $e) {
+ $emailAvailable = false;
+ }
+
+ if (!$emailAvailable && $customerEmail != $email) {
+ $this->logger->error(
+ __('This email address is already assigned to another user.')
+ );
+
+ throw new GraphQlInputException(
+ __('Cannot create a newsletter subscription.')
+ );
+ }
+ }
+
+ /**
+ * Validate if a guest user can be subscribed to a newsletter.
+ *
+ * @throws GraphQlInputException
+ */
+ private function validateGuestSubscription(): void
+ {
+ if (!$this->scopeConfig->getValue(
+ Subscriber::XML_PATH_ALLOW_GUEST_SUBSCRIBE_FLAG,
+ ScopeInterface::SCOPE_STORE
+ )) {
+ throw new GraphQlInputException(
+ __('Guests can not subscribe to the newsletter. You must create an account to subscribe.')
+ );
+ }
+ }
+
+ /**
+ * Verify if email is already subscribed
+ *
+ * @param string $email
+ * @param int $websiteId
+ * @throws GraphQlAlreadyExistsException
+ */
+ private function validateAlreadySubscribed(string $email, int $websiteId): void
+ {
+ try {
+ $subscriberData = $this->subscriberResource->loadBySubscriberEmail($email, $websiteId);
+ } catch (LocalizedException $e) {
+ $subscriberData = [];
+ }
+
+ if (isset($subscriberData['subscriber_status'])
+ && (int)$subscriberData['subscriber_status'] === Subscriber::STATUS_SUBSCRIBED) {
+ throw new GraphQlAlreadyExistsException(
+ __('This email address is already subscribed.')
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/NewsletterGraphQl/README.md b/app/code/Magento/NewsletterGraphQl/README.md
new file mode 100644
index 0000000000000..c65d44fbcfeba
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/README.md
@@ -0,0 +1 @@
+The Magento_NewsletterGraphQl module allows a shopper to subscribe to a newsletter using GraphQL.
diff --git a/app/code/Magento/NewsletterGraphQl/composer.json b/app/code/Magento/NewsletterGraphQl/composer.json
new file mode 100644
index 0000000000000..92352a8a9adfe
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/composer.json
@@ -0,0 +1,30 @@
+{
+ "name": "magento/module-newsletter-graph-ql",
+ "description": "Provides GraphQl functionality for the newsletter subscriptions.",
+ "config": {
+ "sort-packages": true
+ },
+ "type": "magento2-module",
+ "require": {
+ "php": "~7.3.0||~7.4.0",
+ "magento/framework": "*",
+ "magento/module-customer": "*",
+ "magento/module-newsletter": "*",
+ "magento/module-store": "*"
+ },
+ "suggest": {
+ "magento/module-graph-ql": "*"
+ },
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\NewsletterGraphQl\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml
new file mode 100644
index 0000000000000..302a562ec4700
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ -
+
- 1
+ - 2
+ - 3
+ - 4
+
+
+
+
+
diff --git a/app/code/Magento/NewsletterGraphQl/etc/module.xml b/app/code/Magento/NewsletterGraphQl/etc/module.xml
new file mode 100644
index 0000000000000..8bda85d80c830
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/etc/module.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls
new file mode 100644
index 0000000000000..d96756e12caea
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls
@@ -0,0 +1,17 @@
+# Copyright © Magento, Inc. All rights reserved.
+# See COPYING.txt for license details.
+
+type Mutation {
+ subscribeEmailToNewsletter(email: String!): SubscribeEmailToNewsletterOutput @doc(description:"Subscribes the specified email to a newsletter") @resolver(class: "Magento\\NewsletterGraphQl\\Model\\Resolver\\SubscribeEmailToNewsletter")
+}
+
+type SubscribeEmailToNewsletterOutput {
+ status: SubscriptionStatusesEnum @doc(description: "Returns the status of the subscription request")
+}
+
+enum SubscriptionStatusesEnum {
+ NOT_ACTIVE
+ SUBSCRIBED
+ UNSUBSCRIBED
+ UNCONFIRMED
+}
diff --git a/app/code/Magento/NewsletterGraphQl/registration.php b/app/code/Magento/NewsletterGraphQl/registration.php
new file mode 100644
index 0000000000000..82d5512f4afb5
--- /dev/null
+++ b/app/code/Magento/NewsletterGraphQl/registration.php
@@ -0,0 +1,9 @@
+_request->getPostValue();
+ $params = [];
+ foreach ($this->_request->getPostValue() as $name => $value) {
+ if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) {
+ $value = utf8_encode($value);
+ }
+ $params[$name] = $value;
+ }
+ return $params;
}
}
diff --git a/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php
new file mode 100644
index 0000000000000..1cd1230a14634
--- /dev/null
+++ b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php
@@ -0,0 +1,102 @@
+context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class);
+ $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class);
+ $this->context->method('getRequest')
+ ->willReturn($this->request);
+ $this->url = $this->createMock(\Magento\Framework\UrlInterface::class);
+ $this->model = new Redirect(
+ $this->context,
+ $this->url
+ );
+ }
+
+ /**
+ * @param array $postData
+ * @param array $expected
+ * @dataProvider getPostParamsDataProvider
+ */
+ public function testGetPostParams(array $postData, array $expected): void
+ {
+ $this->request->method('getPostValue')
+ ->willReturn($postData);
+ $this->assertEquals($expected, $this->model->getPostParams());
+ }
+
+ /**
+ * @return array
+ */
+ public function getPostParamsDataProvider(): array
+ {
+ return [
+ [
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => 'Ãtienne',
+ 'BILLTOFIRSTNAME' => 'Ãillin',
+ ],
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => 'Ãtienne',
+ 'BILLTOFIRSTNAME' => 'Ãillin',
+ ]
+ ],
+ [
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => mb_convert_encoding('Ãtienne', 'ISO-8859-1'),
+ 'BILLTOFIRSTNAME' => mb_convert_encoding('Ãillin', 'ISO-8859-1'),
+ ],
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => 'Ãtienne',
+ 'BILLTOFIRSTNAME' => 'Ãillin',
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Paypal/Model/Payflowpro.php b/app/code/Magento/Paypal/Model/Payflowpro.php
index 778cd0c728de3..f8d17c5a5fdfe 100644
--- a/app/code/Magento/Paypal/Model/Payflowpro.php
+++ b/app/code/Magento/Paypal/Model/Payflowpro.php
@@ -8,6 +8,8 @@
use Magento\Framework\DataObject;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Exception\State\InvalidTransitionException;
+use Magento\Payment\Gateway\Command\CommandException;
use Magento\Payment\Helper\Formatter;
use Magento\Payment\Model\InfoInterface;
use Magento\Payment\Model\Method\ConfigInterface;
@@ -85,6 +87,8 @@ class Payflowpro extends \Magento\Payment\Model\Method\Cc implements GatewayInte
const RESPONSE_CODE_VOID_ERROR = 108;
+ private const RESPONSE_CODE_AUTHORIZATION_EXPIRED = 10601;
+
const PNREF = 'pnref';
/**#@-*/
@@ -376,7 +380,7 @@ public function getConfigPaymentAction()
* @param float $amount
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\State\InvalidTransitionException
+ * @throws InvalidTransitionException
*/
public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount)
{
@@ -410,7 +414,7 @@ protected function _getCaptureAmount($amount)
* @param float $amount
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\State\InvalidTransitionException
+ * @throws InvalidTransitionException
*/
public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount)
{
@@ -448,7 +452,7 @@ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount)
* @param InfoInterface|Payment|Object $payment
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\State\InvalidTransitionException
+ * @throws InvalidTransitionException
*/
public function void(\Magento\Payment\Model\InfoInterface $payment)
{
@@ -491,14 +495,23 @@ public function canVoid()
*
* @param InfoInterface|Object $payment
* @return $this
+ * @throws CommandException
*/
public function cancel(\Magento\Payment\Model\InfoInterface $payment)
{
if (!$payment->getOrder()->getInvoiceCollection()->count()) {
- return $this->void($payment);
+ try {
+ $this->void($payment);
+ } catch (CommandException $e) {
+ // Ignore error about expiration of authorization transaction.
+ if (strpos($e->getMessage(), (string)self::RESPONSE_CODE_AUTHORIZATION_EXPIRED) === false) {
+ throw $e;
+ }
+ }
+
}
- return false;
+ return $this;
}
/**
@@ -508,7 +521,7 @@ public function cancel(\Magento\Payment\Model\InfoInterface $payment)
* @param float $amount
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\State\InvalidTransitionException
+ * @throws InvalidTransitionException
*/
public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount)
{
@@ -558,7 +571,7 @@ public function fetchTransactionInfo(InfoInterface $payment, $transactionId)
* @return bool
* phpcs:disable Magento2.Functions.StaticFunction
*/
- protected static function _isTransactionUnderReview($status)
+ protected function _isTransactionUnderReview($status)
{
if (in_array($status, [self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_DECLINED_BY_MERCHANT])) {
return false;
@@ -650,21 +663,22 @@ public function buildBasicRequest()
*
* @param DataObject $response
* @return void
- * @throws \Magento\Payment\Gateway\Command\CommandException
- * @throws \Magento\Framework\Exception\State\InvalidTransitionException
+ * @throws CommandException
+ * @throws InvalidTransitionException
*/
public function processErrors(DataObject $response)
{
- if ($response->getResultCode() == self::RESPONSE_CODE_VOID_ERROR) {
- throw new \Magento\Framework\Exception\State\InvalidTransitionException(
+ $resultCode = (int)$response->getResultCode();
+ if ($resultCode === self::RESPONSE_CODE_VOID_ERROR) {
+ throw new InvalidTransitionException(
__("The verification transaction can't be voided. ")
);
- } elseif ($response->getResultCode() != self::RESPONSE_CODE_APPROVED &&
- $response->getResultCode() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER
- ) {
- throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg()));
- } elseif ($response->getOrigresult() == self::RESPONSE_CODE_DECLINED_BY_FILTER) {
- throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg()));
+ }
+ if (!in_array($resultCode, [self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_FRAUDSERVICE_FILTER])) {
+ throw new CommandException(__($response->getRespmsg()));
+ }
+ if ((int)$response->getOrigresult() === self::RESPONSE_CODE_DECLINED_BY_FILTER) {
+ throw new CommandException(__($response->getRespmsg()));
}
}
diff --git a/app/code/Magento/Persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php b/app/code/Magento/Persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php
new file mode 100644
index 0000000000000..1c2b180c5dd1a
--- /dev/null
+++ b/app/code/Magento/Persistent/Model/Checkout/GuestShippingInformationManagementPlugin.php
@@ -0,0 +1,100 @@
+persistenceDataHelper = $persistenceDataHelper;
+ $this->persistenceSessionHelper = $persistenceSessionHelper;
+ $this->customerSession = $customerSession;
+ $this->quoteManager = $quoteManager;
+ }
+
+ /**
+ * Convert shopping cart from persistent cart to guest cart after shipping information saved
+ *
+ * Check if shopping cart is persistent and customer is not logged in, and only one payment method is available,
+ * then converts the shopping cart guest cart.
+ * If only one payment is available, it's preselected by default and the payment information is automatically saved.
+ *
+ * @param GuestShippingInformationManagement $subject
+ * @param PaymentDetailsInterface $result
+ * @return PaymentDetailsInterface
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterSaveAddressInformation(
+ GuestShippingInformationManagement $subject,
+ PaymentDetailsInterface $result
+ ): PaymentDetailsInterface {
+ if ($this->persistenceSessionHelper->isPersistent()
+ && !$this->customerSession->isLoggedIn()
+ && $this->persistenceDataHelper->isShoppingCartPersist()
+ && $this->quoteManager->isPersistent()
+ && count($result->getPaymentMethods()) === 1
+ ) {
+ $this->customerSession->setCustomerId(null);
+ $this->customerSession->setCustomerGroupId(null);
+ $this->quoteManager->convertCustomerCartToGuest();
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php
index ebddfe5a43600..b6504d528fbe4 100644
--- a/app/code/Magento/Persistent/Model/QuoteManager.php
+++ b/app/code/Magento/Persistent/Model/QuoteManager.php
@@ -5,8 +5,17 @@
*/
namespace Magento\Persistent\Model;
+use Magento\Customer\Api\Data\GroupInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Persistent\Helper\Data;
+use Magento\Quote\Api\CartRepositoryInterface;
+use Magento\Quote\Api\Data\CartExtensionFactory;
+use Magento\Quote\Api\Data\CartInterface;
+use Magento\Quote\Model\Quote;
+use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor;
+
/**
- * Class QuoteManager
+ * Quote manager model
*
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
@@ -29,7 +38,7 @@ class QuoteManager
/**
* Persistent data
*
- * @var \Magento\Persistent\Helper\Data
+ * @var Data
*/
protected $persistentData;
@@ -41,26 +50,44 @@ class QuoteManager
protected $_setQuotePersistent = true;
/**
- * @var \Magento\Quote\Api\CartRepositoryInterface
+ * @var CartRepositoryInterface
*/
protected $quoteRepository;
+ /**
+ * @var ShippingAssignmentProcessor
+ */
+ private $shippingAssignmentProcessor;
+
+ /**
+ * @var CartExtensionFactory
+ */
+ private $cartExtensionFactory;
+
/**
* @param \Magento\Persistent\Helper\Session $persistentSession
- * @param \Magento\Persistent\Helper\Data $persistentData
+ * @param Data $persistentData
* @param \Magento\Checkout\Model\Session $checkoutSession
- * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository
+ * @param CartRepositoryInterface $quoteRepository
+ * @param CartExtensionFactory|null $cartExtensionFactory
+ * @param ShippingAssignmentProcessor|null $shippingAssignmentProcessor
*/
public function __construct(
\Magento\Persistent\Helper\Session $persistentSession,
- \Magento\Persistent\Helper\Data $persistentData,
+ Data $persistentData,
\Magento\Checkout\Model\Session $checkoutSession,
- \Magento\Quote\Api\CartRepositoryInterface $quoteRepository
+ CartRepositoryInterface $quoteRepository,
+ ?CartExtensionFactory $cartExtensionFactory = null,
+ ?ShippingAssignmentProcessor $shippingAssignmentProcessor = null
) {
$this->persistentSession = $persistentSession;
$this->persistentData = $persistentData;
$this->checkoutSession = $checkoutSession;
$this->quoteRepository = $quoteRepository;
+ $this->cartExtensionFactory = $cartExtensionFactory
+ ?? ObjectManager::getInstance()->get(CartExtensionFactory::class);
+ $this->shippingAssignmentProcessor = $shippingAssignmentProcessor
+ ?? ObjectManager::getInstance()->get(ShippingAssignmentProcessor::class);
}
/**
@@ -71,7 +98,7 @@ public function __construct(
*/
public function setGuest($checkQuote = false)
{
- /** @var $quote \Magento\Quote\Model\Quote */
+ /** @var $quote Quote */
$quote = $this->checkoutSession->getQuote();
if ($quote && $quote->getId()) {
if ($checkQuote && !$this->persistentData->isShoppingCartPersist() && !$quote->getIsPersistent()) {
@@ -87,17 +114,19 @@ public function setGuest($checkQuote = false)
->setCustomerEmail(null)
->setCustomerFirstname(null)
->setCustomerLastname(null)
- ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID)
+ ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID)
->setIsPersistent(false)
->removeAllAddresses();
//Create guest addresses
$quote->getShippingAddress();
$quote->getBillingAddress();
+ $this->setShippingAssignments($quote);
$quote->collectTotals();
$this->quoteRepository->save($quote);
}
$this->persistentSession->getSession()->removePersistentCookie();
+ $this->persistentSession->setSession(null);
}
/**
@@ -111,7 +140,7 @@ public function setGuest($checkQuote = false)
public function convertCustomerCartToGuest()
{
$quoteId = $this->checkoutSession->getQuoteId();
- /** @var $quote \Magento\Quote\Model\Quote */
+ /** @var $quote Quote */
$quote = $this->quoteRepository->get($quoteId);
if ($quote && $quote->getId()) {
$this->_setQuotePersistent = false;
@@ -126,6 +155,7 @@ public function convertCustomerCartToGuest()
$quote->getAddressesCollection()->walk('setEmail', ['email' => null]);
$quote->collectTotals();
$this->persistentSession->getSession()->removePersistentCookie();
+ $this->persistentSession->setSession(null);
$this->quoteRepository->save($quote);
}
}
@@ -144,7 +174,7 @@ public function expire()
$quote->setIsActive(true)
->setIsPersistent(false)
->setCustomerId(null)
- ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID);
+ ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID);
}
}
@@ -157,4 +187,23 @@ public function isPersistent()
{
return $this->_setQuotePersistent;
}
+
+ /**
+ * Create shipping assignment for shopping cart
+ *
+ * @param CartInterface $quote
+ */
+ private function setShippingAssignments(CartInterface $quote): void
+ {
+ $shippingAssignments = [];
+ if (!$quote->isVirtual() && $quote->getItemsQty() > 0) {
+ $shippingAssignments[] = $this->shippingAssignmentProcessor->create($quote);
+ }
+ $cartExtension = $quote->getExtensionAttributes();
+ if ($cartExtension === null) {
+ $cartExtension = $this->cartExtensionFactory->create();
+ }
+ $cartExtension->setShippingAssignments($shippingAssignments);
+ $quote->setExtensionAttributes($cartExtension);
+ }
}
diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestShippingInformationManagementPluginTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestShippingInformationManagementPluginTest.php
new file mode 100644
index 0000000000000..8712068d4f3c8
--- /dev/null
+++ b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestShippingInformationManagementPluginTest.php
@@ -0,0 +1,117 @@
+persistenceDataHelper = $this->createMock(Data::class);
+ $this->persistenceSessionHelper = $this->createMock(PersistenceSession::class);
+ $this->customerSession = $this->createMock(CustomerSession::class);
+ $this->quoteManager = $this->createMock(QuoteManager::class);
+ $this->model = new GuestShippingInformationManagementPlugin(
+ $this->persistenceDataHelper,
+ $this->persistenceSessionHelper,
+ $this->customerSession,
+ $this->quoteManager
+ );
+ }
+
+ /**
+ * @param array $paymentMethods
+ * @param bool $isLoggedIn
+ * @param bool $isPersistentSessionEnabled
+ * @param bool $isPersistentCartEnabled
+ * @param bool $isCartPersistent
+ * @param bool $isCartConverted
+ * @dataProvider afterSaveAddressInformationDataProvider
+ */
+ public function testAfterSaveAddressInformation(
+ array $paymentMethods,
+ bool $isLoggedIn,
+ bool $isPersistentSessionEnabled,
+ bool $isPersistentCartEnabled,
+ bool $isCartPersistent,
+ bool $isCartConverted
+ ): void {
+ $subject = $this->createMock(GuestShippingInformationManagement::class);
+ $result = $this->createMock(PaymentDetailsInterface::class);
+ $result->method('getPaymentMethods')
+ ->willReturn($paymentMethods);
+ $this->customerSession->method('isLoggedIn')
+ ->willReturn($isLoggedIn);
+ $this->persistenceSessionHelper->method('isPersistent')
+ ->willReturn($isPersistentSessionEnabled);
+ $this->persistenceDataHelper->method('isShoppingCartPersist')
+ ->willReturn($isPersistentCartEnabled);
+ $this->quoteManager->method('isPersistent')
+ ->willReturn($isCartPersistent);
+ $this->customerSession->expects($this->exactly($isCartConverted ? 1 : 0))
+ ->method('setCustomerId')
+ ->with(null);
+ $this->customerSession->expects($this->exactly($isCartConverted ? 1 : 0))
+ ->method('setCustomerGroupId')
+ ->with(null);
+ $this->quoteManager->expects($this->exactly($isCartConverted ? 1 : 0))
+ ->method('convertCustomerCartToGuest');
+ $this->assertSame($result, $this->model->afterSaveAddressInformation($subject, $result));
+ }
+
+ /**
+ * @return array
+ */
+ public function afterSaveAddressInformationDataProvider(): array
+ {
+ return [
+ [['paypal'], false, true, true, true, true],
+ [['paypal'], true, true, true, true, false],
+ [['paypal', 'money_order'], false, true, true, true, false],
+ [['paypal', 'money_order'], true, true, true, true, false],
+ ];
+ }
+}
diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php
index 6b8fe128cb191..5c4a3eb624d3c 100644
--- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php
+++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php
@@ -8,17 +8,24 @@
namespace Magento\Persistent\Test\Unit\Model;
+use Magento\Checkout\Model\Session;
use Magento\Customer\Model\GroupManagement;
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
use Magento\Persistent\Helper\Data;
-use Magento\Persistent\Helper\Session;
use Magento\Persistent\Model\QuoteManager;
use Magento\Quote\Api\CartRepositoryInterface;
+use Magento\Quote\Api\Data\CartExtensionFactory;
+use Magento\Quote\Api\Data\CartExtensionInterface;
+use Magento\Quote\Api\Data\ShippingAssignmentInterface;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Address;
+use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class QuoteManagerTest extends TestCase
{
/**
@@ -27,7 +34,7 @@ class QuoteManagerTest extends TestCase
protected $model;
/**
- * @var Session|MockObject
+ * @var \Magento\Persistent\Helper\Session|MockObject
*/
protected $persistentSessionMock;
@@ -37,7 +44,7 @@ class QuoteManagerTest extends TestCase
protected $persistentDataMock;
/**
- * @var \Magento\Checkout\Model\Session|MockObject
+ * @var Session|MockObject
*/
protected $checkoutSessionMock;
@@ -61,9 +68,19 @@ class QuoteManagerTest extends TestCase
*/
protected $quoteRepositoryMock;
+ /**
+ * @var CartExtensionFactory|MockObject
+ */
+ private $cartExtensionFactory;
+
+ /**
+ * @var ShippingAssignmentProcessor|MockObject
+ */
+ private $shippingAssignmentProcessor;
+
protected function setUp(): void
{
- $this->persistentSessionMock = $this->createMock(Session::class);
+ $this->persistentSessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class);
$this->sessionMock =
$this->getMockBuilder(\Magento\Persistent\Model\Session::class)->addMethods([
'setLoadInactive',
@@ -76,7 +93,7 @@ protected function setUp(): void
->disableOriginalConstructor()
->getMock();
$this->persistentDataMock = $this->createMock(Data::class);
- $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class);
+ $this->checkoutSessionMock = $this->createMock(Session::class);
$this->abstractCollectionMock =
$this->createMock(AbstractCollection::class);
@@ -103,16 +120,25 @@ protected function setUp(): void
'collectTotals',
'removeAllAddresses',
'getIsActive',
+ 'isVirtual',
+ 'getItemsQty',
+ 'getExtensionAttributes',
+ 'setExtensionAttributes',
'__wakeup'
])
->disableOriginalConstructor()
->getMock();
+ $this->cartExtensionFactory = $this->createPartialMock(CartExtensionFactory::class, ['create']);
+ $this->shippingAssignmentProcessor = $this->createPartialMock(ShippingAssignmentProcessor::class, ['create']);
+
$this->model = new QuoteManager(
$this->persistentSessionMock,
$this->persistentDataMock,
$this->checkoutSessionMock,
- $this->quoteRepositoryMock
+ $this->quoteRepositoryMock,
+ $this->cartExtensionFactory,
+ $this->shippingAssignmentProcessor
);
}
@@ -200,7 +226,32 @@ public function testSetGuest()
->method('getSession')->willReturn($this->sessionMock);
$this->sessionMock->expects($this->once())
->method('removePersistentCookie')->willReturn($this->sessionMock);
-
+ $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(false);
+ $this->quoteMock->expects($this->once())->method('getItemsQty')->willReturn(1);
+ $extensionAttributes = $this->createPartialMock(
+ CartExtensionInterface::class,
+ [
+ 'setShippingAssignments',
+ 'getShippingAssignments'
+ ]
+ );
+ $shippingAssignment = $this->createMock(ShippingAssignmentInterface::class);
+ $extensionAttributes->expects($this->once())
+ ->method('setShippingAssignments')
+ ->with([$shippingAssignment]);
+ $this->shippingAssignmentProcessor->expects($this->once())
+ ->method('create')
+ ->with($this->quoteMock)
+ ->willReturn($shippingAssignment);
+ $this->cartExtensionFactory->expects($this->once())
+ ->method('create')
+ ->willReturn($extensionAttributes);
+ $this->quoteMock->expects($this->once())
+ ->method('getExtensionAttributes')
+ ->willReturn(null);
+ $this->quoteMock->expects($this->once())
+ ->method('setExtensionAttributes')
+ ->with($extensionAttributes);
$this->model->setGuest(false);
}
diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml
index fae706fcc5808..3351963231277 100644
--- a/app/code/Magento/Persistent/etc/frontend/di.xml
+++ b/app/code/Magento/Persistent/etc/frontend/di.xml
@@ -52,4 +52,9 @@
+
+
+ Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor\Proxy
+
+
diff --git a/app/code/Magento/Persistent/etc/webapi_rest/di.xml b/app/code/Magento/Persistent/etc/webapi_rest/di.xml
index e955dd81b1993..cb0aec6b460af 100644
--- a/app/code/Magento/Persistent/etc/webapi_rest/di.xml
+++ b/app/code/Magento/Persistent/etc/webapi_rest/di.xml
@@ -9,4 +9,8 @@
+
+
+
diff --git a/app/code/Magento/Persistent/etc/webapi_soap/di.xml b/app/code/Magento/Persistent/etc/webapi_soap/di.xml
index e955dd81b1993..cb0aec6b460af 100644
--- a/app/code/Magento/Persistent/etc/webapi_soap/di.xml
+++ b/app/code/Magento/Persistent/etc/webapi_soap/di.xml
@@ -9,4 +9,8 @@
+
+
+
diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php
index 39148f990b714..4366ef7aaf969 100644
--- a/app/code/Magento/Quote/Model/Quote/Address.php
+++ b/app/code/Magento/Quote/Model/Quote/Address.php
@@ -1019,6 +1019,13 @@ public function collectShippingRates()
*/
public function requestShippingRates(AbstractItem $item = null)
{
+ $storeId = $this->getQuote()->getStoreId() ?: $this->storeManager->getStore()->getId();
+ $taxInclude = $this->_scopeConfig->getValue(
+ 'tax/calculation/price_includes_tax',
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+
/** @var $request RateRequest */
$request = $this->_rateRequestFactory->create();
$request->setAllItems($item ? [$item] : $this->getAllItems());
@@ -1028,9 +1035,11 @@ public function requestShippingRates(AbstractItem $item = null)
$request->setDestStreet($this->getStreetFull());
$request->setDestCity($this->getCity());
$request->setDestPostcode($this->getPostcode());
- $request->setPackageValue($item ? $item->getBaseRowTotal() : $this->getBaseSubtotal());
+ $baseSubtotal = $taxInclude ? $this->getBaseSubtotalTotalInclTax() : $this->getBaseSubtotal();
+ $request->setPackageValue($item ? $item->getBaseRowTotal() : $baseSubtotal);
+ $baseSubtotalWithDiscount = $baseSubtotal + $this->getBaseDiscountAmount();
$packageWithDiscount = $item ? $item->getBaseRowTotal() -
- $item->getBaseDiscountAmount() : $this->getBaseSubtotalWithDiscount();
+ $item->getBaseDiscountAmount() : $baseSubtotalWithDiscount;
$request->setPackageValueWithDiscount($packageWithDiscount);
$request->setPackageWeight($item ? $item->getRowWeight() : $this->getWeight());
$request->setPackageQty($item ? $item->getQty() : $this->getItemQty());
@@ -1038,8 +1047,7 @@ public function requestShippingRates(AbstractItem $item = null)
/**
* Need for shipping methods that use insurance based on price of physical products
*/
- $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $this->getBaseSubtotal() -
- $this->getBaseVirtualAmount();
+ $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $baseSubtotal - $this->getBaseVirtualAmount();
$request->setPackagePhysicalValue($packagePhysicalValue);
$request->setFreeMethodWeight($item ? 0 : $this->getFreeMethodWeight());
@@ -1047,12 +1055,10 @@ public function requestShippingRates(AbstractItem $item = null)
/**
* Store and website identifiers specified from StoreManager
*/
+ $request->setStoreId($storeId);
if ($this->getQuote()->getStoreId()) {
- $storeId = $this->getQuote()->getStoreId();
- $request->setStoreId($storeId);
$request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId());
} else {
- $request->setStoreId($this->storeManager->getStore()->getId());
$request->setWebsiteId($this->storeManager->getWebsite()->getId());
}
$request->setFreeShipping($this->getFreeShipping());
diff --git a/app/code/Magento/Quote/Model/Quote/Item/Processor.php b/app/code/Magento/Quote/Model/Quote/Item/Processor.php
index ef4b853862681..c6bef1cc80bfb 100644
--- a/app/code/Magento/Quote/Model/Quote/Item/Processor.php
+++ b/app/code/Magento/Quote/Model/Quote/Item/Processor.php
@@ -97,7 +97,9 @@ public function prepare(Item $item, DataObject $request, Product $candidate): vo
$item->addQty($candidate->getCartQty());
$customPrice = $request->getCustomPrice();
- $item->setPrice($candidate->getFinalPrice());
+ if (!$item->getParentItem() || $item->getParentItem()->isChildrenCalculated()) {
+ $item->setPrice($candidate->getFinalPrice());
+ }
if (!empty($customPrice)) {
$item->setCustomPrice($customPrice);
$item->setOriginalCustomPrice($customPrice);
diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php
index 48945dacd1738..749e9944a6ad3 100644
--- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php
+++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php
@@ -230,7 +230,8 @@ public function subtractProductFromQuotes($product)
'items_qty' => new \Zend_Db_Expr(
$connection->quoteIdentifier('q.items_qty') . ' - ' . $connection->quoteIdentifier('qi.qty')
),
- 'items_count' => new \Zend_Db_Expr($ifSql)
+ 'items_count' => new \Zend_Db_Expr($ifSql),
+ 'updated_at' => 'q.updated_at',
]
)->join(
['qi' => $this->getTable('quote_item')],
@@ -277,21 +278,27 @@ public function markQuotesRecollect($productIds)
{
$tableQuote = $this->getTable('quote');
$tableItem = $this->getTable('quote_item');
- $subSelect = $this->getConnection()->select()->from(
- $tableItem,
- ['entity_id' => 'quote_id']
- )->where(
- 'product_id IN ( ? )',
- $productIds
- )->group(
- 'quote_id'
- );
-
- $select = $this->getConnection()->select()->join(
- ['t2' => $subSelect],
- 't1.entity_id = t2.entity_id',
- ['trigger_recollect' => new \Zend_Db_Expr('1')]
- );
+ $subSelect = $this->getConnection()
+ ->select()
+ ->from(
+ $tableItem,
+ ['entity_id' => 'quote_id']
+ )->where(
+ 'product_id IN ( ? )',
+ $productIds
+ )->group(
+ 'quote_id'
+ );
+ $select = $this->getConnection()
+ ->select()
+ ->join(
+ ['t2' => $subSelect],
+ 't1.entity_id = t2.entity_id',
+ [
+ 'trigger_recollect' => new \Zend_Db_Expr('1'),
+ 'updated_at' => 't1.updated_at',
+ ]
+ );
$updateQuery = $select->crossUpdateFromSelect(['t1' => $tableQuote]);
$this->getConnection()->query($updateQuery);
diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml
new file mode 100755
index 0000000000000..a14be3b533fa8
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+ ShippingAddressTX
+ BillingAddressTX
+ flatrate
+ flatrate
+
+
+
+
+ PaymentMethodCheckMoneyOrder
+ BillingAddressTX
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml
new file mode 100644
index 0000000000000..3681245311188
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ 1
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml
new file mode 100644
index 0000000000000..f5555394f8d4d
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ application/json
+
+ string
+ string
+ integer
+
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml
new file mode 100644
index 0000000000000..f233954f2cdcf
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+ application/json
+ string
+
+
+
+ application/json
+ string
+
+
+ string
+ string
+ string
+ integer
+ string
+
+ string
+
+ string
+ string
+ string
+ string
+ string
+
+
+ string
+ string
+ string
+ integer
+ string
+
+ string
+
+ string
+ string
+ string
+ string
+ string
+
+ string
+ string
+
+
+
+
+ application/json
+ string
+
+ string
+
+
+
diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
index a8fd794c08757..d4f6778a2ccb8 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
@@ -352,10 +352,40 @@ public function testRequestShippingRates()
$currentCurrencyCode = 'UAH';
+ $this->quote->expects($this->any())
+ ->method('getStoreId')
+ ->willReturn($storeId);
+
+ $this->storeManager->expects($this->at(0))
+ ->method('getStore')
+ ->with($storeId)
+ ->willReturn($this->store);
+ $this->store->expects($this->any())
+ ->method('getWebsiteId')
+ ->willReturn($webSiteId);
+
+ $this->scopeConfig->expects($this->exactly(1))
+ ->method('getValue')
+ ->with(
+ 'tax/calculation/price_includes_tax',
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ )
+ ->willReturn(1);
+
/** @var RateRequest */
$request = $this->getMockBuilder(RateRequest::class)
->disableOriginalConstructor()
- ->setMethods(['setStoreId', 'setWebsiteId', 'setBaseCurrency', 'setPackageCurrency'])
+ ->setMethods(
+ [
+ 'setStoreId',
+ 'setWebsiteId',
+ 'setBaseCurrency',
+ 'setPackageCurrency',
+ 'getBaseSubtotalTotalInclTax',
+ 'getBaseSubtotal'
+ ]
+ )
->getMock();
/** @var Collection */
@@ -434,13 +464,6 @@ public function testRequestShippingRates()
$this->storeManager->method('getStore')
->willReturn($this->store);
- $this->storeManager->expects($this->once())
- ->method('getWebsite')
- ->willReturn($this->website);
-
- $this->store->method('getId')
- ->willReturn($storeId);
-
$this->store->method('getBaseCurrency')
->willReturn($baseCurrency);
@@ -452,10 +475,6 @@ public function testRequestShippingRates()
->method('getCurrentCurrencyCode')
->willReturn($currentCurrencyCode);
- $this->website->expects($this->once())
- ->method('getId')
- ->willReturn($webSiteId);
-
$this->addressRateFactory->expects($this->once())
->method('create')
->willReturn($rate);
diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php
index cbcb7dd0adc3c..3025a72410671 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php
@@ -77,7 +77,16 @@ protected function setUp(): void
$this->itemMock = $this->getMockBuilder(Item::class)
->addMethods(['setOriginalCustomPrice'])
- ->onlyMethods(['getId', 'setOptions', 'setProduct', 'addQty', 'setCustomPrice', 'setData', 'setPrice'])
+ ->onlyMethods([
+ 'getId',
+ 'setOptions',
+ 'setProduct',
+ 'addQty',
+ 'setCustomPrice',
+ 'setData',
+ 'setPrice',
+ 'getParentItem'
+ ])
->disableOriginalConstructor()
->getMock();
$this->quoteItemFactoryMock->expects($this->any())
@@ -438,4 +447,41 @@ public function testPrepareWithResetCountAndNotStickAndSameItemId()
$this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock);
}
+
+ /**
+ * @param bool $isChildrenCalculated
+ * @dataProvider prepareChildProductDataProvider
+ */
+ public function testPrepareChildProduct(bool $isChildrenCalculated): void
+ {
+ $finalPrice = 10;
+ $this->objectMock->method('getResetCount')
+ ->willReturn(false);
+ $this->productMock->method('getFinalPrice')
+ ->willReturn($finalPrice);
+ $this->itemMock->expects($isChildrenCalculated ? $this->once() : $this->never())
+ ->method('setPrice')
+ ->with($finalPrice)
+ ->willReturnSelf();
+ $parentItem = $this->createConfiguredMock(
+ \Magento\Quote\Model\Quote\Item::class,
+ [
+ 'isChildrenCalculated' => $isChildrenCalculated
+ ]
+ );
+ $this->itemMock->method('getParentItem')
+ ->willReturn($parentItem);
+ $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock);
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareChildProductDataProvider(): array
+ {
+ return [
+ [false],
+ [true]
+ ];
+ }
}
diff --git a/app/code/Magento/QuoteAnalytics/etc/reports.xml b/app/code/Magento/QuoteAnalytics/etc/reports.xml
index f57012df23389..325901189dc20 100644
--- a/app/code/Magento/QuoteAnalytics/etc/reports.xml
+++ b/app/code/Magento/QuoteAnalytics/etc/reports.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php
index f73daa715c1df..e959c19a7cbe4 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php
@@ -51,7 +51,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s
$shippingAddressInput = current($shippingAddressesInput) ?? [];
$customerAddressId = $shippingAddressInput['customer_address_id'] ?? null;
- if (!$customerAddressId && !isset($shippingAddressInput['address']['save_in_address_book'])) {
+ if (!$customerAddressId
+ && isset($shippingAddressInput['address'])
+ && !isset($shippingAddressInput['address']['save_in_address_book'])
+ ) {
$shippingAddressInput['address']['save_in_address_book'] = true;
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php
index b2526bdc04e98..654a4bb558632 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php
@@ -42,12 +42,12 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s
}
$shippingMethodInput = current($shippingMethodsInput);
- if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) {
+ if (empty($shippingMethodInput['carrier_code'])) {
throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.'));
}
$carrierCode = $shippingMethodInput['carrier_code'];
- if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) {
+ if (empty($shippingMethodInput['method_code'])) {
throw new GraphQlInputException(__('Required parameter "method_code" is missing.'));
}
$methodCode = $shippingMethodInput['method_code'];
diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php
new file mode 100644
index 0000000000000..c2e94b215956e
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php
@@ -0,0 +1,157 @@
+cartItemRepository = $cartItemRepository;
+ $this->updateCartItem = $updateCartItem;
+ $this->itemRepository = $itemRepository;
+ $this->giftMessageHelper = $giftMessageHelper;
+ $this->giftMessageFactory = $giftMessageFactory;
+ }
+
+ /**
+ * Process cart items
+ *
+ * @param Quote $cart
+ * @param array $items
+ *
+ * @throws GraphQlInputException
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ */
+ public function processCartItems(Quote $cart, array $items): void
+ {
+ foreach ($items as $item) {
+ if (empty($item['cart_item_id'])) {
+ throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.'));
+ }
+
+ $itemId = (int)$item['cart_item_id'];
+ $customizableOptions = $item['customizable_options'] ?? [];
+ $cartItem = $cart->getItemById($itemId);
+
+ if ($cartItem && $cartItem->getParentItemId()) {
+ throw new GraphQlInputException(__('Child items may not be updated.'));
+ }
+
+ if (count($customizableOptions) === 0 && !isset($item['quantity'])) {
+ throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.'));
+ }
+
+ $quantity = (float)$item['quantity'];
+
+ if ($quantity <= 0.0) {
+ $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId);
+ } else {
+ $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions);
+ }
+
+ if (!empty($item['gift_message'])) {
+ try {
+ if (!$this->giftMessageHelper->isMessagesAllowed('items', $cartItem)) {
+ continue;
+ }
+ if (!$this->giftMessageHelper->isMessagesAllowed('item', $cartItem)) {
+ continue;
+ }
+
+ /** @var MessageInterface $giftItemMessage */
+ $giftItemMessage = $this->itemRepository->get($cart->getEntityId(), $itemId);
+
+ if (empty($giftItemMessage)) {
+ /** @var MessageInterface $giftMessage */
+ $giftMessage = $this->giftMessageFactory->create();
+ $this->updateGiftMessageForItem($cart, $giftMessage, $item, $itemId);
+ continue;
+ }
+ } catch (LocalizedException $exception) {
+ throw new GraphQlInputException(__('Gift Message cannot be updated.'));
+ }
+
+ $this->updateGiftMessageForItem($cart, $giftItemMessage, $item, $itemId);
+ }
+ }
+ }
+
+ /**
+ * Update Gift Message for Quote item
+ *
+ * @param Quote $cart
+ * @param MessageInterface $giftItemMessage
+ * @param array $item
+ * @param int $itemId
+ *
+ * @throws GraphQlInputException
+ */
+ private function updateGiftMessageForItem(Quote $cart, MessageInterface $giftItemMessage, array $item, int $itemId)
+ {
+ try {
+ $giftItemMessage->setRecipient($item['gift_message']['to']);
+ $giftItemMessage->setSender($item['gift_message']['from']);
+ $giftItemMessage->setMessage($item['gift_message']['message']);
+ $this->itemRepository->save($cart->getEntityId(), $giftItemMessage, $itemId);
+ } catch (LocalizedException $exception) {
+ throw new GraphQlInputException(__('Gift Message cannot be updated'));
+ }
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php
index 0be95eccc39e5..e8aa8d612c670 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php
@@ -7,17 +7,12 @@
namespace Magento\QuoteGraphQl\Model\Resolver;
-use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer;
use Magento\GraphQl\Model\Query\ContextInterface;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
-use Magento\Quote\Api\CartManagementInterface;
-use Magento\Quote\Model\QuoteIdMaskFactory;
-use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface;
-use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel;
+use Magento\Quote\Model\Cart\CustomerCartResolver;
/**
* Get cart for the customer
@@ -25,48 +20,19 @@
class CustomerCart implements ResolverInterface
{
/**
- * @var CreateEmptyCartForCustomer
+ * @var CustomerCartResolver
*/
- private $createEmptyCartForCustomer;
+ private $customerCartResolver;
/**
- * @var CartManagementInterface
- */
- private $cartManagement;
-
- /**
- * @var QuoteIdMaskFactory
- */
- private $quoteIdMaskFactory;
-
- /**
- * @var QuoteIdMaskResourceModel
- */
- private $quoteIdMaskResourceModel;
- /**
- * @var QuoteIdToMaskedQuoteIdInterface
- */
- private $quoteIdToMaskedQuoteId;
-
- /**
- * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer
- * @param CartManagementInterface $cartManagement
- * @param QuoteIdMaskFactory $quoteIdMaskFactory
- * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel
- * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId
+ * CustomerCart constructor.
+ *
+ * @param CustomerCartResolver $customerCartResolver
*/
public function __construct(
- CreateEmptyCartForCustomer $createEmptyCartForCustomer,
- CartManagementInterface $cartManagement,
- QuoteIdMaskFactory $quoteIdMaskFactory,
- QuoteIdMaskResourceModel $quoteIdMaskResourceModel,
- QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId
+ CustomerCartResolver $customerCartResolver
) {
- $this->createEmptyCartForCustomer = $createEmptyCartForCustomer;
- $this->cartManagement = $cartManagement;
- $this->quoteIdMaskFactory = $quoteIdMaskFactory;
- $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel;
- $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId;
+ $this->customerCartResolver = $customerCartResolver;
}
/**
@@ -76,22 +42,17 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
{
$currentUserId = $context->getUserId();
- /** @var ContextInterface $context */
+ /**
+ * @var ContextInterface $context
+ */
if (false === $context->getExtensionAttributes()->getIsCustomer()) {
throw new GraphQlAuthorizationException(__('The request is allowed for logged in customer'));
}
- try {
- $cart = $this->cartManagement->getCartForCustomer($currentUserId);
- } catch (NoSuchEntityException $e) {
- $this->createEmptyCartForCustomer->execute($currentUserId, null);
- $cart = $this->cartManagement->getCartForCustomer($currentUserId);
- }
- $maskedId = $this->quoteIdToMaskedQuoteId->execute((int) $cart->getId());
- if (empty($maskedId)) {
- $quoteIdMask = $this->quoteIdMaskFactory->create();
- $quoteIdMask->setQuoteId((int) $cart->getId());
- $this->quoteIdMaskResourceModel->save($quoteIdMask);
+ try {
+ $cart = $this->customerCartResolver->resolve($currentUserId);
+ } catch (\Exception $e) {
+ $cart = null;
}
return [
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php
index dd4ce8fe7f7a6..c2e4bfa44c9bb 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php
@@ -71,14 +71,15 @@ public function __construct(
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
- if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) {
+ if (empty($args['input']['cart_id'])) {
throw new GraphQlInputException(__('Required parameter "cart_id" is missing'));
}
- $maskedCartId = $args['input']['cart_id'];
- if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) {
+ if (empty($args['input']['payment_method']['code'])) {
throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.'));
}
+
+ $maskedCartId = $args['input']['cart_id'];
$paymentData = $args['input']['payment_method'];
$storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
index fa90f08e4b553..005baaad0e1e5 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
@@ -14,53 +14,43 @@
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\Quote\Api\CartItemRepositoryInterface;
use Magento\Quote\Api\CartRepositoryInterface;
-use Magento\Quote\Model\Quote;
use Magento\QuoteGraphQl\Model\Cart\GetCartForUser;
-use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem;
+use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider;
/**
* @inheritdoc
*/
class UpdateCartItems implements ResolverInterface
{
- /**
- * @var UpdateCartItem
- */
- private $updateCartItem;
-
/**
* @var GetCartForUser
*/
private $getCartForUser;
/**
- * @var CartItemRepositoryInterface
+ * @var CartRepositoryInterface
*/
- private $cartItemRepository;
+ private $cartRepository;
/**
- * @var CartRepositoryInterface
+ * @var UpdateCartItemsProvider
*/
- private $cartRepository;
+ private $updateCartItems;
/**
- * @param GetCartForUser $getCartForUser
- * @param CartItemRepositoryInterface $cartItemRepository
- * @param UpdateCartItem $updateCartItem
+ * @param GetCartForUser $getCartForUser
* @param CartRepositoryInterface $cartRepository
+ * @param UpdateCartItemsProvider $updateCartItems
*/
public function __construct(
GetCartForUser $getCartForUser,
- CartItemRepositoryInterface $cartItemRepository,
- UpdateCartItem $updateCartItem,
- CartRepositoryInterface $cartRepository
+ CartRepositoryInterface $cartRepository,
+ UpdateCartItemsProvider $updateCartItems
) {
$this->getCartForUser = $getCartForUser;
- $this->cartItemRepository = $cartItemRepository;
- $this->updateCartItem = $updateCartItem;
$this->cartRepository = $cartRepository;
+ $this->updateCartItems = $updateCartItems;
}
/**
@@ -71,6 +61,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
if (empty($args['input']['cart_id'])) {
throw new GraphQlInputException(__('Required parameter "cart_id" is missing.'));
}
+
$maskedCartId = $args['input']['cart_id'];
if (empty($args['input']['cart_items'])
@@ -78,13 +69,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
) {
throw new GraphQlInputException(__('Required parameter "cart_items" is missing.'));
}
- $cartItems = $args['input']['cart_items'];
+ $cartItems = $args['input']['cart_items'];
$storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
$cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
try {
- $this->processCartItems($cart, $cartItems);
+ $this->updateCartItems->processCartItems($cart, $cartItems);
$this->cartRepository->save($cart);
} catch (NoSuchEntityException $e) {
throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e);
@@ -98,39 +89,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
],
];
}
-
- /**
- * Process cart items
- *
- * @param Quote $cart
- * @param array $items
- * @throws GraphQlInputException
- * @throws LocalizedException
- */
- private function processCartItems(Quote $cart, array $items): void
- {
- foreach ($items as $item) {
- if (empty($item['cart_item_id'])) {
- throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.'));
- }
- $itemId = (int)$item['cart_item_id'];
- $customizableOptions = $item['customizable_options'] ?? [];
-
- $cartItem = $cart->getItemById($itemId);
- if ($cartItem && $cartItem->getParentItemId()) {
- throw new GraphQlInputException(__('Child items may not be updated.'));
- }
-
- if (count($customizableOptions) === 0 && !isset($item['quantity'])) {
- throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.'));
- }
- $quantity = (float)$item['quantity'];
-
- if ($quantity <= 0.0) {
- $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId);
- } else {
- $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions);
- }
- }
- }
}
diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json
index 0652d39b5f426..25f089cf75a62 100644
--- a/app/code/Magento/QuoteGraphQl/composer.json
+++ b/app/code/Magento/QuoteGraphQl/composer.json
@@ -13,7 +13,8 @@
"magento/module-customer-graph-ql": "*",
"magento/module-sales": "*",
"magento/module-directory": "*",
- "magento/module-graph-ql": "*"
+ "magento/module-graph-ql": "*",
+ "magento/module-gift-message": "*"
},
"suggest": {
"magento/module-graph-ql-cache": "*"
diff --git a/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php
new file mode 100644
index 0000000000000..a8a9f78df7f28
--- /dev/null
+++ b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php
@@ -0,0 +1,62 @@
+getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver');
+ /** @var FacebookWebDriver $webDriver */
+ $webDriver = $magentoWebDriver->webDriver;
+ $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow));
+ while (!empty($rows)) {
+ $rows[0]->click();
+ $magentoWebDriver->waitForPageLoad(30);
+ $magentoWebDriver->click($deleteButton);
+ $magentoWebDriver->waitForPageLoad(30);
+ $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10);
+ $magentoWebDriver->waitForPageLoad(60);
+ $magentoWebDriver->click($modalAcceptButton);
+ $magentoWebDriver->waitForPageLoad(60);
+ $magentoWebDriver->waitForLoadingMaskToDisappear();
+ $magentoWebDriver->waitForElementVisible($successMessageContainer, 10);
+ $magentoWebDriver->see($successMessage, $successMessageContainer);
+ $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow));
+ }
+ } catch (\Exception $e) {
+ $this->fail($e->getMessage());
+ }
+ }
+}
diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php
index b4f2e132f6de4..e81d1f9589405 100644
--- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php
+++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Compared.php
@@ -46,23 +46,15 @@ public function getItemCollection()
$collection = $this->getData('item_collection');
if ($collection === null) {
if ($collection = $this->getCreateOrderModel()->getCustomerCompareList()) {
- $collection = $collection->getItemCollection()->useProductItem(
- true
- )->setStoreId(
- $this->getQuote()->getStoreId()
- )->addStoreFilter(
- $this->getQuote()->getStoreId()
- )->setCustomerId(
- $this->getCustomerId()
- )->addAttributeToSelect(
- 'name'
- )->addAttributeToSelect(
- 'price'
- )->addAttributeToSelect(
- 'image'
- )->addAttributeToSelect(
- 'status'
- )->load();
+ $collection = $collection->getItemCollection()
+ ->useProductItem()
+ ->setStoreId($this->getQuote()->getStoreId())
+ ->addStoreFilter($this->getQuote()->getStoreId())
+ ->setCustomerId($this->getCustomerId())
+ ->addAttributeToSelect('name')
+ ->addAttributeToSelect('price')->addAttributeToSelect('image')
+ ->addAttributeToSelect('status')
+ ->load();
}
$this->setData('item_collection', $collection);
}
diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php
index 8442d5b36466e..c9f251621f9de 100644
--- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php
+++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Pcompared.php
@@ -89,13 +89,11 @@ public function getItemCollection()
// get products to skip
$skipProducts = [];
if ($collection = $this->getCreateOrderModel()->getCustomerCompareList()) {
- $collection = $collection->getItemCollection()->useProductItem(
- true
- )->setStoreId(
- $this->getStoreId()
- )->setCustomerId(
- $this->getCustomerId()
- )->load();
+ $collection = $collection->getItemCollection()
+ ->useProductItem()
+ ->setStoreId($this->getStoreId())
+ ->setCustomerId($this->getCustomerId())
+ ->load();
foreach ($collection as $_item) {
$skipProducts[] = $_item->getProductId();
}
diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php
index a3242228b28e0..978aec1b79ec4 100644
--- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php
+++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php
@@ -5,12 +5,15 @@
*/
namespace Magento\Sales\Cron;
-use Magento\Quote\Model\ResourceModel\Quote\Collection;
+use Exception;
+use Magento\Quote\Model\QuoteRepository;
+use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection;
use Magento\Sales\Model\ResourceModel\Collection\ExpiredQuotesCollection;
use Magento\Store\Model\StoreManagerInterface;
+use Psr\Log\LoggerInterface;
/**
- * Class CleanExpiredQuotes
+ * Cron job for cleaning expired Quotes
*/
class CleanExpiredQuotes
{
@@ -24,16 +27,32 @@ class CleanExpiredQuotes
*/
private $storeManager;
+ /**
+ * @var QuoteRepository
+ */
+ private $quoteRepository;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
/**
* @param StoreManagerInterface $storeManager
* @param ExpiredQuotesCollection $expiredQuotesCollection
+ * @param QuoteRepository $quoteRepository
+ * @param LoggerInterface $logger
*/
public function __construct(
StoreManagerInterface $storeManager,
- ExpiredQuotesCollection $expiredQuotesCollection
+ ExpiredQuotesCollection $expiredQuotesCollection,
+ QuoteRepository $quoteRepository,
+ LoggerInterface $logger
) {
$this->storeManager = $storeManager;
$this->expiredQuotesCollection = $expiredQuotesCollection;
+ $this->quoteRepository = $quoteRepository;
+ $this->logger = $logger;
}
/**
@@ -45,9 +64,41 @@ public function execute()
{
$stores = $this->storeManager->getStores(true);
foreach ($stores as $store) {
- /** @var $quotes Collection */
- $quotes = $this->expiredQuotesCollection->getExpiredQuotes($store);
- $quotes->walk('delete');
+ /** @var $quoteCollection QuoteCollection */
+ $quoteCollection = $this->expiredQuotesCollection->getExpiredQuotes($store);
+ $quoteCollection->setPageSize(50);
+
+ // Last page returns 1 even when we don't have any results
+ $lastPage = $quoteCollection->getSize() ? $quoteCollection->getLastPageNumber() : 0;
+
+ for ($currentPage = $lastPage; $currentPage >= 1; $currentPage--) {
+ $quoteCollection->setCurPage($currentPage);
+
+ $this->deleteQuotes($quoteCollection);
+ }
}
}
+
+ /**
+ * Deletes all quotes in collection
+ *
+ * @param QuoteCollection $quoteCollection
+ */
+ private function deleteQuotes(QuoteCollection $quoteCollection): void
+ {
+ foreach ($quoteCollection as $quote) {
+ try {
+ $this->quoteRepository->delete($quote);
+ } catch (Exception $e) {
+ $message = sprintf(
+ 'Unable to delete expired quote (ID: %s): %s',
+ $quote->getId(),
+ (string)$e
+ );
+ $this->logger->error($message);
+ }
+ }
+
+ $quoteCollection->clear();
+ }
}
diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php
index 6e029ac468370..345fffc414fbc 100644
--- a/app/code/Magento/Sales/Model/Order/ItemRepository.php
+++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php
@@ -167,10 +167,7 @@ public function deleteById($id)
public function save(OrderItemInterface $entity)
{
if ($entity->getProductOption()) {
- $request = $this->getBuyRequest($entity);
- $productOptions = $entity->getProductOptions();
- $productOptions['info_buyRequest'] = $request->toArray();
- $entity->setProductOptions($productOptions);
+ $entity->setProductOptions($this->getItemProductOptions($entity));
}
$this->metadata->getMapper()->save($entity);
@@ -178,6 +175,23 @@ public function save(OrderItemInterface $entity)
return $this->registry[$entity->getEntityId()];
}
+ /**
+ * Return product options
+ *
+ * @param OrderItemInterface $entity
+ * @return array
+ */
+ private function getItemProductOptions(OrderItemInterface $entity): array
+ {
+ $request = $this->getBuyRequest($entity);
+ $productOptions = $entity->getProductOptions();
+ $productOptions['info_buyRequest'] = $productOptions && !empty($productOptions['info_buyRequest'])
+ ? array_merge($productOptions['info_buyRequest'], $request->toArray())
+ : $request->toArray();
+
+ return $productOptions;
+ }
+
/**
* Set parent item.
*
diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php
index 253dbd43fa580..6ddbce49829eb 100644
--- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php
+++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php
@@ -80,11 +80,9 @@ public function draw()
$lines = [];
// draw Product name
- $lines[0] = [
- [
+ $lines[0][] = [
'text' => $this->string->split($this->prepareText((string)$item->getName()), 35, true, true),
'feed' => 35
- ]
];
// draw SKU
diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php
index 19d9b6f300eba..b1d2deb248ba1 100644
--- a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php
+++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php
@@ -43,13 +43,13 @@ public function getAllCommentCollection($orderId)
$commentSelects = [];
foreach (['invoice', 'shipment', 'creditmemo'] as $entityTypeCode) {
$mainTable = $resource->getTableName('sales_' . $entityTypeCode);
- $slaveTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment');
+ $commentTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment');
$select = $read->select()->from(
['main' => $mainTable],
['entity_id' => 'order_id', 'entity_type_code' => new \Zend_Db_Expr("'{$entityTypeCode}'")]
)->join(
- ['slave' => $slaveTable],
- 'main.entity_id = slave.parent_id',
+ ['comment' => $commentTable],
+ 'main.entity_id = comment.parent_id',
$fields
)->where(
'main.order_id = ?',
diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml
index 0888132669177..1c67d778937d1 100644
--- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml
+++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml
@@ -10,8 +10,10 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
+
+
diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
index dc3a8e9f69aca..0529c66a04d8c 100644
--- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
+++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
@@ -8,7 +8,7 @@
+
+
+
+
+ - Magento\SalesGraphQl\Model\OrderItemTypeResolver
+
+
+
+
+
+
+ - Magento\SalesGraphQl\Model\InvoiceItemTypeResolver
+
+
+
+
diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
index f823c25cf2d9f..099a3ffb959c4 100644
--- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
@@ -2,20 +2,7 @@
# See COPYING.txt for license details.
type Query {
- customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @doc(description: "List of customer orders") @cache(cacheable: false)
-}
-
-type CustomerOrder @doc(description: "Order mapping fields") {
- id: Int
- increment_id: String @deprecated(reason: "Use the order_number instead.")
- order_number: String! @doc(description: "The order number")
- created_at: String
- grand_total: Float
- status: String
-}
-
-type CustomerOrders {
- items: [CustomerOrder] @doc(description: "Array of orders")
+ customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @deprecated(reason: "Use orders from customer instead") @cache(cacheable: false)
}
type Mutation {
@@ -33,6 +20,190 @@ type CheckoutUserInputError @doc(description:"An error encountered while adding
code: CheckoutUserInputErrorCodes! @doc(description: "Checkout-specific error code")
}
+type Customer {
+ orders (
+ filter: CustomerOrdersFilterInput @doc(description: "Defines the filter to use for searching customer orders"),
+ currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1"),
+ pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20"),
+ ): CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders") @cache(cacheable: false)
+}
+
+input CustomerOrdersFilterInput @doc(description: "Identifies the filter to use for filtering orders.") {
+ number: FilterStringTypeInput @doc(description: "Filters by order number.")
+}
+
+type CustomerOrders @doc(description: "The collection of orders that match the conditions defined in the filter") {
+ items: [CustomerOrder]! @doc(description: "An array of customer orders")
+ page_info: SearchResultPageInfo @doc(description: "An object that includes the current_page, page_info, and page_size values specified in the query")
+ total_count: Int @doc(description: "The total count of customer orders")
+}
+
+type CustomerOrder @doc(description: "Contains details about each of the customer's orders") {
+ id: ID! @doc(description: "Unique identifier for the order")
+ order_date: String! @doc(description: "The date the order was placed")
+ status: String! @doc(description: "The current status of the order")
+ number: String! @doc(description: "The order number")
+ items: [OrderItemInterface] @doc(description: "An array containing the items purchased in this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItems")
+ total: OrderTotal @doc(description: "Contains details about the calculated totals for this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderTotal")
+ invoices: [Invoice]! @doc(description: "A list of invoices for the order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoices")
+ shipments: [OrderShipment] @doc(description: "A list of shipments for the order")
+ payment_methods: [PaymentMethod] @doc(description: "Payment details for the order")
+ shipping_address: CustomerAddress @doc(description: "The shipping address for the order")
+ billing_address: CustomerAddress @doc(description: "The billing address for the order")
+ carrier: String @doc(description: "The shipping carrier for the order delivery")
+ shipping_method: String @doc(description: "The delivery method for the order")
+ comments: [CommentItem] @doc(description: "Comments about the order")
+ increment_id: String @deprecated(reason: "Use the id attribute instead")
+ order_number: String! @deprecated(reason: "Use the number attribute instead")
+ created_at: String @deprecated(reason: "Use the order_date attribute instead")
+ grand_total: Float @deprecated(reason: "Use the totals.grand_total attribute instead")
+}
+
+interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\OrderItemTypeResolver") {
+ id: ID! @doc(description: "The unique identifier of the order item")
+ product_name: String @doc(description: "The name of the base product")
+ product_sku: String! @doc(description: "The SKU of the base product")
+ product_url_key: String @doc(description: "URL key of the base product")
+ product_type: String @doc(description: "The type of product, such as simple, configurable, or bundle")
+ status: String @doc(description: "The status of the order item")
+ product_sale_price: Money! @doc(description: "The sale price of the base product, including selected options")
+ discounts: [Discount] @doc(description: "The final discount information for the product")
+ selected_options: [OrderItemOption] @doc(description: "The selected options for the base product, such as color or size")
+ entered_options: [OrderItemOption] @doc(description: "The entered option for the base product, such as a logo or image")
+ quantity_ordered: Float @doc(description: "The number of units ordered for this item")
+ quantity_shipped: Float @doc(description: "The number of shipped items")
+ quantity_refunded: Float @doc(description: "The number of refunded items")
+ quantity_invoiced: Float @doc(description: "The number of invoiced items")
+ quantity_canceled: Float @doc(description: "The number of canceled items")
+ quantity_returned: Float @doc(description: "The number of returned items")
+}
+
+type OrderItem implements OrderItemInterface {
+}
+
+type BundleOrderItem implements OrderItemInterface {
+ bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\BundleOptions")
+}
+
+type ItemSelectedBundleOption @doc(description: "A list of options of the selected bundle product") {
+ id: ID! @doc(description: "The unique identifier of the option")
+ label: String! @doc(description: "The label of the option")
+ values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option")
+}
+
+type ItemSelectedBundleOptionValue @doc(description: "A list of values for the selected bundle product") {
+ id: ID! @doc(description: "The unique identifier of the value")
+ product_name: String! @doc(description: "The name of the child bundle product")
+ product_sku: String! @doc(description: "The SKU of the child bundle product")
+ quantity: Float! @doc(description: "Indicates how many of this bundle product were ordered")
+ price: Money! @doc(description: "The price of the child bundle product")
+}
+
+type OrderItemOption @doc(description: "Represents order item options like selected or entered") {
+ id: String! @doc(description: "The name of the option")
+ value: String! @doc(description: "The value of the option")
+}
+
+type TaxItem @doc(description: "The tax item details") {
+ amount: Money! @doc(description: "The amount of tax applied to the item")
+ title: String! @doc(description: "A title that describes the tax")
+ rate: Float! @doc(description: "The rate used to calculate the tax")
+}
+
+type OrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") {
+ subtotal: Money! @doc(description: "The subtotal of the order, excluding shipping, discounts, and taxes")
+ discounts: [Discount] @doc(description: "The applied discounts to the order")
+ total_tax: Money! @doc(description: "The amount of tax applied to the order")
+ taxes: [TaxItem] @doc(description: "The order tax details")
+ grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes")
+ base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency")
+ total_shipping: Money! @doc(description: "The shipping amount for the order")
+ shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the order")
+}
+
+type Invoice @doc(description: "Invoice details") {
+ id: ID! @doc(description: "The ID of the invoice, used for API purposes")
+ number: String! @doc(description: "Sequential invoice number")
+ total: InvoiceTotal @doc(description: "Invoice total amount details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\InvoiceTotal")
+ items: [InvoiceItemInterface] @doc(description: "Invoiced product details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\InvoiceItems")
+ comments: [CommentItem] @doc(description: "Comments on the invoice")
+}
+
+interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\InvoiceItemTypeResolver") {
+ id: ID! @doc(description: "The unique ID of the invoice item")
+ order_item: OrderItemInterface @doc(description: "Contains details about an individual order item")
+ product_name: String @doc(description: "The name of the base product")
+ product_sku: String! @doc(description: "The SKU of the base product")
+ product_sale_price: Money! @doc(description: "The sale price for the base product including selected options")
+ discounts: [Discount] @doc(description: "Contains information about the final discount amount for the base product, including discounts on options")
+ quantity_invoiced: Float @doc(description: "The number of invoiced items")
+}
+
+type InvoiceItem implements InvoiceItemInterface {
+}
+
+type BundleInvoiceItem implements InvoiceItemInterface{
+ bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\BundleOptions")
+}
+
+type InvoiceTotal @doc(description: "Contains price details from an invoice"){
+ subtotal: Money! @doc(description: "The subtotal of the invoice, excluding shipping, discounts, and taxes")
+ discounts: [Discount] @doc(description: "The applied discounts to the invoice")
+ total_tax: Money! @doc(description: "The amount of tax applied to the invoice")
+ taxes: [TaxItem] @doc(description: "The order tax details")
+ grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes")
+ base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency")
+ total_shipping: Money! @doc(description: "The shipping amount for the invoice")
+ shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the invoice")
+}
+
+type ShippingHandling @doc(description: "The Shipping handling details") {
+ total_amount: Money! @doc(description: "The total amount for shipping")
+ amount_including_tax: Money @doc(description: "The shipping amount, including tax")
+ amount_excluding_tax: Money @doc(description: "The shipping amount, excluding tax")
+ taxes: [TaxItem] @doc(description: "Contains details about taxes applied for shipping")
+ discounts: [Discount] @doc(description: "The applied discounts to the shipping")
+}
+
+type OrderShipment @doc(description: "Order shipment details") {
+ id: ID! @doc(description: "The unique ID of the shipment")
+ number: String! @doc(description: "The sequential credit shipment number")
+ tracking: [ShipmentTracking] @doc(description: "Contains shipment tracking details")
+ items: [ShipmentItem] @doc(description: "Contains items included in the shipment")
+ comments: [CommentItem] @doc(description: "Comments added to the shipment")
+}
+
+type CommentItem @doc(description: "Comment item details") {
+ timestamp: String! @doc(description: "The timestamp of the comment")
+ message: String! @doc(description: "The text of the message")
+}
+
+type ShipmentItem @doc(description: "Order shipment item details") {
+ id: ID! @doc(description: "The unique ID of the shipment item")
+ order_item: OrderItemInterface @doc(description: "The shipped order item")
+ product_name: String @doc(description: "The name of the base product")
+ product_sku: String! @doc(description: "The SKU of the base product")
+ product_sale_price: Money! @doc(description: "The sale price for the base product")
+ quantity_shipped: Float! @doc(description: "The number of shipped items")
+}
+
+type ShipmentTracking @doc(description: "Order shipment tracking details") {
+ title: String! @doc(description: "The shipment tracking title")
+ carrier: String! @doc(description: "The shipping carrier for the order delivery")
+ number: String @doc(description: "The tracking number of the order shipment")
+}
+
+type PaymentMethod @doc(description: "Contains details about the payment method used to pay for the order") {
+ name: String! @doc(description: "The label that describes the payment method")
+ type: String! @doc(description: "The payment method code that indicates how the order was paid for")
+ additional_data: [KeyValue] @doc(description: "Additional data per payment method type")
+}
+
+type KeyValue @doc(description: "The key-value type") {
+ name: String @doc(description: "The name part of the name/value pair")
+ value: String @doc(description: "The value part of the name/value pair")
+}
+
enum CheckoutUserInputErrorCodes {
REORDER_NOT_AVAILABLE
PRODUCT_NOT_FOUND
diff --git a/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php
new file mode 100644
index 0000000000000..b1fda3e78672b
--- /dev/null
+++ b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php
@@ -0,0 +1,223 @@
+deltaPriceRound = $deltaPriceRound;
+ $this->priceCurrency = $priceCurrency;
+ }
+
+ /**
+ * Retrieve shipping amount by quote address and shipping method
+ *
+ * @param AddressInterface $address
+ * @return float
+ */
+ public function calculateShippingAmountWhenAppliedToShipping(
+ AddressInterface $address
+ ): float {
+ $shippingAmount = (float) $address->getShippingAmount();
+ if ($shippingAmount == 0.0) {
+ $address->setCollectShippingRates(true);
+ $address->collectShippingRates();
+ $shippingRates = $address->getAllShippingRates();
+ foreach ($shippingRates as $shippingRate) {
+ if ($shippingRate->getCode() === $address->getShippingMethod()
+ ) {
+ $shippingAmount = (float) $shippingRate->getPrice();
+ break;
+ }
+ }
+ }
+ return $shippingAmount;
+ }
+
+ /**
+ * Get the available discount amount calculated from the ration
+ *
+ * @param float $ruleDiscount
+ * @param float $qty
+ * @param float $baseItemPrice
+ * @param float $baseRuleTotals
+ * @param string $discountType
+ * @return float
+ */
+ public function getDiscountAmount(
+ float $ruleDiscount,
+ float $qty,
+ float $baseItemPrice,
+ float $baseRuleTotals,
+ string $discountType
+ ): float {
+ $ratio = $baseItemPrice * $qty / $baseRuleTotals;
+ return $this->deltaPriceRound->round(
+ $ruleDiscount * $ratio,
+ $discountType
+ );
+ }
+
+ /**
+ * Get shipping discount amount
+ *
+ * @param Rule $rule
+ * @param float $shippingAmount
+ * @param float $quoteBaseSubtotal
+ * @return float
+ */
+ public function getShippingDiscountAmount(
+ Rule $rule,
+ float $shippingAmount,
+ float $quoteBaseSubtotal
+ ): float {
+ $ratio = $shippingAmount / $quoteBaseSubtotal;
+ return $this->priceCurrency
+ ->roundPrice(
+ $rule->getDiscountAmount() * $ratio
+ );
+ }
+
+ /**
+ * Check if the current quote is multi shipping or not
+ *
+ * @param Quote $quote
+ * @return bool
+ */
+ public function checkMultiShippingQuote(Quote $quote): bool
+ {
+ $isMultiShipping = false;
+ $extensionAttributes = $quote->getExtensionAttributes();
+ if (!$quote->isVirtual() &&
+ $extensionAttributes &&
+ $extensionAttributes->getShippingAssignments()) {
+ $shippingAssignments = $extensionAttributes->getShippingAssignments();
+ if (count($shippingAssignments) > 1) {
+ $isMultiShipping = true;
+ }
+ }
+ return $isMultiShipping;
+ }
+
+ /**
+ * Get base rule totals for multi shipping addresses
+ *
+ * @param Quote $quote
+ * @return float
+ */
+ public function getQuoteTotalsForMultiShipping(Quote $quote): float
+ {
+ $quoteTotal = $quote->getBaseSubtotal();
+ $extensionAttributes = $quote->getExtensionAttributes();
+ $shippingAssignments = $extensionAttributes->getShippingAssignments();
+ $totalShippingPrice = 0.0;
+ foreach ($shippingAssignments as $assignment) {
+ $totalShippingPrice += $assignment->getShipping()->getAddress()->getBaseShippingInclTax();
+ }
+ return $quoteTotal + $totalShippingPrice;
+ }
+
+ /**
+ * Get base rule totals for regular shipping address
+ *
+ * @param Quote\Address $address
+ * @param float $baseRuleTotals
+ * @return float
+ */
+ public function getQuoteTotalsForRegularShipping(
+ Quote\Address $address,
+ float $baseRuleTotals
+ ): float {
+ $baseRuleTotals += $this->calculateShippingAmountWhenAppliedToShipping(
+ $address
+ );
+ return $baseRuleTotals;
+ }
+
+ /**
+ * Get base rule totals
+ *
+ * @param int $isAppliedToShipping
+ * @param Quote $quote
+ * @param bool $isMultiShipping
+ * @param Quote\Address $address
+ * @param float $baseRuleTotals
+ * @return float
+ */
+ public function getBaseRuleTotals(
+ int $isAppliedToShipping,
+ Quote $quote,
+ bool $isMultiShipping,
+ Quote\Address $address,
+ float $baseRuleTotals
+ ): float {
+ if ($isAppliedToShipping) {
+ $baseRuleTotals = ($quote->getIsMultiShipping() && $isMultiShipping) ?
+ $this->getQuoteTotalsForMultiShipping($quote) :
+ $this->getQuoteTotalsForRegularShipping($address, $baseRuleTotals);
+ } else {
+ if ($quote->getIsMultiShipping() && $isMultiShipping) {
+ $baseRuleTotals = $quote->getBaseSubtotal();
+ }
+ }
+ return (float) $baseRuleTotals;
+ }
+
+ /**
+ * Get available discount amount
+ *
+ * @param Rule $rule
+ * @param Quote $quote
+ * @param bool $isMultiShipping
+ * @param array $cartRules
+ * @param float $baseDiscountAmount
+ * @param float $availableDiscountAmount
+ * @return float
+ */
+ public function getAvailableDiscountAmount(
+ Rule $rule,
+ Quote $quote,
+ bool $isMultiShipping,
+ array $cartRules,
+ float $baseDiscountAmount,
+ float $availableDiscountAmount
+ ): float {
+ if ($quote->getIsMultiShipping() && $isMultiShipping) {
+ $availableDiscountAmount = (float)$cartRules[$rule->getId()] - $baseDiscountAmount;
+ } else {
+ $availableDiscountAmount -= $baseDiscountAmount;
+ }
+ return $availableDiscountAmount;
+ }
+}
diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php
index e44200614fa00..b4585bb047c44 100644
--- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php
+++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php
@@ -5,8 +5,13 @@
*/
namespace Magento\SalesRule\Model\Rule\Action\Discount;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Pricing\PriceCurrencyInterface;
+use Magento\Quote\Model\Quote\Item\AbstractItem;
+use Magento\SalesRule\Helper\CartFixedDiscount;
use Magento\SalesRule\Model\DeltaPriceRound;
+use Magento\SalesRule\Model\Rule;
use Magento\SalesRule\Model\Validator;
/**
@@ -26,6 +31,11 @@ class CartFixed extends AbstractDiscount
*/
private $deltaPriceRound;
+ /**
+ * @var CartFixedDiscount
+ */
+ private $cartFixedDiscountHelper;
+
/**
* @var string
*/
@@ -36,35 +46,47 @@ class CartFixed extends AbstractDiscount
* @param DataFactory $discountDataFactory
* @param PriceCurrencyInterface $priceCurrency
* @param DeltaPriceRound $deltaPriceRound
+ * @param CartFixedDiscount|null $cartFixedDiscount
*/
public function __construct(
Validator $validator,
DataFactory $discountDataFactory,
PriceCurrencyInterface $priceCurrency,
- DeltaPriceRound $deltaPriceRound
+ DeltaPriceRound $deltaPriceRound,
+ ?CartFixedDiscount $cartFixedDiscount = null
) {
$this->deltaPriceRound = $deltaPriceRound;
-
+ $this->cartFixedDiscountHelper = $cartFixedDiscount ?:
+ ObjectManager::getInstance()->get(CartFixedDiscount::class);
parent::__construct($validator, $discountDataFactory, $priceCurrency);
}
/**
* Fixed discount for cart calculation
*
- * @param \Magento\SalesRule\Model\Rule $rule
- * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
+ * @param Rule $rule
+ * @param AbstractItem $item
* @param float $qty
- * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data
+ * @return Data
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function calculate($rule, $item, $qty)
{
- /** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */
+ /** @var Data $discountData */
$discountData = $this->discountFactory->create();
$ruleTotals = $this->validator->getRuleItemTotalsInfo($rule->getId());
+ $baseRuleTotals = $ruleTotals['base_items_price'] ?? 0.0;
+ $address = $item->getAddress();
+ $shippingMethod = $address->getShippingMethod();
+ $isAppliedToShipping = (int) $rule->getApplyToShipping();
$quote = $item->getQuote();
+ $ruleDiscount = (float) $rule->getDiscountAmount();
+ $isMultiShipping = $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote);
$itemPrice = $this->validator->getItemPrice($item);
$baseItemPrice = $this->validator->getItemBasePrice($item);
$itemOriginalPrice = $this->validator->getItemOriginalPrice($item);
@@ -74,41 +96,91 @@ public function calculate($rule, $item, $qty)
if (!isset($cartRules[$rule->getId()])) {
$cartRules[$rule->getId()] = $rule->getDiscountAmount();
}
-
- $availableDiscountAmount = (float)$cartRules[$rule->getId()];
+ $availableDiscountAmount = (float) $cartRules[$rule->getId()];
$discountType = self::$discountType . $rule->getId();
if ($availableDiscountAmount > 0) {
$store = $quote->getStore();
if ($ruleTotals['items_count'] <= 1) {
+ $baseRuleTotals = $shippingMethod ?
+ $this->cartFixedDiscountHelper
+ ->getBaseRuleTotals(
+ $isAppliedToShipping,
+ $quote,
+ $isMultiShipping,
+ $address,
+ $baseRuleTotals
+ ) : $baseRuleTotals;
+ $availableDiscountAmount = $this->cartFixedDiscountHelper
+ ->getDiscountAmount(
+ $ruleDiscount,
+ $qty,
+ $baseItemPrice,
+ $baseRuleTotals,
+ $discountType
+ );
$quoteAmount = $this->priceCurrency->convert($availableDiscountAmount, $store);
$baseDiscountAmount = min($baseItemPrice * $qty, $availableDiscountAmount);
$this->deltaPriceRound->reset($discountType);
} else {
- $ratio = $baseItemPrice * $qty / $ruleTotals['base_items_price'];
- $maximumItemDiscount = $this->deltaPriceRound->round(
- $rule->getDiscountAmount() * $ratio,
- $discountType
- );
-
+ $baseRuleTotals = $shippingMethod ?
+ $this->cartFixedDiscountHelper
+ ->getBaseRuleTotals(
+ $isAppliedToShipping,
+ $quote,
+ $isMultiShipping,
+ $address,
+ $baseRuleTotals
+ ) : $baseRuleTotals;
+ $maximumItemDiscount =$this->cartFixedDiscountHelper
+ ->getDiscountAmount(
+ $ruleDiscount,
+ $qty,
+ $baseItemPrice,
+ $baseRuleTotals,
+ $discountType
+ );
$quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store);
-
$baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount);
$this->validator->decrementRuleItemTotalsCount($rule->getId());
}
- $baseDiscountAmount = $this->priceCurrency->round($baseDiscountAmount);
+ $baseDiscountAmount = $this->priceCurrency->roundPrice($baseDiscountAmount);
- $availableDiscountAmount -= $baseDiscountAmount;
+ $availableDiscountAmount = $this->cartFixedDiscountHelper
+ ->getAvailableDiscountAmount(
+ $rule,
+ $quote,
+ $isMultiShipping,
+ $cartRules,
+ $baseDiscountAmount,
+ $availableDiscountAmount
+ );
$cartRules[$rule->getId()] = $availableDiscountAmount;
+ if ($isAppliedToShipping &&
+ $isMultiShipping &&
+ $ruleTotals['items_count'] <= 1) {
+ $estimatedShippingAmount = (float) $address->getBaseShippingInclTax();
+ $shippingDiscountAmount = $this->cartFixedDiscountHelper->
+ getShippingDiscountAmount(
+ $rule,
+ $estimatedShippingAmount,
+ $baseRuleTotals
+ );
+ $cartRules[$rule->getId()] -= $shippingDiscountAmount;
+ if ($cartRules[$rule->getId()] < 0.0) {
+ $baseDiscountAmount += $cartRules[$rule->getId()];
+ $quoteAmount += $cartRules[$rule->getId()];
+ }
+ }
if ($availableDiscountAmount <= 0) {
$this->deltaPriceRound->reset($discountType);
}
- $discountData->setAmount($this->priceCurrency->round(min($itemPrice * $qty, $quoteAmount)));
+ $discountData->setAmount($this->priceCurrency->roundPrice(min($itemPrice * $qty, $quoteAmount)));
$discountData->setBaseAmount($baseDiscountAmount);
$discountData->setOriginalAmount(min($itemOriginalPrice * $qty, $quoteAmount));
- $discountData->setBaseOriginalAmount($this->priceCurrency->round($baseItemOriginalPrice));
+ $discountData->setBaseOriginalAmount($this->priceCurrency->roundPrice($baseItemOriginalPrice));
}
$quote->setCartFixedRules($cartRules);
diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php
index 06bc7e12fb973..0fc0b062c7887 100644
--- a/app/code/Magento/SalesRule/Model/Validator.php
+++ b/app/code/Magento/SalesRule/Model/Validator.php
@@ -6,8 +6,11 @@
namespace Magento\SalesRule\Model;
+use Magento\Framework\App\ObjectManager;
use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Model\Quote\Item\AbstractItem;
+use Magento\SalesRule\Helper\CartFixedDiscount;
+use Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory;
/**
* SalesRule Validator Model
@@ -61,7 +64,7 @@ class Validator extends \Magento\Framework\Model\AbstractModel
protected $_catalogData = null;
/**
- * @var \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory
+ * @var CollectionFactory
*/
protected $_collectionFactory;
@@ -97,10 +100,15 @@ class Validator extends \Magento\Framework\Model\AbstractModel
*/
protected $counter = 0;
+ /**
+ * @var CartFixedDiscount
+ */
+ private $cartFixedDiscountHelper;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
- * @param \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory
+ * @param CollectionFactory $collectionFactory
* @param \Magento\Catalog\Helper\Data $catalogData
* @param Utility $utility
* @param RulesApplier $rulesApplier
@@ -110,12 +118,13 @@ class Validator extends \Magento\Framework\Model\AbstractModel
* @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
+ * @param CartFixedDiscount|null $cartFixedDiscount
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry,
- \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory,
+ CollectionFactory $collectionFactory,
\Magento\Catalog\Helper\Data $catalogData,
\Magento\SalesRule\Model\Utility $utility,
\Magento\SalesRule\Model\RulesApplier $rulesApplier,
@@ -124,7 +133,8 @@ public function __construct(
\Magento\Framework\Message\ManagerInterface $messageManager,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
+ array $data = [],
+ ?CartFixedDiscount $cartFixedDiscount = null
) {
$this->_collectionFactory = $collectionFactory;
$this->_catalogData = $catalogData;
@@ -133,6 +143,8 @@ public function __construct(
$this->priceCurrency = $priceCurrency;
$this->validators = $validators;
$this->messageManager = $messageManager;
+ $this->cartFixedDiscountHelper = $cartFixedDiscount ?:
+ ObjectManager::getInstance()->get(CartFixedDiscount::class);
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
@@ -158,6 +170,7 @@ public function init($websiteId, $customerGroupId, $couponCode)
*
* @param Address|null $address
* @return \Magento\SalesRule\Model\ResourceModel\Rule\Collection
+ * @throws \Zend_Db_Select_Exception
*/
protected function _getRules(Address $address = null)
{
@@ -221,6 +234,7 @@ public function setSkipActionsValidation($flag)
*
* @param AbstractItem $item
* @return bool
+ * @throws \Zend_Db_Select_Exception
*/
public function canApplyRules(AbstractItem $item)
{
@@ -259,6 +273,7 @@ public function reset(Address $address)
*
* @param AbstractItem $item
* @return $this
+ * @throws \Zend_Db_Select_Exception
*/
public function process(AbstractItem $item)
{
@@ -288,6 +303,9 @@ public function process(AbstractItem $item)
* @param Address $address
* @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ * @throws \Zend_Db_Select_Exception
*/
public function processShippingAmount(Address $address)
{
@@ -313,6 +331,7 @@ public function processShippingAmount(Address $address)
case \Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION:
$rulePercent = max(0, 100 - $rule->getDiscountAmount());
// break is intentionally omitted
+ // no break
case \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION:
$discountAmount = ($shippingAmount - $address->getShippingDiscountAmount()) * $rulePercent / 100;
$baseDiscountAmount = ($baseShippingAmount -
@@ -331,7 +350,40 @@ public function processShippingAmount(Address $address)
$baseDiscountAmount = $rule->getDiscountAmount();
break;
case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION:
- // Shouldn't be proceed according to MAGETWO-96403
+ $cartRules = $address->getCartFixedRules();
+ $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore());
+ $isAppliedToShipping = (int) $rule->getApplyToShipping();
+ if (!isset($cartRules[$rule->getId()])) {
+ $cartRules[$rule->getId()] = $rule->getDiscountAmount();
+ }
+ if ($cartRules[$rule->getId()] > 0) {
+ $shippingAmount = $address->getShippingAmount() - $address->getShippingDiscountAmount();
+ $quoteBaseSubtotal = (float) $quote->getBaseSubtotal();
+ $isMultiShipping = $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote);
+ if ($isAppliedToShipping) {
+ $quoteBaseSubtotal = ($quote->getIsMultiShipping() && $isMultiShipping) ?
+ $this->cartFixedDiscountHelper->getQuoteTotalsForMultiShipping($quote) :
+ $this->cartFixedDiscountHelper->getQuoteTotalsForRegularShipping(
+ $address,
+ $quoteBaseSubtotal
+ );
+ $discountAmount = $this->cartFixedDiscountHelper->
+ getShippingDiscountAmount(
+ $rule,
+ $shippingAmount,
+ $quoteBaseSubtotal
+ );
+ $baseDiscountAmount = $discountAmount;
+ } else {
+ $discountAmount = min($shippingAmount, $quoteAmount);
+ $baseDiscountAmount = min(
+ $baseShippingAmount - $address->getBaseShippingDiscountAmount(),
+ $cartRules[$rule->getId()]
+ );
+ }
+ $cartRules[$rule->getId()] -= $baseDiscountAmount;
+ }
+ $address->setCartFixedRules($cartRules);
break;
}
@@ -363,6 +415,8 @@ public function processShippingAmount(Address $address)
* @param mixed $items
* @param Address $address
* @return $this
+ * @throws \Zend_Validate_Exception
+ * @throws \Zend_Db_Select_Exception
*/
public function initTotals($items, Address $address)
{
@@ -505,6 +559,7 @@ public function prepareDescription($address, $separator = ', ')
* @param array $items
* @param Address $address
* @return array $items
+ * @throws \Zend_Db_Select_Exception
*/
public function sortItemsByPriority($items, Address $address = null)
{
@@ -560,6 +615,7 @@ public function decrementRuleItemTotalsCount($key)
*
* @param AbstractItem $item
* @return bool
+ * @throws \Zend_Validate_Exception
*/
public function canApplyDiscount(AbstractItem $item)
{
diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml
new file mode 100644
index 0000000000000..85437650efc35
--- /dev/null
+++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Open Cart Price Rule grid and delete all rules one by one. Need to avoid interference with other tests that test cart price rules.
+
+
+
+
+
+
+
+ {{AdminDataGridTableSection.firstNotEmptyRow}}
+ {{AdminConfirmationModalSection.ok}}
+ {{AdminMainActionsSection.delete}}
+ {{AdminMessagesSection.success}}
+ You deleted the rule.
+
+
+
+
+
diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml
index 24c3a7cd44bc8..9f4168575595a 100644
--- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml
+++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml
@@ -55,9 +55,11 @@
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml
index 09b45cd554056..b77cfaf02d232 100644
--- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml
+++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml
@@ -59,9 +59,11 @@
-
-
-
+
+
+
+
+
rule = $this->getMockBuilder(DataObject::class)
- ->setMockClassName('Rule')
- ->setMethods(null)
+ $this->rule = $this->getMockBuilder(Rule::class)
+ ->setMethods(['getId', 'getApplyToShipping'])
->disableOriginalConstructor()
->getMock();
$this->item = $this->createMock(AbstractItem::class);
@@ -83,11 +95,12 @@ protected function setUp(): void
$this->quote = $this->getMockBuilder(Quote::class)
->addMethods(['getCartFixedRules', 'setCartFixedRules'])
- ->onlyMethods(['getStore'])
+ ->onlyMethods(['getStore', 'getExtensionAttributes', 'isVirtual'])
->disableOriginalConstructor()
->getMock();
- $this->address = $this->createMock(
- Address::class
+ $this->address = $this->createPartialMock(
+ Address::class,
+ ['getShippingMethod']
);
$this->item->expects($this->any())->method('getQuote')->willReturn($this->quote);
$this->item->expects($this->any())->method('getAddress')->willReturn($this->address);
@@ -98,10 +111,24 @@ protected function setUp(): void
DataFactory::class,
['create']
);
- $dataFactory->expects($this->any())->method('create')->willReturn($this->data);
+ $dataFactory->method('create')->willReturn($this->data);
$this->priceCurrency = $this->getMockBuilder(PriceCurrencyInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['roundPrice'])
+ ->getMockForAbstractClass();
+ $this->deltaPriceRound = $this->getMockBuilder(DeltaPriceRound::class)
+ ->setMethods(['round'])
+ ->disableOriginalConstructor()
->getMock();
- $deltaPriceRound = $this->getMockBuilder(DeltaPriceRound::class)
+ $this->cartFixedDiscountHelper = $this->getMockBuilder(CartFixedDiscount::class)
+ ->setMethods([
+ 'calculateShippingAmountWhenAppliedToShipping',
+ 'getDiscountAmount',
+ 'checkMultiShippingQuote',
+ 'getQuoteTotalsForMultiShipping',
+ 'getQuoteTotalsForRegularShipping',
+ 'getBaseRuleTotals',
+ 'getAvailableDiscountAmount'])
->disableOriginalConstructor()
->getMock();
@@ -109,78 +136,170 @@ protected function setUp(): void
$this->validator,
$dataFactory,
$this->priceCurrency,
- $deltaPriceRound
+ $this->deltaPriceRound,
+ $this->cartFixedDiscountHelper
);
}
/**
* @covers \Magento\SalesRule\Model\Rule\Action\Discount\CartFixed::calculate
+ * @dataProvider dataProviderActions
+ * @param array $shipping
+ * @param array $ruleDetails
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- public function testCalculate()
+ public function testCalculate(array $shipping, array $ruleDetails): void
{
- $ruleItemTotals = [
- 'items_price' => 100,
- 'base_items_price' => 100,
- 'items_count' => 1,
- ];
+ $this->rule->setData(['id' => $ruleDetails['id'], 'discount_amount' => $ruleDetails['discounted_amount']]);
+ $this->rule
+ ->expects($this->any())
+ ->method('getId')
+ ->will(
+ $this->returnValue(
+ $ruleDetails['id']
+ )
+ );
+ $this->rule
+ ->expects($this->any())
+ ->method('getApplyToShipping')
+ ->will(
+ $this->returnValue(
+ $shipping['is_applied_to_shipping']
+ )
+ );
+ $this->cartFixedDiscountHelper
+ ->expects($this->any())
+ ->method('getDiscountAmount')
+ ->will(
+ $this->returnValue(
+ $ruleDetails['discounted_amount']
+ )
+ );
+ $cartExtensionMock = $this->getMockBuilder(CartExtensionInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getShippingAssignments'])
+ ->getMockForAbstractClass();
+ $this->quote->expects($this->any())->method('getCartFixedRules')->will($this->returnValue([]));
+ $store = $this->createMock(Store::class);
+ $this->priceCurrency
+ ->expects($this->atLeastOnce())
+ ->method('convert')
+ ->willReturnArgument($ruleDetails['rounded_amount']);
+ $this->priceCurrency
+ ->expects($this->atLeastOnce())
+ ->method('roundPrice')
+ ->willReturnArgument($ruleDetails['rounded_amount']);
+ $this->deltaPriceRound
+ ->expects($this->any())
+ ->method('round')
+ ->willReturnArgument($ruleDetails['base_items_price']);
+ $this->quote->expects($this->any())->method('getStore')->will($this->returnValue($store));
+ $this->quote->method('isVirtual')
+ ->willReturn(false);
+ $this->quote->method('getExtensionAttributes')
+ ->willReturn($cartExtensionMock);
- $this->rule->setData(['id' => 1, 'discount_amount' => 10.0]);
+ $cartExtensionMock->method('getShippingAssignments')
+ ->willReturn($shipping['shipping_assignment']);
- $this->quote->expects($this->any())->method('getCartFixedRules')->willReturn([]);
- $store = $this->createMock(Store::class);
- $this->priceCurrency->expects($this->atLeastOnce())->method('convert')->willReturnArgument(0);
- $this->priceCurrency->expects($this->atLeastOnce())->method('round')->willReturnArgument(0);
- $this->quote->expects($this->any())->method('getStore')->willReturn($store);
+ $this->address
+ ->expects($this->once())
+ ->method('getShippingMethod')
+ ->will(
+ $this->returnValue(
+ $shipping['shipping_method']
+ )
+ );
- $this->validator->expects($this->once())
+ /** validators data */
+ $this->validator
+ ->expects($this->once())
+ ->method('getItemPrice')
+ ->with($this->item)
+ ->will($this->returnValue($ruleDetails['items_price']));
+ $this->validator
+ ->expects($this->once())
+ ->method('getItemBasePrice')
+ ->with($this->item)
+ ->will($this->returnValue($ruleDetails['base_items_price']));
+ $this->validator
+ ->expects($this->once())
+ ->method('getItemOriginalPrice')
+ ->with($this->item)
+ ->will($this->returnValue($ruleDetails['items_price']));
+ $this->validator
+ ->expects($this->once())
+ ->method('getItemBaseOriginalPrice')
+ ->with($this->item)
+ ->will($this->returnValue($ruleDetails['items_price']));
+ $this->validator
+ ->expects($this->once())
->method('getRuleItemTotalsInfo')
->with($this->rule->getId())
- ->willReturn($ruleItemTotals);
+ ->will($this->returnValue($ruleDetails));
- /** validators data */
- $this->validator->expects(
- $this->once()
- )->method(
- 'getItemPrice'
- )->with(
- $this->item
- )->willReturn(
- 100
- );
- $this->validator->expects(
- $this->once()
- )->method(
- 'getItemBasePrice'
- )->with(
- $this->item
- )->willReturn(
- 100
- );
- $this->validator->expects(
- $this->once()
- )->method(
- 'getItemOriginalPrice'
- )->with(
- $this->item
- )->willReturn(
- 100
- );
- $this->validator->expects(
- $this->once()
- )->method(
- 'getItemBaseOriginalPrice'
- )->with(
- $this->item
- )->willReturn(
- 100
- );
+ $this->quote->expects($this->once())->method('setCartFixedRules')->with([1 => $ruleDetails['cart_rules']]);
+ $this->model->calculate($this->rule, $this->item, $ruleDetails['items_count']);
- $this->quote->expects($this->once())->method('setCartFixedRules')->with([1 => 0.0]);
- $this->model->calculate($this->rule, $this->item, 1);
+ $this->assertEquals($this->data->getAmount(), $ruleDetails['base_items_price']);
+ $this->assertEquals($this->data->getBaseAmount(), $ruleDetails['base_items_price']);
+ $this->assertEquals($this->data->getOriginalAmount(), $ruleDetails['base_items_price']);
+ $this->assertEquals($this->data->getBaseOriginalAmount(), $ruleDetails['items_price']);
+ }
+
+ /**
+ * @return array
+ */
+ public static function dataProviderActions()
+ {
+ return [
+ 'regular shipping with single item and single shipping' => [
+ [
+ 'shipping_method' => 'flatrate_flatrate',
+ 'is_applied_to_shipping' => 0,
+ 'shipping_assignment' => ['test_assignment_1']
+ ],
+ [ 'id' => 1,
+ 'base_items_price' => 10.0,
+ 'items_price' => 100.0,
+ 'items_count' => 1,
+ 'rounded_amount' => 0.0,
+ 'discounted_amount' => 10.0,
+ 'cart_rules' => 0.0
+ ]
+ ],
+ 'regular shipping with two items and single shipping' => [
+ [
+ 'shipping_method' => 'flatrate_flatrate',
+ 'is_applied_to_shipping' => 0,
+ 'shipping_assignment' => ['test_assignment_1']
+ ],
+ [ 'id' => 1,
+ 'base_items_price' => 10.0,
+ 'items_price' => 100.0,
+ 'items_count' => 2,
+ 'rounded_amount' => 0.0,
+ 'discounted_amount' => 10.0,
+ 'cart_rules' => 0.0
+ ]
+ ],
+ 'regular shipping with two items and multiple shipping' => [
+ [
+ 'shipping_method' => 'flatrate_flatrate',
+ 'is_applied_to_shipping' => 0,
+ 'shipping_assignment' => ['test_assignment_1', 'test_assignment_2']
+ ],
+ [ 'id' => 1,
+ 'base_items_price' => 10.0,
+ 'items_price' => 200.0,
+ 'items_count' => 2,
+ 'rounded_amount' => 0.0,
+ 'discounted_amount' => 10.0,
+ 'cart_rules' => 0.0
+ ]
+ ]
- $this->assertEquals($this->data->getAmount(), 10);
- $this->assertEquals($this->data->getBaseAmount(), 10);
- $this->assertEquals($this->data->getOriginalAmount(), 10);
- $this->assertEquals($this->data->getBaseOriginalAmount(), 100);
+ ];
}
}
diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php
index 8ba770e588924..4224cfafb3c8c 100644
--- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php
+++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php
@@ -8,6 +8,7 @@
namespace Magento\SalesRule\Test\Unit\Model;
use Magento\Catalog\Helper\Data;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Message\Manager;
use Magento\Framework\Message\ManagerInterface;
use Magento\Framework\Model\Context;
@@ -15,11 +16,14 @@
use Magento\Framework\Registry;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Framework\Validator\AbstractValidator;
+use Magento\Quote\Api\Data\CartExtensionInterface;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Model\Quote\Item;
use Magento\Quote\Model\Quote\Item\AbstractItem;
-use Magento\SalesRule\Model\ResourceModel\Rule\Collection;
+use Magento\Rule\Model\Action\Collection;
+use Magento\SalesRule\Helper\CartFixedDiscount;
+use Magento\SalesRule\Model\ResourceModel\Rule\Collection as RuleCollection;
use Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory;
use Magento\SalesRule\Model\Rule;
use Magento\SalesRule\Model\RulesApplier;
@@ -31,8 +35,9 @@
use PHPUnit\Framework\TestCase;
/**
- * Tests for Magento\SalesRule\Model\Validator
- * @@SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * Test sales rule model validator
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class ValidatorTest extends TestCase
{
@@ -72,7 +77,7 @@ class ValidatorTest extends TestCase
protected $utility;
/**
- * @var Collection|MockObject
+ * @var RuleCollection|MockObject
*/
protected $ruleCollection;
@@ -91,6 +96,11 @@ class ValidatorTest extends TestCase
*/
private $priceCurrency;
+ /**
+ * @var CartFixedDiscount|MockObject
+ */
+ private $cartFixedDiscountHelper;
+
protected function setUp(): void
{
$this->helper = new ObjectManager($this);
@@ -128,7 +138,7 @@ protected function setUp(): void
$this->utility = $this->createMock(Utility::class);
$this->validators = $this->createPartialMock(Pool::class, ['getValidators']);
$this->messageManager = $this->createMock(Manager::class);
- $this->ruleCollection = $this->getMockBuilder(Collection::class)
+ $this->ruleCollection = $this->getMockBuilder(RuleCollection::class)
->disableOriginalConstructor()
->getMock();
$ruleCollectionFactoryMock = $this->prepareRuleCollectionMock($this->ruleCollection);
@@ -136,7 +146,18 @@ protected function setUp(): void
->disableOriginalConstructor()
->setMethods(['roundPrice'])
->getMockForAbstractClass();
-
+ $this->cartFixedDiscountHelper = $this->getMockBuilder(CartFixedDiscount::class)
+ ->setMethods([
+ 'calculateShippingAmountWhenAppliedToShipping',
+ 'getDiscountAmount',
+ 'getShippingDiscountAmount',
+ 'checkMultiShippingQuote',
+ 'getQuoteTotalsForMultiShipping',
+ 'getQuoteTotalsForRegularShipping',
+ 'getBaseRuleTotals',
+ 'getAvailableDiscountAmount'])
+ ->disableOriginalConstructor()
+ ->getMock();
/** @var Validator|MockObject $validator */
$this->model = $this->helper->getObject(
Validator::class,
@@ -149,7 +170,8 @@ protected function setUp(): void
'rulesApplier' => $this->rulesApplier,
'validators' => $this->validators,
'messageManager' => $this->messageManager,
- 'priceCurrency' => $this->priceCurrency
+ 'priceCurrency' => $this->priceCurrency,
+ 'cartFixedDiscountHelper' => $this->cartFixedDiscountHelper
]
);
$this->model->setWebsiteId(1);
@@ -169,6 +191,7 @@ protected function setUp(): void
/**
* @return Item|MockObject
+ * @throws LocalizedException
*/
protected function getQuoteItemMock()
{
@@ -206,9 +229,8 @@ public function testCanApplyRules()
);
$item = $this->getQuoteItemMock();
$rule = $this->createMock(Rule::class);
- $actionsCollection = $this->getMockBuilder(\Magento\Rule\Model\Action\Collection::class)->addMethods(
- ['validate']
- )
+ $actionsCollection = $this->getMockBuilder(Collection::class)
+ ->addMethods(['validate'])
->disableOriginalConstructor()
->getMock();
$actionsCollection->expects($this->any())
@@ -414,9 +436,8 @@ public function testInitTotalsCanApplyDiscount()
$this->utility->expects($this->once())->method('getItemQty')->willReturn(1);
$this->utility->expects($this->any())->method('canProcessRule')->willReturn(true);
- $actionsCollection = $this->getMockBuilder(\Magento\Rule\Model\Action\Collection::class)->addMethods(
- ['validate']
- )
+ $actionsCollection = $this->getMockBuilder(Collection::class)
+ ->addMethods(['validate'])
->disableOriginalConstructor()
->getMock();
$actionsCollection->expects($this->at(0))->method('validate')->with($item1)->willReturn(true);
@@ -517,10 +538,12 @@ public function testProcessShippingAmountProcessDisabled()
* @param int $ruleDiscount
* @param int $shippingDiscount
* @dataProvider dataProviderActions
+ * @throws \Zend_Db_Select_Exception
*/
public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount): void
{
- $shippingAmount = 5;
+ $shippingAmount = 5.0;
+ $quoteBaseSubTotal = 10.0;
$ruleMock = $this->getMockBuilder(Rule::class)
->disableOriginalConstructor()
@@ -552,7 +575,7 @@ public function testProcessShippingAmountActions($action, $ruleDiscount, $shippi
$this->model->getCouponCode()
);
- $addressMock = $this->setupAddressMock($shippingAmount);
+ $addressMock = $this->setupAddressMock($shippingAmount, $quoteBaseSubTotal);
self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock));
self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount());
@@ -573,11 +596,13 @@ public static function dataProviderActions()
}
/**
- * @param null|int $shippingAmount
+ * @param float $shippingAmount
+ * @param float $quoteBaseSubTotal
* @return MockObject
*/
- protected function setupAddressMock($shippingAmount = null)
+ protected function setupAddressMock($shippingAmount = 0.0, $quoteBaseSubTotal = 0.0)
{
+ $shippingAssignments = ['test_assignment_1'];
$storeMock = $this->getMockBuilder(Store::class)
->disableOriginalConstructor()
->setMethods([])
@@ -585,8 +610,18 @@ protected function setupAddressMock($shippingAmount = null)
$quoteMock = $this->getMockBuilder(Quote::class)
->disableOriginalConstructor()
- ->setMethods(['setAppliedRuleIds', 'getStore'])
+ ->setMethods([
+ 'setAppliedRuleIds',
+ 'getStore',
+ 'getBaseSubtotal',
+ 'getExtensionAttributes',
+ 'isVirtual'
+ ])
->getMock();
+ $cartExtensionMock = $this->getMockBuilder(CartExtensionInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getShippingAssignments'])
+ ->getMockForAbstractClass();
$quoteMock->method('getStore')
->willReturn($storeMock);
@@ -594,6 +629,26 @@ protected function setupAddressMock($shippingAmount = null)
$quoteMock->method('setAppliedRuleIds')
->willReturnSelf();
+ $quoteMock->method('isVirtual')
+ ->willReturn(false);
+
+ $quoteMock->method('getBaseSubtotal')
+ ->willReturn($quoteBaseSubTotal);
+
+ $this->cartFixedDiscountHelper
+ ->method('getQuoteTotalsForRegularShipping')
+ ->willReturn($quoteBaseSubTotal);
+
+ $this->cartFixedDiscountHelper
+ ->method('getShippingDiscountAmount')
+ ->willReturn($shippingAmount);
+
+ $quoteMock->method('getExtensionAttributes')
+ ->willReturn($cartExtensionMock);
+
+ $cartExtensionMock->method('getShippingAssignments')
+ ->willReturn($shippingAssignments);
+
$this->addressMock->method('getShippingAmountForDiscount')
->willReturn($shippingAmount);
diff --git a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js
index dfb3f909345b3..6868617c1c449 100644
--- a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js
+++ b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js
@@ -22,9 +22,6 @@ define([
*/
toggleDisabled: function (action) {
switch (action) {
- case 'cart_fixed':
- this.disabled(true);
- break;
default:
this.disabled(false);
}
diff --git a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js
index 13b701c6fe65a..21f49fb3080fc 100644
--- a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js
+++ b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js
@@ -8,6 +8,9 @@ var config = {
mixins: {
'Magento_Checkout/js/action/select-payment-method': {
'Magento_SalesRule/js/action/select-payment-method-mixin': true
+ },
+ 'Magento_Checkout/js/model/shipping-save-processor': {
+ 'Magento_SalesRule/js/model/shipping-save-processor-mixin': true
}
}
}
diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js
new file mode 100644
index 0000000000000..193acb8eed2f4
--- /dev/null
+++ b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+define([
+ 'mage/utils/wrapper',
+ 'Magento_Checkout/js/model/quote',
+ 'Magento_SalesRule/js/model/coupon'
+], function (wrapper, quote, coupon) {
+ 'use strict';
+
+ return function (shippingSaveProcessor) {
+ shippingSaveProcessor.saveShippingInformation = wrapper.wrapSuper(
+ shippingSaveProcessor.saveShippingInformation,
+ function (type) {
+ var updateCouponCallback;
+
+ /**
+ * Update coupon form
+ */
+ updateCouponCallback = function () {
+ if (quote.totals() && !quote.totals()['coupon_code']) {
+ coupon.setCouponCode('');
+ coupon.setIsApplied(false);
+ }
+ };
+
+ return this._super(type).done(updateCouponCallback);
+ }
+ );
+
+ return shippingSaveProcessor;
+ };
+});
diff --git a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php
index 57a61fecae5ca..76159dc8320e1 100644
--- a/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php
+++ b/app/code/Magento/SampleData/Console/Command/SampleDataDeployCommand.php
@@ -7,63 +7,83 @@
namespace Magento\SampleData\Console\Command;
use Composer\Console\Application;
+use Composer\Console\ApplicationFactory;
+use Exception;
use Magento\Framework\App\Filesystem\DirectoryList;
+use Magento\Framework\Console\Cli;
+use Magento\Framework\Exception\FileSystemException;
+use Magento\Framework\Exception\InvalidArgumentException;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Filesystem;
+use Magento\Framework\Serialize\Serializer\Json;
+use Magento\SampleData\Model\Dependency;
use Magento\Setup\Model\PackagesAuth;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\ArrayInputFactory;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Command for deployment of Sample Data
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class SampleDataDeployCommand extends Command
{
const OPTION_NO_UPDATE = 'no-update';
/**
- * @var \Magento\Framework\Filesystem
+ * @var Filesystem
*/
private $filesystem;
/**
- * @var \Magento\SampleData\Model\Dependency
+ * @var Dependency
*/
private $sampleDataDependency;
/**
- * @var \Symfony\Component\Console\Input\ArrayInputFactory
+ * @var ArrayInputFactory
* @deprecated 100.1.0
*/
private $arrayInputFactory;
/**
- * @var \Composer\Console\ApplicationFactory
+ * @var ApplicationFactory
*/
private $applicationFactory;
/**
- * @param \Magento\Framework\Filesystem $filesystem
- * @param \Magento\SampleData\Model\Dependency $sampleDataDependency
- * @param \Symfony\Component\Console\Input\ArrayInputFactory $arrayInputFactory
- * @param \Composer\Console\ApplicationFactory $applicationFactory
+ * @var Json
+ */
+ private $serializer;
+
+ /**
+ * @param Filesystem $filesystem
+ * @param Dependency $sampleDataDependency
+ * @param ArrayInputFactory $arrayInputFactory
+ * @param ApplicationFactory $applicationFactory
+ * @param Json $serializer
*/
public function __construct(
- \Magento\Framework\Filesystem $filesystem,
- \Magento\SampleData\Model\Dependency $sampleDataDependency,
- \Symfony\Component\Console\Input\ArrayInputFactory $arrayInputFactory,
- \Composer\Console\ApplicationFactory $applicationFactory
+ Filesystem $filesystem,
+ Dependency $sampleDataDependency,
+ ArrayInputFactory $arrayInputFactory,
+ ApplicationFactory $applicationFactory,
+ Json $serializer
) {
$this->filesystem = $filesystem;
$this->sampleDataDependency = $sampleDataDependency;
$this->arrayInputFactory = $arrayInputFactory;
$this->applicationFactory = $applicationFactory;
+ $this->serializer = $serializer;
parent::__construct();
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function configure()
{
@@ -79,15 +99,42 @@ protected function configure()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int
+ * @throws FileSystemException
+ * @throws LocalizedException
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
- $rootJson = json_decode($this->filesystem->getDirectoryRead(DirectoryList::ROOT)->readFile("composer.json"));
- if (!isset($rootJson->version)) {
- // @codingStandardsIgnoreLine
- $output->writeln('' . 'Git installations must deploy sample data from GitHub; see https://devdocs.magento.com/guides/v2.3/install-gde/install/sample-data-after-clone.html for more information.' . ' ');
- return;
+ $rootJson = $this->serializer->unserialize(
+ $this->filesystem->getDirectoryRead(
+ DirectoryList::ROOT
+ )->readFile("composer.json")
+ );
+ if (!isset($rootJson['version'])) {
+ $magentoProductPackage = array_filter(
+ $rootJson['require'],
+ function ($package) {
+ return false !== strpos($package, 'magento/product-');
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ $version = reset($magentoProductPackage);
+ $output->writeln(
+ '' .
+ // @codingStandardsIgnoreLine
+ 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.' .
+ ' '
+ );
+ $restoreVersion = new ArrayInput([
+ 'command' => 'config',
+ 'setting-key' => 'version',
+ 'setting-value' => [$version],
+ '--quiet' => 1
+ ]);
}
$this->updateMemoryLimit();
$this->createAuthFile();
@@ -109,6 +156,12 @@ protected function execute(InputInterface $input, OutputInterface $output)
/** @var Application $application */
$application = $this->applicationFactory->create();
$application->setAutoExit(false);
+ if (!empty($restoreVersion)) {
+ $result = $application->run($restoreVersion, clone $output);
+ if ($result === 0) {
+ $output->writeln('The field "version" has been restored. ');
+ }
+ }
$result = $application->run($commandInput, $output);
if ($result !== 0) {
$output->writeln(
@@ -116,9 +169,15 @@ protected function execute(InputInterface $input, OutputInterface $output)
. ''
);
$application->resetComposer();
+
+ return Cli::RETURN_FAILURE;
}
+
+ return Cli::RETURN_SUCCESS;
} else {
$output->writeln('' . 'There is no sample data for current set of modules.' . ' ');
+
+ return Cli::RETURN_FAILURE;
}
}
@@ -128,7 +187,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
* We create auth.json with correct permissions instead of relying on Composer.
*
* @return void
- * @throws \Exception
+ * @throws LocalizedException
*/
private function createAuthFile()
{
@@ -137,30 +196,51 @@ private function createAuthFile()
if (!$directory->isExist(PackagesAuth::PATH_TO_AUTH_FILE)) {
try {
$directory->writeFile(PackagesAuth::PATH_TO_AUTH_FILE, '{}');
- } catch (\Exception $e) {
- $message = 'Error in writing Auth file '
- . $directory->getAbsolutePath(PackagesAuth::PATH_TO_AUTH_FILE)
- . '. Please check permissions for writing.';
- throw new \Exception($message);
+ } catch (Exception $e) {
+ throw new LocalizedException(__(
+ 'Error in writing Auth file %1. Please check permissions for writing.',
+ $directory->getAbsolutePath(PackagesAuth::PATH_TO_AUTH_FILE)
+ ));
}
}
}
/**
+ * Updates PHP memory limit
+ *
+ * @throws InvalidArgumentException
* @return void
*/
private function updateMemoryLimit()
{
if (function_exists('ini_set')) {
- @ini_set('display_errors', 1);
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ $result = ini_set('display_errors', 1);
+ if ($result === false) {
+ $error = error_get_last();
+ throw new InvalidArgumentException(__(
+ 'Failed to set ini option display_errors to value 1. %1',
+ $error['message']
+ ));
+ }
$memoryLimit = trim(ini_get('memory_limit'));
if ($memoryLimit != -1 && $this->getMemoryInBytes($memoryLimit) < 756 * 1024 * 1024) {
- @ini_set('memory_limit', '756M');
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ $result = ini_set('memory_limit', '756M');
+ if ($result === false) {
+ $error = error_get_last();
+ throw new InvalidArgumentException(__(
+ 'Failed to set ini option memory_limit to 756M. %1',
+ $error['message']
+ ));
+ }
}
}
}
/**
+ * Retrieves the memory size in bytes
+ *
* @param string $value
* @return int
*/
diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md
index c71439b929013..e0666ba73fe24 100644
--- a/app/code/Magento/SampleData/README.md
+++ b/app/code/Magento/SampleData/README.md
@@ -11,7 +11,7 @@ You can deploy sample data from one of the following sources:
* From the Magento composer repository, optionally using Magento CLI
* From the Magento GitHub repository
-If your Magento code base was cloned from the `master` branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch.
+If your Magento code base was cloned from the mainline branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch.
### Deploy Sample Data from Composer Repository
@@ -46,7 +46,7 @@ Each package corresponds to a sample data module. The complete list of available
To deploy sample data from the GitHub repository:
-1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the `master` branch, choose the `master` branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`.
+1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the mainline branch, choose the mainline branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`.
2. Link the sample data and your Magento instance by running: `# php -f /dev/tools/build-sample-data.php -- --ce-source=""`
## Install Sample Data
diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php
index 51235dbffc417..3bf664ea6b0d2 100644
--- a/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php
+++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/AbstractSampleDataCommandTest.php
@@ -20,10 +20,22 @@
use Symfony\Component\Console\Input\ArrayInputFactory;
/**
+ * Common class for tests
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
abstract class AbstractSampleDataCommandTest extends TestCase
{
+ /*
+ * Expected arguments for `composer config` to set missing field "version"
+ */
+ private const STUB_EXPECTED_COMPOSER_CONFIG = [
+ 'command' => 'config',
+ 'setting-key' => 'version',
+ 'setting-value' => ['0.0.1'],
+ '--quiet' => 1
+ ];
+
/**
* @var ReadInterface|MockObject
*/
@@ -60,8 +72,10 @@ abstract class AbstractSampleDataCommandTest extends TestCase
protected $applicationFactoryMock;
/**
- * @return void
+ * @var int
*/
+ private $appRunResult;
+
protected function setUp(): void
{
$this->directoryReadMock = $this->getMockForAbstractClass(ReadInterface::class);
@@ -74,47 +88,84 @@ protected function setUp(): void
}
/**
- * @param array $sampleDataPackages Array in form [package_name => version_constraint]
- * @param string $pathToComposerJson Fake path to composer.json
- * @param int $appRunResult Composer exit code
+ * @param array $sampleDataPackages Array in form [package_name => version_constraint]
+ * @param string $pathToComposerJson Fake path to composer.json
+ * @param int $appRunResult Composer exit code
+ * @param array $composerJsonContent Content of the composer.json
* @param array $additionalComposerArgs Additional arguments that composer expects
*/
protected function setupMocks(
$sampleDataPackages,
$pathToComposerJson,
$appRunResult,
+ $composerJsonContent = [],
$additionalComposerArgs = []
) {
- $this->directoryReadMock->expects($this->any())->method('getAbsolutePath')->willReturn($pathToComposerJson);
- $this->directoryReadMock->expects($this->any())->method('readFile')->with('composer.json')->willReturn(
- '{"version": "0.0.1"}'
- );
- $this->filesystemMock->expects($this->any())->method('getDirectoryRead')->with(DirectoryList::ROOT)->willReturn(
- $this->directoryReadMock
- );
- $this->sampleDataDependencyMock->expects($this->any())->method('getSampleDataPackages')->willReturn(
- $sampleDataPackages
- );
+ $this->appRunResult = $appRunResult;
+ $this->directoryReadMock->expects($this->any())
+ ->method('getAbsolutePath')
+ ->willReturn($pathToComposerJson);
+ $this->directoryReadMock->expects($this->any())
+ ->method('readFile')
+ ->with('composer.json')
+ ->willReturn(json_encode($composerJsonContent));
+ $this->filesystemMock->expects($this->any())
+ ->method('getDirectoryRead')
+ ->with(DirectoryList::ROOT)
+ ->willReturn($this->directoryReadMock);
+ $this->sampleDataDependencyMock->expects($this->any())
+ ->method('getSampleDataPackages')
+ ->willReturn($sampleDataPackages);
$this->arrayInputFactoryMock->expects($this->never())->method('create');
- $this->applicationMock->expects($this->any())
- ->method('run')
- ->with(
- new ArrayInput(
- array_merge(
- $this->expectedComposerArguments(
- $sampleDataPackages,
- $pathToComposerJson
+ if (!array_key_exists('version', $composerJsonContent)) {
+ $this->applicationMock->expects($this->any())
+ ->method('run')
+ ->withConsecutive(
+ [
+ 'input' => new ArrayInput(
+ self::STUB_EXPECTED_COMPOSER_CONFIG
),
- $additionalComposerArgs
- )
- ),
- $this->anything()
- )
- ->willReturn($appRunResult);
+ 'output' => $this->anything()
+ ],
+ [
+ 'input' => new ArrayInput(
+ array_merge(
+ $this->expectedComposerArgumentsSampleDataCommands(
+ $sampleDataPackages,
+ $pathToComposerJson
+ ),
+ $additionalComposerArgs
+ )
+ ),
+ 'output' => $this->anything()
+ ]
+ )->willReturnOnConsecutiveCalls(
+ $this->returnValue(0),
+ $this->returnValue($appRunResult)
+ );
+ } else {
+ $this->applicationMock->expects($this->any())
+ ->method('run')
+ ->with(
+ new ArrayInput(
+ array_merge(
+ $this->expectedComposerArgumentsSampleDataCommands(
+ $sampleDataPackages,
+ $pathToComposerJson
+ ),
+ $additionalComposerArgs
+ )
+ ),
+ $this->anything()
+ )
+ ->willReturn($appRunResult);
+ }
if (($appRunResult !== 0) && !empty($sampleDataPackages)) {
- $this->applicationMock->expects($this->once())->method('resetComposer')->willReturnSelf();
+ $this->applicationMock->expects($this->any())
+ ->method('resetComposer')
+ ->willReturnSelf();
}
$this->applicationFactoryMock->expects($this->any())
@@ -123,14 +174,14 @@ protected function setupMocks(
}
/**
- * Expected arguments for composer based on sample data packages and composer.json path
+ * Expected arguments for composer based on sample data command
*
* @param array $sampleDataPackages
* @param string $pathToComposerJson
* @return array
*/
- abstract protected function expectedComposerArguments(
+ abstract protected function expectedComposerArgumentsSampleDataCommands(
array $sampleDataPackages,
string $pathToComposerJson
- ) : array;
+ ): array;
}
diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php
index 45db83403b4f5..a1186d6015871 100644
--- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php
+++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataDeployCommandTest.php
@@ -7,26 +7,54 @@
namespace Magento\SampleData\Test\Unit\Console\Command;
+use Exception;
use Magento\Framework\App\Filesystem\DirectoryList;
+use Magento\Framework\Serialize\Serializer\Json;
use Magento\SampleData\Console\Command\SampleDataDeployCommand;
use Magento\Setup\Model\PackagesAuth;
+use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Tester\CommandTester;
class SampleDataDeployCommandTest extends AbstractSampleDataCommandTest
{
/**
+ * @var Json|MockObject
+ */
+ private $serializerMock;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->serializerMock = $this->createMock(Json::class);
+ }
+
+ /**
+ * Sets mock for unserialization composer content
+ * @param array $composerJsonContent
+ * @return void
+ */
+ protected function setupMockForSerializer(array $composerJsonContent): void
+ {
+ $this->serializerMock->expects($this->any())
+ ->method('unserialize')
+ ->will($this->returnValue($composerJsonContent));
+ }
+
+ /**
+ * Sets mocks for auth file
+ *
* @param bool $authExist True to test with existing auth.json, false without
+ * @return void
*/
- protected function setupMocksForAuthFile($authExist)
+ protected function setupMocksForAuthFile(bool $authExist): void
{
$this->directoryWriteMock->expects($this->once())
->method('isExist')
->with(PackagesAuth::PATH_TO_AUTH_FILE)
->willReturn($authExist);
- $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once())->method('writeFile')->with(
- PackagesAuth::PATH_TO_AUTH_FILE,
- '{}'
- );
+ $this->directoryWriteMock->expects($authExist ? $this->never() : $this->once())
+ ->method('writeFile')
+ ->with(PackagesAuth::PATH_TO_AUTH_FILE, '{}');
$this->filesystemMock->expects($this->once())
->method('getDirectoryWrite')
->with(DirectoryList::COMPOSER_HOME)
@@ -34,18 +62,30 @@ protected function setupMocksForAuthFile($authExist)
}
/**
- * @param array $sampleDataPackages
- * @param int $appRunResult - int 0 if everything went fine, or an error code
- * @param string $expectedMsg
- * @param bool $authExist
- * @return void
+ * @param array $sampleDataPackages
+ * @param int $appRunResult - int 0 if everything went fine, or an error code
+ * @param array $composerJsonContent
+ * @param string $expectedMsg
+ * @param bool $authExist
+ * @return void
*
* @dataProvider processDataProvider
*/
- public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist)
- {
- $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult);
+ public function testExecute(
+ array $sampleDataPackages,
+ int $appRunResult,
+ array $composerJsonContent,
+ string $expectedMsg,
+ bool $authExist
+ ): void {
+ $this->setupMocks(
+ $sampleDataPackages,
+ '/path/to/composer.json',
+ $appRunResult,
+ $composerJsonContent
+ );
$this->setupMocksForAuthFile($authExist);
+ $this->setupMockForSerializer($composerJsonContent);
$commandTester = $this->createCommandTester();
$commandTester->execute([]);
@@ -53,23 +93,31 @@ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedM
}
/**
- * @param array $sampleDataPackages
- * @param int $appRunResult - int 0 if everything went fine, or an error code
- * @param string $expectedMsg
- * @param bool $authExist
- * @return void
+ * @param array $sampleDataPackages
+ * @param int $appRunResult - int 0 if everything went fine, or an error code
+ * @param array $composerJsonContent
+ * @param string $expectedMsg
+ * @param bool $authExist
+ * @return void
*
* @dataProvider processDataProvider
*/
- public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg, $authExist)
- {
+ public function testExecuteWithNoUpdate(
+ array $sampleDataPackages,
+ int $appRunResult,
+ array $composerJsonContent,
+ string $expectedMsg,
+ bool $authExist
+ ): void {
$this->setupMocks(
$sampleDataPackages,
'/path/to/composer.json',
$appRunResult,
+ $composerJsonContent,
['--no-update' => 1]
);
$this->setupMocksForAuthFile($authExist);
+ $this->setupMockForSerializer($composerJsonContent);
$commandInput = ['--no-update' => 1];
$commandTester = $this->createCommandTester();
@@ -79,14 +127,20 @@ public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult
}
/**
+ * Data provider
+ *
* @return array
*/
- public function processDataProvider()
+ public function processDataProvider(): array
{
return [
'No sample data found' => [
'sampleDataPackages' => [],
'appRunResult' => 1,
+ 'composerJsonContent' => [
+ 'require' => ["magento/product-community-edition" => "0.0.1"],
+ 'version' => '0.0.1'
+ ],
'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL,
'authExist' => true,
],
@@ -95,15 +149,36 @@ public function processDataProvider()
'magento/module-cms-sample-data' => '1.0.0-beta',
],
'appRunResult' => 1,
+ 'composerJsonContent' => [
+ 'require' => ["magento/product-community-edition" => "0.0.1"],
+ 'version' => '0.0.1'
+ ],
'expectedMsg' => 'There is an error during sample data deployment. Composer file will be reverted.'
. PHP_EOL,
'authExist' => false,
],
+ 'Successful sample data installation without field "version"' => [
+ 'sampleDataPackages' => [
+ 'magento/module-cms-sample-data' => '1.0.0-beta',
+ ],
+ 'appRunResult' => 0,
+ 'composerJsonContent' => [
+ 'require' => ["magento/product-community-edition" => "0.0.1"]
+ ],
+ // @codingStandardsIgnoreLine
+ 'expectedMsg' => 'We don\'t recommend to remove the "version" field from your composer.json; see https://getcomposer.org/doc/02-libraries.md#library-versioning for more information.'
+ . PHP_EOL . 'The field "version" has been restored.' . PHP_EOL,
+ 'authExist' => true,
+ ],
'Successful sample data installation' => [
'sampleDataPackages' => [
'magento/module-cms-sample-data' => '1.0.0-beta',
],
'appRunResult' => 0,
+ 'composerJsonContent' => [
+ 'require' => ["magento/product-community-edition" => "0.0.1"],
+ 'version' => '0.0.1'
+ ],
'expectedMsg' => '',
'authExist' => true,
],
@@ -113,7 +188,7 @@ public function processDataProvider()
/**
* @return void
*/
- public function testExecuteWithException()
+ public function testExecuteWithException(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage(
@@ -122,12 +197,17 @@ public function testExecuteWithException()
$this->directoryReadMock->expects($this->once())
->method('readFile')
->with('composer.json')
- ->willReturn('{"version": "0.0.1"}');
+ ->willReturn('{"require": {"magento/product-community-edition": "0.0.1"}, "version": "0.0.1"}');
+ $this->serializerMock->expects($this->any())
+ ->method('unserialize')
+ ->will($this->returnValue([
+ 'require' => ["magento/product-community-edition" => "0.0.1"],
+ 'version' => '0.0.1'
+ ]));
$this->filesystemMock->expects($this->once())
->method('getDirectoryRead')
->with(DirectoryList::ROOT)
->willReturn($this->directoryReadMock);
-
$this->directoryWriteMock->expects($this->once())
->method('isExist')
->with(PackagesAuth::PATH_TO_AUTH_FILE)
@@ -135,7 +215,7 @@ public function testExecuteWithException()
$this->directoryWriteMock->expects($this->once())
->method('writeFile')
->with(PackagesAuth::PATH_TO_AUTH_FILE, '{}')
- ->willThrowException(new \Exception('Something went wrong...'));
+ ->willThrowException(new Exception('Something went wrong...'));
$this->directoryWriteMock->expects($this->once())
->method('getAbsolutePath')
->with(PackagesAuth::PATH_TO_AUTH_FILE)
@@ -153,15 +233,15 @@ public function testExecuteWithException()
*/
private function createCommandTester(): CommandTester
{
- $commandTester = new CommandTester(
+ return new CommandTester(
new SampleDataDeployCommand(
$this->filesystemMock,
$this->sampleDataDependencyMock,
$this->arrayInputFactoryMock,
- $this->applicationFactoryMock
+ $this->applicationFactoryMock,
+ $this->serializerMock
)
);
- return $commandTester;
}
/**
@@ -169,10 +249,10 @@ private function createCommandTester(): CommandTester
* @param $pathToComposerJson
* @return array
*/
- protected function expectedComposerArguments(
+ protected function expectedComposerArgumentsSampleDataCommands(
array $sampleDataPackages,
string $pathToComposerJson
- ) : array {
+ ): array {
return [
'command' => 'require',
'--working-dir' => $pathToComposerJson,
diff --git a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php
index cbb562ff10f25..9883100ce5c49 100644
--- a/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php
+++ b/app/code/Magento/SampleData/Test/Unit/Console/Command/SampleDataRemoveCommandTest.php
@@ -10,20 +10,32 @@
use Magento\SampleData\Console\Command\SampleDataRemoveCommand;
use Symfony\Component\Console\Tester\CommandTester;
+/**
+ * Tests for command `sampledata:remove`
+ */
class SampleDataRemoveCommandTest extends AbstractSampleDataCommandTest
{
-
/**
- * @param array $sampleDataPackages
- * @param int $appRunResult - int 0 if everything went fine, or an error code
- * @param string $expectedMsg
- * @return void
+ * @param array $sampleDataPackages
+ * @param int $appRunResult - int 0 if everything went fine, or an error code
+ * @param array $composerJsonContent
+ * @param string $expectedMsg
+ * @return void
*
* @dataProvider processDataProvider
*/
- public function testExecute(array $sampleDataPackages, $appRunResult, $expectedMsg)
- {
- $this->setupMocks($sampleDataPackages, '/path/to/composer.json', $appRunResult);
+ public function testExecute(
+ array $sampleDataPackages,
+ int $appRunResult,
+ array $composerJsonContent,
+ string $expectedMsg
+ ): void {
+ $this->setupMocks(
+ $sampleDataPackages,
+ '/path/to/composer.json',
+ $appRunResult,
+ $composerJsonContent
+ );
$commandTester = $this->createCommandTester();
$commandTester->execute([]);
@@ -31,19 +43,25 @@ public function testExecute(array $sampleDataPackages, $appRunResult, $expectedM
}
/**
- * @param array $sampleDataPackages
- * @param int $appRunResult - int 0 if everything went fine, or an error code
- * @param string $expectedMsg
- * @return void
+ * @param array $sampleDataPackages
+ * @param int $appRunResult - int 0 if everything went fine, or an error code
+ * @param array $composerJsonContent
+ * @param string $expectedMsg
+ * @return void
*
* @dataProvider processDataProvider
*/
- public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult, $expectedMsg)
- {
+ public function testExecuteWithNoUpdate(
+ array $sampleDataPackages,
+ int $appRunResult,
+ array $composerJsonContent,
+ string $expectedMsg
+ ): void {
$this->setupMocks(
$sampleDataPackages,
'/path/to/composer.json',
$appRunResult,
+ $composerJsonContent,
['--no-update' => 1]
);
$commandInput = ['--no-update' => 1];
@@ -55,32 +73,51 @@ public function testExecuteWithNoUpdate(array $sampleDataPackages, $appRunResult
}
/**
+ * Data provider
+ *
* @return array
*/
- public function processDataProvider()
+ public function processDataProvider(): array
{
return [
- 'No sample data found' => [
- 'sampleDataPackages' => [],
+ 'No sample data found in require' => [
+ 'sampleDataPackages' => [
+ 'magento/module-cms-sample-data' => '1.0.0-beta',
+ ],
'appRunResult' => 1,
- 'expectedMsg' => 'There is no sample data for current set of modules.' . PHP_EOL,
+ 'composerJsonContent' => [
+ "require" => [
+ "magento/product-community-edition" => "0.0.1",
+ ],
+ "version" => "0.0.1"
+ ],
+ 'expectedMsg' => 'There is an error during remove sample data.' . PHP_EOL,
],
- 'Successful sample data installation' => [
+ 'Successful sample data removing' => [
'sampleDataPackages' => [
'magento/module-cms-sample-data' => '1.0.0-beta',
],
'appRunResult' => 0,
+ 'composerJsonContent' => [
+ "require" => [
+ "magento/product-community-edition" => "0.0.1",
+ "magento/module-cms-sample-data" => "1.0.0-beta",
+ ],
+ "version" => "0.0.1"
+ ],
'expectedMsg' => '',
],
];
}
/**
+ * Creates command tester
+ *
* @return CommandTester
*/
private function createCommandTester(): CommandTester
{
- $commandTester = new CommandTester(
+ return new CommandTester(
new SampleDataRemoveCommand(
$this->filesystemMock,
$this->sampleDataDependencyMock,
@@ -88,15 +125,16 @@ private function createCommandTester(): CommandTester
$this->applicationFactoryMock
)
);
- return $commandTester;
}
/**
+ * Returns expected arguments for command `composer remove`
+ *
* @param $sampleDataPackages
* @param $pathToComposerJson
* @return array
*/
- protected function expectedComposerArguments(
+ protected function expectedComposerArgumentsSampleDataCommands(
array $sampleDataPackages,
string $pathToComposerJson
) : array {
diff --git a/app/code/Magento/Search/Model/SearchEngine/Validator.php b/app/code/Magento/Search/Model/SearchEngine/Validator.php
index f4fc8a9a62e0e..264e7c69dd520 100644
--- a/app/code/Magento/Search/Model/SearchEngine/Validator.php
+++ b/app/code/Magento/Search/Model/SearchEngine/Validator.php
@@ -22,7 +22,7 @@ class Validator implements ValidatorInterface
/**
* @var array
*/
- private $engineBlacklist = ['mysql' => 'MySQL'];
+ private $excludedEngineList = ['mysql' => 'MySQL'];
/**
* @var ValidatorInterface[]
@@ -32,16 +32,16 @@ class Validator implements ValidatorInterface
/**
* @param ScopeConfigInterface $scopeConfig
* @param array $engineValidators
- * @param array $engineBlacklist
+ * @param array $excludedEngineList
*/
public function __construct(
ScopeConfigInterface $scopeConfig,
array $engineValidators = [],
- array $engineBlacklist = []
+ array $excludedEngineList = []
) {
$this->scopeConfig = $scopeConfig;
$this->engineValidators = $engineValidators;
- $this->engineBlacklist = array_merge($this->engineBlacklist, $engineBlacklist);
+ $this->excludedEngineList = array_merge($this->excludedEngineList, $excludedEngineList);
}
/**
@@ -51,9 +51,9 @@ public function validate(): array
{
$errors = [];
$currentEngine = $this->scopeConfig->getValue('catalog/search/engine');
- if (isset($this->engineBlacklist[$currentEngine])) {
- $blacklistedEngine = $this->engineBlacklist[$currentEngine];
- $errors[] = "Your current search engine, '{$blacklistedEngine}', is not supported."
+ if (isset($this->excludedEngineList[$currentEngine])) {
+ $excludedEngine = $this->excludedEngineList[$currentEngine];
+ $errors[] = "Your current search engine, '{$excludedEngine}', is not supported."
. " You must install a supported search engine before upgrading."
. " See the System Upgrade Guide for more information.";
}
diff --git a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php
index c91c0fce9dd47..cc272ccb60162 100644
--- a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php
+++ b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php
@@ -34,7 +34,7 @@ protected function setUp(): void
[
'scopeConfig' => $this->scopeConfigMock,
'engineValidators' => ['otherEngine' => $this->otherEngineValidatorMock],
- 'engineBlacklist' => ['badEngine' => 'Bad Engine']
+ 'excludedEngineList' => ['badEngine' => 'Bad Engine']
]
);
}
@@ -54,7 +54,7 @@ public function testValidateValid()
$this->assertEquals($expectedErrors, $this->validator->validate());
}
- public function testValidateBlacklist()
+ public function testValidateExcludedList()
{
$this->scopeConfigMock
->expects($this->once())
diff --git a/app/code/Magento/Security/Model/Plugin/Auth.php b/app/code/Magento/Security/Model/Plugin/Auth.php
index 833b4e4c1b774..b388ef6115867 100644
--- a/app/code/Magento/Security/Model/Plugin/Auth.php
+++ b/app/code/Magento/Security/Model/Plugin/Auth.php
@@ -35,6 +35,8 @@ public function __construct(
}
/**
+ * Add warning message if other sessions terminated
+ *
* @param \Magento\Backend\Model\Auth $authModel
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
@@ -43,11 +45,13 @@ public function afterLogin(\Magento\Backend\Model\Auth $authModel)
{
$this->sessionsManager->processLogin();
if ($this->sessionsManager->getCurrentSession()->isOtherSessionsTerminated()) {
- $this->messageManager->addWarning(__('All other open sessions for this account were terminated.'));
+ $this->messageManager->addWarningMessage(__('All other open sessions for this account were terminated.'));
}
}
/**
+ * Handle logout process
+ *
* @param \Magento\Backend\Model\Auth $authModel
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml
index a75f65dffeca3..83e3479c753e4 100644
--- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml
@@ -18,8 +18,6 @@
-
-
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml
index 3d04f3eed4daf..3fffbcd480761 100644
--- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewRoleTest.xml
@@ -18,8 +18,6 @@
-
-
@@ -41,7 +39,7 @@
-
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml
similarity index 97%
rename from app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml
rename to app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml
index 3fb798521fb45..1421b589d5669 100644
--- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpiration.xml
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml
similarity index 97%
rename from app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml
rename to app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml
index 5d12650351bc0..9a9ae8f3872ba 100644
--- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpiration.xml
+++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php
index c431f1ecda332..dd86b3b574ead 100644
--- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php
+++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php
@@ -64,7 +64,7 @@ protected function setUp(): void
$this->messageManager = $this->getMockForAbstractClass(
ManagerInterface::class,
- ['addWarning'],
+ ['addWarningMessage'],
'',
false
);
@@ -100,7 +100,7 @@ public function testAfterLogin()
->method('isOtherSessionsTerminated')
->willReturn(true);
$this->messageManager->expects($this->once())
- ->method('addWarning')
+ ->method('addWarningMessage')
->with($warningMessage);
$this->model->afterLogin($this->authMock);
diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php
index ea5659cf909ff..21839e1057125 100644
--- a/app/code/Magento/Sitemap/Model/Sitemap.php
+++ b/app/code/Magento/Sitemap/Model/Sitemap.php
@@ -571,15 +571,15 @@ protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $pr
foreach ($images->getCollection() as $image) {
$row .= '';
$row .= '' . $this->_escaper->escapeUrl($image->getUrl()) . ' ';
- $row .= '' . $this->_escaper->escapeHtml($images->getTitle()) . ' ';
+ $row .= '' . $this->escapeXmlText($images->getTitle()) . ' ';
if ($image->getCaption()) {
- $row .= '' . $this->_escaper->escapeHtml($image->getCaption()) . ' ';
+ $row .= '' . $this->escapeXmlText($image->getCaption()) . ' ';
}
$row .= ' ';
}
// Add PageMap image for Google web search
$row .= '';
- $row .= ' ';
+ $row .= ' ';
$row .= ' ';
$row .= ' ';
}
@@ -587,6 +587,20 @@ protected function _getSitemapRow($url, $lastmod = null, $changefreq = null, $pr
return '' . $row . ' ';
}
+ /**
+ * Escape string for XML context.
+ *
+ * @param string $text
+ * @return string
+ */
+ private function escapeXmlText(string $text): string
+ {
+ $doc = new \DOMDocument('1.0', 'UTF-8');
+ $fragment = $doc->createDocumentFragment();
+ $fragment->appendChild($doc->createTextNode($text));
+ return $doc->saveXML($fragment);
+ }
+
/**
* Get sitemap index row
*
diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php
index d8f182adf7732..bfd2c47164cf6 100644
--- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php
+++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php
@@ -15,6 +15,8 @@
use Magento\Framework\Filesystem\Directory\Write as DirectoryWrite;
use Magento\Framework\Filesystem\File\Write;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Framework\Translate\InlineInterface;
+use Magento\Framework\ZendEscaper;
use Magento\Sitemap\Helper\Data;
use Magento\Sitemap\Model\ItemProvider\ConfigReaderInterface;
use Magento\Sitemap\Model\ItemProvider\ItemProviderInterface;
@@ -551,7 +553,7 @@ protected function getModelMock($mockBeforeSave = false)
new DataObject(
[
'url' => $storeBaseMediaUrl . 'i/m/image1.png',
- 'caption' => 'caption & > title < "'
+ 'caption' => 'Copyright © caption ™ & > title < "'
]
),
new DataObject(
@@ -610,7 +612,8 @@ private function getModelConstructorArgs()
$objectManager = new ObjectManager($this);
$escaper = $objectManager->getObject(Escaper::class);
-
+ $this->setPrivatePropertyValue($escaper, 'escaper', $objectManager->getObject(ZendEscaper::class));
+ $this->setPrivatePropertyValue($escaper, 'translateInline', $this->createMock(InlineInterface::class));
$constructArguments = $objectManager->getConstructArguments(
Sitemap::class,
[
@@ -796,4 +799,21 @@ public function getDocumentRootFromBaseDirUrlDataProvider(): array
],
];
}
+
+ /**
+ * @param mixed $object
+ * @param string $attributeName
+ * @param string $value
+ */
+ private function setPrivatePropertyValue($object, $attributeName, $value): void
+ {
+ $attribute = new \ReflectionProperty($object, $attributeName);
+ if ($attribute->isPublic()) {
+ $object->$attributeName = $value;
+ } else {
+ $attribute->setAccessible(true);
+ $attribute->setValue($object, $value);
+ $attribute->setAccessible(false);
+ }
+ }
}
diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml
index f31c390c04ec6..ff8087a52e42f 100644
--- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml
+++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml
@@ -15,7 +15,7 @@
http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png
Product & > title < "
- caption & > title < "
+ Copyright © caption ™ & > title < "
http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png
diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml
index 5118663d06372..93b9e159d4b04 100644
--- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml
+++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml
@@ -33,7 +33,7 @@
http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png
Product & > title < "
- caption & > title < "
+ Copyright © caption ™ & > title < "
http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png
diff --git a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php
index 537fec4c75df6..8f6011f1ae56f 100644
--- a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php
+++ b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php
@@ -6,7 +6,7 @@
namespace Magento\Store\Api\Data;
/**
- * StoreConfig interface
+ * Interface for store config
*
* @api
* @since 100.0.2
@@ -141,7 +141,7 @@ public function setWeightUnit($weightUnit);
public function getBaseUrl();
/**
- * set base URL
+ * Set base URL
*
* @param string $baseUrl
* @return $this
@@ -201,7 +201,7 @@ public function setBaseMediaUrl($baseMediaUrl);
public function getSecureBaseUrl();
/**
- * set secure base URL
+ * Set secure base URL
*
* @param string $secureBaseUrl
* @return $this
diff --git a/app/code/Magento/Store/Model/Data/StoreConfig.php b/app/code/Magento/Store/Model/Data/StoreConfig.php
index 6634e2cb05bd9..e68d98b162613 100644
--- a/app/code/Magento/Store/Model/Data/StoreConfig.php
+++ b/app/code/Magento/Store/Model/Data/StoreConfig.php
@@ -6,7 +6,7 @@
namespace Magento\Store\Model\Data;
/**
- * Class StoreConfig
+ * Allows to get and set store config values
*
* @codeCoverageIgnore
*/
@@ -188,7 +188,7 @@ public function getBaseUrl()
}
/**
- * set base URL
+ * Set base URL
*
* @param string $baseUrl
* @return $this
@@ -293,7 +293,7 @@ public function getSecureBaseUrl()
}
/**
- * set secure base URL
+ * Set secure base URL
*
* @param string $secureBaseUrl
* @return $this
@@ -367,7 +367,7 @@ public function setSecureBaseMediaUrl($secureBaseMediaUrl)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*
* @return \Magento\Store\Api\Data\StoreConfigExtensionInterface|null
*/
@@ -377,7 +377,7 @@ public function getExtensionAttributes()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*
* @param \Magento\Store\Api\Data\StoreConfigExtensionInterface $extensionAttributes
* @return $this
diff --git a/app/code/Magento/Store/Model/Service/StoreConfigManager.php b/app/code/Magento/Store/Model/Service/StoreConfigManager.php
index b3c2208a58361..debb08438a3b4 100644
--- a/app/code/Magento/Store/Model/Service/StoreConfigManager.php
+++ b/app/code/Magento/Store/Model/Service/StoreConfigManager.php
@@ -5,6 +5,9 @@
*/
namespace Magento\Store\Model\Service;
+/**
+ * Allows to get store config
+ */
class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterface
{
/**
@@ -53,6 +56,8 @@ public function __construct(
}
/**
+ * Get store configs
+ *
* @param string[] $storeCodes list of stores by store codes, will return all if storeCodes is not set
* @return \Magento\Store\Api\Data\StoreConfigInterface[]
*/
@@ -71,6 +76,8 @@ public function getStoreConfigs(array $storeCodes = null)
}
/**
+ * Get store config
+ *
* @param \Magento\Store\Model\Store $store
* @return \Magento\Store\Api\Data\StoreConfigInterface
*/
diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
new file mode 100644
index 0000000000000..4a403364a91e3
--- /dev/null
+++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Switch the Storefront to the provided Store.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml
index 5bd8f6e2349fc..2da9e91e1fddd 100644
--- a/app/code/Magento/Store/etc/di.xml
+++ b/app/code/Magento/Store/etc/di.xml
@@ -65,7 +65,6 @@
-
diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php
index 59f9831789a35..0baee00f468a0 100644
--- a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php
+++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php
@@ -55,11 +55,10 @@ public function __construct(
*/
public function getStoreConfigData(StoreInterface $store): array
{
- $storeConfigData = array_merge(
+ return array_merge(
$this->getBaseConfigData($store),
$this->getExtendedConfigData((int)$store->getId())
);
- return $storeConfigData;
}
/**
@@ -72,7 +71,7 @@ private function getBaseConfigData(StoreInterface $store) : array
{
$storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()]));
- $storeConfigData = [
+ return [
'id' => $storeConfig->getId(),
'code' => $storeConfig->getCode(),
'website_id' => $storeConfig->getWebsiteId(),
@@ -88,9 +87,9 @@ private function getBaseConfigData(StoreInterface $store) : array
'secure_base_url' => $storeConfig->getSecureBaseUrl(),
'secure_base_link_url' => $storeConfig->getSecureBaseLinkUrl(),
'secure_base_static_url' => $storeConfig->getSecureBaseStaticUrl(),
- 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl()
+ 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl(),
+ 'store_name' => $store->getName()
];
- return $storeConfigData;
}
/**
diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
index f3771b704c3e9..3a0143821d8b9 100644
--- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
@@ -23,11 +23,4 @@
-
-
-
- - store/information/name
-
-
-
diff --git a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php
index fc13372520945..9ba1083adab74 100644
--- a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php
+++ b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php
@@ -5,11 +5,17 @@
*/
namespace Magento\Swatches\Block\LayeredNavigation;
-use Magento\Eav\Model\Entity\Attribute;
+use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
+use Magento\Catalog\Model\Layer\Filter\Item as FilterItem;
use Magento\Catalog\Model\ResourceModel\Layer\Filter\AttributeFactory;
-use Magento\Framework\View\Element\Template;
+use Magento\Eav\Model\Entity\Attribute;
use Magento\Eav\Model\Entity\Attribute\Option;
-use Magento\Catalog\Model\Layer\Filter\Item as FilterItem;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\View\Element\Template;
+use Magento\Framework\View\Element\Template\Context;
+use Magento\Swatches\Helper\Data;
+use Magento\Swatches\Helper\Media;
+use Magento\Theme\Block\Html\Pager;
/**
* Class RenderLayered Render Swatches at Layered Navigation
@@ -37,7 +43,7 @@ class RenderLayered extends Template
protected $eavAttribute;
/**
- * @var \Magento\Catalog\Model\Layer\Filter\AbstractFilter
+ * @var AbstractFilter
*/
protected $filter;
@@ -47,41 +53,52 @@ class RenderLayered extends Template
protected $layerAttribute;
/**
- * @var \Magento\Swatches\Helper\Data
+ * @var Data
*/
protected $swatchHelper;
/**
- * @var \Magento\Swatches\Helper\Media
+ * @var Media
*/
protected $mediaHelper;
/**
- * @param Template\Context $context
+ * @var Pager
+ */
+ private $htmlPagerBlock;
+
+ /**
+ * @param Context $context
* @param Attribute $eavAttribute
* @param AttributeFactory $layerAttribute
- * @param \Magento\Swatches\Helper\Data $swatchHelper
- * @param \Magento\Swatches\Helper\Media $mediaHelper
+ * @param Data $swatchHelper
+ * @param Media $mediaHelper
* @param array $data
+ * @param Pager|null $htmlPagerBlock
*/
public function __construct(
- \Magento\Framework\View\Element\Template\Context $context,
+ Context $context,
Attribute $eavAttribute,
AttributeFactory $layerAttribute,
- \Magento\Swatches\Helper\Data $swatchHelper,
- \Magento\Swatches\Helper\Media $mediaHelper,
- array $data = []
+ Data $swatchHelper,
+ Media $mediaHelper,
+ array $data = [],
+ ?Pager $htmlPagerBlock = null
) {
$this->eavAttribute = $eavAttribute;
$this->layerAttribute = $layerAttribute;
$this->swatchHelper = $swatchHelper;
$this->mediaHelper = $mediaHelper;
+ $this->htmlPagerBlock = $htmlPagerBlock ?? ObjectManager::getInstance()->get(Pager::class);
parent::__construct($context, $data);
}
/**
+ * Set filter and attribute objects
+ *
* @param \Magento\Catalog\Model\Layer\Filter\AbstractFilter $filter
+ *
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
*/
@@ -94,6 +111,8 @@ public function setSwatchFilter(\Magento\Catalog\Model\Layer\Filter\AbstractFilt
}
/**
+ * Get attribute swatch data
+ *
* @return array
*/
public function getSwatchData()
@@ -114,30 +133,46 @@ public function getSwatchData()
$attributeOptionIds = array_keys($attributeOptions);
$swatches = $this->swatchHelper->getSwatchesByOptionsId($attributeOptionIds);
- $data = [
+ return [
'attribute_id' => $this->eavAttribute->getId(),
'attribute_code' => $this->eavAttribute->getAttributeCode(),
'attribute_label' => $this->eavAttribute->getStoreLabel(),
'options' => $attributeOptions,
'swatches' => $swatches,
];
-
- return $data;
}
/**
+ * Build filter option url
+ *
* @param string $attributeCode
* @param int $optionId
+ *
* @return string
*/
public function buildUrl($attributeCode, $optionId)
{
- $query = [$attributeCode => $optionId];
- return $this->_urlBuilder->getUrl('*/*/*', ['_current' => true, '_use_rewrite' => true, '_query' => $query]);
+ $query = [
+ $attributeCode => $optionId,
+ // exclude current page from urls
+ $this->htmlPagerBlock->getPageVarName() => null
+ ];
+
+ return $this->_urlBuilder->getUrl(
+ '*/*/*',
+ [
+ '_current' => true,
+ '_use_rewrite' => true,
+ '_query' => $query
+ ]
+ );
}
/**
+ * Get view data for option with no results
+ *
* @param Option $swatchOption
+ *
* @return array
*/
protected function getUnusedOption(Option $swatchOption)
@@ -150,8 +185,11 @@ protected function getUnusedOption(Option $swatchOption)
}
/**
+ * Get option data if visible
+ *
* @param FilterItem[] $filterItems
* @param Option $swatchOption
+ *
* @return array
*/
protected function getFilterOption(array $filterItems, Option $swatchOption)
@@ -166,8 +204,11 @@ protected function getFilterOption(array $filterItems, Option $swatchOption)
}
/**
+ * Get view data for option
+ *
* @param FilterItem $filterItem
* @param Option $swatchOption
+ *
* @return array
*/
protected function getOptionViewData(FilterItem $filterItem, Option $swatchOption)
@@ -187,15 +228,20 @@ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOptio
}
/**
+ * Check if option should be visible
+ *
* @param FilterItem $filterItem
+ *
* @return bool
*/
protected function isOptionVisible(FilterItem $filterItem)
{
- return $this->isOptionDisabled($filterItem) && $this->isShowEmptyResults() ? false : true;
+ return !($this->isOptionDisabled($filterItem) && $this->isShowEmptyResults());
}
/**
+ * Check if attribute values should be visible with no results
+ *
* @return bool
*/
protected function isShowEmptyResults()
@@ -204,7 +250,10 @@ protected function isShowEmptyResults()
}
/**
+ * Check if option should be disabled
+ *
* @param FilterItem $filterItem
+ *
* @return bool
*/
protected function isOptionDisabled(FilterItem $filterItem)
@@ -213,8 +262,11 @@ protected function isOptionDisabled(FilterItem $filterItem)
}
/**
+ * Retrieve filter item by id
+ *
* @param FilterItem[] $filterItems
* @param integer $id
+ *
* @return bool|FilterItem
*/
protected function getFilterItemById(array $filterItems, $id)
@@ -228,14 +280,15 @@ protected function getFilterItemById(array $filterItems, $id)
}
/**
+ * Get swatch image path
+ *
* @param string $type
* @param string $filename
+ *
* @return string
*/
public function getSwatchPath($type, $filename)
{
- $imagePath = $this->mediaHelper->getSwatchAttributeImage($type, $filename);
-
- return $imagePath;
+ return $this->mediaHelper->getSwatchAttributeImage($type, $filename);
}
}
diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml
index 97a391137d8e3..5f3ec07bd4983 100644
--- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml
+++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml
@@ -19,6 +19,7 @@
+
@@ -41,6 +42,7 @@
+
diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml
new file mode 100644
index 0000000000000..c6266e034bffc
--- /dev/null
+++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php
index 4056bf27f571e..06960c409b476 100644
--- a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php
+++ b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php
@@ -18,6 +18,7 @@
use Magento\Swatches\Block\LayeredNavigation\RenderLayered;
use Magento\Swatches\Helper\Data;
use Magento\Swatches\Helper\Media;
+use Magento\Theme\Block\Html\Pager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -28,35 +29,60 @@
*/
class RenderLayeredTest extends TestCase
{
- /** @var MockObject */
- protected $contextMock;
-
- /** @var MockObject */
- protected $requestMock;
-
- /** @var MockObject */
- protected $urlBuilder;
-
- /** @var MockObject */
- protected $eavAttributeMock;
-
- /** @var MockObject */
- protected $layerAttributeFactoryMock;
-
- /** @var MockObject */
- protected $layerAttributeMock;
-
- /** @var MockObject */
- protected $swatchHelperMock;
-
- /** @var MockObject */
- protected $mediaHelperMock;
-
- /** @var MockObject */
- protected $filterMock;
-
- /** @var MockObject */
- protected $block;
+ /**
+ * @var RenderLayered|MockObject
+ */
+ private $block;
+
+ /**
+ * @var Context|MockObject
+ */
+ private $contextMock;
+
+ /**
+ * @var RequestInterface|MockObject
+ */
+ private $requestMock;
+
+ /**
+ * @var Url|MockObject
+ */
+ private $urlBuilder;
+
+ /**
+ * @var Attribute|MockObject
+ */
+ private $eavAttributeMock;
+
+ /**
+ * @var AttributeFactory|MockObject
+ */
+ private $layerAttributeFactoryMock;
+
+ /**
+ * @var \Magento\Catalog\Model\ResourceModel\Layer\Filter\Attribute|MockObject
+ */
+ private $layerAttributeMock;
+
+ /**
+ * @var Data|MockObject
+ */
+ private $swatchHelperMock;
+
+ /**
+ * @var Media|MockObject
+ */
+ private $mediaHelperMock;
+
+ /**
+ * @var AbstractFilter|MockObject
+ */
+ private $filterMock;
+
+ /**
+ * @var Pager|MockObject
+ */
+ private $htmlBlockPagerMock;
protected function setUp(): void
{
@@ -66,8 +92,8 @@ protected function setUp(): void
Url::class,
['getCurrentUrl', 'getRedirectUrl', 'getUrl']
);
- $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock);
- $this->contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilder);
+ $this->contextMock->method('getRequest')->willReturn($this->requestMock);
+ $this->contextMock->method('getUrlBuilder')->willReturn($this->urlBuilder);
$this->eavAttributeMock = $this->createMock(Attribute::class);
$this->layerAttributeFactoryMock = $this->createPartialMock(
AttributeFactory::class,
@@ -80,6 +106,7 @@ protected function setUp(): void
$this->swatchHelperMock = $this->createMock(Data::class);
$this->mediaHelperMock = $this->createMock(Media::class);
$this->filterMock = $this->createMock(AbstractFilter::class);
+ $this->htmlBlockPagerMock = $this->createMock(Pager::class);
$this->block = $this->getMockBuilder(RenderLayered::class)
->setMethods(['filter', 'eavAttribute'])
@@ -91,6 +118,7 @@ protected function setUp(): void
$this->swatchHelperMock,
$this->mediaHelperMock,
[],
+ $this->htmlBlockPagerMock
]
)
->getMock();
@@ -114,7 +142,7 @@ public function testGetSwatchData()
$item3 = $this->createMock(Item::class);
$item4 = $this->createMock(Item::class);
- $item1->expects($this->any())->method('__call')->withConsecutive(
+ $item1->method('__call')->withConsecutive(
['getValue'],
['getCount'],
['getValue'],
@@ -128,9 +156,9 @@ public function testGetSwatchData()
'Yellow'
);
- $item2->expects($this->any())->method('__call')->with('getValue')->willReturn('blue');
+ $item2->method('__call')->with('getValue')->willReturn('blue');
- $item3->expects($this->any())->method('__call')->withConsecutive(
+ $item3->method('__call')->withConsecutive(
['getValue'],
['getCount']
)->willReturnOnConsecutiveCalls(
@@ -138,7 +166,7 @@ public function testGetSwatchData()
0
);
- $item4->expects($this->any())->method('__call')->withConsecutive(
+ $item4->method('__call')->withConsecutive(
['getValue'],
['getCount'],
['getValue'],
@@ -162,22 +190,22 @@ public function testGetSwatchData()
$this->block->method('filter')->willReturn($this->filterMock);
$option1 = $this->createMock(Option::class);
- $option1->expects($this->any())->method('getValue')->willReturn('yellow');
+ $option1->method('getValue')->willReturn('yellow');
$option2 = $this->createMock(Option::class);
- $option2->expects($this->any())->method('getValue')->willReturn(null);
+ $option2->method('getValue')->willReturn(null);
$option3 = $this->createMock(Option::class);
- $option3->expects($this->any())->method('getValue')->willReturn('red');
+ $option3->method('getValue')->willReturn('red');
$option4 = $this->createMock(Option::class);
- $option4->expects($this->any())->method('getValue')->willReturn('green');
+ $option4->method('getValue')->willReturn('green');
$eavAttribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class);
$eavAttribute->expects($this->once())
->method('getOptions')
->willReturn([$option1, $option2, $option3, $option4]);
- $eavAttribute->expects($this->any())->method('getIsFilterable')->willReturn(0);
+ $eavAttribute->method('getIsFilterable')->willReturn(0);
$this->filterMock->expects($this->once())->method('getAttributeModel')->willReturn($eavAttribute);
$this->block->method('eavAttribute')->willReturn($eavAttribute);
@@ -200,7 +228,7 @@ public function testGetSwatchDataException()
{
$this->block->method('filter')->willReturn($this->filterMock);
$this->block->setSwatchFilter($this->filterMock);
- $this->expectException('\RuntimeException');
+ $this->expectException(\RuntimeException::class);
$this->block->getSwatchData();
}
diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php
index 5798b94e31a70..764b2e9ca42f0 100644
--- a/app/code/Magento/Theme/Block/Html/Pager.php
+++ b/app/code/Magento/Theme/Block/Html/Pager.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Theme\Block\Html;
/**
@@ -466,7 +467,26 @@ public function getPageUrl($page)
*/
public function getLimitUrl($limit)
{
- return $this->getPagerUrl([$this->getLimitVarName() => $limit]);
+ return $this->getPagerUrl($this->getPageLimitParams($limit));
+ }
+
+ /**
+ * Return page limit params
+ *
+ * @param int $limit
+ * @return array
+ */
+ private function getPageLimitParams(int $limit): array
+ {
+ $data = [$this->getLimitVarName() => $limit];
+
+ $currentPage = $this->getCurrentPage();
+ $availableCount = (int) ceil($this->getTotalNum() / $limit);
+ if ($currentPage !== 1 && $availableCount < $currentPage) {
+ $data = array_merge($data, [$this->getPageVarName() => $availableCount === 1 ? null : $availableCount]);
+ }
+
+ return $data;
}
/**
diff --git a/app/code/Magento/Theme/Model/Config/Customization.php b/app/code/Magento/Theme/Model/Config/Customization.php
index 6a6872d794b1b..7430730451110 100644
--- a/app/code/Magento/Theme/Model/Config/Customization.php
+++ b/app/code/Magento/Theme/Model/Config/Customization.php
@@ -5,23 +5,34 @@
*/
namespace Magento\Theme\Model\Config;
+use Magento\Framework\App\Area;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\View\Design\Theme\ThemeProviderInterface;
+use Magento\Framework\View\Design\ThemeInterface;
+use Magento\Framework\View\DesignInterface;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Theme\Model\ResourceModel\Theme\Collection;
+use Magento\Theme\Model\Theme\StoreThemesResolverInterface;
+use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver;
+
/**
* Theme customization config model
*/
class Customization
{
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $_storeManager;
/**
- * @var \Magento\Framework\View\DesignInterface
+ * @var DesignInterface
*/
protected $_design;
/**
- * @var \Magento\Framework\View\Design\Theme\ThemeProviderInterface
+ * @var ThemeProviderInterface
*/
protected $themeProvider;
@@ -40,20 +51,28 @@ class Customization
* @see self::_prepareThemeCustomizations()
*/
protected $_unassignedTheme;
+ /**
+ * @var StoreUserAgentThemeResolver|mixed|null
+ */
+ private $storeThemesResolver;
/**
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\View\DesignInterface $design
- * @param \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider
+ * @param StoreManagerInterface $storeManager
+ * @param DesignInterface $design
+ * @param ThemeProviderInterface $themeProvider
+ * @param StoreThemesResolverInterface|null $storeThemesResolver
*/
public function __construct(
- \Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\View\DesignInterface $design,
- \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider
+ StoreManagerInterface $storeManager,
+ DesignInterface $design,
+ ThemeProviderInterface $themeProvider,
+ ?StoreThemesResolverInterface $storeThemesResolver = null
) {
$this->_storeManager = $storeManager;
$this->_design = $design;
$this->themeProvider = $themeProvider;
+ $this->storeThemesResolver = $storeThemesResolver
+ ?? ObjectManager::getInstance()->get(StoreThemesResolverInterface::class);
}
/**
@@ -93,13 +112,14 @@ public function getStoresByThemes()
{
$storesByThemes = [];
$stores = $this->_storeManager->getStores();
- /** @var $store \Magento\Store\Model\Store */
+ /** @var $store Store */
foreach ($stores as $store) {
- $themeId = $this->_getConfigurationThemeId($store);
- if (!isset($storesByThemes[$themeId])) {
- $storesByThemes[$themeId] = [];
+ foreach ($this->storeThemesResolver->getThemes($store) as $themeId) {
+ if (!isset($storesByThemes[$themeId])) {
+ $storesByThemes[$themeId] = [];
+ }
+ $storesByThemes[$themeId][] = $store;
}
- $storesByThemes[$themeId][] = $store;
}
return $storesByThemes;
}
@@ -107,8 +127,8 @@ public function getStoresByThemes()
/**
* Check if current theme has assigned to any store
*
- * @param \Magento\Framework\View\Design\ThemeInterface $theme
- * @param null|\Magento\Store\Model\Store $store
+ * @param ThemeInterface $theme
+ * @param null|Store $store
* @return bool
*/
public function isThemeAssignedToStore($theme, $store = null)
@@ -133,8 +153,8 @@ public function hasThemeAssigned()
/**
* Is theme assigned to specific store
*
- * @param \Magento\Framework\View\Design\ThemeInterface $theme
- * @param \Magento\Store\Model\Store $store
+ * @param ThemeInterface $theme
+ * @param Store $store
* @return bool
*/
protected function _isThemeAssignedToSpecificStore($theme, $store)
@@ -145,21 +165,21 @@ protected function _isThemeAssignedToSpecificStore($theme, $store)
/**
* Get configuration theme id
*
- * @param \Magento\Store\Model\Store $store
+ * @param Store $store
* @return int
*/
protected function _getConfigurationThemeId($store)
{
return $this->_design->getConfigurationDesignTheme(
- \Magento\Framework\App\Area::AREA_FRONTEND,
+ Area::AREA_FRONTEND,
['store' => $store]
);
}
/**
* Fetch theme customization and sort them out to arrays:
- * self::_assignedTheme and self::_unassignedTheme.
*
+ * Set self::_assignedTheme and self::_unassignedTheme.
* NOTE: To get into "assigned" list theme customization not necessary should be assigned to store-view directly.
* It can be set to website or as default theme and be used by store-view via config fallback mechanism.
*
@@ -167,15 +187,15 @@ protected function _getConfigurationThemeId($store)
*/
protected function _prepareThemeCustomizations()
{
- /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection */
- $themeCollection = $this->themeProvider->getThemeCustomizations(\Magento\Framework\App\Area::AREA_FRONTEND);
+ /** @var Collection $themeCollection */
+ $themeCollection = $this->themeProvider->getThemeCustomizations(Area::AREA_FRONTEND);
$assignedThemes = $this->getStoresByThemes();
$this->_assignedTheme = [];
$this->_unassignedTheme = [];
- /** @var $theme \Magento\Framework\View\Design\ThemeInterface */
+ /** @var $theme ThemeInterface */
foreach ($themeCollection as $theme) {
if (isset($assignedThemes[$theme->getId()])) {
$theme->setAssignedStores($assignedThemes[$theme->getId()]);
diff --git a/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php
new file mode 100644
index 0000000000000..26bd5604294d1
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php
@@ -0,0 +1,90 @@
+design = $design;
+ $this->themeCollectionFactory = $themeCollectionFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $theme = $this->design->getConfigurationDesignTheme(
+ Area::AREA_FRONTEND,
+ ['store' => $store]
+ );
+ $themes = [];
+ if ($theme) {
+ if (!is_numeric($theme)) {
+ $registeredThemes = $this->getRegisteredThemes();
+ if (isset($registeredThemes[$theme])) {
+ $themes[] = $registeredThemes[$theme]->getId();
+ }
+ } else {
+ $themes[] = $theme;
+ }
+ }
+ return $themes;
+ }
+
+ /**
+ * Get system registered themes.
+ *
+ * @return ThemeInterface[]
+ */
+ private function getRegisteredThemes(): array
+ {
+ if ($this->registeredThemes === null) {
+ $this->registeredThemes = [];
+ /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $collection */
+ $collection = $this->themeCollectionFactory->create();
+ $themes = $collection->loadRegisteredThemes();
+ /** @var ThemeInterface $theme */
+ foreach ($themes as $theme) {
+ $this->registeredThemes[$theme->getCode()] = $theme;
+ }
+ }
+ return $this->registeredThemes;
+ }
+}
diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php
new file mode 100644
index 0000000000000..5be86c08f7c51
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php
@@ -0,0 +1,57 @@
+resolvers = $resolvers;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $themes = [];
+ foreach ($this->resolvers as $resolver) {
+ foreach ($resolver->getThemes($store) as $theme) {
+ $themes[] = $theme;
+ }
+ }
+ return array_values(array_unique($themes));
+ }
+}
diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php
new file mode 100644
index 0000000000000..bb2cd73300c02
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php
@@ -0,0 +1,24 @@
+scopeConfig = $scopeConfig;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $config = $this->scopeConfig->getValue(
+ self::XML_PATH_THEME_USER_AGENT,
+ ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ $rules = $config ? $this->serializer->unserialize($config) : [];
+ $themes = [];
+ if ($rules) {
+ $themes = array_values(array_unique(array_column($rules, 'value')));
+ }
+ return $themes;
+ }
+}
diff --git a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php
similarity index 88%
rename from lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php
rename to app/code/Magento/Theme/Plugin/LoadDesignPlugin.php
index 2cda49c43c2ce..c4f8d3a905d0f 100644
--- a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php
+++ b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php
@@ -4,7 +4,7 @@
* See COPYING.txt for license details.
*/
-namespace Magento\Framework\App\Action\Plugin;
+namespace Magento\Theme\Plugin;
use Magento\Framework\App\ActionInterface;
use Magento\Framework\Config\Dom\ValidationException;
@@ -21,12 +21,12 @@ class LoadDesignPlugin
/**
* @var DesignLoader
*/
- protected $_designLoader;
+ private $designLoader;
/**
* @var MessageManagerInterface
*/
- protected $messageManager;
+ private $messageManager;
/**
* @param DesignLoader $designLoader
@@ -36,7 +36,7 @@ public function __construct(
DesignLoader $designLoader,
MessageManagerInterface $messageManager
) {
- $this->_designLoader = $designLoader;
+ $this->designLoader = $designLoader;
$this->messageManager = $messageManager;
}
@@ -50,7 +50,7 @@ public function __construct(
public function beforeExecute(ActionInterface $subject)
{
try {
- $this->_designLoader->load();
+ $this->designLoader->load();
} catch (LocalizedException $e) {
if ($e->getPrevious() instanceof ValidationException) {
/** @var MessageInterface $message */
diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
index ac16c56b17f1b..fd0ef1db0219a 100644
--- a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
@@ -91,6 +91,60 @@ public function testGetPages(): void
$this->assertEquals($expectedPages, $this->pager->getPages());
}
+ /**
+ * Test get limit url.
+ *
+ * @dataProvider limitUrlDataProvider
+ *
+ * @param int $page
+ * @param int $size
+ * @param int $limit
+ * @param array $expectedParams
+ * @return void
+ */
+ public function testGetLimitUrl(int $page, int $size, int $limit, array $expectedParams): void
+ {
+ $expectedArray = [
+ '_current' => true,
+ '_escape' => true,
+ '_use_rewrite' => true,
+ '_fragment' => null,
+ '_query' => $expectedParams,
+ ];
+
+ $collectionMock = $this->createMock(Collection::class);
+ $collectionMock->expects($this->once())
+ ->method('getCurPage')
+ ->willReturn($page);
+ $collectionMock->expects($this->once())
+ ->method('getSize')
+ ->willReturn($size);
+ $this->setCollectionProperty($collectionMock);
+
+ $this->urlBuilderMock->expects($this->once())
+ ->method('getUrl')
+ ->with('*/*/*', $expectedArray);
+
+ $this->pager->getLimitUrl($limit);
+ }
+
+ /**
+ * DataProvider for testGetLimitUrl
+ *
+ * @return array
+ */
+ public function limitUrlDataProvider(): array
+ {
+ return [
+ [2, 21, 10, ['limit' => 10]],
+ [3, 21, 10, ['limit' => 10]],
+ [2, 21, 20, ['limit' => 20]],
+ [3, 21, 50, ['limit' => 50, 'p' => null]],
+ [2, 11, 20, ['limit' => 20, 'p' => null]],
+ [4, 40, 20, ['limit' => 20, 'p' => 2]],
+ ];
+ }
+
/**
* Set Collection
*
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
index 82678d4b4277d..438853b9935e6 100644
--- a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
@@ -13,9 +13,10 @@
use Magento\Framework\App\Area;
use Magento\Framework\DataObject;
use Magento\Framework\View\DesignInterface;
+use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Theme\Model\Config\Customization;
-use Magento\Theme\Model\ResourceModel\Theme\Collection;
+use Magento\Theme\Model\Theme\StoreThemesResolverInterface;
use Magento\Theme\Model\Theme\ThemeProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -32,47 +33,37 @@ class CustomizationTest extends TestCase
*/
protected $designPackage;
- /**
- * @var Collection
- */
- protected $themeCollection;
-
/**
* @var Customization
*/
protected $model;
/**
- * @var ThemeProvider|\PHPUnit\Framework\MockObject_MockBuilder
+ * @var ThemeProvider|MockObject
*/
protected $themeProviderMock;
+ /**
+ * @var StoreThemesResolverInterface|MockObject
+ */
+ private $storeThemesResolver;
protected function setUp(): void
{
- $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
- ->getMock();
- $this->designPackage = $this->getMockBuilder(DesignInterface::class)
- ->getMock();
- $this->themeCollection = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $collectionFactory = $this->getMockBuilder(\Magento\Theme\Model\ResourceModel\Theme\CollectionFactory::class)
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $collectionFactory->expects($this->any())->method('create')->willReturn($this->themeCollection);
+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock();
+ $this->designPackage = $this->getMockBuilder(DesignInterface::class)->getMock();
$this->themeProviderMock = $this->getMockBuilder(ThemeProvider::class)
->disableOriginalConstructor()
->setMethods(['getThemeCustomizations', 'getThemeByFullPath'])
->getMock();
+ $this->storeThemesResolver = $this->createMock(StoreThemesResolverInterface::class);
+
$this->model = new Customization(
$this->storeManager,
$this->designPackage,
- $this->themeProviderMock
+ $this->themeProviderMock,
+ $this->storeThemesResolver
);
}
@@ -84,13 +75,15 @@ protected function setUp(): void
*/
public function testGetAssignedThemeCustomizations()
{
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
-
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
+
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -108,13 +101,15 @@ public function testGetAssignedThemeCustomizations()
*/
public function testGetUnassignedThemeCustomizations()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -131,13 +126,15 @@ public function testGetUnassignedThemeCustomizations()
*/
public function testGetStoresByThemes()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$stores = $this->model->getStoresByThemes();
$this->assertArrayHasKey($this->getAssignedTheme()->getId(), $stores);
@@ -148,15 +145,17 @@ public function testGetStoresByThemes()
* @covers \Magento\Theme\Model\Config\Customization::_getConfigurationThemeId
* @covers \Magento\Theme\Model\Config\Customization::__construct
*/
- public function testIsThemeAssignedToDefaultStore()
+ public function testIsThemeAssignedToAnyStore()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -198,10 +197,10 @@ protected function getUnassignedTheme()
}
/**
- * @return DataObject
+ * @return StoreInterface|MockObject
*/
protected function getStore()
{
- return new DataObject(['id' => 55]);
+ return $this->createConfiguredMock(StoreInterface::class, ['getId' => 55]);
}
}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php
new file mode 100644
index 0000000000000..939b47a42ce85
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php
@@ -0,0 +1,115 @@
+createMock(CollectionFactory::class);
+ $this->design = $this->createMock(DesignInterface::class);
+ $this->model = new StoreDefaultThemeResolver(
+ $themeCollectionFactory,
+ $this->design
+ );
+ $registeredThemes = [];
+ $registeredThemes[] = $this->createConfiguredMock(
+ ThemeInterface::class,
+ [
+ 'getId' => 1,
+ 'getCode' => 'Magento/luma',
+ ]
+ );
+ $registeredThemes[] = $this->createConfiguredMock(
+ ThemeInterface::class,
+ [
+ 'getId' => 2,
+ 'getCode' => 'Magento/blank',
+ ]
+ );
+ $collection = $this->createMock(Collection::class);
+ $collection->method('getIterator')
+ ->willReturn(new ArrayIterator($registeredThemes));
+ $collection->method('loadRegisteredThemes')
+ ->willReturnSelf();
+ $themeCollectionFactory->method('create')
+ ->willReturn($collection);
+ }
+
+ /**
+ * Test that method returns default theme associated to given store.
+ *
+ * @param string|null $defaultTheme
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(?string $defaultTheme, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ $this->design->expects($this->once())
+ ->method('getConfigurationDesignTheme')
+ ->with(
+ Area::AREA_FRONTEND,
+ ['store' => $store]
+ )
+ ->willReturn($defaultTheme);
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ null,
+ []
+ ],
+ [
+ '1',
+ [1]
+ ],
+ [
+ 'Magento/blank',
+ [2]
+ ],
+ [
+ 'Magento/theme',
+ []
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php
new file mode 100644
index 0000000000000..b80ec4ae83887
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php
@@ -0,0 +1,115 @@
+resolvers = [];
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->model = new StoreThemesResolver($this->resolvers);
+ }
+
+ /**
+ * Test that constructor SHOULD throw an exception when resolver is not instance of StoreThemesResolverInterface.
+ */
+ public function testInvalidConstructorArguments(): void
+ {
+ $resolver = $this->createMock(StoreInterface::class);
+ $this->expectExceptionObject(
+ new \InvalidArgumentException(
+ sprintf(
+ 'Instance of %s is expected, got %s instead.',
+ StoreThemesResolverInterface::class,
+ get_class($resolver)
+ )
+ )
+ );
+ $this->model = new StoreThemesResolver(
+ [
+ $resolver
+ ]
+ );
+ }
+
+ /**
+ * Test that method returns aggregated themes from resolvers
+ *
+ * @param array $themes
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(array $themes, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ foreach ($this->resolvers as $key => $resolver) {
+ $resolver->expects($this->once())
+ ->method('getThemes')
+ ->willReturn($themes[$key]);
+ }
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ [
+ [],
+ [],
+ []
+ ],
+ []
+ ],
+ [
+ [
+ ['1'],
+ [],
+ ['1']
+ ],
+ ['1']
+ ],
+ [
+ [
+ ['1'],
+ ['2'],
+ ['1']
+ ],
+ ['1', '2']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php
new file mode 100644
index 0000000000000..1ef4b17ca6562
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php
@@ -0,0 +1,105 @@
+scopeConfig = $this->createMock(ScopeConfigInterface::class);
+ $this->serializer = new Json();
+ $this->model = new StoreUserAgentThemeResolver(
+ $this->scopeConfig,
+ $this->serializer
+ );
+ }
+
+ /**
+ * Test that method returns user-agent rules associated themes.
+ *
+ * @param array|null $config
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(?array $config, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ $this->scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->with('design/theme/ua_regexp', ScopeInterface::SCOPE_STORE, $store)
+ ->willReturn($config !== null ? $this->serializer->serialize($config) : $config);
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ null,
+ []
+ ],
+ [
+ [],
+ []
+ ],
+ [
+ [
+ [
+ 'search' => '\/Chrome\/i',
+ 'regexp' => '\/Chrome\/i',
+ 'value' => '1',
+ ],
+ ],
+ ['1']
+ ],
+ [
+ [
+ [
+ 'search' => '\/Chrome\/i',
+ 'regexp' => '\/Chrome\/i',
+ 'value' => '1',
+ ],
+ [
+ 'search' => '\/mozila\/i',
+ 'regexp' => '\/mozila\/i',
+ 'value' => '2',
+ ],
+ ],
+ ['1', '2']
+ ]
+ ];
+ }
+}
diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php
similarity index 80%
rename from lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php
rename to app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php
index 549d45a986cf0..4efcc584986d1 100644
--- a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php
@@ -3,15 +3,13 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-declare(strict_types=1);
-
-namespace Magento\Framework\App\Test\Unit\Action\Plugin;
+namespace Magento\Theme\Test\Unit\Plugin;
use Magento\Framework\App\Action\Action;
-use Magento\Framework\App\Action\Plugin\LoadDesignPlugin;
use Magento\Framework\App\ActionInterface;
use Magento\Framework\Message\ManagerInterface;
use Magento\Framework\View\DesignLoader;
+use Magento\Theme\Plugin\LoadDesignPlugin;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -26,7 +24,7 @@ public function testBeforeExecute()
$designLoaderMock = $this->createMock(DesignLoader::class);
/** @var MockObject|ManagerInterface $messageManagerMock */
- $messageManagerMock = $this->getMockForAbstractClass(ManagerInterface::class);
+ $messageManagerMock = $this->createMock(ManagerInterface::class);
$plugin = new LoadDesignPlugin($designLoaderMock, $messageManagerMock);
diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml
index 921e6bfc6ecf1..15107adb931c9 100644
--- a/app/code/Magento/Theme/etc/di.xml
+++ b/app/code/Magento/Theme/etc/di.xml
@@ -18,6 +18,7 @@
+
Magento\Framework\App\Cache\Type\Config
@@ -104,6 +105,9 @@
Magento\Store\Model\ScopeInterface::SCOPE_STORE
+
+
+
@@ -309,4 +313,12 @@
configured_design_cache
+
+
+
+ - Magento\Theme\Model\Theme\StoreDefaultThemeResolver
+ - Magento\Theme\Model\Theme\StoreUserAgentThemeResolver
+
+
+
diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js
index f5580461f7d9e..423ac707c6572 100644
--- a/app/code/Magento/Theme/view/base/requirejs-config.js
+++ b/app/code/Magento/Theme/view/base/requirejs-config.js
@@ -4,8 +4,8 @@
*/
var config = {
- 'waitSeconds': 0,
- 'map': {
+ waitSeconds: 0,
+ map: {
'*': {
'ko': 'knockoutjs/knockout',
'knockout': 'knockoutjs/knockout',
@@ -13,7 +13,7 @@ var config = {
'rjsResolver': 'mage/requirejs/resolver'
}
},
- 'shim': {
+ shim: {
'jquery/jquery-migrate': ['jquery'],
'jquery/jstree/jquery.hotkeys': ['jquery'],
'jquery/hover-intent': ['jquery'],
@@ -28,7 +28,7 @@ var config = {
},
'magnifier/magnifier': ['jquery']
},
- 'paths': {
+ paths: {
'jquery/validate': 'jquery/jquery.validate',
'jquery/hover-intent': 'jquery/jquery.hoverIntent',
'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-fp',
@@ -40,11 +40,11 @@ var config = {
'tinycolor': 'jquery/spectrum/tinycolor',
'jquery-ui-modules': 'jquery/ui-modules'
},
- 'deps': [
+ deps: [
'jquery/jquery-migrate'
],
- 'config': {
- 'mixins': {
+ config: {
+ mixins: {
'jquery/jstree/jquery.jstree': {
'mage/backend/jstree-mixin': true
},
@@ -52,7 +52,7 @@ var config = {
'jquery/patches/jquery': true
}
},
- 'text': {
+ text: {
'headers': {
'X-Requested-With': 'XMLHttpRequest'
}
@@ -60,6 +60,27 @@ var config = {
}
};
+/* eslint-disable max-depth */
+/**
+ * Adds polyfills only for browser contexts which prevents bundlers from including them.
+ */
+if (typeof window !== 'undefined' && window.document) {
+ /**
+ * Polyfill localStorage and sessionStorage for browsers that do not support them.
+ */
+ try {
+ if (!window.localStorage || !window.sessionStorage) {
+ throw new Error();
+ }
+
+ localStorage.setItem('storage_test', 1);
+ localStorage.removeItem('storage_test');
+ } catch (e) {
+ config.deps.push('mage/polyfill');
+ }
+}
+/* eslint-enable max-depth */
+
require(['jquery'], function ($) {
'use strict';
diff --git a/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml b/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml
index a4a10ef3f6ee9..96f8fbed4c041 100644
--- a/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml
+++ b/app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml
@@ -10,7 +10,6 @@
-
diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml
index 55798169cdf75..b42cabde6cd85 100644
--- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml
+++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml
@@ -14,7 +14,6 @@