-
= $block->escapeHtml(__('Please type the letters and numbers below')) ?>
+
+
+ = $block->escapeHtml(__('Please type the letters and numbers below')) ?>
+
-
+
data-validate="{required:true}"
+ id="captcha_= $block->escapeHtmlAttr($block->getFormId()) ?>"
+ autocomplete="off"/>
- isCaseSensitive()) :?>
-
- = $block->escapeHtml(__('Attention : Captcha is case sensitive.'), ['strong']) ?>
-
+ isCaseSensitive()):?>
+
= /* @noEscape */ $note ?>
diff --git a/app/code/Magento/Captcha/view/frontend/web/js/action/refresh.js b/app/code/Magento/Captcha/view/frontend/web/js/action/refresh.js
index c82278269ade..030a990ce2a8 100644
--- a/app/code/Magento/Captcha/view/frontend/web/js/action/refresh.js
+++ b/app/code/Magento/Captcha/view/frontend/web/js/action/refresh.js
@@ -4,18 +4,21 @@
*/
define([
- 'mage/storage'
-], function (storage) {
+ 'jquery', 'mage/url'
+], function ($, urlBuilder) {
'use strict';
return function (refreshUrl, formId, imageSource) {
- return storage.post(
- refreshUrl,
- JSON.stringify({
+ return $.ajax({
+ url: urlBuilder.build(refreshUrl),
+ type: 'POST',
+ async: false,
+ data: JSON.stringify({
'formId': formId
}),
- false
- ).done(
+ global: false,
+ contentType: 'application/json'
+ }).done(
function (response) {
if (response.imgSrc) {
imageSource(response.imgSrc);
diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js
index d79c42a71156..55f3782e78f7 100644
--- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js
+++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js
@@ -21,6 +21,7 @@ define([
},
dataScope: 'global',
currentCaptcha: null,
+ subscribedFormIds: [],
/**
* @return {*}
@@ -56,7 +57,7 @@ define([
*/
checkCustomerData: function (formId, captchaData, captcha) {
if (!_.isEmpty(captchaData) &&
- !_.isEmpty(captchaData)[formId] &&
+ !_.isEmpty(captchaData[formId]) &&
captchaData[formId].timestamp > captcha.timestamp
) {
if (!captcha.isRequired() && captchaData[formId].isRequired) {
@@ -74,9 +75,12 @@ define([
* @param {Object} captcha
*/
subscribeCustomerData: function (formId, captcha) {
- customerData.get('captcha').subscribe(function (captchaData) {
- this.checkCustomerData(formId, captchaData, captcha);
- }.bind(this));
+ if (this.subscribedFormIds.includes(formId) === false) {
+ this.subscribedFormIds.push(formId);
+ customerData.get('captcha').subscribe(function (captchaData) {
+ this.checkCustomerData(formId, captchaData, captcha);
+ }.bind(this));
+ }
},
/**
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php
index 89239a2e3e60..35f30ed8d3d6 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php
@@ -61,7 +61,7 @@ public function __construct(
\Magento\Framework\Data\FormFactory $formFactory,
Yesno $yesNo,
Data $eavData,
- array $disableScopeChangeList = ['sku'],
+ array $disableScopeChangeList = [],
array $data = []
) {
$this->_yesNo = $yesNo;
diff --git a/app/code/Magento/Catalog/Block/Navigation.php b/app/code/Magento/Catalog/Block/Navigation.php
index 1d4ad5a03619..be42f9df4965 100644
--- a/app/code/Magento/Catalog/Block/Navigation.php
+++ b/app/code/Magento/Catalog/Block/Navigation.php
@@ -152,6 +152,8 @@ public function getCacheKeyInfo()
$shortCacheId = array_values($shortCacheId);
$shortCacheId = implode('|', $shortCacheId);
+ // md5() here is not for cryptographic use.
+ // phpcs:ignore Magento2.Security.InsecureFunction
$shortCacheId = md5($shortCacheId);
$cacheId['category_path'] = $this->getCurrentCategoryKey();
diff --git a/app/code/Magento/Catalog/Block/Product/ImageFactory.php b/app/code/Magento/Catalog/Block/Product/ImageFactory.php
index d43a77695e36..1126429a7f9f 100644
--- a/app/code/Magento/Catalog/Block/Product/ImageFactory.php
+++ b/app/code/Magento/Catalog/Block/Product/ImageFactory.php
@@ -165,7 +165,7 @@ public function create(Product $product, string $imageId, array $attributes = nu
'image_url' => $imageAsset->getUrl(),
'width' => $imageMiscParams['image_width'],
'height' => $imageMiscParams['image_height'],
- 'label' => $this->getLabel($product, $imageMiscParams['image_type']),
+ 'label' => $this->getLabel($product, $imageMiscParams['image_type'] ?? ''),
'ratio' => $this->getRatio($imageMiscParams['image_width'] ?? 0, $imageMiscParams['image_height'] ?? 0),
'custom_attributes' => $this->filterCustomAttributes($attributes),
'class' => $this->getClass($attributes),
diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php
index b181a5392905..5b8cb6a19199 100644
--- a/app/code/Magento/Catalog/Block/Product/ListProduct.php
+++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Block\Product;
@@ -141,14 +142,7 @@ public function getLayer()
*/
public function getLoadedProductCollection()
{
- $collection = $this->_getProductCollection();
-
- $categoryId = $this->getLayer()->getCurrentCategory()->getId();
- foreach ($collection as $product) {
- $product->setData('category_id', $categoryId);
- }
-
- return $collection;
+ return $this->_getProductCollection();
}
/**
@@ -205,6 +199,14 @@ protected function _beforeToHtml()
$collection->load();
}
+ $categoryId = $this->getLayer()->getCurrentCategory()->getId();
+
+ if ($categoryId) {
+ foreach ($collection as $product) {
+ $product->setData('category_id', $categoryId);
+ }
+ }
+
return parent::_beforeToHtml();
}
@@ -461,7 +463,7 @@ private function initializeProductCollection()
// if the product is associated with any category
if ($categories->count()) {
// show products from this category
- $this->setCategoryId(current($categories->getIterator())->getId());
+ $this->setCategoryId($categories->getIterator()->current()->getId());
}
}
diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php
index 6cc565235215..e24b3d881bdf 100644
--- a/app/code/Magento/Catalog/Block/Product/View.php
+++ b/app/code/Magento/Catalog/Block/Product/View.php
@@ -177,21 +177,26 @@ public function getJsonConfig()
{
/* @var $product \Magento\Catalog\Model\Product */
$product = $this->getProduct();
+ $tierPrices = [];
+ $priceInfo = $product->getPriceInfo();
+ $tierPricesList = $priceInfo->getPrice('tier_price')->getTierPriceList();
+ foreach ($tierPricesList as $tierPrice) {
+ $tierPriceData = [
+ 'qty' => $tierPrice['price_qty'],
+ 'price' => $tierPrice['website_price'],
+ ];
+ $tierPrices[] = $tierPriceData;
+ }
if (!$this->hasOptions()) {
$config = [
'productId' => $product->getId(),
- 'priceFormat' => $this->_localeFormat->getPriceFormat()
+ 'priceFormat' => $this->_localeFormat->getPriceFormat(),
+ 'tierPrices' => $tierPrices
];
return $this->_jsonEncoder->encode($config);
}
- $tierPrices = [];
- $priceInfo = $product->getPriceInfo();
- $tierPricesList = $priceInfo->getPrice('tier_price')->getTierPriceList();
- foreach ($tierPricesList as $tierPrice) {
- $tierPrices[] = $tierPrice['price']->getValue() * 1;
- }
$config = [
'productId' => (int)$product->getId(),
'priceFormat' => $this->_localeFormat->getPriceFormat(),
diff --git a/app/code/Magento/Catalog/Block/Product/View/Options.php b/app/code/Magento/Catalog/Block/Product/View/Options.php
index c457b20cd090..26c6bab80992 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Options.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Options.php
@@ -60,6 +60,11 @@ class Options extends \Magento\Framework\View\Element\Template
*/
protected $_catalogData;
+ /**
+ * @var \Magento\Framework\Stdlib\ArrayUtils
+ */
+ private $arrayUtils;
+
/**
* @param \Magento\Framework\View\Element\Template\Context $context
* @param \Magento\Framework\Pricing\Helper\Data $pricingHelper
@@ -93,7 +98,7 @@ public function __construct(
* Retrieve product object
*
* @return Product
- * @throws \LogicExceptions
+ * @throws \LogicException
*/
public function getProduct()
{
diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php
index af921959f8e2..0f8c978de649 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php
@@ -168,7 +168,10 @@ public function getTimeHtml()
$dayPartHtml = $this->_getHtmlSelect(
'day_part'
)->setOptions(
- ['am' => __('AM'), 'pm' => __('PM')]
+ [
+ 'am' => $this->escapeHtml(__('AM')),
+ 'pm' => $this->escapeHtml(__('PM'))
+ ]
)->getHtml();
}
$hoursHtml = $this->_getSelectFromToHtml('hour', $hourStart, $hourEnd);
diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php
index c5c08a0552f4..93732b1b52f0 100644
--- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php
+++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php
@@ -153,6 +153,7 @@ public function getCurrentProductData()
$this->productRenderCollectorComposite
->collect($product, $productRender);
$data = $this->hydrator->extract($productRender);
+ $data['is_available'] = $product->isAvailable();
$currentProductData = [
'items' => [
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
index 3651a9bc6ade..f71058a71519 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php
@@ -7,6 +7,7 @@
namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute;
use Magento\AsynchronousOperations\Api\Data\OperationInterface;
+use Magento\Catalog\Model\Product\Filter\DateTime as DateTimeFilter;
use Magento\Catalog\Model\ProductFactory;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Eav\Model\Config;
@@ -14,6 +15,7 @@
use Magento\Backend\App\Action;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
/**
@@ -67,6 +69,11 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut
*/
private $productFactory;
+ /**
+ * @var DateTimeFilter
+ */
+ private $dateTimeFilter;
+
/**
* @param Action\Context $context
* @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper
@@ -76,9 +83,10 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut
* @param \Magento\Framework\Serialize\SerializerInterface $serializer
* @param \Magento\Authorization\Model\UserContextInterface $userContext
* @param int $bulkSize
- * @param TimezoneInterface $timezone
- * @param Config $eavConfig
- * @param ProductFactory $productFactory
+ * @param TimezoneInterface|null $timezone
+ * @param Config|null $eavConfig
+ * @param ProductFactory|null $productFactory
+ * @param DateTimeFilter|null $dateTimeFilter
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -92,7 +100,8 @@ public function __construct(
int $bulkSize = 100,
TimezoneInterface $timezone = null,
Config $eavConfig = null,
- ProductFactory $productFactory = null
+ ProductFactory $productFactory = null,
+ ?DateTimeFilter $dateTimeFilter = null
) {
parent::__construct($context, $attributeHelper);
$this->bulkManagement = $bulkManagement;
@@ -106,6 +115,7 @@ public function __construct(
$this->eavConfig = $eavConfig ?: ObjectManager::getInstance()
->get(Config::class);
$this->productFactory = $productFactory ?? ObjectManager::getInstance()->get(ProductFactory::class);
+ $this->dateTimeFilter = $dateTimeFilter ?? ObjectManager::getInstance()->get(DateTimeFilter::class);
}
/**
@@ -155,8 +165,6 @@ public function execute()
*/
private function sanitizeProductAttributes($attributesData)
{
- $dateFormat = $this->timezone->getDateFormat(\IntlDateFormatter::SHORT);
-
foreach ($attributesData as $attributeCode => $value) {
if ($attributeCode === ProductAttributeInterface::CODE_HAS_WEIGHT) {
continue;
@@ -170,16 +178,10 @@ private function sanitizeProductAttributes($attributesData)
}
if ($attribute->getBackendType() === 'datetime') {
- if (!empty($value)) {
- $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]);
- $filterInternal = new \Zend_Filter_NormalizedToLocalized(
- ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT]
- );
- $value = $filterInternal->filter($filterInput->filter($value));
- } else {
- $value = null;
- }
- $attributesData[$attributeCode] = $value;
+ $attributesData[$attributeCode] = $this->filterDate(
+ $value,
+ $attribute->getFrontendInput() === 'datetime'
+ );
} elseif ($attribute->getFrontendInput() === 'multiselect') {
// Check if 'Change' checkbox has been checked by admin for this attribute
$isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode);
@@ -196,6 +198,24 @@ private function sanitizeProductAttributes($attributesData)
return $attributesData;
}
+ /**
+ * Get the date and time value in internal format and timezone
+ *
+ * @param string $value
+ * @param bool $isDatetime
+ * @return string|null
+ * @throws LocalizedException
+ */
+ private function filterDate(string $value, bool $isDatetime = false): ?string
+ {
+ $date = !empty($value) ? $this->dateTimeFilter->filter($value) : null;
+ if ($date && $isDatetime) {
+ $date = $this->timezone->convertConfigTimeToUtc($date, DateTime::DATETIME_PHP_FORMAT);
+ }
+
+ return $date;
+ }
+
/**
* Validate product attributes data.
*
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php
index d5fed1782bc6..7a6b71db9dde 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute.php
@@ -126,6 +126,8 @@ protected function generateCode($label)
);
$validatorAttrCode = new \Zend_Validate_Regex(['pattern' => '/^[a-z][a-z_0-9]{0,29}[a-z0-9]$/']);
if (!$validatorAttrCode->isValid($code)) {
+ // md5() here is not for cryptographic use.
+ // phpcs:ignore Magento2.Security.InsecureFunction
$code = 'attr_' . ($code ?: substr(md5(time()), 0, 8));
}
return $code;
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 4ca9d4b0d060..440716e45691 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
@@ -185,6 +185,9 @@ public function execute()
}
$attributeId = $this->getRequest()->getParam('attribute_id');
+ if (!empty($data['attribute_id']) && $data['attribute_id'] != $attributeId) {
+ $attributeId = $data['attribute_id'];
+ }
/** @var ProductAttributeInterface $model */
$model = $this->attributeFactory->create();
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php
index 1d6939acacfd..81ab67bdf26d 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php
@@ -102,6 +102,6 @@ private function isAttributeShouldNotBeUpdated(Product $product, array $useDefau
{
$considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === '1';
- return ($value === '' && $considerUseDefaultsAttribute && !$product->getData($attribute));
+ return ($value === '' && $considerUseDefaultsAttribute && ($product->getData($attribute) === null));
}
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php
index c779c01cd7d7..0edd43923060 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php
@@ -9,9 +9,12 @@
namespace Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Backend\App\Action\Context;
+use Magento\Backend\Model\View\Result\Redirect;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Exception\LocalizedException;
use Magento\Ui\Component\MassAction\Filter;
@@ -20,7 +23,7 @@
/**
* Class \Magento\Catalog\Controller\Adminhtml\Product\MassDelete
*/
-class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface
+class MassDelete extends Product implements HttpPostActionInterface
{
/**
* Massactions filter
@@ -49,8 +52,8 @@ class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product implement
* @param Builder $productBuilder
* @param Filter $filter
* @param CollectionFactory $collectionFactory
- * @param ProductRepositoryInterface $productRepository
- * @param LoggerInterface $logger
+ * @param ProductRepositoryInterface|null $productRepository
+ * @param LoggerInterface|null $logger
*/
public function __construct(
Context $context,
@@ -63,20 +66,23 @@ public function __construct(
$this->filter = $filter;
$this->collectionFactory = $collectionFactory;
$this->productRepository = $productRepository ?:
- \Magento\Framework\App\ObjectManager::getInstance()->create(ProductRepositoryInterface::class);
+ ObjectManager::getInstance()->create(ProductRepositoryInterface::class);
$this->logger = $logger ?:
- \Magento\Framework\App\ObjectManager::getInstance()->create(LoggerInterface::class);
+ ObjectManager::getInstance()->create(LoggerInterface::class);
parent::__construct($context, $productBuilder);
}
/**
* Mass Delete Action
*
- * @return \Magento\Backend\Model\View\Result\Redirect
+ * @return Redirect
+ * @throws LocalizedException
*/
public function execute()
{
$collection = $this->filter->getCollection($this->collectionFactory->create());
+ $collection->addMediaGalleryData();
+
$productDeleted = 0;
$productDeletedError = 0;
/** @var \Magento\Catalog\Model\Product $product */
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php
index 77c9cfcd40f0..035b50f3ada5 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php
@@ -12,6 +12,9 @@
use Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Framework\App\ObjectManager;
use Magento\Store\Model\StoreManagerInterface;
+use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException;
+use Magento\CatalogUrlRewrite\Model\Product\Validator as ProductUrlRewriteValidator;
+use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator;
/**
* Product validate
@@ -57,6 +60,16 @@ class Validate extends Product implements HttpPostActionInterface, HttpGetAction
*/
private $storeManager;
+ /**
+ * @var ProductUrlPathGenerator
+ */
+ private $productUrlPathGenerator;
+
+ /**
+ * @var ProductUrlRewriteValidator
+ */
+ private $productUrlRewriteValidator;
+
/**
* @param Action\Context $context
* @param Builder $productBuilder
@@ -65,6 +78,8 @@ class Validate extends Product implements HttpPostActionInterface, HttpGetAction
* @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
* @param \Magento\Framework\View\LayoutFactory $layoutFactory
* @param \Magento\Catalog\Model\ProductFactory $productFactory
+ * @param ProductUrlRewriteValidator $productUrlRewriteValidator
+ * @param ProductUrlPathGenerator $productUrlPathGenerator
*/
public function __construct(
\Magento\Backend\App\Action\Context $context,
@@ -73,7 +88,9 @@ public function __construct(
\Magento\Catalog\Model\Product\Validator $productValidator,
\Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
\Magento\Framework\View\LayoutFactory $layoutFactory,
- \Magento\Catalog\Model\ProductFactory $productFactory
+ \Magento\Catalog\Model\ProductFactory $productFactory,
+ ProductUrlRewriteValidator $productUrlRewriteValidator,
+ ProductUrlPathGenerator $productUrlPathGenerator
) {
$this->_dateFilter = $dateFilter;
$this->productValidator = $productValidator;
@@ -81,6 +98,8 @@ public function __construct(
$this->resultJsonFactory = $resultJsonFactory;
$this->layoutFactory = $layoutFactory;
$this->productFactory = $productFactory;
+ $this->productUrlRewriteValidator = $productUrlRewriteValidator;
+ $this->productUrlPathGenerator = $productUrlPathGenerator;
}
/**
@@ -130,11 +149,22 @@ public function execute()
$resource->getAttribute('news_from_date')->setMaxValue($product->getNewsToDate());
$resource->getAttribute('custom_design_from')->setMaxValue($product->getCustomDesignTo());
+ if (!$product->getUrlKey()) {
+ $urlKey = $this->productUrlPathGenerator->getUrlKey($product);
+ $product->setUrlKey($urlKey);
+ }
+ $this->productUrlRewriteValidator->validateUrlKeyConflicts($product);
$this->productValidator->validate($product, $this->getRequest(), $response);
} catch (\Magento\Eav\Model\Entity\Attribute\Exception $e) {
$response->setError(true);
$response->setAttribute($e->getAttributeCode());
$response->setMessages([$e->getMessage()]);
+ } catch (UrlAlreadyExistsException $e) {
+ $this->messageManager->addExceptionMessage($e);
+ $layout = $this->layoutFactory->create();
+ $layout->initMessages();
+ $response->setError(true);
+ $response->setHtmlMessage($layout->getMessagesBlock()->getGroupedHtml());
} catch (\Magento\Framework\Exception\LocalizedException $e) {
$response->setError(true);
$response->setMessages([$e->getMessage()]);
diff --git a/app/code/Magento/Catalog/Controller/Product/View.php b/app/code/Magento/Catalog/Controller/Product/View.php
index 570b8f541b76..07c94658643d 100644
--- a/app/code/Magento/Catalog/Controller/Product/View.php
+++ b/app/code/Magento/Catalog/Controller/Product/View.php
@@ -5,12 +5,22 @@
*/
namespace Magento\Catalog\Controller\Product;
+use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Catalog\Model\Design;
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Controller\Result\Forward;
+use Magento\Framework\Controller\Result\ForwardFactory;
+use Magento\Framework\Controller\Result\Redirect;
+use Magento\Framework\DataObject;
+use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Json\Helper\Data;
use Magento\Framework\View\Result\PageFactory;
use Magento\Catalog\Controller\Product as ProductAction;
+use Magento\Store\Model\StoreManagerInterface;
+use Psr\Log\LoggerInterface;
/**
* View a product on storefront. Needs to be accessible by POST because of the store switching
@@ -25,57 +35,84 @@ class View extends ProductAction implements HttpGetActionInterface, HttpPostActi
protected $viewHelper;
/**
- * @var \Magento\Framework\Controller\Result\ForwardFactory
+ * @var ForwardFactory
*/
protected $resultForwardFactory;
/**
- * @var \Magento\Framework\View\Result\PageFactory
+ * @var PageFactory
*/
protected $resultPageFactory;
/**
- * @var \Psr\Log\LoggerInterface
+ * @var LoggerInterface
*/
private $logger;
/**
- * @var \Magento\Framework\Json\Helper\Data
+ * @var Data
*/
private $jsonHelper;
+ /**
+ * @var Design
+ */
+ private $catalogDesign;
+
+ /**
+ * @var ProductRepositoryInterface
+ */
+ private $productRepository;
+
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
/**
* Constructor
*
* @param Context $context
* @param \Magento\Catalog\Helper\Product\View $viewHelper
- * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory
+ * @param ForwardFactory $resultForwardFactory
* @param PageFactory $resultPageFactory
- * @param \Psr\Log\LoggerInterface $logger
- * @param \Magento\Framework\Json\Helper\Data $jsonHelper
+ * @param LoggerInterface|null $logger
+ * @param Data|null $jsonHelper
+ * @param Design|null $catalogDesign
+ * @param ProductRepositoryInterface|null $productRepository
+ * @param StoreManagerInterface|null $storeManager
*/
public function __construct(
Context $context,
\Magento\Catalog\Helper\Product\View $viewHelper,
- \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory,
+ ForwardFactory $resultForwardFactory,
PageFactory $resultPageFactory,
- \Psr\Log\LoggerInterface $logger = null,
- \Magento\Framework\Json\Helper\Data $jsonHelper = null
+ ?LoggerInterface $logger = null,
+ ?Data $jsonHelper = null,
+ ?Design $catalogDesign = null,
+ ?ProductRepositoryInterface $productRepository = null,
+ ?StoreManagerInterface $storeManager = null
) {
parent::__construct($context);
$this->viewHelper = $viewHelper;
$this->resultForwardFactory = $resultForwardFactory;
$this->resultPageFactory = $resultPageFactory;
$this->logger = $logger ?: ObjectManager::getInstance()
- ->get(\Psr\Log\LoggerInterface::class);
+ ->get(LoggerInterface::class);
$this->jsonHelper = $jsonHelper ?: ObjectManager::getInstance()
- ->get(\Magento\Framework\Json\Helper\Data::class);
+ ->get(Data::class);
+ $this->catalogDesign = $catalogDesign ?: ObjectManager::getInstance()
+ ->get(Design::class);
+ $this->productRepository = $productRepository ?: ObjectManager::getInstance()
+ ->get(ProductRepositoryInterface::class);
+ $this->storeManager = $storeManager ?: ObjectManager::getInstance()
+ ->get(StoreManagerInterface::class);
}
/**
* Redirect if product failed to load
*
- * @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\Controller\Result\Forward
+ * @return Redirect|Forward
*/
protected function noProductRedirect()
{
@@ -93,7 +130,7 @@ protected function noProductRedirect()
/**
* Product view action
*
- * @return \Magento\Framework\Controller\Result\Forward|\Magento\Framework\Controller\Result\Redirect
+ * @return Forward|Redirect
*/
public function execute()
{
@@ -130,16 +167,17 @@ public function execute()
}
// Prepare helper and params
- $params = new \Magento\Framework\DataObject();
+ $params = new DataObject();
$params->setCategoryId($categoryId);
$params->setSpecifyOptions($specifyOptions);
// Render page
try {
+ $this->applyCustomDesign($productId);
$page = $this->resultPageFactory->create();
$this->viewHelper->prepareAndRender($page, $productId, $this, $params);
return $page;
- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ } catch (NoSuchEntityException $e) {
return $this->noProductRedirect();
} catch (\Exception $e) {
$this->logger->critical($e);
@@ -148,4 +186,19 @@ public function execute()
return $resultForward;
}
}
+
+ /**
+ * Apply custom design from product design settings
+ *
+ * @param int $productId
+ * @throws NoSuchEntityException
+ */
+ private function applyCustomDesign(int $productId): void
+ {
+ $product = $this->productRepository->getById($productId, false, $this->storeManager->getStore()->getId());
+ $settings = $this->catalogDesign->getDesignSettings($product);
+ if ($settings->getCustomDesign()) {
+ $this->catalogDesign->applyCustomDesign($settings->getCustomDesign());
+ }
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php
index e296c8d3b897..438618d89b43 100644
--- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php
+++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php
@@ -64,11 +64,11 @@ public function build(Filter $filter): string
->select()
->from(
[Collection::MAIN_TABLE_ALIAS => $entityResourceModel->getEntityTable()],
- Collection::MAIN_TABLE_ALIAS . '.' . $entityResourceModel->getEntityIdField()
+ Collection::MAIN_TABLE_ALIAS . '.' . $attribute->getEntityIdField()
)->joinLeft(
[$tableAlias => $attribute->getBackendTable()],
$tableAlias . '.' . $attribute->getEntityIdField() . '=' . Collection::MAIN_TABLE_ALIAS .
- '.' . $entityResourceModel->getEntityIdField() . ' AND ' . $tableAlias . '.' .
+ '.' . $attribute->getEntityIdField() . ' AND ' . $tableAlias . '.' .
$attribute->getIdFieldName() . '=' . $attribute->getAttributeId(),
''
)->where($tableAlias . '.value is null');
diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Startdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Startdate.php
index fde99604e9ae..03e9fbb1e913 100644
--- a/app/code/Magento/Catalog/Model/Attribute/Backend/Startdate.php
+++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Startdate.php
@@ -7,7 +7,7 @@
/**
*
- * Speical Start Date attribute backend
+ * Special Start Date attribute backend
*
* @api
*
@@ -83,7 +83,7 @@ public function validate($object)
$attr = $this->getAttribute();
$maxDate = $attr->getMaxValue();
$startDate = $this->_getValueForSave($object);
- if ($startDate === false) {
+ if ($startDate === false || $startDate === null) {
return true;
}
diff --git a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php
index cf194615b1f3..e58383f7d9be 100644
--- a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php
+++ b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php
@@ -45,6 +45,11 @@ class ScopeOverriddenValue
*/
private $resourceConnection;
+ /**
+ * @var FilterBuilder
+ */
+ private $filterBuilder;
+
/**
* ScopeOverriddenValue constructor.
* @param AttributeRepository $attributeRepository
diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php
index 538a721d356d..981a3d81f0e7 100644
--- a/app/code/Magento/Catalog/Model/Category.php
+++ b/app/code/Magento/Catalog/Model/Category.php
@@ -1159,7 +1159,10 @@ public function reindex()
*/
public function afterDeleteCommit()
{
- $this->reindex();
+ if ($this->getIsActive() || $this->getDeletedChildrenIds()) {
+ $this->reindex();
+ }
+
return parent::afterDeleteCommit();
}
diff --git a/app/code/Magento/Catalog/Model/Category/AttributeRepository.php b/app/code/Magento/Catalog/Model/Category/AttributeRepository.php
index bde5e08d9099..3243bf718e66 100644
--- a/app/code/Magento/Catalog/Model/Category/AttributeRepository.php
+++ b/app/code/Magento/Catalog/Model/Category/AttributeRepository.php
@@ -24,6 +24,11 @@ class AttributeRepository implements CategoryAttributeRepositoryInterface
*/
protected $eavAttributeRepository;
+ /**
+ * @var \Magento\Eav\Model\Config
+ */
+ private $eavConfig;
+
/**
* @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder
* @param \Magento\Framework\Api\FilterBuilder $filterBuilder
diff --git a/app/code/Magento/Catalog/Model/Category/FileInfo.php b/app/code/Magento/Catalog/Model/Category/FileInfo.php
index f5aec60b2fcc..adaf9304d8b3 100644
--- a/app/code/Magento/Catalog/Model/Category/FileInfo.php
+++ b/app/code/Magento/Catalog/Model/Category/FileInfo.php
@@ -5,12 +5,15 @@
*/
namespace Magento\Catalog\Model\Category;
+use Magento\Catalog\Model\Category\Media\PathResolverFactory;
+use Magento\Catalog\Model\Category\Media\PathResolverInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\File\Mime;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\Framework\Filesystem\Directory\ReadInterface;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Filesystem\ExtendedDriverInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -121,11 +124,15 @@ private function getPubDirectory()
*/
public function getMimeType($fileName)
{
- $filePath = $this->getFilePath($fileName);
- $absoluteFilePath = $this->getMediaDirectory()->getAbsolutePath($filePath);
-
- $result = $this->mime->getMimeType($absoluteFilePath);
- return $result;
+ if ($this->getMediaDirectory()->getDriver() instanceof ExtendedDriverInterface) {
+ return $this->mediaDirectory->getDriver()->getMetadata($fileName)['mimetype'];
+ } else {
+ return $this->mime->getMimeType(
+ $this->getMediaDirectory()->getAbsolutePath(
+ $this->getFilePath($fileName)
+ )
+ );
+ }
}
/**
diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php
index c4ff12bbf0f9..3ced479057e0 100644
--- a/app/code/Magento/Catalog/Model/Config.php
+++ b/app/code/Magento/Catalog/Model/Config.php
@@ -44,6 +44,11 @@ class Config extends \Magento\Eav\Model\Config
*/
protected $_productTypesById;
+ /**
+ * @var array
+ */
+ private $_productTypesByName;
+
/**
* Array of attributes codes needed for product load
*
@@ -175,16 +180,6 @@ public function __construct(
);
}
- /**
- * Initialize resource model
- *
- * @return void
- */
- protected function _construct()
- {
- $this->_init(\Magento\Catalog\Model\ResourceModel\Config::class);
- }
-
/**
* Set store id
*
diff --git a/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php
index 0ae128b34d34..aecb3da8fd3d 100644
--- a/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php
+++ b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php
@@ -16,8 +16,8 @@ class CatalogMediaConfig
{
private const XML_PATH_CATALOG_MEDIA_URL_FORMAT = 'web/url/catalog_media_url_format';
- const IMAGE_OPTIMIZATION_PARAMETERS = 'image_optimization_parameters';
- const HASH = 'hash';
+ public const IMAGE_OPTIMIZATION_PARAMETERS = 'image_optimization_parameters';
+ public const HASH = 'hash';
/**
* @var ScopeConfigInterface
@@ -41,10 +41,16 @@ public function __construct(ScopeConfigInterface $scopeConfig)
*/
public function getMediaUrlFormat($scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null): string
{
- return $this->scopeConfig->getValue(
- CatalogMediaConfig::XML_PATH_CATALOG_MEDIA_URL_FORMAT,
+ $value = $this->scopeConfig->getValue(
+ self::XML_PATH_CATALOG_MEDIA_URL_FORMAT,
$scopeType,
$scopeCode
);
+
+ if ($value === null) {
+ return self::HASH;
+ }
+
+ return (string)$value;
}
}
diff --git a/app/code/Magento/Catalog/Model/CustomOptions/CustomOption.php b/app/code/Magento/Catalog/Model/CustomOptions/CustomOption.php
index df9961518f47..655602cd8e7b 100644
--- a/app/code/Magento/Catalog/Model/CustomOptions/CustomOption.php
+++ b/app/code/Magento/Catalog/Model/CustomOptions/CustomOption.php
@@ -17,6 +17,11 @@
class CustomOption extends AbstractExtensibleModel implements CustomOptionInterface
{
+ /**
+ * @var FileProcessor
+ */
+ private $fileProcessor;
+
/**
* @param Context $context
* @param Registry $registry
diff --git a/app/code/Magento/Catalog/Model/Design.php b/app/code/Magento/Catalog/Model/Design.php
index fed18a5a6091..3db91aa4c782 100644
--- a/app/code/Magento/Catalog/Model/Design.php
+++ b/app/code/Magento/Catalog/Model/Design.php
@@ -3,12 +3,22 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Catalog\Model;
+use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager as CategoryLayoutManager;
use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager as ProductLayoutManager;
use Magento\Framework\App\ObjectManager;
-use \Magento\Framework\TranslateInterface;
+use Magento\Framework\Data\Collection\AbstractDb;
+use Magento\Framework\DataObject;
+use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Model\Context;
+use Magento\Framework\Model\ResourceModel\AbstractResource;
+use Magento\Framework\Registry;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Framework\TranslateInterface;
+use Magento\Framework\View\DesignInterface;
/**
* Catalog Custom Category design Model
@@ -28,12 +38,12 @@ class Design extends \Magento\Framework\Model\AbstractModel
/**
* Design package instance
*
- * @var \Magento\Framework\View\DesignInterface
+ * @var DesignInterface
*/
protected $_design = null;
/**
- * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
+ * @var TimezoneInterface
*/
protected $_localeDate;
@@ -53,29 +63,43 @@ class Design extends \Magento\Framework\Model\AbstractModel
private $productLayoutUpdates;
/**
- * @param \Magento\Framework\Model\Context $context
- * @param \Magento\Framework\Registry $registry
- * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
- * @param \Magento\Framework\View\DesignInterface $design
- * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource
- * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
+ * @var Session
+ */
+ private $catalogSession;
+
+ /**
+ * @var CategoryRepositoryInterface
+ */
+ private $categoryRepository;
+
+ /**
+ * @param Context $context
+ * @param Registry $registry
+ * @param TimezoneInterface $localeDate
+ * @param DesignInterface $design
+ * @param AbstractResource|null $resource
+ * @param AbstractDb|null $resourceCollection
* @param array $data
* @param TranslateInterface|null $translator
* @param CategoryLayoutManager|null $categoryLayoutManager
* @param ProductLayoutManager|null $productLayoutManager
+ * @param Session|null $catalogSession
+ * @param CategoryRepositoryInterface|null $categoryRepository
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
- \Magento\Framework\Model\Context $context,
- \Magento\Framework\Registry $registry,
- \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
- \Magento\Framework\View\DesignInterface $design,
- \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
- \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
+ Context $context,
+ Registry $registry,
+ TimezoneInterface $localeDate,
+ DesignInterface $design,
+ AbstractResource $resource = null,
+ AbstractDb $resourceCollection = null,
array $data = [],
TranslateInterface $translator = null,
?CategoryLayoutManager $categoryLayoutManager = null,
- ?ProductLayoutManager $productLayoutManager = null
+ ?ProductLayoutManager $productLayoutManager = null,
+ ?Session $catalogSession = null,
+ ?CategoryRepositoryInterface $categoryRepository = null
) {
$this->_localeDate = $localeDate;
$this->_design = $design;
@@ -84,6 +108,10 @@ public function __construct(
?? ObjectManager::getInstance()->get(CategoryLayoutManager::class);
$this->productLayoutUpdates = $productLayoutManager
?? ObjectManager::getInstance()->get(ProductLayoutManager::class);
+ $this->catalogSession = $catalogSession
+ ?? ObjectManager::getInstance()->get(Session::class);
+ $this->categoryRepository = $categoryRepository
+ ?? ObjectManager::getInstance()->get(CategoryRepositoryInterface::class);
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
@@ -104,12 +132,23 @@ public function applyCustomDesign($design)
* Get custom layout settings
*
* @param Category|Product $object
- * @return \Magento\Framework\DataObject
+ * @return DataObject
*/
public function getDesignSettings($object)
{
if ($object instanceof Product) {
$currentCategory = $object->getCategory();
+ if (!$currentCategory) {
+ $lastId = $this->catalogSession->getLastVisitedCategoryId();
+ if ($object->canBeShowInCategory($lastId)) {
+ $categoryId = $lastId;
+ try {
+ $currentCategory = $this->categoryRepository->get($categoryId);
+ } catch (NoSuchEntityException $e) {
+ $currentCategory = null;
+ }
+ }
+ }
} else {
$currentCategory = $object;
}
@@ -134,14 +173,17 @@ public function getDesignSettings($object)
* Extract custom layout settings from category or product object
*
* @param Category|Product $object
- * @return \Magento\Framework\DataObject
+ * @return DataObject
*/
protected function _extractSettings($object)
{
- $settings = new \Magento\Framework\DataObject();
+ $settings = new DataObject();
if (!$object) {
return $settings;
}
+ $settings->setPageLayout($object->getPageLayout());
+ $settings->setLayoutUpdates((array)$object->getCustomLayoutUpdate());
+
$date = $object->getCustomDesignDate();
if (array_key_exists(
'from',
@@ -155,28 +197,28 @@ protected function _extractSettings($object)
$date['to']
)
) {
- $settings->setCustomDesign(
- $object->getCustomDesign()
- )->setPageLayout(
- $object->getPageLayout()
- )->setLayoutUpdates(
- (array)$object->getCustomLayoutUpdate()
- );
+ if ($object->getCustomDesign()) {
+ $settings->setCustomDesign($object->getCustomDesign());
+ }
+ if ($object->getCustomLayout()) {
+ $settings->setPageLayout($object->getCustomLayout());
+ }
if ($object instanceof Category) {
$this->categoryLayoutUpdates->extractCustomSettings($object, $settings);
} elseif ($object instanceof Product) {
$this->productLayoutUpdates->extractCustomSettings($object, $settings);
}
}
+
return $settings;
}
/**
* Merge custom design settings
*
- * @param \Magento\Framework\DataObject $categorySettings
- * @param \Magento\Framework\DataObject $productSettings
- * @return \Magento\Framework\DataObject
+ * @param DataObject $categorySettings
+ * @param DataObject $productSettings
+ * @return DataObject
*/
protected function _mergeSettings($categorySettings, $productSettings)
{
@@ -190,6 +232,21 @@ protected function _mergeSettings($categorySettings, $productSettings)
$update = array_merge($categorySettings->getLayoutUpdates(), $productSettings->getLayoutUpdates());
$categorySettings->setLayoutUpdates($update);
}
+ if ($categorySettings->getPageLayoutHandles()) {
+ $handles = [];
+ foreach ($categorySettings->getPageLayoutHandles() as $key => $value) {
+ $handles[$key] = [
+ 'handle' => 'catalog_category_view',
+ 'value' => $value,
+ ];
+ }
+ $categorySettings->setPageLayoutHandles($handles);
+ }
+ if ($productSettings->getPageLayoutHandles()) {
+ $handle = array_merge($categorySettings->getPageLayoutHandles(), $productSettings->getPageLayoutHandles());
+ $categorySettings->setPageLayoutHandles($handle);
+ }
+
return $categorySettings;
}
}
diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php
index 6aff6488164f..0b987f05d162 100644
--- a/app/code/Magento/Catalog/Model/ImageUploader.php
+++ b/app/code/Magento/Catalog/Model/ImageUploader.php
@@ -3,12 +3,12 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Catalog\Model;
use Magento\Framework\App\Filesystem\DirectoryList;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\File\Uploader;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\File\Name;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
@@ -211,7 +211,7 @@ public function moveFileFromTmp($imageName, $returnRelativePath = false)
$baseTmpImagePath = $this->getFilePath($baseTmpPath, $imageName);
try {
- $this->coreFileStorageDatabase->copyFile(
+ $this->coreFileStorageDatabase->renameFile(
$baseTmpImagePath,
$baseImagePath
);
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
index a7c5cdf412e6..e94c7a4ee0cf 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php
@@ -12,6 +12,8 @@
use Magento\Catalog\Model\Config;
use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction;
use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
+use Magento\Catalog\Model\Indexer\Category\Product;
+use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\DB\Query\Generator as QueryGenerator;
@@ -63,6 +65,18 @@ class Full extends AbstractAction
*/
private $processManager;
+ /**
+ * @var DeploymentConfig|null
+ */
+ private $deploymentConfig;
+
+ /**
+ * Deployment config path
+ *
+ * @var string
+ */
+ private const DEPLOYMENT_CONFIG_INDEXER_BATCHES = 'indexer/batch_size/';
+
/**
* @param ResourceConnection $resource
* @param StoreManagerInterface $storeManager
@@ -73,7 +87,8 @@ class Full extends AbstractAction
* @param MetadataPool|null $metadataPool
* @param int|null $batchRowsCount
* @param ActiveTableSwitcher|null $activeTableSwitcher
- * @param ProcessManager $processManager
+ * @param ProcessManager|null $processManager
+ * @param DeploymentConfig|null $deploymentConfig
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -86,7 +101,8 @@ public function __construct(
MetadataPool $metadataPool = null,
$batchRowsCount = null,
ActiveTableSwitcher $activeTableSwitcher = null,
- ProcessManager $processManager = null
+ ProcessManager $processManager = null,
+ ?DeploymentConfig $deploymentConfig = null
) {
parent::__construct(
$resource,
@@ -107,6 +123,7 @@ public function __construct(
$this->batchRowsCount = $batchRowsCount;
$this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class);
$this->processManager = $processManager ?: $objectManager->get(ProcessManager::class);
+ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class);
}
/**
@@ -266,6 +283,11 @@ private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition,
$columns = array_keys(
$this->connection->describeTable($this->tableMaintainer->getMainTmpTable((int)$store->getId()))
);
+
+ $this->batchRowsCount = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . Product::INDEXER_ID
+ ) ?? $this->batchRowsCount;
+
$this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount);
$select = $this->connection->select();
diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Dynamic/Manual.php b/app/code/Magento/Catalog/Model/Layer/Filter/Dynamic/Manual.php
index bfa461312783..ee452b08c43e 100644
--- a/app/code/Magento/Catalog/Model/Layer/Filter/Dynamic/Manual.php
+++ b/app/code/Magento/Catalog/Model/Layer/Filter/Dynamic/Manual.php
@@ -18,6 +18,41 @@ class Manual implements AlgorithmInterface
{
const XML_PATH_RANGE_MAX_INTERVALS = 'catalog/layered_navigation/price_range_max_intervals';
+ /**
+ * @var Algorithm
+ */
+ private $algorithm;
+
+ /**
+ * @var \Magento\Catalog\Model\Layer
+ */
+ private $layer;
+
+ /**
+ * @var ScopeConfigInterface
+ */
+ private $scopeConfig;
+
+ /**
+ * @var Render
+ */
+ private $render;
+
+ /**
+ * @var Registry
+ */
+ private $coreRegistry;
+
+ /**
+ * @var Range
+ */
+ private $range;
+
+ /**
+ * @var Price
+ */
+ private $resource;
+
/**
* @param Algorithm $algorithm
* @param Resolver $layerResolver
diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php
index 82d252acd990..355d5bd36e7d 100644
--- a/app/code/Magento/Catalog/Model/Product.php
+++ b/app/code/Magento/Catalog/Model/Product.php
@@ -828,9 +828,6 @@ public function getStoreIds()
if (!$this->hasStoreIds()) {
$storeIds = [];
if ($websiteIds = $this->getWebsiteIds()) {
- if (!$this->isObjectNew() && $this->_storeManager->isSingleStoreMode()) {
- $websiteIds = array_keys($websiteIds);
- }
foreach ($websiteIds as $websiteId) {
$websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds();
$storeIds[] = $websiteStores;
@@ -877,15 +874,17 @@ public function getAttributes($groupId = null, $skipSuper = false)
*/
public function beforeSave()
{
- $this->setTypeHasOptions(false);
- $this->setTypeHasRequiredOptions(false);
- $this->setHasOptions(false);
- $this->setRequiredOptions(false);
+ if ($this->getData('has_options') === null) {
+ $this->setHasOptions(false);
+ }
+ if ($this->getData('required_options') === null) {
+ $this->setRequiredOptions(false);
+ }
$this->getTypeInstance()->beforeSave($this);
- $hasOptions = false;
- $hasRequiredOptions = false;
+ $hasOptions = $this->getData('has_options') === "1";
+ $hasRequiredOptions = $this->getData('required_options') === "1";
/**
* $this->_canAffectOptions - set by type instance only
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php
index 9cb2ac014589..32a417a935e8 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php
@@ -86,9 +86,11 @@ public function execute($entity, $arguments = [])
$identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField();
$priceRows = array_filter($priceRows);
$productId = (int) $entity->getData($identifierField);
+ $pricesStored = $this->getPricesStored($priceRows);
+ $pricesMerged = $this->mergePrices($priceRows, $pricesStored);
// prepare and save data
- foreach ($priceRows as $data) {
+ foreach ($pricesMerged as $data) {
$isPriceWebsiteGlobal = (int)$data['website_id'] === 0;
if ($isGlobal === $isPriceWebsiteGlobal
|| !empty($data['price_qty'])
@@ -109,4 +111,51 @@ public function execute($entity, $arguments = [])
return $entity;
}
+
+ /**
+ * Merge prices
+ *
+ * @param array $prices
+ * @param array $pricesStored
+ * @return array
+ */
+ private function mergePrices(array $prices, array $pricesStored): array
+ {
+ if (!$pricesStored) {
+ return $prices;
+ }
+ $pricesId = [];
+ $pricesStoredId = [];
+ foreach ($prices as $price) {
+ if (isset($price['price_id'])) {
+ $pricesId[$price['price_id']] = $price;
+ }
+ }
+ foreach ($pricesStored as $price) {
+ if (isset($price['price_id'])) {
+ $pricesStoredId[$price['price_id']] = $price;
+ }
+ }
+ $pricesAdd = array_diff_key($pricesStoredId, $pricesId);
+ foreach ($pricesAdd as $price) {
+ $prices[] = $price;
+ }
+ return $prices;
+ }
+
+ /**
+ * Get stored prices
+ *
+ * @param array $prices
+ * @return array
+ */
+ private function getPricesStored(array $prices): array
+ {
+ $pricesStored = [];
+ $price = reset($prices);
+ if (isset($price['product_id']) && $price['product_id']) {
+ $pricesStored = $this->tierPriceResource->loadPriceData($price['product_id']);
+ }
+ return $pricesStored;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php
index 4d148f078ae4..a562b3203490 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php
@@ -126,6 +126,7 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib
$attribute->setAttributeId($existingModel->getAttributeId());
$attribute->setIsUserDefined($existingModel->getIsUserDefined());
$attribute->setFrontendInput($existingModel->getFrontendInput());
+ $attribute->setBackendModel($existingModel->getBackendModel());
$this->updateDefaultFrontendLabel($attribute, $existingModel);
} else {
diff --git a/app/code/Magento/Catalog/Model/Product/Authorization.php b/app/code/Magento/Catalog/Model/Product/Authorization.php
index 4022eb34e65e..3394b65f9eeb 100644
--- a/app/code/Magento/Catalog/Model/Product/Authorization.php
+++ b/app/code/Magento/Catalog/Model/Product/Authorization.php
@@ -129,7 +129,7 @@ private function hasProductChanged(ProductModel $product, ?array $oldProduct = n
//No new value
continue;
}
- if (!in_array($newValue, $oldValues, true)) {
+ if ($newValue !== null && !in_array($newValue, $oldValues, true)) {
return true;
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php
index 68d0877c6cd6..d8cfb2eff264 100644
--- a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php
+++ b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverComposite.php
@@ -16,7 +16,7 @@
class ItemResolverComposite implements ItemResolverInterface
{
/** @var string[] */
- private $itemResolvers = [];
+ private $itemResolvers;
/** @var ItemResolverInterface[] */
private $itemResolversInstances = [];
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CopyHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CopyHandler.php
new file mode 100644
index 000000000000..5e4d3e391618
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/CopyHandler.php
@@ -0,0 +1,173 @@
+metadata = $metadataPool->getMetadata(ProductInterface::class);
+ $this->galleryResourceModel = $galleryResourceModel;
+ $this->attributeRepository = $attributeRepository;
+ $this->attributeValue = $attributeValue;
+ $this->json = $json;
+ }
+
+ /**
+ * Copy gallery data from one product to another
+ *
+ * @param Product $product
+ * @param array $arguments
+ * @return void
+ */
+ public function execute($product, $arguments = []): void
+ {
+ $fromId = (int) $arguments['original_link_id'];
+ $toId = $product->getData($this->metadata->getLinkField());
+ $attributeId = $this->getAttribute()->getAttributeId();
+ $valueIdMap = $this->galleryResourceModel->duplicate($attributeId, [], $fromId, $toId);
+ $gallery = $this->getMediaGalleryCollection($product);
+
+ if (!empty($gallery['images'])) {
+ $images = [];
+ foreach ($gallery['images'] as $key => $image) {
+ $valueId = $image['value_id'] ?? null;
+ $newKey = $key;
+ if ($valueId !== null) {
+ $newValueId = $valueId;
+ if (isset($valueIdMap[$valueId])) {
+ $newValueId = $valueIdMap[$valueId];
+ }
+ if (((int) $valueId) === $key) {
+ $newKey = $newValueId;
+ }
+ $image['value_id'] = $newValueId;
+ }
+ $images[$newKey] = $image;
+ }
+ $gallery['images'] = $images;
+ $attrCode = $this->getAttribute()->getAttributeCode();
+ $product->setData($attrCode, $gallery);
+ }
+
+ //Copy media attribute values from one product to another
+ if (isset($arguments['media_attribute_codes'])) {
+ $values = $this->attributeValue->getValues(
+ ProductInterface::class,
+ $fromId,
+ $arguments['media_attribute_codes']
+ );
+ if ($values) {
+ foreach (array_keys($values) as $key) {
+ $values[$key][$this->metadata->getLinkField()] = $product->getData($this->metadata->getLinkField());
+ unset($values[$key]['value_id']);
+ }
+ $this->attributeValue->insertValues(
+ ProductInterface::class,
+ $values
+ );
+ }
+ }
+ }
+
+ /**
+ * Get product media gallery collection
+ *
+ * @param Product $product
+ * @return array
+ */
+ private function getMediaGalleryCollection(Product $product): array
+ {
+ $attrCode = $this->getAttribute()->getAttributeCode();
+ $value = $product->getData($attrCode);
+
+ if (is_array($value) && isset($value['images'])) {
+ if (!is_array($value['images']) && strlen($value['images']) > 0) {
+ $value['images'] = $this->json->unserialize($value['images']);
+ }
+
+ if (!is_array($value['images'])) {
+ $value['images'] = [];
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns media gallery attribute instance
+ *
+ * @return ProductAttributeInterface
+ */
+ private function getAttribute(): ProductAttributeInterface
+ {
+ if (!$this->attribute) {
+ $this->attribute = $this->attributeRepository->get(
+ ProductInterface::MEDIA_GALLERY
+ );
+ }
+
+ return $this->attribute;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/DeleteHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/DeleteHandler.php
new file mode 100644
index 000000000000..16adccb29b25
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/DeleteHandler.php
@@ -0,0 +1,99 @@
+metadata = $metadataPool->getMetadata(ProductInterface::class);
+ $this->galleryResourceModel = $galleryResourceModel;
+ $this->attributeValue = $attributeValue;
+ }
+
+ /**
+ * Delete all media gallery records for provided product
+ *
+ * @param Product $product
+ * @param array $arguments
+ * @return void
+ */
+ public function execute($product, $arguments = []): void
+ {
+ $valuesId = $this->getMediaGalleryValuesId($product);
+ if ($valuesId) {
+ $this->galleryResourceModel->deleteGallery($valuesId);
+ }
+ if (isset($arguments['media_attribute_codes'])) {
+ $values = $this->attributeValue->getValues(
+ ProductInterface::class,
+ (int) $product->getData($this->metadata->getLinkField()),
+ $arguments['media_attribute_codes']
+ );
+ if ($values) {
+ $this->attributeValue->deleteValues(
+ ProductInterface::class,
+ $values
+ );
+ }
+ }
+ }
+
+ /**
+ * Get product media gallery values IDs
+ *
+ * @param Product $product
+ * @return array
+ */
+ private function getMediaGalleryValuesId(Product $product): array
+ {
+ $connection = $this->galleryResourceModel->getConnection();
+ $select = $connection->select()
+ ->from($this->galleryResourceModel->getTable(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE))
+ ->where(
+ $this->metadata->getLinkField() . '=?',
+ $product->getData($this->metadata->getLinkField()),
+ \Zend_Db::INT_TYPE
+ );
+ return $connection->fetchCol($select);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
index 6a1392d776d3..edee9aef508d 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
@@ -88,9 +88,6 @@ protected function processDeletedImages($product, array &$images)
foreach ($images as $image) {
if (!empty($image['removed'])) {
if (!empty($image['value_id'])) {
- if (preg_match('/\.\.(\\\|\/)/', $image['file'])) {
- continue;
- }
$recordsToDelete[] = $image['value_id'];
if (!in_array($image['file'], $imagesToNotDelete)) {
$imagesToDelete[] = $image['file'];
@@ -116,7 +113,8 @@ protected function processDeletedImages($product, array &$images)
private function canDeleteImage(string $file): bool
{
$catalogPath = $this->mediaConfig->getBaseMediaPath();
- return $this->mediaDirectory->isFile($catalogPath . $file)
+ $filePath = $this->mediaDirectory->getRelativePath($catalogPath . $file);
+ return $this->mediaDirectory->isFile($filePath)
&& $this->resourceModel->countImageUses($file) <= 1;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Repository.php b/app/code/Magento/Catalog/Model/Product/Option/Repository.php
index bb4e247de32d..bb0f0c479223 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Repository.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Repository.php
@@ -158,6 +158,7 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt
$option->setData('product_id', $product->getData($metadata->getLinkField()));
$option->setData('store_id', $product->getStoreId());
+ $backedOptions = $option->getValues();
if ($option->getOptionId()) {
$options = $product->getOptions();
if (!$options) {
@@ -174,6 +175,9 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt
}
$originalValues = $persistedOption->getValues();
$newValues = $option->getData('values');
+ if (!$newValues) {
+ $newValues = $this->getOptionValues($option);
+ }
if ($newValues) {
if (isset($originalValues)) {
$newValues = $this->markRemovedValues($newValues, $originalValues);
@@ -182,6 +186,8 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt
}
}
$option->save();
+ // Required for API response data consistency
+ $option->setValues($backedOptions);
return $option;
}
@@ -249,4 +255,28 @@ private function getHydratorPool()
}
return $this->hydratorPool;
}
+
+ /**
+ * Get Option values from property
+ *
+ * Gets Option values stored in property, modifies for needed format and clears the property
+ *
+ * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option
+ * @return array|null
+ */
+ private function getOptionValues(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $option): ?array
+ {
+ if ($option->getValues() === null) {
+ return null;
+ }
+
+ $optionValues = [];
+
+ foreach ($option->getValues() as $optionValue) {
+ $optionValues[] = $optionValue->getData();
+ }
+ $option->setValues(null);
+
+ return $optionValues;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php
index 9cb6cda4d0a0..121ff2a5db9b 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php
@@ -7,26 +7,47 @@
namespace Magento\Catalog\Model\Product\Option;
+use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
+use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface as OptionRepository;
+use Magento\Catalog\Model\Product\Option;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\EntityManager\Operation\ExtensionInterface;
+use Magento\Catalog\Model\ResourceModel\Product\Relation;
+use Magento\Framework\Exception\CouldNotSaveException;
/**
- * Class SaveHandler
+ * SaveHandler for product option
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class SaveHandler implements ExtensionInterface
{
+ /**
+ * @var string[]
+ */
+ private $compositeProductTypes = ['grouped', 'configurable', 'bundle'];
+
/**
* @var OptionRepository
*/
protected $optionRepository;
+ /**
+ * @var Relation
+ */
+ private $relation;
+
/**
* @param OptionRepository $optionRepository
+ * @param Relation|null $relation
*/
public function __construct(
- OptionRepository $optionRepository
+ OptionRepository $optionRepository,
+ ?Relation $relation = null
) {
$this->optionRepository = $optionRepository;
+ $this->relation = $relation ?: ObjectManager::getInstance()->get(Relation::class);
}
/**
@@ -34,7 +55,7 @@ public function __construct(
*
* @param object $entity
* @param array $arguments
- * @return \Magento\Catalog\Api\Data\ProductInterface|object
+ * @return ProductInterface|object
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function execute($entity, $arguments = [])
@@ -47,20 +68,19 @@ public function execute($entity, $arguments = [])
$optionIds = [];
if ($options) {
- $optionIds = array_map(function ($option) {
- /** @var \Magento\Catalog\Model\Product\Option $option */
+ $optionIds = array_map(function (Option $option) {
return $option->getOptionId();
}, $options);
}
- /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */
+ /** @var ProductInterface $entity */
foreach ($this->optionRepository->getProductOptions($entity) as $option) {
if (!in_array($option->getOptionId(), $optionIds)) {
$this->optionRepository->delete($option);
}
}
if ($options) {
- $this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), (string)$entity->getSku());
+ $this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), $entity);
}
return $entity;
@@ -71,15 +91,43 @@ public function execute($entity, $arguments = [])
*
* @param array $options
* @param bool $hasChangedSku
- * @param string $newSku
+ * @param ProductInterface $product
+ * @return void
+ * @throws CouldNotSaveException
*/
- private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku)
+ private function processOptionsSaving(array $options, bool $hasChangedSku, ProductInterface $product): void
{
+ $isProductHasRelations = $this->isProductHasRelations($product);
+ /** @var ProductCustomOptionInterface $option */
foreach ($options as $option) {
+ if (!$isProductHasRelations && $option->getIsRequire()) {
+ $message = 'Required custom options cannot be added to a simple product'
+ . ' that is a part of a composite product.';
+ throw new CouldNotSaveException(__($message));
+ }
+
if ($hasChangedSku && $option->hasData('product_sku')) {
- $option->setProductSku($newSku);
+ $option->setProductSku($product->getSku());
}
$this->optionRepository->save($option);
}
}
+
+ /**
+ * Check if product doesn't belong to composite product
+ *
+ * @param ProductInterface $product
+ * @return bool
+ */
+ private function isProductHasRelations(ProductInterface $product): bool
+ {
+ $result = true;
+ if (!in_array($product->getId(), $this->compositeProductTypes)
+ && $this->relation->getRelationsByChildren([$product->getId()])
+ ) {
+ $result = false;
+ }
+
+ return $result;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php
index 934ff4804509..f227e14e6af4 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php
@@ -213,7 +213,7 @@ public function validate($processingParams, $option)
}
}
- $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name'])));
+ $fileHash = hash('sha256', $tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name'])));
$userValue = [
'type' => $fileInfo['type'],
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php
index 100ad37273cf..43bbe6f67338 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php
@@ -125,7 +125,7 @@ public function validate($optionValue, $option)
*/
protected function buildSecretKey($fileRelativePath)
{
- return substr(md5($this->rootDirectory->readFile($fileRelativePath)), 0, 20);
+ return substr(hash('sha256', $this->rootDirectory->readFile($fileRelativePath)), 0, 20);
}
/**
diff --git a/app/code/Magento/Catalog/Model/Product/Price/SpecialPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/SpecialPriceStorage.php
index 83a2d1340794..a0f6fcf315c7 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/SpecialPriceStorage.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/SpecialPriceStorage.php
@@ -6,12 +6,22 @@
namespace Magento\Catalog\Model\Product\Price;
+use Magento\Catalog\Api\Data\SpecialPriceInterface;
+use Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory;
+use Magento\Catalog\Api\SpecialPriceStorageInterface;
+use Magento\Catalog\Model\Product\Price\Validation\InvalidSkuProcessor;
+use Magento\Catalog\Model\Product\Price\Validation\Result;
+use Magento\Catalog\Model\ProductIdLocatorInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Catalog\Helper\Data;
+use Magento\Store\Api\StoreRepositoryInterface;
/**
* Special price storage presents efficient price API and is used to retrieve, update or delete special prices.
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class SpecialPriceStorage implements \Magento\Catalog\Api\SpecialPriceStorageInterface
+class SpecialPriceStorage implements SpecialPriceStorageInterface
{
/**
* @var \Magento\Catalog\Api\SpecialPriceInterface
@@ -19,52 +29,59 @@ class SpecialPriceStorage implements \Magento\Catalog\Api\SpecialPriceStorageInt
private $specialPriceResource;
/**
- * @var \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory
+ * @var SpecialPriceInterfaceFactory
*/
private $specialPriceFactory;
/**
- * @var \Magento\Catalog\Model\ProductIdLocatorInterface
+ * @var ProductIdLocatorInterface
*/
private $productIdLocator;
/**
- * @var \Magento\Store\Api\StoreRepositoryInterface
+ * @var StoreRepositoryInterface
*/
private $storeRepository;
/**
- * @var \Magento\Catalog\Model\Product\Price\Validation\Result
+ * @var Result
*/
private $validationResult;
/**
- * @var \Magento\Catalog\Model\Product\Price\Validation\InvalidSkuProcessor
+ * @var InvalidSkuProcessor
*/
private $invalidSkuProcessor;
/**
* @var array
*/
- private $allowedProductTypes = [];
+ private $allowedProductTypes;
+
+ /**
+ * @var Data
+ */
+ private $catalogData;
/**
* @param \Magento\Catalog\Api\SpecialPriceInterface $specialPriceResource
- * @param \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory $specialPriceFactory
- * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator
- * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository
- * @param \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult
- * @param \Magento\Catalog\Model\Product\Price\Validation\InvalidSkuProcessor $invalidSkuProcessor
- * @param array $allowedProductTypes [optional]
+ * @param SpecialPriceInterfaceFactory $specialPriceFactory
+ * @param ProductIdLocatorInterface $productIdLocator
+ * @param StoreRepositoryInterface $storeRepository
+ * @param Result $validationResult
+ * @param InvalidSkuProcessor $invalidSkuProcessor
+ * @param array $allowedProductTypes
+ * @param Data|null $catalogData
*/
public function __construct(
\Magento\Catalog\Api\SpecialPriceInterface $specialPriceResource,
- \Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory $specialPriceFactory,
- \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator,
- \Magento\Store\Api\StoreRepositoryInterface $storeRepository,
- \Magento\Catalog\Model\Product\Price\Validation\Result $validationResult,
- \Magento\Catalog\Model\Product\Price\Validation\InvalidSkuProcessor $invalidSkuProcessor,
- array $allowedProductTypes = []
+ SpecialPriceInterfaceFactory $specialPriceFactory,
+ ProductIdLocatorInterface $productIdLocator,
+ StoreRepositoryInterface $storeRepository,
+ Result $validationResult,
+ InvalidSkuProcessor $invalidSkuProcessor,
+ array $allowedProductTypes = [],
+ ?Data $catalogData = null
) {
$this->specialPriceResource = $specialPriceResource;
$this->specialPriceFactory = $specialPriceFactory;
@@ -73,10 +90,11 @@ public function __construct(
$this->validationResult = $validationResult;
$this->invalidSkuProcessor = $invalidSkuProcessor;
$this->allowedProductTypes = $allowedProductTypes;
+ $this->catalogData = $catalogData ?: ObjectManager::getInstance()->get(Data::class);
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function get(array $skus)
{
@@ -85,7 +103,7 @@ public function get(array $skus)
$prices = [];
foreach ($rawPrices as $rawPrice) {
- /** @var \Magento\Catalog\Api\Data\SpecialPriceInterface $price */
+ /** @var SpecialPriceInterface $price */
$price = $this->specialPriceFactory->create();
$sku = isset($rawPrice['sku'])
? $rawPrice['sku']
@@ -102,7 +120,7 @@ public function get(array $skus)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function update(array $prices)
{
@@ -113,7 +131,7 @@ public function update(array $prices)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function delete(array $prices)
{
@@ -140,52 +158,14 @@ private function retrieveValidPrices(array $prices)
foreach ($prices as $key => $price) {
if (!$price->getSku() || in_array($price->getSku(), $failedSkus)) {
- $this->validationResult->addFailedItem(
- $key,
- __(
- 'The product that was requested doesn\'t exist. Verify the product and try again. '
- . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.',
- [
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- ),
- [
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- );
+ $errorMessage = 'The product that was requested doesn\'t exist. Verify the product and try again. '
+ . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.';
+ $this->addFailedItemPrice($price, $key, $errorMessage, []);
}
+ $this->checkStore($price, $key);
$this->checkPrice($price, $key);
$this->checkDate($price, $price->getPriceFrom(), 'Price From', $key);
$this->checkDate($price, $price->getPriceTo(), 'Price To', $key);
- try {
- $this->storeRepository->getById($price->getStoreId());
- } catch (NoSuchEntityException $e) {
- $this->validationResult->addFailedItem(
- $key,
- __(
- 'Requested store is not found. '
- . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.',
- [
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- ),
- [
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- );
- }
}
foreach ($this->validationResult->getFailedRowIds() as $id) {
@@ -195,77 +175,95 @@ private function retrieveValidPrices(array $prices)
return $prices;
}
+ /**
+ * Check that store exists and is global when price scope is global and otherwise add error to aggregator.
+ *
+ * @param SpecialPriceInterface $price
+ * @param int $key
+ * @return void
+ */
+ private function checkStore(SpecialPriceInterface $price, int $key): void
+ {
+ if ($this->catalogData->isPriceGlobal() && $price->getStoreId() !== 0) {
+ $errorMessage = 'Could not change non global Price when price scope is global. '
+ . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.';
+ $this->addFailedItemPrice($price, $key, $errorMessage, []);
+ }
+
+ try {
+ $this->storeRepository->getById($price->getStoreId());
+ } catch (NoSuchEntityException $e) {
+ $errorMessage = 'Requested store is not found. '
+ . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.';
+ $this->addFailedItemPrice($price, $key, $errorMessage, []);
+ }
+ }
+
/**
* Check that date value is correct and add error to aggregator if it contains incorrect data.
*
- * @param \Magento\Catalog\Api\Data\SpecialPriceInterface $price
+ * @param SpecialPriceInterface $price
* @param string $value
* @param string $label
* @param int $key
* @return void
*/
- private function checkDate(\Magento\Catalog\Api\Data\SpecialPriceInterface $price, $value, $label, $key)
+ private function checkDate(SpecialPriceInterface $price, $value, $label, $key)
{
if ($value && !$this->isCorrectDateValue($value)) {
- $this->validationResult->addFailedItem(
- $key,
- __(
- 'Invalid attribute %label = %priceTo. '
- . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.',
- [
- 'label' => $label,
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- ),
- [
- 'label' => $label,
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- );
+ $errorMessage = 'Invalid attribute %label = %priceTo. '
+ . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.';
+ $this->addFailedItemPrice($price, $key, $errorMessage, ['label' => $label]);
}
}
/**
- * Check that provided price value is not empty and not lower then zero and add error to aggregator if price
+ * Check price.
+ *
+ * Verify that provided price value is not empty and not lower then zero and add error to aggregator if price
* contains not valid data.
*
- * @param \Magento\Catalog\Api\Data\SpecialPriceInterface $price
+ * @param SpecialPriceInterface $price
* @param int $key
* @return void
*/
- private function checkPrice(\Magento\Catalog\Api\Data\SpecialPriceInterface $price, $key)
+ private function checkPrice(SpecialPriceInterface $price, int $key): void
{
if (null === $price->getPrice() || $price->getPrice() < 0) {
- $this->validationResult->addFailedItem(
- $key,
- __(
- 'Invalid attribute Price = %price. '
- . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.',
- [
- 'price' => $price->getPrice(),
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- ),
- [
- 'price' => $price->getPrice(),
- 'SKU' => $price->getSku(),
- 'storeId' => $price->getStoreId(),
- 'priceFrom' => $price->getPriceFrom(),
- 'priceTo' => $price->getPriceTo()
- ]
- );
+ $errorMessage = 'Invalid attribute Price = %price. '
+ . 'Row ID: SKU = %SKU, Store ID: %storeId, Price From: %priceFrom, Price To: %priceTo.';
+ $this->addFailedItemPrice($price, $key, $errorMessage, ['price' => $price->getPrice()]);
}
}
+ /**
+ * Adds failed item price to validation result
+ *
+ * @param SpecialPriceInterface $price
+ * @param int $key
+ * @param string $message
+ * @param array $firstParam
+ * @return void
+ */
+ private function addFailedItemPrice(
+ SpecialPriceInterface $price,
+ int $key,
+ string $message,
+ array $firstParam
+ ): void {
+ $additionalInfo = [];
+ if ($firstParam) {
+ $additionalInfo = array_merge($additionalInfo, $firstParam);
+ }
+
+ $additionalInfo['SKU'] = $price->getSku();
+ $additionalInfo['storeId'] = $price->getStoreId();
+ $additionalInfo['priceFrom'] = $price->getPriceFrom();
+ $additionalInfo['priceTo'] = $price->getPriceTo();
+
+ $this->validationResult->addFailedItem($key, __($message, $additionalInfo), $additionalInfo);
+ }
+
/**
* Retrieve SKU by product ID.
*
diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php
index a2e151c1c962..61f64e7c4695 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php
@@ -56,6 +56,16 @@ class TierPriceFactory
*/
private $customerGroupsByCode = [];
+ /**
+ * @var \Magento\Framework\Api\SearchCriteriaBuilder
+ */
+ private $searchCriteriaBuilder;
+
+ /**
+ * @var \Magento\Framework\Api\FilterBuilder
+ */
+ private $filterBuilder;
+
/**
* TierPriceBuilder constructor.
*
diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/InvalidSkuProcessor.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/InvalidSkuProcessor.php
index 744fa76ff69b..b124da7b18d6 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/Validation/InvalidSkuProcessor.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/InvalidSkuProcessor.php
@@ -11,6 +11,16 @@
*/
class InvalidSkuProcessor
{
+ /**
+ * @var \Magento\Catalog\Model\ProductIdLocatorInterface
+ */
+ private $productIdLocator;
+
+ /**
+ * @var \Magento\Catalog\Api\ProductRepositoryInterface
+ */
+ private $productRepository;
+
/**
* @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator
* @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
diff --git a/app/code/Magento/Catalog/Model/Product/Type.php b/app/code/Magento/Catalog/Model/Product/Type.php
index d7dc74e0d0cc..c2603d475e7d 100644
--- a/app/code/Magento/Catalog/Model/Product/Type.php
+++ b/app/code/Magento/Catalog/Model/Product/Type.php
@@ -127,6 +127,8 @@ public function factory($product)
$typeModel = $this->_productTypePool->get($typeModelName);
$typeModel->setConfig($types[$typeId]);
+ $typeModel->setTypeId($typeId);
+
return $typeModel;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php
index 19f6461d44b6..6c08c493e803 100644
--- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php
+++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php
@@ -504,21 +504,17 @@ public function processFileQueue()
/** @var $uploader \Zend_File_Transfer_Adapter_Http */
$uploader = $queueOptions['uploader'] ?? null;
$isUploaded = false;
- if ($uploader && $uploader->isValid()) {
+ if ($uploader && $uploader->isValid($src)) {
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
$path = pathinfo($dst, PATHINFO_DIRNAME);
$uploader = $this->uploaderFactory->create(['fileId' => $src]);
$uploader->setFilesDispersion(false);
$uploader->setAllowRenameFiles(true);
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
$isUploaded = $uploader->save($path, pathinfo($dst, PATHINFO_FILENAME));
}
if (empty($src) || empty($dst) || !$isUploaded) {
- /**
- * @todo: show invalid option
- */
- if (isset($queueOptions['option'])) {
- $queueOptions['option']->setIsValid(false);
- }
throw new \Magento\Framework\Exception\LocalizedException(
__('The file upload failed. Try to upload again.')
);
@@ -756,6 +752,9 @@ protected function _removeNotApplicableAttributes($product)
*/
public function beforeSave($product)
{
+ if (!$product->getTypeId()) {
+ $product->setTypeId($this->_typeId);
+ }
$this->_removeNotApplicableAttributes($product);
$product->canAffectOptions(true);
return $this;
diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php
index 206f3dba0ee6..8956d1d4c95a 100644
--- a/app/code/Magento/Catalog/Model/Product/Type/Price.php
+++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php
@@ -298,7 +298,7 @@ public function getTierPrice($qty, $product)
$custGroup = $this->_getCustomerGroupId($product);
if ($qty) {
- $prevQty = 1;
+ $prevQty = 0;
$prevPrice = $product->getPrice();
$prevGroup = $allGroupsId;
diff --git a/app/code/Magento/Catalog/Model/ProductIdLocator.php b/app/code/Magento/Catalog/Model/ProductIdLocator.php
index 2d382164f264..00a4a9751907 100644
--- a/app/code/Magento/Catalog/Model/ProductIdLocator.php
+++ b/app/code/Magento/Catalog/Model/ProductIdLocator.php
@@ -124,7 +124,7 @@ public function retrieveProductIdsBySkus(array $skus)
private function truncateToLimit()
{
if (count($this->idsBySku) > $this->idsLimit) {
- $this->idsBySku = array_slice($this->idsBySku, round($this->idsLimit / -2));
+ $this->idsBySku = array_slice($this->idsBySku, $this->idsLimit * -1, null, true);
}
}
}
diff --git a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php
index db9f4de14295..b0ac93942b10 100644
--- a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php
+++ b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php
@@ -152,6 +152,15 @@ private function getUrlBuilder()
*/
private function isDateWithDateInternal(array $optionValue): bool
{
- return array_key_exists('date_internal', $optionValue) && array_key_exists('date', $optionValue);
+ $hasDate = !empty($optionValue['day'])
+ && !empty($optionValue['month'])
+ && !empty($optionValue['year']);
+
+ $hasTime = !empty($optionValue['hour'])
+ && isset($optionValue['minute']);
+
+ $hasDateInternal = !empty($optionValue['date_internal']);
+
+ return $hasDateInternal && ($hasDate || $hasTime || !empty($optionValue['date']));
}
}
diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php
index fefeafe46e1c..fdee74c9559c 100644
--- a/app/code/Magento/Catalog/Model/ProductRepository.php
+++ b/app/code/Magento/Catalog/Model/ProductRepository.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Api\CategoryLinkManagementInterface;
use Magento\Catalog\Api\Data\ProductExtension;
use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap;
use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
@@ -17,6 +18,7 @@
use Magento\Framework\Api\ImageContentValidatorInterface;
use Magento\Framework\Api\ImageProcessorInterface;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
+use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\DB\Adapter\ConnectionException;
use Magento\Framework\DB\Adapter\DeadlockException;
use Magento\Framework\DB\Adapter\LockWaitException;
@@ -28,6 +30,8 @@
use Magento\Framework\Exception\StateException;
use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException;
use Magento\Framework\Exception\ValidatorException;
+use Magento\Store\Model\Store;
+use Magento\Catalog\Api\Data\EavAttributeInterface;
/**
* @inheritdoc
@@ -178,6 +182,11 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa
*/
private $linkManagement;
+ /**
+ * @var ScopeOverriddenValue
+ */
+ private $scopeOverriddenValue;
+
/**
* ProductRepository constructor.
* @param ProductFactory $productFactory
@@ -205,6 +214,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa
* @param int $cacheLimit [optional]
* @param ReadExtensions $readExtensions
* @param CategoryLinkManagementInterface $linkManagement
+ * @param ScopeOverriddenValue|null $scopeOverriddenValue
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -233,7 +243,8 @@ public function __construct(
\Magento\Framework\Serialize\Serializer\Json $serializer = null,
$cacheLimit = 1000,
ReadExtensions $readExtensions = null,
- CategoryLinkManagementInterface $linkManagement = null
+ CategoryLinkManagementInterface $linkManagement = null,
+ ?ScopeOverriddenValue $scopeOverriddenValue = null
) {
$this->productFactory = $productFactory;
$this->collectionFactory = $collectionFactory;
@@ -260,6 +271,8 @@ public function __construct(
->get(ReadExtensions::class);
$this->linkManagement = $linkManagement ?: \Magento\Framework\App\ObjectManager::getInstance()
->get(CategoryLinkManagementInterface::class);
+ $this->scopeOverriddenValue = $scopeOverriddenValue ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(ScopeOverriddenValue::class);
}
/**
@@ -514,9 +527,12 @@ public function save(ProductInterface $product, $saveOptions = false)
{
$assignToCategories = false;
$tierPrices = $product->getData('tier_price');
+ $productDataToChange = $product->getData();
try {
- $existingProduct = $product->getId() ? $this->getById($product->getId()) : $this->get($product->getSku());
+ $existingProduct = $product->getId() ?
+ $this->getById($product->getId()) :
+ $this->get($product->getSku());
$product->setData(
$this->resourceModel->getLinkField(),
@@ -548,7 +564,6 @@ public function save(ProductInterface $product, $saveOptions = false)
$productDataArray['store_id'] = (int) $this->storeManager->getStore()->getId();
}
$product = $this->initializeProductData($productDataArray, empty($existingProduct));
-
$this->processLinks($product, $productLinks);
if (isset($productDataArray['media_gallery'])) {
$this->processMediaGallery($product, $productDataArray['media_gallery']['images']);
@@ -569,6 +584,48 @@ public function save(ProductInterface $product, $saveOptions = false)
$product->setData('tier_price', $tierPrices);
}
+ try {
+ $stores = $product->getStoreIds();
+ $websites = $product->getWebsiteIds();
+ } catch (NoSuchEntityException $exception) {
+ $stores = null;
+ $websites = null;
+ }
+
+ if (!empty($existingProduct) && is_array($stores) && is_array($websites)) {
+ $hasDataChanged = false;
+ $productAttributes = $product->getAttributes();
+ if ($productAttributes !== null
+ && $product->getStoreId() !== Store::DEFAULT_STORE_ID
+ && (count($stores) > 1 || count($websites) === 1)
+ ) {
+ foreach ($productAttributes as $attribute) {
+ $attributeCode = $attribute->getAttributeCode();
+ $value = $product->getData($attributeCode);
+ if ($existingProduct->getData($attributeCode) === $value
+ && $attribute->getScope() !== EavAttributeInterface::SCOPE_GLOBAL_TEXT
+ && !is_array($value)
+ && $attribute->getData('frontend_input') !== 'media_image'
+ && !$attribute->isStatic()
+ && !array_key_exists($attributeCode, $productDataToChange)
+ && $value !== null
+ && !$this->scopeOverriddenValue->containsValue(
+ ProductInterface::class,
+ $product,
+ $attributeCode,
+ $product->getStoreId()
+ )
+ ) {
+ $product->setData($attributeCode);
+ $hasDataChanged = true;
+ }
+ }
+ if ($hasDataChanged) {
+ $product->setData('_edit_mode', true);
+ }
+ }
+ }
+
$this->saveProduct($product);
if ($assignToCategories === true && $product->getCategoryIds()) {
$this->linkManagement->assignProductToCategories(
@@ -619,7 +676,7 @@ public function deleteById($sku)
/**
* @inheritdoc
*/
- public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
+ public function getList(SearchCriteriaInterface $searchCriteria)
{
/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */
$collection = $this->collectionFactory->create();
@@ -628,6 +685,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr
$collection->addAttributeToSelect('*');
$collection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner');
$collection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner');
+ $this->joinPositionField($collection, $searchCriteria);
$this->collectionProcessor->process($searchCriteria, $collection);
@@ -856,4 +914,37 @@ private function saveProduct($product): void
);
}
}
+
+ /**
+ * Join category position field to make sorting by position possible.
+ *
+ * @param Collection $collection
+ * @param SearchCriteriaInterface $searchCriteria
+ * @return void
+ */
+ private function joinPositionField(
+ Collection $collection,
+ SearchCriteriaInterface $searchCriteria
+ ): void {
+ $categoryIds = [[]];
+ foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
+ foreach ($filterGroup->getFilters() as $filter) {
+ if ($filter->getField() === 'category_id') {
+ $filterValue = $filter->getValue();
+ $categoryIds[] = is_array($filterValue) ? $filterValue : explode(',', $filterValue);
+ }
+ }
+ }
+ $categoryIds = array_unique(array_merge(...$categoryIds));
+ if (count($categoryIds) === 1) {
+ $collection->joinField(
+ 'position',
+ 'catalog_category_product',
+ 'position',
+ 'product_id=entity_id',
+ ['category_id' => current($categoryIds)],
+ 'left'
+ );
+ }
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
index ed2df0f10ac3..18f38f7d2bc2 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
@@ -232,7 +232,10 @@ protected function _beforeDelete(\Magento\Framework\DataObject $object)
*/
protected function _afterDelete(DataObject $object)
{
- $this->indexerProcessor->markIndexerAsInvalid();
+ if ($object->getIsActive() || $object->getDeletedChildrenIds()) {
+ $this->indexerProcessor->markIndexerAsInvalid();
+ }
+
return parent::_afterDelete($object);
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php
new file mode 100644
index 000000000000..f49ddef01ca7
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/MediaImageDeleteProcessor.php
@@ -0,0 +1,146 @@
+imageConfig = $imageConfig;
+ $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+ $this->imageProcessor = $imageProcessor;
+ $this->productGallery = $productGallery;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Process $product data and remove image from gallery after product delete
+ *
+ * @param DataObject $product
+ * @return void
+ */
+ public function execute(DataObject $product): void
+ {
+ try {
+ $productImages = $product->getMediaGalleryImages();
+ foreach ($productImages as $image) {
+ $imageFile = $image->getFile();
+ if ($imageFile) {
+ $this->deleteProductImage($image, $product, $imageFile);
+ }
+ }
+ } catch (Exception $e) {
+ $this->logger->critical($e);
+ }
+ }
+
+ /**
+ * Check if image exists and is not used by any other products
+ *
+ * @param string $file
+ * @return bool
+ */
+ private function canDeleteImage(string $file): bool
+ {
+ return $this->productGallery->countImageUses($file) <= 1;
+ }
+
+ /**
+ * Delete the physical image if it's existed and not used by other products
+ *
+ * @param string $imageFile
+ * @param string $filePath
+ * @throws FileSystemException
+ */
+ private function deletePhysicalImage(string $imageFile, string $filePath): void
+ {
+ if ($this->canDeleteImage($imageFile)) {
+ $this->mediaDirectory->delete($filePath);
+ }
+ }
+
+ /**
+ * Remove product image
+ *
+ * @param DataObject $image
+ * @param ProductInterface $product
+ * @param string $imageFile
+ */
+ private function deleteProductImage(
+ DataObject $image,
+ ProductInterface $product,
+ string $imageFile
+ ): void {
+ $catalogPath = $this->imageConfig->getBaseMediaPath();
+ $filePath = $catalogPath . $imageFile;
+
+ if ($this->mediaDirectory->isFile($filePath)) {
+ try {
+ $this->productGallery->deleteGallery($image->getValueId());
+ $this->imageProcessor->removeImage($product, $imageFile);
+ $this->deletePhysicalImage($imageFile, $filePath);
+ } catch (Exception $e) {
+ $this->logger->critical($e);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
index b174e4beb635..b3c50015d9db 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
@@ -100,6 +100,11 @@ class Product extends AbstractResource
*/
private $eavAttributeManagement;
+ /**
+ * @var MediaImageDeleteProcessor
+ */
+ private $mediaImageDeleteProcessor;
+
/**
* @param \Magento\Eav\Model\Entity\Context $context
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -114,6 +119,7 @@ class Product extends AbstractResource
* @param TableMaintainer|null $tableMaintainer
* @param UniqueValidationInterface|null $uniqueValidator
* @param AttributeManagementInterface|null $eavAttributeManagement
+ * @param MediaImageDeleteProcessor|null $mediaImageDeleteProcessor
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -129,7 +135,8 @@ public function __construct(
$data = [],
TableMaintainer $tableMaintainer = null,
UniqueValidationInterface $uniqueValidator = null,
- AttributeManagementInterface $eavAttributeManagement = null
+ AttributeManagementInterface $eavAttributeManagement = null,
+ ?MediaImageDeleteProcessor $mediaImageDeleteProcessor = null
) {
$this->_categoryCollectionFactory = $categoryCollectionFactory;
$this->_catalogCategory = $catalogCategory;
@@ -148,6 +155,8 @@ public function __construct(
$this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
$this->eavAttributeManagement = $eavAttributeManagement
?? ObjectManager::getInstance()->get(AttributeManagementInterface::class);
+ $this->mediaImageDeleteProcessor = $mediaImageDeleteProcessor
+ ?? ObjectManager::getInstance()->get(MediaImageDeleteProcessor::class);
}
/**
@@ -819,4 +828,16 @@ protected function getAttributeRow($entity, $object, $attribute)
$data['store_id'] = $object->getStoreId();
return $data;
}
+
+ /**
+ * After delete entity process
+ *
+ * @param DataObject $object
+ * @return $this
+ */
+ protected function _afterDelete(DataObject $object)
+ {
+ $this->mediaImageDeleteProcessor->execute($object);
+ return parent::_afterDelete($object);
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Attribute/Backend/Tierprice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Attribute/Backend/Tierprice.php
index e3edc4a0dc48..b310f6e68774 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Attribute/Backend/Tierprice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Attribute/Backend/Tierprice.php
@@ -35,6 +35,7 @@ protected function _loadPriceDataColumns($columns)
$columns = parent::_loadPriceDataColumns($columns);
$columns['price_qty'] = 'qty';
$columns['percentage_value'] = 'percentage_value';
+ $columns['product_id'] = $this->getProductIdFieldName();
return $columns;
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
index 3f908663c8e5..a247e6b09760 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
@@ -916,7 +916,12 @@ public function addWebsiteFilter($websites = null)
}
$this->_productLimitationFilters['website_ids'] = $websites;
- $this->_applyProductLimitations();
+
+ if ($this->getStoreId() == Store::DEFAULT_STORE_ID) {
+ $this->_productLimitationJoinWebsite();
+ } else {
+ $this->_applyProductLimitations();
+ }
return $this;
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php
index 5ff6207074b6..28e637ad0560 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php
@@ -452,6 +452,9 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu
// Duplicate per store gallery values
$select = $this->getConnection()->select()->from(
$this->getTable(self::GALLERY_VALUE_TABLE)
+ )->where(
+ $linkField . ' = ?',
+ $originalProductId
)->where(
'value_id IN(?)',
array_keys($valueIdMap)
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculator.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculator.php
index 810b1b9a07c4..664ed9ead16d 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculator.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculator.php
@@ -6,6 +6,10 @@
namespace Magento\Catalog\Model\ResourceModel\Product\Indexer\Price;
+use Magento\Framework\App\DeploymentConfig;
+use Magento\Framework\App\ObjectManager;
+use Magento\Catalog\Model\Indexer\Product\Price\Processor;
+
/**
* Ensure that size of index MEMORY table is enough for configured rows count in batch.
*/
@@ -27,16 +31,33 @@ class BatchSizeCalculator
private $batchSizeAdjusters;
/**
- * BatchSizeCalculator constructor.
+ * @var DeploymentConfig|null
+ */
+ private $deploymentConfig;
+
+ /**
+ * Deployment config path
+ *
+ * @var string
+ */
+ private const DEPLOYMENT_CONFIG_INDEXER_BATCHES = 'indexer/batch_size/';
+
+ /**
* @param array $batchRowsCount
* @param array $estimators
* @param array $batchSizeAdjusters
+ * @param DeploymentConfig|null $deploymentConfig
*/
- public function __construct(array $batchRowsCount, array $estimators, array $batchSizeAdjusters)
- {
+ public function __construct(
+ array $batchRowsCount,
+ array $estimators,
+ array $batchSizeAdjusters,
+ ?DeploymentConfig $deploymentConfig = null
+ ) {
$this->batchRowsCount = $batchRowsCount;
$this->estimators = $estimators;
$this->batchSizeAdjusters = $batchSizeAdjusters;
+ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class);
}
/**
@@ -50,9 +71,18 @@ public function __construct(array $batchRowsCount, array $estimators, array $bat
*/
public function estimateBatchSize(\Magento\Framework\DB\Adapter\AdapterInterface $connection, $indexerTypeId)
{
- $batchRowsCount = isset($this->batchRowsCount[$indexerTypeId])
- ? $this->batchRowsCount[$indexerTypeId]
- : $this->batchRowsCount['default'];
+ $batchRowsCount = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . Processor::INDEXER_ID . '/' . $indexerTypeId,
+ $batchRowsCount = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . Processor::INDEXER_ID . '/' . 'default'
+ )
+ );
+
+ if (is_null($batchRowsCount)) {
+ $batchRowsCount = isset($this->batchRowsCount[$indexerTypeId])
+ ? $this->batchRowsCount[$indexerTypeId]
+ : $this->batchRowsCount['default'];
+ }
/** @var \Magento\Framework\Indexer\BatchSizeManagementInterface $calculator */
$calculator = isset($this->estimators[$indexerTypeId])
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductRelationsCalculator.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductRelationsCalculator.php
index d03c0c51703a..730fba32c099 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductRelationsCalculator.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductRelationsCalculator.php
@@ -11,6 +11,11 @@
*/
class CompositeProductRelationsCalculator
{
+ /**
+ * @var DefaultPrice
+ */
+ private $indexerResource;
+
/**
* @param DefaultPrice $indexerResource
*/
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php
index 77407ed699fb..be1d1637b853 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php
@@ -128,6 +128,11 @@ public function getQuery(array $dimensions, string $productType, array $entityId
['cwd' => $this->getTable('catalog_product_index_website')],
'pw.website_id = cwd.website_id',
[]
+ )->joinLeft(
+ // customer group website limitations
+ ['cgw' => $this->getTable('customer_group_excluded_website')],
+ 'cg.customer_group_id = cgw.customer_group_id AND pw.website_id = cgw.website_id',
+ []
)->joinLeft(
// we need this only for BCC in case someone expects table `tp` to be present in query
['tp' => $this->getTable('catalog_product_index_tier_price')],
@@ -227,6 +232,9 @@ public function getQuery(array $dimensions, string $productType, array $entityId
$select->where('e.entity_id IN(?)', $entityIds);
}
+ // exclude websites that are limited for customer group
+ $select->where('cgw.website_id IS NULL');
+
/**
* throw event for backward compatibility
*/
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php
index 7eb19193f8bd..448b3769c715 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php
@@ -6,10 +6,18 @@
namespace Magento\Catalog\Model\ResourceModel\Product\Price;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\Catalog\Api\SpecialPriceInterface;
+use Magento\Catalog\Model\ProductIdLocatorInterface;
+use Magento\Catalog\Model\ResourceModel\Attribute;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\Exception\CouldNotDeleteException;
+use Magento\Framework\App\ObjectManager;
+
/**
* Special price resource.
*/
-class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface
+class SpecialPrice implements SpecialPriceInterface
{
/**
* Price storage table.
@@ -26,24 +34,24 @@ class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface
private $datetimeTable = 'catalog_product_entity_datetime';
/**
- * @var \Magento\Catalog\Model\ResourceModel\Attribute
+ * @var Attribute
*/
private $attributeResource;
/**
- * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface
+ * @var ProductAttributeRepositoryInterface
*/
private $attributeRepository;
/**
- * @var \Magento\Catalog\Model\ProductIdLocatorInterface
+ * @var ProductIdLocatorInterface
*/
private $productIdLocator;
/**
* Metadata pool.
*
- * @var \Magento\Framework\EntityManager\MetadataPool
+ * @var MetadataPool
*/
private $metadataPool;
@@ -76,16 +84,16 @@ class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface
private $itemsPerOperation = 500;
/**
- * @param \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource
- * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository
- * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator
- * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
+ * @param Attribute $attributeResource
+ * @param ProductAttributeRepositoryInterface $attributeRepository
+ * @param ProductIdLocatorInterface $productIdLocator
+ * @param MetadataPool $metadataPool
*/
public function __construct(
- \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource,
- \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository,
- \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator,
- \Magento\Framework\EntityManager\MetadataPool $metadataPool
+ Attribute $attributeResource,
+ ProductAttributeRepositoryInterface $attributeRepository,
+ ProductIdLocatorInterface $productIdLocator,
+ MetadataPool $metadataPool
) {
$this->attributeResource = $attributeResource;
$this->attributeRepository = $attributeRepository;
@@ -94,7 +102,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function get(array $skus)
{
@@ -132,7 +140,7 @@ public function get(array $skus)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function update(array $prices)
{
@@ -187,49 +195,63 @@ public function update(array $prices)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function delete(array $prices)
{
- $skus = array_unique(
- array_map(function ($price) {
- return $price->getSku();
- }, $prices)
- );
- $ids = $this->retrieveAffectedIds($skus);
$connection = $this->attributeResource->getConnection();
- $connection->beginTransaction();
- try {
- foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
- $this->attributeResource->getConnection()->delete(
- $this->attributeResource->getTable($this->priceTable),
- [
- 'attribute_id = ?' => $this->getPriceAttributeId(),
- $this->getEntityLinkField() . ' IN (?)' => $idsBunch
- ]
- );
- }
- foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
- $this->attributeResource->getConnection()->delete(
- $this->attributeResource->getTable($this->datetimeTable),
- [
- 'attribute_id IN (?)' => [$this->getPriceFromAttributeId(), $this->getPriceToAttributeId()],
- $this->getEntityLinkField() . ' IN (?)' => $idsBunch
- ]
- );
+
+ foreach ($this->getStoreSkus($prices) as $storeId => $skus) {
+
+ $ids = $this->retrieveAffectedIds(array_unique($skus));
+ $connection->beginTransaction();
+ try {
+ foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
+ $connection->delete(
+ $this->attributeResource->getTable($this->priceTable),
+ [
+ 'attribute_id = ?' => $this->getPriceAttributeId(),
+ 'store_id = ?' => $storeId,
+ $this->getEntityLinkField() . ' IN (?)' => $idsBunch
+ ]
+ );
+ }
+ foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
+ $connection->delete(
+ $this->attributeResource->getTable($this->datetimeTable),
+ [
+ 'attribute_id IN (?)' => [$this->getPriceFromAttributeId(), $this->getPriceToAttributeId()],
+ 'store_id = ?' => $storeId,
+ $this->getEntityLinkField() . ' IN (?)' => $idsBunch
+ ]
+ );
+ }
+ $connection->commit();
+ } catch (\Exception $e) {
+ $connection->rollBack();
+ throw new CouldNotDeleteException(__('Could not delete Prices'), $e);
}
- $connection->commit();
- } catch (\Exception $e) {
- $connection->rollBack();
- throw new \Magento\Framework\Exception\CouldNotDeleteException(
- __('Could not delete Prices'),
- $e
- );
}
return true;
}
+ /**
+ * Returns associative arrays of store_id as key and array of skus as value.
+ *
+ * @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $priceItems
+ * @return array
+ */
+ private function getStoreSkus(array $priceItems): array
+ {
+ $storeSkus = [];
+ foreach ($priceItems as $priceItem) {
+ $storeSkus[$priceItem->getStoreId()][] = $priceItem->getSku();
+ }
+
+ return $storeSkus;
+ }
+
/**
* Get link field.
*
@@ -312,9 +334,9 @@ private function retrieveAffectedIds(array $skus)
$affectedIds = [];
foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $productIds) {
- $affectedIds = array_merge($affectedIds, array_keys($productIds));
+ $affectedIds[] = array_keys($productIds);
}
- return array_unique($affectedIds);
+ return array_unique(array_merge([], ...$affectedIds));
}
}
diff --git a/app/code/Magento/Catalog/Model/Webapi/Product/Option/Type/File/Processor.php b/app/code/Magento/Catalog/Model/Webapi/Product/Option/Type/File/Processor.php
index 92302dd9ccf4..371d9e6c091f 100644
--- a/app/code/Magento/Catalog/Model/Webapi/Product/Option/Type/File/Processor.php
+++ b/app/code/Magento/Catalog/Model/Webapi/Product/Option/Type/File/Processor.php
@@ -59,7 +59,7 @@ public function processFileContent(ImageContentInterface $imageContent)
$filePath = $this->saveFile($imageContent);
$fileAbsolutePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($filePath);
- $fileHash = md5($this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->readFile($filePath));
+ $fileHash = hash('sha256', $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->readFile($filePath));
$imageSize = getimagesize($fileAbsolutePath);
$result = [
'type' => $imageContent->getType(),
diff --git a/app/code/Magento/Catalog/Observer/Compare/BindCustomerLoginObserver.php b/app/code/Magento/Catalog/Observer/Compare/BindCustomerLoginObserver.php
index 27e1b4470415..0f165d96494a 100644
--- a/app/code/Magento/Catalog/Observer/Compare/BindCustomerLoginObserver.php
+++ b/app/code/Magento/Catalog/Observer/Compare/BindCustomerLoginObserver.php
@@ -14,6 +14,11 @@
*/
class BindCustomerLoginObserver implements ObserverInterface
{
+ /**
+ * @var Item
+ */
+ private $item;
+
/**
* @param Item $item
*/
diff --git a/app/code/Magento/Catalog/Observer/Compare/BindCustomerLogoutObserver.php b/app/code/Magento/Catalog/Observer/Compare/BindCustomerLogoutObserver.php
index 11b8d3f854f6..ece2a48da62a 100644
--- a/app/code/Magento/Catalog/Observer/Compare/BindCustomerLogoutObserver.php
+++ b/app/code/Magento/Catalog/Observer/Compare/BindCustomerLogoutObserver.php
@@ -14,6 +14,11 @@
*/
class BindCustomerLogoutObserver implements ObserverInterface
{
+ /**
+ * @var Item
+ */
+ private $item;
+
/**
* @param Item $item
*/
diff --git a/app/code/Magento/Catalog/Plugin/Api/ProductLinkRepositoryInterface/ReindexAfterDeleteByIdProductLinksPlugin.php b/app/code/Magento/Catalog/Plugin/Api/ProductLinkRepositoryInterface/ReindexAfterDeleteByIdProductLinksPlugin.php
new file mode 100644
index 000000000000..91e8629ec212
--- /dev/null
+++ b/app/code/Magento/Catalog/Plugin/Api/ProductLinkRepositoryInterface/ReindexAfterDeleteByIdProductLinksPlugin.php
@@ -0,0 +1,57 @@
+fullProductIndexer = $fullProductIndexer;
+ $this->productRepository = $productRepository;
+ }
+
+ /**
+ * Complex reindex after product links has been deleted.
+ *
+ * @param ProductLinkRepositoryInterface $subject
+ * @param bool $result
+ * @param string $sku
+ * @param string $type
+ * @param string $linkedProductSku
+ * @return bool
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterDeleteById(ProductLinkRepositoryInterface $subject, bool $result, $sku): bool
+ {
+ $product = $this->productRepository->get($sku);
+ $this->fullProductIndexer->executeRow($product->getId());
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Catalog/Plugin/Api/ProductLinkRepositoryInterface/ReindexAfterSaveProductLinksPlugin.php b/app/code/Magento/Catalog/Plugin/Api/ProductLinkRepositoryInterface/ReindexAfterSaveProductLinksPlugin.php
new file mode 100644
index 000000000000..480399035a7a
--- /dev/null
+++ b/app/code/Magento/Catalog/Plugin/Api/ProductLinkRepositoryInterface/ReindexAfterSaveProductLinksPlugin.php
@@ -0,0 +1,56 @@
+fullProductIndexer = $fullProductIndexer;
+ $this->productRepository = $productRepository;
+ }
+
+ /**
+ * Complex reindex after product links has been saved.
+ *
+ * @param ProductLinkRepositoryInterface $subject
+ * @param bool $result
+ * @param ProductLinkInterface $entity
+ * @return bool
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterSave(ProductLinkRepositoryInterface $subject, bool $result, ProductLinkInterface $entity): bool
+ {
+ $product = $this->productRepository->get($entity->getSku());
+ $this->fullProductIndexer->executeRow($product->getId());
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Catalog/Plugin/Model/Indexer/Category/Product/MaxHeapTableSizeProcessorOnFullReindex.php b/app/code/Magento/Catalog/Plugin/Model/Indexer/Category/Product/MaxHeapTableSizeProcessorOnFullReindex.php
index e655a66e9b91..cb4df8f162b4 100644
--- a/app/code/Magento/Catalog/Plugin/Model/Indexer/Category/Product/MaxHeapTableSizeProcessorOnFullReindex.php
+++ b/app/code/Magento/Catalog/Plugin/Model/Indexer/Category/Product/MaxHeapTableSizeProcessorOnFullReindex.php
@@ -15,6 +15,11 @@
*/
class MaxHeapTableSizeProcessorOnFullReindex
{
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
/**
* @var MaxHeapTableSizeProcessor
*/
diff --git a/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php b/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php
new file mode 100644
index 000000000000..ef3abddf91e6
--- /dev/null
+++ b/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php
@@ -0,0 +1,66 @@
+galleryResource = $galleryResource;
+ $this->mediaGalleryReadHandler = $mediaGalleryReadHandler;
+ }
+
+ /**
+ * Delete media gallery after deleting product
+ *
+ * @param ProductRepositoryInterface $subject
+ * @param callable $proceed
+ * @param ProductInterface $product
+ * @return bool
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function aroundDelete(
+ ProductRepositoryInterface $subject,
+ callable $proceed,
+ ProductInterface $product
+ ): bool {
+ $mediaGalleryAttributeId = $this->mediaGalleryReadHandler->getAttribute()->getAttributeId();
+ $mediaGallery = $this->galleryResource->loadProductGalleryByAttributeId($product, $mediaGalleryAttributeId);
+
+ $result = $proceed($product);
+
+ if ($mediaGallery) {
+ $this->galleryResource->deleteGallery(array_column($mediaGallery, 'value_id'));
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductExtendedActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductExtendedActionGroup.xml
index cf8a3879886f..340095e2339a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductExtendedActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductExtendedActionGroup.xml
@@ -15,7 +15,9 @@
-
-
+
+
+ ['{{groupPrice.customer_group}}']
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductImageOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductImageOnProductPageActionGroup.xml
new file mode 100644
index 000000000000..3dca650c2dcf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductImageOnProductPageActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validates that the provided product image is present and correct.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductInfoOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductInfoOnEditPageActionGroup.xml
new file mode 100644
index 000000000000..9e0d13c1bc91
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductInfoOnEditPageActionGroup.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Verifies the general data on the Edit product details page in admin for a product.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeSeoUrlKeyToDefaultValueWithoutRedirectActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeSeoUrlKeyToDefaultValueWithoutRedirectActionGroup.xml
new file mode 100644
index 000000000000..a8539859c6d7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeSeoUrlKeyToDefaultValueWithoutRedirectActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Requires navigation to category creation/edit page. Selects 'Use Default Value' checkbox for the 'URL Key' with 'Create Permanent Redirect for old URL' unchecked.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml
index 60438e23e084..2fb18ddfc9eb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryLinkActionGroup.xml
@@ -17,5 +17,7 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml
index fe5b0ae1a64c..703b37079aee 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml
@@ -21,7 +21,7 @@
-
+
{{AdminDataGridTableSection.firstNotEmptyRow2}}
{{AdminConfirmationModalSection.ok}}
{{AdminMainActionsSection.delete}}
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminExpandProductDesignSectionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminExpandProductDesignSectionActionGroup.xml
new file mode 100644
index 000000000000..f3d742f28cab
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminExpandProductDesignSectionActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Expand the Design section on the Product Details page in Admin.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductNameOnProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductNameOnProductFormActionGroup.xml
new file mode 100644
index 000000000000..e2adc0937608
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductNameOnProductFormActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Fills in Name field on the Admin Products creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductPriceFieldAndPressEnterOnProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductPriceFieldAndPressEnterOnProductEditPageActionGroup.xml
new file mode 100644
index 000000000000..ed10792d09cf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductPriceFieldAndPressEnterOnProductEditPageActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductQtyOnProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductQtyOnProductFormActionGroup.xml
new file mode 100644
index 000000000000..225638f066a2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductQtyOnProductFormActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Fills in Quantity field on the Admin Products creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductSkuOnProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductSkuOnProductFormActionGroup.xml
new file mode 100644
index 000000000000..f7b4baaeef8e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductSkuOnProductFormActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Fills in Sku field on the Admin Products creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductAttributeActionGroup.xml
new file mode 100644
index 000000000000..10133e45db36
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductAttributeActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductAttributeSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductAttributeSaveActionGroup.xml
new file mode 100644
index 000000000000..aec420d6650a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductAttributeSaveActionGroup.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductImagesSectionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductImagesSectionActionGroup.xml
index 4d49b13a8bf5..fc965dcfc629 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductImagesSectionActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductImagesSectionActionGroup.xml
@@ -13,6 +13,8 @@
Requires the navigation to the Product page. Opens 'Image and Videos' section.
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAssertImageAltTextActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAssertImageAltTextActionGroup.xml
new file mode 100644
index 000000000000..bf1324fce969
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAssertImageAltTextActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Assert product image alt text.
+
+
+
+
+
+
+
+
+
+
+
+ {{altText}}
+ actualAltText
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetVisibleInAdvancedSearchActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetVisibleInAdvancedSearchActionGroup.xml
new file mode 100644
index 000000000000..bd7c59ffc385
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetVisibleInAdvancedSearchActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Set 'Visible in Advanced Search' value for product attribute
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductChangeImageAltTextActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductChangeImageAltTextActionGroup.xml
new file mode 100644
index 000000000000..e56328bbb5c6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductChangeImageAltTextActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Change product image alt text.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveCategoryFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveCategoryFormActionGroup.xml
index 5521cc2a7d0b..30a9dd4276eb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveCategoryFormActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveCategoryFormActionGroup.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAsNewDateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAsNewDateActionGroup.xml
new file mode 100644
index 000000000000..219bc73655c8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAsNewDateActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminNoValidationErrorForPriceFieldOnProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminNoValidationErrorForPriceFieldOnProductEditPageActionGroup.xml
new file mode 100644
index 000000000000..c1281fdaa499
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminNoValidationErrorForPriceFieldOnProductEditPageActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductFormAdvancedPricingAddTierPriceActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductFormAdvancedPricingAddTierPriceActionGroup.xml
new file mode 100644
index 000000000000..64acee0b077c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductFormAdvancedPricingAddTierPriceActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Check tier price on Advanced Pricing dialog on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+ $priceMinWidth
+ 60px
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminValidationErrorAppearedForPriceFieldOnProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminValidationErrorAppearedForPriceFieldOnProductEditPageActionGroup.xml
new file mode 100644
index 000000000000..c206ee582b51
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminValidationErrorAppearedForPriceFieldOnProductEditPageActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageAdminProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageAdminProductPageActionGroup.xml
index 6ea154d2b01d..c2a2dd5c13de 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageAdminProductPageActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageAdminProductPageActionGroup.xml
@@ -8,7 +8,7 @@
-
+
Validates that the provided Product Image is present and correct.
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontAttributeOptionQuantityInLayeredNavigationActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontAttributeOptionQuantityInLayeredNavigationActionGroup.xml
new file mode 100644
index 000000000000..f1ffc1475e87
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontAttributeOptionQuantityInLayeredNavigationActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Asserts visible attribute option quantity
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductAttributeLabelVisibleActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductAttributeLabelVisibleActionGroup.xml
new file mode 100644
index 000000000000..ea2ea3b12eba
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductAttributeLabelVisibleActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductDropDownOptionValueActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductDropDownOptionValueActionGroup.xml
new file mode 100644
index 000000000000..a633c3bc06c5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductDropDownOptionValueActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Validates that the provided Product Option is selected under the provided Drop-down Attribute.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByCustomDateRangeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByCustomDateRangeActionGroup.xml
new file mode 100644
index 000000000000..30b15abb234d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByCustomDateRangeActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Date Filter.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml
index f2524d6e68bf..670eb04b55bd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameIsNotOnProductMainPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameIsNotOnProductMainPageActionGroup.xml
new file mode 100644
index 000000000000..ccda16f37085
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameIsNotOnProductMainPageActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Validates that the provided Product Name is NOT present on a Storefront page.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertRelatedProductOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertRelatedProductOnProductPageActionGroup.xml
new file mode 100644
index 000000000000..2d31eae0aedd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertRelatedProductOnProductPageActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validates that the provided Product Name is present on Product details page.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertSubCategoryNameIsNotShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertSubCategoryNameIsNotShownInMenuActionGroup.xml
new file mode 100644
index 000000000000..157138f5e667
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertSubCategoryNameIsNotShownInMenuActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Validate that the subcategory is not present in menu on Frontend.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateToCategoryUrlActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateToCategoryUrlActionGroup.xml
new file mode 100644
index 000000000000..f93862788654
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateToCategoryUrlActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Goes to the Storefront Category page for the provided Category URL.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageSelectDropDownOptionValueActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageSelectDropDownOptionValueActionGroup.xml
index 31b18e1f0d37..9cb3f3faf2f3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageSelectDropDownOptionValueActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageSelectDropDownOptionValueActionGroup.xml
@@ -18,5 +18,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontRemoveFirstProductFromCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontRemoveFirstProductFromCompareActionGroup.xml
new file mode 100644
index 000000000000..fefcdb6930c6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontRemoveFirstProductFromCompareActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Open Compare Products list and remove a product
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
index 9639bc39b45f..0fb4f3ef3205 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
@@ -11,6 +11,7 @@
simpleCategory
simplecategory
+ simplecategory
true
@@ -20,12 +21,14 @@
SimpleSubCategory
simplesubcategory
+ simplesubcategory
true
true
NewRootCategory
newrootcategory
+ newrootcategory
true
true
1
@@ -45,22 +48,27 @@
FirstLevelSubCategory
firstlevelsubcategory
+ firstlevelsubcategory
SecondLevelSubCategory
secondlevelsubcategory
+ secondlevelsubcategory
ThirdLevelSubCategory
- subcategory
+ thirdlevelsubcategory
+ thirdlevelsubcategory
FourthLevelSubCategory
- subcategory
+ fourthlevelsubcategory
+ fourthlevelsubcategory
FifthLevelCategory
- category
+ fifthlevelcategory
+ fifthlevelcategory
SimpleRootSubCategory
@@ -73,6 +81,7 @@
SubCategory
subcategory
+ subcategory
true
true
@@ -95,6 +104,7 @@
NotInclMenu
notinclemenu
+ notinclemenu
true
false
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
index 6e40499d0efe..6f270feb20b6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
@@ -8,11 +8,16 @@
-
+
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
image/jpeg
test_image.jpg
+
+ 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
+ image/jpeg
+ adobe-base.jpg
+
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
image/png
@@ -21,4 +26,21 @@
magento-logo.png
+
+ iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAFpOLgnAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAADKgAwAEAAAAAQAAADIAAAAAaawubwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAAN1JREFUaAXtllEKwlAMBFvvfwsP5XEUhf2MzLOwFp3+BF42TTL7KN236+2+Dc9lOH8dny65T6uMk341MY77MfSpcFxzKnien7xomd7yPha8ux7J/SWl5csXWitxmezKy6O1SUigKC6EKSJxhQSKFVx+u5AXEVU8sUlwoyguhCkicYUEiuJCmCISV0igKC6EKSJxhQSKlZ87NMlBUcX3gzOichdBmIoiHSnCRq10BGEqinSkCBu10hGEqSjSkSJs1EpHEKaiSEeKsFErHUGYiiIdKcJGrXQEYSqKfsaRB19MEgdQHhXpAAAAAElFTkSuQmCC
+ image/png
+ blue.png
+ 5da9660dc7c0536210547e0000f9a9cf
+ b1e89172f4d192f00c1d947741759180
+
+
+ iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAFpOLgnAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAADKgAwAEAAAAAQAAADIAAAAAaawubwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAAN1JREFUaAXtllEKwlAMBFuP4f2v4PkUhf2MzLOwFp3+BF42TTL7KN1v1+2+Dc9lOH8dny65T6uMk341MY77MfSpcFxzKnien7xomd7yPha8ux7J/SWl5csXWitxmezKy6O1SUigKC6EKSJxhQSKFVx+u5AXEVU8sUlwoyguhCkicYUEiuJCmCISV0igKC6EKSJxhQSKlZ87NMlBUcX3gzOichdBmIoiHSnCRq10BGEqinSkCBu10hGEqSjSkSJs1EpHEKaiSEeKsFErHUGYiiIdKcJGrXQEYSqKfsaRB7YwDVopmL/PAAAAAElFTkSuQmCC
+ image/png
+ red.png
+ 55942e709d0a1c85d5174839c2e55cea
+ 263ab50a24625c8d4f855cee8b947daa
+
+
+ c0459a796c5b8ee74254472c235a7460
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
index 7016a1c1d035..5cc6b4ea3429 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
@@ -20,6 +20,18 @@
false
TestImageContent
+
+ image
+ Adobe Base
+ 1
+
+ - image
+ - small_image
+ - thumbnail
+
+ false
+ AdobeBaseContent
+
image
Magento Logo
@@ -30,6 +42,23 @@
false
MagentoLogoImageContent
+
+ image
+ Blue PNG
+ 1
+
+ - image
+ - small_image
+ - thumbnail
+ - swatch
+
+ false
+ BluePngImageContent
+
+
+ Red PNG
+ RedPngImageContent
+
Magento Logo
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
index 5375459122e6..567b3940eb6f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
@@ -11,7 +11,7 @@
testProductName
testSku
- testurlkey
+ testproductname
simple
4
4
@@ -39,6 +39,10 @@
TestFooBar
foobar
+
+ Simple Product Double Space
+ simple-product double-space
+
Pursuit Lumaflex™ Tone Band
x™
@@ -59,9 +63,9 @@
api-simple-product
- SimpleProduct
+ Simple Product
SimpleProduct
- simpleproduct
+ simple-product-
simple
4
123.00
@@ -72,6 +76,11 @@
EavStockItem
CustomAttributeCategoryIds
+
+ SimpleProduct
+ SimpleProduct
+ simpleproduct
+
partialTestSku
@@ -135,8 +144,9 @@
CustomAttributeCategoryIds
- SimpleProduct
- SimpleProduct
+ Simple Product 2
+ SimpleProduct2
+ simple-product-2-
simple
4
123.00
@@ -147,15 +157,15 @@
EavStockItem
- simple
- simple
+ Simple Product 3
+ SimpleProduct3
+ simple-product-3-
simple
4
123.00
4
1
1000
- simple
EavStockItem
CustomAttributeCategoryIds
@@ -1392,9 +1402,34 @@
16.00
+
+ 90.00
+
+
+ 95.00
+
+
+ 80.00
+
ProductOptionField
ProductOptionField2
+
+ ProductWithSku24MB01-
+ 24 MB01
+
+
+ ProductWithSku24MB02-
+ 24 MB02
+
+
+ ProductWithSku24MB04-
+ 24 MB04
+
+
+ ProductWithSku24MB06-
+ 24 MB06
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml
index b763fda489b2..fb587da75940 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml
@@ -86,4 +86,18 @@
0.1
1
+
+ 80
+ fixed
+ 0
+ ALL GROUPS
+ 2
+
+
+ 70
+ fixed
+ 0
+ General
+ 3
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml
deleted file mode 100644
index 3965cfa1f958..000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
- cms/wysiwyg/enabled
- 0
- Yes
- enabled
-
-
- cms/wysiwyg/editor
- 0
- Yes
- mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php
new file mode 100644
index 000000000000..14867cd130cd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Helper/LocalFileAssertions.php
@@ -0,0 +1,337 @@
+driver = new File();
+ }
+
+ /**
+ * Create a text file
+ *
+ * @param string $filePath
+ * @param string $text
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function createTextFile($filePath, $text): void
+ {
+ $realPath = $this->expandPath($filePath);
+ $this->driver->filePutContents($realPath, $text);
+ }
+
+ /**
+ * Delete a file if it exists
+ *
+ * @param string $filePath
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function deleteFileIfExists($filePath): void
+ {
+ $realPath = $this->expandPath($filePath);
+ if ($this->driver->isExists($realPath)) {
+ $this->driver->deleteFile($realPath);
+ }
+ }
+
+ /**
+ * Recursive delete directory
+ *
+ * @param string $path
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function deleteDirectory($path): void
+ {
+ $realPath = $this->expandPath($path);
+ if ($this->driver->isExists($realPath)) {
+ $this->driver->deleteDirectory($realPath);
+ }
+ }
+
+ /**
+ * Copy source into destination
+ *
+ * @param string $source
+ * @param string $destination
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function copy($source, $destination): void
+ {
+ $sourceRealPath = $this->expandPath($source);
+ $destinationRealPath = $this->expandPath($destination);
+ $this->driver->copy($sourceRealPath, $destinationRealPath);
+ }
+
+ /**
+ * Create directory
+ *
+ * @param string $path
+ * @param int $permissions
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function createDirectory($path, $permissions = 0777): void
+ {
+ $permissions = $this->convertOctalStringToDecimalInt($permissions);
+ $sourceRealPath = $this->expandPath($path);
+ $oldUmask = umask(0);
+ $this->driver->createDirectory($sourceRealPath, $permissions);
+ umask($oldUmask);
+ }
+
+ /**
+ * Assert a file exists
+ *
+ * @param string $filePath
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertFileExists($filePath, $message = ''): void
+ {
+ $realPath = $this->expandPath($filePath);
+ $this->assertTrue($this->driver->isExists($realPath), "Failed asserting $filePath exists. " . $message);
+ }
+
+ /**
+ * Asserts that a file with the given glob pattern exists in the given path
+ *
+ * @param string $path
+ * @param string $pattern
+ * @param string $message
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertGlobbedFileExists($path, $pattern, $message = ''): void
+ {
+ $realPath = $this->expandPath($path);
+ $files = $this->driver->search($pattern, $realPath);
+ $this->assertNotEmpty($files, "Failed asserting file matching glob pattern \"$pattern\" at location \"$path\" is not empty. " . $message);
+ }
+
+ /**
+ * Asserts that a file or directory exists
+ *
+ * @param string $path
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertPathExists($path, $message = ''): void
+ {
+ $realPath = $this->expandPath($path);
+ $this->assertTrue($this->driver->isExists($realPath), "Failed asserting $path exists. " . $message);
+ }
+
+ /**
+ * Asserts that a file or directory does not exist
+ *
+ * @param string $path
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertPathDoesNotExist($path, $message = ''): void
+ {
+ $realPath = $this->expandPath($path);
+ $this->assertFalse($this->driver->isExists($realPath), "Failed asserting $path does not exist. " . $message);
+ }
+
+ /**
+ * Assert a file does not exist
+ *
+ * @param string $filePath
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertFileDoesNotExist($filePath, $message = ''): void
+ {
+ $realPath = $this->expandPath($filePath);
+ $this->assertFalse($this->driver->isExists($realPath), $message);
+ }
+
+ /**
+ * Assert a file has no contents
+ *
+ * @param string $filePath
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertFileEmpty($filePath, $message = ''): void
+ {
+ $realPath = $this->expandPath($filePath);
+ $this->assertEmpty($this->driver->fileGetContents($realPath), "Failed asserting $filePath is empty. " . $message);
+ }
+
+ /**
+ * Assert a file is not empty
+ *
+ * @param string $filePath
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertFileNotEmpty($filePath, $message = ''): void
+ {
+ $realPath = $this->expandPath($filePath);
+ $this->assertNotEmpty($this->driver->fileGetContents($realPath), "Failed asserting $filePath is not empty. " . $message);
+ }
+
+ /**
+ * Assert a file contains a given string
+ *
+ * @param string $filePath
+ * @param string $text
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertFileContainsString($filePath, $text, $message = ''): void
+ {
+ $realPath = $this->expandPath($filePath);
+ $this->assertStringContainsString($text, $this->driver->fileGetContents($realPath), "Failed asserting $filePath contains $text. " . $message);
+ }
+
+ /**
+ * Asserts that a file with the given glob pattern at the given path contains a given string
+ *
+ * @param string $path
+ * @param string $pattern
+ * @param string $text
+ * @param int $fileIndex
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertGlobbedFileContainsString($path, $pattern, $text, $fileIndex = 0, $message = ''): void
+ {
+ $realPath = $this->expandPath($path);
+ $files = $this->driver->search($pattern, $realPath);
+ $this->assertStringContainsString($text, $this->driver->fileGetContents($files[$fileIndex] ?? ''), "Failed asserting file of index \"$fileIndex\" matching glob pattern \"$pattern\" at location \"$path\" contains $text. " . $message);
+ }
+
+ /**
+ * Assert a file does not contain a given string
+ *
+ * @param string $filePath
+ * @param string $text
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertFileDoesNotContainString($filePath, $text, $message = ''): void
+ {
+ $realPath = $this->expandPath($filePath);
+ $this->assertStringNotContainsString($text, $this->driver->fileGetContents($realPath), "Failed asserting $filePath does not contain $text. " . $message);
+ }
+
+ /**
+ * Asserts that a directory is empty
+ *
+ * @param string $path
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertDirectoryEmpty($path, $message = ''): void
+ {
+ $realPath = $this->expandPath($path);
+ $this->assertEmpty($this->driver->readDirectory($realPath), "Failed asserting $path is empty. " . $message);
+ }
+
+ /**
+ * Asserts that a directory is not empty
+ *
+ * @param string $path
+ * @param string $message
+ * @return void
+ *
+ * @throws \Magento\Framework\Exception\FileSystemException
+ */
+ public function assertDirectoryNotEmpty($path, $message = ''): void
+ {
+ $realPath = $this->expandPath($path);
+ $this->assertNotEmpty($this->driver->readDirectory($realPath), "Failed asserting $path is not empty. " . $message);
+ }
+
+ /**
+ * Helper function to convert an octal string to its decimal equivalent
+ *
+ * @param string $string
+ * @return int
+ *
+ */
+ private function convertOctalStringToDecimalInt($string): int
+ {
+ if (is_string($string)) {
+ $string = octdec($string);
+ }
+ return $string;
+ }
+
+ /**
+ * Helper function to construct the real path to the file
+ *
+ * If the given path isn't an absolute path then assume it's in context of the Magento root
+ *
+ * @param string $filePath
+ * @return string
+ */
+ private function expandPath($filePath): string
+ {
+ return (substr($filePath, 0, 1) === '/') ? $filePath : MAGENTO_BP . '/' . $filePath;
+
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
index 416e0d27a182..e0640ba39acd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
@@ -11,5 +11,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml
index ea4f4bf53eb7..14bf132c6a0c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml
@@ -11,5 +11,6 @@
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml
index d7dc38a4a88f..5f3a836de4af 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml
@@ -25,5 +25,7 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml
index 91ac52a91a4c..672df19a8e66 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml
@@ -26,6 +26,8 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml
index d70c48f2b00e..590b9a185e5e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml
@@ -75,6 +75,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml
index c2de91aadbc0..0de3e6de1dee 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml
@@ -13,6 +13,8 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml
index c51d481d7dc5..ff7edf14a50f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml
@@ -11,5 +11,6 @@
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
index 6b67f5609f7f..f3a0919f6c72 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
@@ -39,5 +39,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml
index 848035b911aa..3131e9a89e8c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml
@@ -16,8 +16,9 @@
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml
index ad31be6b277e..f8000a25efdc 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductCompareMainSection.xml
@@ -14,5 +14,7 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
index 5efa094e2c35..63b50e2d82f0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml
index e1499a248435..fad0b26262d0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml
@@ -22,9 +22,7 @@
-
-
-
+
@@ -33,9 +31,7 @@
-
-
-
+
@@ -58,9 +54,7 @@
-
-
-
+
@@ -84,8 +78,7 @@
-
-
+
@@ -98,11 +91,13 @@
-
+
-
+
+
+
@@ -114,11 +109,11 @@
-
-
+
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
index 2dc840b60f3b..bed5297041dd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
@@ -42,9 +42,7 @@
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
index d1e292ff5644..d713660d7ee6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
@@ -42,7 +42,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml
index 029c304873ce..e1f5798b3741 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml
@@ -11,7 +11,9 @@
-
+
+
+
@@ -48,7 +50,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
index 5ea7253619ed..8a00e30e9ed2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
@@ -21,7 +21,9 @@
-
+
+
+
@@ -103,7 +105,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
index 73019bb5ec0e..a574b0c7eabf 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
@@ -59,17 +59,15 @@
-
-
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml
index 1ed079b12d1f..8add42ec7493 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml
@@ -55,14 +55,14 @@
-
+
-
+
@@ -79,14 +79,14 @@
-
+
-
+
@@ -109,19 +109,19 @@
-
+
-
+
-
+
@@ -213,7 +213,7 @@
$1,500.00
grabTextFromSubtotalField6
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml
index 0b29d2edb661..6ffd15746809 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml
@@ -32,7 +32,7 @@
-
+
@@ -41,6 +41,10 @@
+
+
+
+
@@ -60,7 +64,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
index b52d18f3c020..7a99afa3dcba 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
@@ -42,7 +42,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml
index a4a42a9999a5..f361345478b5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml
@@ -41,7 +41,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
index 99fd1cf3caf3..88d24540b11f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
@@ -42,7 +42,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
index 3fff2c118ae6..f956c7331942 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
@@ -124,7 +124,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml
index 32c1599355f8..8e45c223fbf4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml
@@ -89,7 +89,7 @@
-
+
@@ -124,7 +124,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
index 0525e7543acc..1ba9c3a6ddc7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
@@ -64,7 +64,7 @@
-
+
@@ -114,7 +114,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml
index ce3dd8fa0873..61b7b8f6eaca 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml
@@ -97,7 +97,7 @@
-
+
@@ -166,7 +166,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml
index 6e607ca012ba..72f1fdb1ae63 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml
@@ -27,7 +27,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
index 8d0534891a29..6886775bff57 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
@@ -71,15 +71,13 @@
-
-
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml
index f61a97219903..fcffd272a2fe 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml
@@ -67,7 +67,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml
index 4173254c66fc..d6fb7d0aa2c8 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml
@@ -80,12 +80,12 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
index af72dba3f805..31ad92afb9d4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
@@ -36,7 +36,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml
index 10cba3ab209e..4758bf63859b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml
@@ -114,7 +114,7 @@
-
+
@@ -130,7 +130,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
index 9db8e74b6ae7..5931193dbe7c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
@@ -82,10 +82,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
index 7447c75a778a..47c7f86067cf 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
@@ -31,9 +31,7 @@
-
-
-
+
@@ -64,14 +62,12 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
index df2124759686..fc09a1ac07d5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
@@ -31,9 +31,7 @@
-
-
-
+
@@ -67,13 +65,11 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
index 2f86209da1eb..a2bd771c58fe 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
@@ -31,9 +31,7 @@
-
-
-
+
@@ -68,13 +66,11 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
index 7b555aa84be0..45b776a6c871 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
@@ -86,10 +86,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
index 37a4bc613f65..8f56a062c911 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
@@ -55,7 +55,7 @@
-
+
@@ -110,7 +110,7 @@
-
+
@@ -126,12 +126,16 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml
index d1110f593545..069baf6544d3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml
@@ -35,7 +35,7 @@
-
+
@@ -79,7 +79,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml
index 6b3768cc88b3..e61684b91c08 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml
@@ -32,13 +32,24 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductNotVisibleIndividuallyTest.xml
index 12cd5454ea8e..deaa736ea6b4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductNotVisibleIndividuallyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductNotVisibleIndividuallyTest.xml
@@ -23,7 +23,11 @@
-
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml
index ac2e86a57245..819835dead30 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml
@@ -5,8 +5,8 @@
* See COPYING.txt for license details.
*/
-->
-
+
@@ -21,18 +21,29 @@
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml
index 9f51d6227aa1..32cce3633b10 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml
@@ -18,11 +18,20 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml
index 3e5ccfd8bf3b..1c478eb4b654 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml
@@ -22,6 +22,11 @@
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml
index 24f87cca958a..b1f18a770ea0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml
@@ -18,13 +18,22 @@
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml
index 13efee209a55..47c7868b2400 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml
@@ -48,10 +48,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
index a00714e412b0..0c1785b8f725 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
@@ -22,6 +22,10 @@
+
+
+
+
@@ -32,9 +36,7 @@
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml
index fd6db45b1716..7d57d81e565b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml
@@ -13,17 +13,19 @@
-
+
-
+
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml
index cffefc4cd74c..4c02c57dae53 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml
@@ -25,31 +25,54 @@
-
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
index 3141db87f697..c9670ba5a8a7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
@@ -118,18 +118,20 @@
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml
index f2840758d59a..0c4709b01da4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml
@@ -91,22 +91,25 @@
-
+
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
As low as ${{tierPriceOnVirtualProduct.price}}
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
index bf0d5d99a23b..5b1c3c7afc31 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
@@ -87,18 +87,31 @@
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
@@ -107,21 +120,32 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
index 4599d0c27521..2c05b1515bc9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
@@ -35,7 +35,7 @@
-
+
@@ -45,7 +45,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
index 3514f53e8b93..0cb59ee54ae9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
@@ -66,9 +66,7 @@
-
-
-
+
@@ -95,9 +93,7 @@
-
-
-
+
@@ -179,7 +175,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
index bdd1a4b4c70f..2c6cc08d7689 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
@@ -34,7 +34,7 @@
-
+
@@ -44,7 +44,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
index dcfcbd699fc6..de72de276929 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
@@ -35,7 +35,7 @@
-
+
@@ -45,7 +45,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
index 283ed72e62fa..979762e1d98b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
@@ -20,7 +20,9 @@
-
+
+
+
@@ -46,7 +48,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
index 4dd76e55f933..e8ed35ecb4a9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
@@ -32,9 +32,7 @@
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml
new file mode 100644
index 000000000000..161c349b4d29
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml
index 30ab17f65f3c..44df83a4a729 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml
@@ -14,8 +14,8 @@
-
-
+
+
@@ -51,35 +51,43 @@
-
+
+
+
+
+
+
-
-
+
+
-
+
-
-
+
+
-
+
+
+
+
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
index 3da19eb59801..ff9d99f76d6b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
@@ -66,12 +66,10 @@
-
-
-
+
-
+
@@ -98,7 +96,7 @@
-
+
@@ -109,7 +107,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml
index 1c55b09151cf..96f905c3d916 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml
@@ -22,7 +22,7 @@
true
-
+
@@ -110,7 +110,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
index fe3ffbb4fc1d..2a03effb51ae 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
@@ -59,7 +59,7 @@
-
+
@@ -88,7 +88,7 @@
-
+
@@ -99,7 +99,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
index 8eb3d9bbb806..c81e47a84189 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
@@ -56,7 +56,7 @@
-
+
@@ -76,10 +76,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
index 0af391147481..002f11b5adfc 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
@@ -140,7 +140,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
index 94d7ea14b096..edde693c5120 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
@@ -156,9 +156,9 @@
-
+
-
+
@@ -167,7 +167,7 @@
-
+
@@ -176,7 +176,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml
index e06a7f3c5679..a422d781b816 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml
@@ -35,9 +35,7 @@
-
-
-
+
@@ -80,8 +78,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateWithCustomLocaleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateWithCustomLocaleTest.xml
new file mode 100644
index 000000000000..eae9cebd7e63
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateWithCustomLocaleTest.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml
index 6cbf03a02f3b..b9f199c3954e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml
@@ -46,7 +46,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml
index 2311369db48f..809096626270 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml
@@ -66,7 +66,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml
index e989aa3758cf..2cdbc26f90f7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml
@@ -66,9 +66,7 @@
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml
index b2ed7b9628f3..49add85f7680 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml
@@ -58,8 +58,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml
new file mode 100644
index 000000000000..c3e939b4155c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml
index 8a33f6132aeb..17dac7600ef9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml
@@ -106,7 +106,7 @@
-
+
@@ -118,7 +118,7 @@
-
+
@@ -142,9 +142,7 @@
-
-
-
+
@@ -155,11 +153,11 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml
index e1c97b205d4f..9dae1259cc4f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml
@@ -11,11 +11,11 @@
-
+
-
-
-
+
+
+
@@ -29,6 +29,7 @@
+
@@ -36,16 +37,16 @@
-
+
-
+
-
-
+
+
-
+
@@ -87,12 +88,12 @@
-
+
-
+
@@ -100,7 +101,7 @@
-
+
@@ -108,12 +109,13 @@
-
+
-
-
+
+
+
@@ -123,19 +125,19 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml
index fc18531eca35..92966e0a0095 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml
@@ -70,7 +70,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml
index 3c90de572988..a3ab78a11e09 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml
@@ -78,7 +78,7 @@
-
+
@@ -98,13 +98,14 @@
-
+
-
+
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml
index a9f8eab9a582..b31ef59e30bb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml
@@ -31,9 +31,7 @@
-
-
-
+
@@ -46,9 +44,7 @@
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml
index e562faf52392..e800b72f3385 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml
@@ -62,7 +62,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml
index f82294ece647..20fb1f6dc4f4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml
@@ -65,16 +65,11 @@
-
-
-
-
-
-
-
+
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml
index f7f87da77b40..a6523c018bdb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml
@@ -31,10 +31,7 @@
-
-
-
-
+
@@ -76,13 +73,11 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml
index b316e3194c98..2c7e26d4084b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml
@@ -33,11 +33,9 @@
-
+
-
-
-
+
@@ -53,7 +51,7 @@
-
+
@@ -67,13 +65,11 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml
index 2124efed3129..13891c58c601 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml
@@ -35,9 +35,7 @@
-
-
-
+
@@ -68,13 +66,11 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml
index 70d6932f6fc1..9e5fd6261ee0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml
@@ -71,20 +71,31 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml
index f9c90b119061..26380bd2861d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml
@@ -69,20 +69,24 @@
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml
index 420a0604f038..34aea06bb69c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml
@@ -27,9 +27,7 @@
-
-
-
+
@@ -68,10 +66,10 @@
-
+
-
+
@@ -112,42 +110,52 @@
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {{simpleProductTierPrice300InStock.storefrontStatus}}
- productStockAvailableStatus
-
-
-
- ${{simpleProductTierPrice300InStock.price}}
- productPriceAmount
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml
index 9cae9cc3f1f4..ab0fcea919af 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml
@@ -81,10 +81,14 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml
index d2e145adb6a8..39ff6d99b5b7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml
@@ -19,7 +19,7 @@
Use AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest instead
-
+
@@ -108,7 +108,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml
index aad946dced83..3ee9d0a9262c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml
@@ -36,72 +36,97 @@
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml
index 9cb326ca6d80..6f8356f86986 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml
@@ -51,10 +51,10 @@
-
+
-
+
@@ -89,7 +89,6 @@
-
@@ -98,36 +97,47 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {{simpleProductRegularPrice245InStock.storefrontStatus}}
- productStockAvailableStatus
-
-
-
- ${{simpleProductRegularPrice245InStock.price}}
- productPriceAmount
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml
index 38c0a7449210..4de9552b863b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml
@@ -36,98 +36,145 @@
-
-
-
-
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {{simpleProductRegularPrice32501InStock.storefrontStatus}}
- productStockAvailableStatus
-
-
-
- ${{simpleProductRegularPrice32501InStock.price}}
- productPriceAmount
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml
index 4ce3af599123..312baff6b8bc 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml
@@ -51,10 +51,10 @@
-
+
-
+
@@ -89,43 +89,52 @@
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {{simpleProductRegularPrice325InStock.storefrontStatus}}
- productStockAvailableStatus
-
-
-
- ${{simpleProductRegularPrice325InStock.price}}
- productPriceAmount
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml
index 2316e36eb0b9..6066a02110f1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml
@@ -121,23 +121,29 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {{simpleProductRegularPriceCustomOptions.storefrontStatus}}
- productStockAvailableStatus
-
-
-
- ${{simpleProductRegularPriceCustomOptions.price}}
- productPriceAmount
-
+
+
+
+
+
+
+
@@ -157,7 +163,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml
index dcec3e3f8bd5..04fc2b70a1cb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml
@@ -36,95 +36,163 @@
-
-
-
-
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
- {{simpleProductRegularPrice32503OutOfStock.storefrontStatus}}
- productStockAvailableStatus
-
-
-
- ${{simpleProductRegularPrice32503OutOfStock.price}}
- productPriceAmount
-
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml
index f4375ad499df..2a03187af26b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml
@@ -116,14 +116,18 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml
index 0cf0d22094cb..4e0b5a635594 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml
@@ -207,7 +207,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml
index 343326547254..3cef25a62775 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml
@@ -96,7 +96,7 @@
-
+
@@ -122,10 +122,14 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml
index 0d6a1c87c62f..1e87a10fe753 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml
@@ -100,14 +100,18 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml
index f423cd6c7780..41c187975a92 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml
@@ -105,18 +105,22 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml
index 78a15ee7eb19..df2e7f530158 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml
@@ -130,10 +130,15 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml
index f64e628c0edb..409ed2cd272a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml
@@ -114,7 +114,7 @@
-
+
@@ -146,10 +146,14 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml
index 6e835f2e5e98..0123c226d3fa 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml
@@ -114,7 +114,7 @@
-
+
@@ -146,10 +146,15 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualProductSetEditContentTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualProductSetEditContentTest.xml
index 1e7b7cc14f9c..ea562cbbd52b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualProductSetEditContentTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualProductSetEditContentTest.xml
@@ -35,6 +35,6 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualSetEditRelatedProductsTest.xml
index f70f5757ec5c..f12512e99812 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualSetEditRelatedProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVirtualSetEditRelatedProductsTest.xml
@@ -15,13 +15,9 @@
-
-
-
-
-
+
+
-
@@ -38,6 +34,6 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
index 5431a19461f7..3faa9e8cf795 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
@@ -18,6 +18,8 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
index 2095d56ce6c5..2f4e20c7c8b5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
@@ -109,11 +109,7 @@
-
-
-
-
-
+
@@ -159,14 +155,14 @@
-
-
+
+
-
-
+
+
-
-
+
+
@@ -224,14 +220,14 @@
-
-
+
+
-
-
+
+
-
-
+
+
@@ -257,8 +253,8 @@
-
-
+
+
@@ -296,8 +292,8 @@
-
-
+
+
@@ -333,9 +329,7 @@
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
index e50c124f7cfd..9bebe97a8b34 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
@@ -22,8 +22,12 @@
-
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml
index e67940274039..1094af357b18 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml
@@ -24,18 +24,14 @@
-
-
-
+
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml
index 845299b3e33b..a5ed6b0a370d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml
@@ -24,20 +24,27 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml
index 916bcd7405ec..9c18ba6cd654 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml
@@ -49,7 +49,7 @@
-
+
@@ -60,7 +60,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml
index f6292a3a96c4..662ff5d0195c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml
@@ -63,7 +63,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAssertProductFinalPriceChangesDynamicallyOnProductPageWithTierPricesConfiguredTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAssertProductFinalPriceChangesDynamicallyOnProductPageWithTierPricesConfiguredTest.xml
new file mode 100644
index 000000000000..ca36442543e4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAssertProductFinalPriceChangesDynamicallyOnProductPageWithTierPricesConfiguredTest.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml
index 64ad34825785..fe7bbd49dd40 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml
@@ -75,9 +75,7 @@
-
-
-
+
@@ -96,13 +94,10 @@
-
-
-
-
+
-
-
+
+
@@ -116,8 +111,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml
index 4d04a25c8d12..8034e7911cb0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml
@@ -67,9 +67,7 @@
-
-
-
+
@@ -90,13 +88,11 @@
-
-
-
+
-
-
+
+
@@ -114,8 +110,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml
index 69f78c63e267..b1caeac384d8 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchSimpleProductBySkuWithHyphenTest.xml
@@ -15,12 +15,9 @@
-
+
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml
index b87cce398498..5ef2fd65fec1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAdvanceCatalogSearchVirtualProductBySkuWithHyphenTest.xml
@@ -15,12 +15,9 @@
-
-
+
+
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml
index 3ff477070cc3..d8c798dbc140 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml
@@ -43,10 +43,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml
index a2316efb3a74..befb64d01bfd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml
@@ -43,10 +43,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckCustomOptionPriceDifferentCurrencyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckCustomOptionPriceDifferentCurrencyTest.xml
index a8368fcd9503..38f72fe24bb4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckCustomOptionPriceDifferentCurrencyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckCustomOptionPriceDifferentCurrencyTest.xml
@@ -20,6 +20,7 @@
+
@@ -29,6 +30,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
index ca561e4af70d..c9be526e095a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
@@ -188,13 +188,11 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml
index 5d584bed989b..d0c6c4fe86ae 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml
@@ -170,7 +170,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml
index 9731b66209df..34d712f49e33 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml
@@ -34,9 +34,7 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml
index 1032c322053d..02b21394770c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml
@@ -42,7 +42,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml
index 9819357704d4..9c248a289ebd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml
@@ -36,7 +36,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml
index a311b63418a6..203fa753d10f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml
@@ -26,6 +26,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
index c7817ed181ae..d56faf9d5dec 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
@@ -46,7 +46,7 @@
-
+
@@ -54,7 +54,7 @@
-
+
@@ -62,7 +62,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
index 71041ee7e134..dad3e05d81a5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
@@ -120,9 +120,8 @@
-
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml
index ce419167e951..e8bb48cfdd62 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml
@@ -12,14 +12,11 @@
-
-
-
-
-
+
+
@@ -45,38 +42,39 @@
-
+
-
+
+
-
-
+
-
+
-
+
+
-
+
-
+
-
+
-
+
@@ -84,14 +82,13 @@
-
+
-
-
-
+
+
@@ -117,6 +114,6 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml
index 78fbed1aef3a..107000a33799 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml
@@ -44,7 +44,7 @@
-
+
@@ -61,10 +61,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml
index 164701fa5bc6..5ff0a002e11e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml
@@ -69,7 +69,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml
index 662a251be3b2..f931c81afd93 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml
@@ -20,7 +20,9 @@
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml
index d4bc095dbe9b..845b5e7863f7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml
@@ -20,7 +20,9 @@
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnCatalogTest.xml
new file mode 100644
index 000000000000..502a20d3fd6f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnCatalogTest.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnProductTest.xml
new file mode 100644
index 000000000000..4aa746e8cc03
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnProductTest.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml
deleted file mode 100644
index ee6ff0c22454..000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml
deleted file mode 100644
index f75053f495c4..000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php
index 157f64133549..55551a4ed603 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php
@@ -195,7 +195,7 @@ public function testGetIdentities()
$this->catCollectionMock->expects($this->once())
->method('getIterator')
- ->willReturn([$currentCategory]);
+ ->willReturn(new \ArrayIterator([$currentCategory]));
$this->prodCollectionMock->expects($this->any())
->method('getIterator')
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php
index 6026d1462e46..87f5be4b2133 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Ui/ProductViewCounterTest.php
@@ -166,6 +166,7 @@ public function testGetCurrentProductDataWithNonEmptyProduct()
{
$productMock = $this->getMockBuilder(ProductInterface::class)
->disableOriginalConstructor()
+ ->addMethods(['isAvailable'])
->getMockForAbstractClass();
$productRendererMock = $this->getMockBuilder(ProductRenderInterface::class)
->disableOriginalConstructor()
@@ -173,7 +174,6 @@ public function testGetCurrentProductDataWithNonEmptyProduct()
$storeMock = $this->getMockBuilder(Store::class)
->disableOriginalConstructor()
->getMock();
-
$this->registryMock->expects($this->once())
->method('registry')
->with('product')
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php
index 2560b56a04e8..f38ffcd822cd 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php
@@ -216,6 +216,29 @@ public function setupInputDataProvider()
['description', null, 'descr text'],
],
],
+ 'update_product_with_empty_string_attribute' => [
+ 'requestProductData' => [
+ 'name' => 'testName3',
+ 'sku' => 'testSku3',
+ 'price' => '103',
+ 'special_price' => '100',
+ 'custom_attribute' => '',
+ ],
+ 'useDefaults' => [],
+ 'expectedProductData' => [
+ 'name' => 'testName3',
+ 'sku' => 'testSku3',
+ 'price' => '103',
+ 'special_price' => '100',
+ 'custom_attribute' => '',
+ ],
+ 'initialProductData' => [
+ ['name', null, 'testName2'],
+ ['sku', null, 'testSku2'],
+ ['price', null, '101'],
+ ['custom_attribute', null, '0'],
+ ],
+ ],
];
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Product/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Product/ViewTest.php
new file mode 100644
index 000000000000..32bab09cb98a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Product/ViewTest.php
@@ -0,0 +1,147 @@
+getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->requestMock = $this->getMockBuilder(RequestInterface::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['isPost'])
+ ->getMockForAbstractClass();
+ $contextMock->expects($this->any())
+ ->method('getRequest')
+ ->willReturn($this->requestMock);
+ $viewHelperMock = $this->getMockBuilder(ViewHelper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $resultForwardFactoryMock = $this->getMockBuilder(ForwardFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultPageFactoryMock = $this->getMockBuilder(PageFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultPageFactoryMock = $this->getMockBuilder(PageFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->catalogDesignMock = $this->getMockBuilder(Design::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->productRepositoryMock = $this->getMockBuilder(ProductRepository::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->productInterfaceMock = $this->getMockBuilder(ProductInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class);
+ $storeMock = $this->createMock(Store::class);
+ $this->storeManagerMock->method('getStore')->willReturn($storeMock);
+
+ $this->view = new View(
+ $contextMock,
+ $viewHelperMock,
+ $resultForwardFactoryMock,
+ $this->resultPageFactoryMock,
+ null,
+ null,
+ $this->catalogDesignMock,
+ $this->productRepositoryMock,
+ $this->storeManagerMock
+ );
+ }
+
+ /**
+ * Verify that product custom design theme is applied before product rendering
+ */
+ public function testExecute(): void
+ {
+ $themeId = 3;
+ $this->requestMock->method('isPost')
+ ->willReturn(false);
+ $this->productRepositoryMock->method('getById')
+ ->willReturn($this->productInterfaceMock);
+ $dataObjectMock = $this->getMockBuilder(DataObject::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getCustomDesign'])
+ ->getMock();
+ $dataObjectMock->method('getCustomDesign')
+ ->willReturn($themeId);
+ $this->catalogDesignMock->method('getDesignSettings')
+ ->willReturn($dataObjectMock);
+ $this->catalogDesignMock->expects($this->once())
+ ->method('applyCustomDesign')
+ ->with($themeId);
+ $viewResultPageMock = $this->getMockBuilder(Page::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resultPageFactoryMock->method('create')
+ ->willReturn($viewResultPageMock);
+ $this->view->execute();
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php
index c807ea881da6..8721e8661446 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php
@@ -180,4 +180,143 @@ public function testExecuteWithException(): void
$this->saveHandler->execute($product);
}
+
+ /**
+ * @param $tierPrices
+ * @param $tierPricesStored
+ * @param $tierPricesExpected
+ * @dataProvider executeWithWebsitePriceDataProvider
+ */
+ public function testExecuteWithWebsitePrice($tierPrices, $tierPricesStored, $tierPricesExpected): void
+ {
+ $productId = 10;
+ $linkField = 'entity_id';
+
+ /** @var MockObject $product */
+ $product = $this->getMockBuilder(ProductInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getData','setData', 'getStoreId'])
+ ->getMockForAbstractClass();
+ $product->expects($this->atLeastOnce())->method('getData')->willReturnMap(
+ [
+ ['tier_price', $tierPrices],
+ ['entity_id', $productId]
+ ]
+ );
+ $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0);
+ $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1);
+ $store = $this->getMockBuilder(StoreInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getWebsiteId'])
+ ->getMockForAbstractClass();
+ $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(1);
+ $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store);
+ /** @var MockObject $attribute */
+ $attribute = $this->getMockBuilder(ProductAttributeInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getName', 'isScopeGlobal'])
+ ->getMockForAbstractClass();
+ $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price');
+ $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(false);
+ $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price')
+ ->willReturn($attribute);
+ $productMetadata = $this->getMockBuilder(EntityMetadataInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getLinkField'])
+ ->getMockForAbstractClass();
+ $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField);
+ $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata')
+ ->with(ProductInterface::class)
+ ->willReturn($productMetadata);
+ $customerGroup = $this->getMockBuilder(GroupInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getId'])
+ ->getMockForAbstractClass();
+ $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200);
+ $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup')
+ ->willReturn($customerGroup);
+ $this->tierPriceResource
+ ->expects($this->at(1))
+ ->method('savePriceData')
+ ->with(new \Magento\Framework\DataObject($tierPricesExpected[0]))
+ ->willReturnSelf();
+ $this->tierPriceResource
+ ->expects($this->at(2))
+ ->method('savePriceData')
+ ->with(new \Magento\Framework\DataObject($tierPricesExpected[1]))
+ ->willReturnSelf();
+ $this->tierPriceResource
+ ->expects($this->at(3))
+ ->method('savePriceData')
+ ->with(new \Magento\Framework\DataObject($tierPricesExpected[2]))
+ ->willReturnSelf();
+ $this->tierPriceResource
+ ->expects($this->atLeastOnce())
+ ->method('loadPriceData')
+ ->willReturn($tierPricesStored);
+
+ $this->assertEquals($product, $this->saveHandler->execute($product));
+ }
+
+ public function executeWithWebsitePriceDataProvider(): array
+ {
+ $productId = 10;
+ return [[
+ 'tierPrices' => [
+ [
+ 'price_id' => 1,
+ 'website_id' => 0,
+ 'price_qty' => 1,
+ 'cust_group' => 0,
+ 'price' => 10,
+ 'product_id' => $productId
+ ],[
+ 'price_id' => 2,
+ 'website_id' => 1,
+ 'price_qty' => 2,
+ 'cust_group' => 3200,
+ 'price' => null,
+ 'percentage_value' => 20,
+ 'product_id' => $productId
+ ]
+ ],
+ 'tierPricesStored' => [
+ [
+ 'price_id' => 3,
+ 'website_id' => 1,
+ 'price_qty' => 3,
+ 'cust_group' => 0,
+ 'price' => 30,
+ 'product_id' => $productId
+ ]
+ ],
+ 'tierPricesExpected' => [
+ [
+ 'website_id' => 0,
+ 'qty' => 1,
+ 'customer_group_id' => 0,
+ 'all_groups' => 0,
+ 'value' => 10,
+ 'percentage_value' => null,
+ 'entity_id' => $productId
+ ],[
+ 'website_id' => 1,
+ 'qty' => 2,
+ 'customer_group_id' => 0,
+ 'all_groups' => 1,
+ 'value' => null,
+ 'percentage_value' => 20,
+ 'entity_id' => $productId
+ ],[
+ 'website_id' => 1,
+ 'qty' => 3,
+ 'customer_group_id' => 0,
+ 'all_groups' => 0,
+ 'value' => 30,
+ 'percentage_value' => null,
+ 'entity_id' => $productId
+ ]
+ ]
+ ]];
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php
index 6928f9161b81..96948ed8d1aa 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/FileInfoTest.php
@@ -129,21 +129,13 @@ public function testGetMimeType()
$absoluteFilePath = '/a/b/c/pub/media/catalog/category/filename.ext1';
$expected = 'ext1';
-
- $this->mediaDirectory->expects($this->at(0))
- ->method('getAbsolutePath')
- ->with(null)
- ->willReturn('/a/b/c/pub/media/');
-
- $this->mediaDirectory->expects($this->at(1))
- ->method('getAbsolutePath')
- ->with(null)
- ->willReturn('/a/b/c/pub/media/');
-
- $this->mediaDirectory->expects($this->at(2))
- ->method('getAbsolutePath')
- ->with('/catalog/category/filename.ext1')
- ->willReturn($absoluteFilePath);
+ $this->mediaDirectory->method('getAbsolutePath')
+ ->willReturnMap(
+ [
+ [null, '/a/b/c/pub/media'],
+ ['/catalog/category/filename.ext1', $absoluteFilePath]
+ ]
+ );
$this->mime->expects($this->once())
->method('getMimeType')
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php
index 673e12a5b42b..2924bf66949c 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php
@@ -267,13 +267,16 @@ public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload()
{
$attributeId = 1;
$attributeCode = 'existing_attribute_code';
+ $backendModel = 'backend_model';
$attributeMock = $this->createMock(Attribute::class);
$attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode);
$attributeMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId);
+ $attributeMock->expects($this->once())->method('setBackendModel')->with($backendModel)->willReturnSelf();
$existingModelMock = $this->createMock(Attribute::class);
$existingModelMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode);
$existingModelMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId);
+ $existingModelMock->expects($this->once())->method('getBackendModel')->willReturn($backendModel);
$this->eavAttributeRepositoryMock->expects($this->any())
->method('get')
@@ -292,6 +295,7 @@ public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload()
*/
public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload()
{
+ $backendModel = 'backend_model';
$labelMock = $this->getMockForAbstractClass(AttributeFrontendLabelInterface::class);
$labelMock->expects($this->any())->method('getStoreId')->willReturn(1);
$labelMock->expects($this->any())->method('getLabel')->willReturn('Store Scope Label');
@@ -304,11 +308,13 @@ public function testSaveSavesDefaultFrontendLabelIfItIsPresentInPayload()
$attributeMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn(null);
$attributeMock->expects($this->any())->method('getFrontendLabels')->willReturn([$labelMock]);
$attributeMock->expects($this->any())->method('getOptions')->willReturn([]);
+ $attributeMock->expects($this->once())->method('setBackendModel')->with($backendModel)->willReturnSelf();
$existingModelMock = $this->createMock(Attribute::class);
$existingModelMock->expects($this->any())->method('getDefaultFrontendLabel')->willReturn('Default Label');
$existingModelMock->expects($this->any())->method('getAttributeId')->willReturn($attributeId);
$existingModelMock->expects($this->any())->method('getAttributeCode')->willReturn($attributeCode);
+ $existingModelMock->expects($this->once())->method('getBackendModel')->willReturn($backendModel);
$this->eavAttributeRepositoryMock->expects($this->any())
->method('get')
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php
index ac67ba52d772..016e755f27b4 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/RepositoryTest.php
@@ -262,7 +262,7 @@ public function testSave()
->getMock();
$optionCollection->expects($this->once())->method('getProductOptions')->willReturn([$this->optionMock]);
$this->optionCollectionFactory->expects($this->once())->method('create')->willReturn($optionCollection);
- $this->optionMock->expects($this->once())->method('getValues')->willReturn([
+ $this->optionMock->expects($this->exactly(2))->method('getValues')->willReturn([
$originalValue1,
$originalValue2,
$originalValue3
@@ -291,7 +291,7 @@ public function testSaveWhenOptionTypeWasChanged()
->getMock();
$optionCollection->expects($this->once())->method('getProductOptions')->willReturn([$this->optionMock]);
$this->optionCollectionFactory->expects($this->once())->method('create')->willReturn($optionCollection);
- $this->optionMock->expects($this->once())->method('getValues')->willReturn(null);
+ $this->optionMock->expects($this->exactly(2))->method('getValues')->willReturn(null);
$this->assertEquals($this->optionMock, $this->optionRepository->save($this->optionMock));
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php
index fa8a3fc1e605..82916cf42ebe 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php
@@ -11,9 +11,13 @@
use Magento\Catalog\Model\Product\Option;
use Magento\Catalog\Model\Product\Option\Repository;
use Magento\Catalog\Model\Product\Option\SaveHandler;
+use Magento\Catalog\Model\ResourceModel\Product\Relation;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+/**
+ * Test for \Magento\Catalog\Model\Product\Option\SaveHandler.
+ */
class SaveHandlerTest extends TestCase
{
/**
@@ -36,6 +40,14 @@ class SaveHandlerTest extends TestCase
*/
protected $optionRepository;
+ /**
+ * @var Relation|MockObject
+ */
+ private $relationMock;
+
+ /**
+ * @inheridoc
+ */
protected function setUp(): void
{
$this->entity = $this->getMockBuilder(Product::class)
@@ -47,11 +59,19 @@ protected function setUp(): void
$this->optionRepository = $this->getMockBuilder(Repository::class)
->disableOriginalConstructor()
->getMock();
+ $this->relationMock = $this->getMockBuilder(Relation::class)
+ ->disableOriginalConstructor()
+ ->getMock();
- $this->model = new SaveHandler($this->optionRepository);
+ $this->model = new SaveHandler($this->optionRepository, $this->relationMock);
}
- public function testExecute()
+ /**
+ * Test for execute
+ *
+ * @return void
+ */
+ public function testExecute(): void
{
$this->optionMock->expects($this->any())->method('getOptionId')->willReturn(5);
$this->entity->expects($this->once())->method('getOptions')->willReturn([$this->optionMock]);
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php
index c14bb7f524d0..61bbaf94e9ff 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php
@@ -21,6 +21,7 @@
use Magento\Store\Model\StoreManagerInterface;
use Magento\Store\Model\Website;
use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\MockObject\RuntimeException;
use PHPUnit\Framework\TestCase;
/**
@@ -110,7 +111,7 @@ protected function setUp(): void
$this->groupManagementMock->expects($this->any())->method('getAllCustomersGroup')
->willReturn($group);
$this->tierPriceExtensionFactoryMock = $this->getMockBuilder(ProductTierPriceExtensionFactory::class)
- ->setMethods(['create'])
+ ->onlyMethods(['create'])
->disableOriginalConstructor()
->getMock();
$this->model = $this->objectManagerHelper->getObject(
@@ -182,9 +183,7 @@ function () {
);
// create sample TierPrice objects that would be coming from a REST call
- $tierPriceExtensionMock = $this->getMockBuilder(ProductTierPriceExtensionInterface::class)
- ->setMethods(['getWebsiteId', 'setWebsiteId', 'getPercentageValue', 'setPercentageValue'])
- ->getMockForAbstractClass();
+ $tierPriceExtensionMock = $this->getProductTierPriceExtensionInterfaceMock();
$tierPriceExtensionMock->expects($this->any())->method('getWebsiteId')->willReturn($expectedWebsiteId);
$tierPriceExtensionMock->expects($this->any())->method('getPercentageValue')->willReturn(null);
$tp1 = $this->objectManagerHelper->getObject(TierPrice::class);
@@ -226,9 +225,7 @@ function () {
$this->assertEquals($tps[$i]->getQty(), $tpData['price_qty'], 'Qty does not match');
}
- $tierPriceExtensionMock = $this->getMockBuilder(ProductTierPriceExtensionInterface::class)
- ->setMethods(['getWebsiteId', 'setWebsiteId', 'getPercentageValue', 'setPercentageValue'])
- ->getMockForAbstractClass();
+ $tierPriceExtensionMock = $this->getProductTierPriceExtensionInterfaceMock();
$tierPriceExtensionMock->expects($this->any())->method('getPercentageValue')->willReturn(50);
$tierPriceExtensionMock->expects($this->any())->method('setWebsiteId');
$this->tierPriceExtensionFactoryMock->expects($this->any())
@@ -289,9 +286,7 @@ function () {
return $this->objectManagerHelper->getObject(TierPrice::class);
}
);
- $tierPriceExtensionMock = $this->getMockBuilder(ProductTierPriceExtensionInterface::class)
- ->onlyMethods(['getPercentageValue', 'setPercentageValue'])
- ->getMockForAbstractClass();
+ $tierPriceExtensionMock = $this->getProductTierPriceExtensionInterfaceMock();
$tierPriceExtensionMock->method('getPercentageValue')
->willReturn(50);
$this->tierPriceExtensionFactoryMock->method('create')
@@ -299,4 +294,22 @@ function () {
$this->assertInstanceOf(TierPrice::class, $this->model->getTierPrices($this->product)[0]);
}
+
+ /**
+ * Build ProductTierPriceExtensionInterface mock.
+ *
+ * @return MockObject
+ */
+ private function getProductTierPriceExtensionInterfaceMock(): MockObject
+ {
+ $mockBuilder = $this->getMockBuilder(ProductTierPriceExtensionInterface::class)
+ ->disableOriginalConstructor();
+ try {
+ $mockBuilder->addMethods(['getPercentageValue', 'setPercentageValue', 'setWebsiteId', 'getWebsiteId']);
+ } catch (RuntimeException $e) {
+ // ProductTierPriceExtensionInterface already generated and has all necessary methods.
+ }
+
+ return $mockBuilder->getMock();
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php
index a68eb3e652da..a0c682aa3e41 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php
@@ -13,7 +13,6 @@
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\EntityManager\EntityMetadataInterface;
use Magento\Framework\EntityManager\MetadataPool;
-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -23,14 +22,19 @@
class ProductIdLocatorTest extends TestCase
{
/**
- * @var MetadataPool|MockObject
+ * @var int
*/
- private $metadataPool;
+ private $idsLimit;
/**
- * @var CollectionFactory|MockObject
+ * @var string
*/
- private $collectionFactory;
+ private $linkField;
+
+ /**
+ * @var Collection|MockObject
+ */
+ private $collection;
/**
* @var ProductIdLocator
@@ -38,79 +42,125 @@ class ProductIdLocatorTest extends TestCase
private $model;
/**
- * Set up.
- *
- * @return void
+ * @inheritDoc
*/
protected function setUp(): void
{
- $this->metadataPool = $this->getMockBuilder(MetadataPool::class)
- ->setMethods(['getMetadata'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->collectionFactory = $this
- ->getMockBuilder(CollectionFactory::class)
+ $metadataPool = $this->createMock(MetadataPool::class);
+ $collectionFactory = $this->getMockBuilder(CollectionFactory::class)
->setMethods(['create'])
->disableOriginalConstructor()
->getMock();
+ $this->idsLimit = 4;
- $objectManager = new ObjectManager($this);
- $this->model = $objectManager->getObject(
- ProductIdLocator::class,
- [
- 'metadataPool' => $this->metadataPool,
- 'collectionFactory' => $this->collectionFactory,
- ]
- );
+ $this->linkField = 'entity_id';
+ $metaDataInterface = $this->createMock(EntityMetadataInterface::class);
+ $metaDataInterface->method('getLinkField')
+ ->willReturn($this->linkField);
+ $metadataPool->method('getMetadata')
+ ->with(ProductInterface::class)
+ ->willReturn($metaDataInterface);
+
+ $this->collection = $this->createMock(Collection::class);
+ $collectionFactory->method('create')
+ ->willReturn($this->collection);
+
+ $this->model = new ProductIdLocator($metadataPool, $collectionFactory, $this->idsLimit);
}
- /**
- * Test retrieve
- */
public function testRetrieveProductIdsBySkus()
{
$skus = ['sku_1', 'sku_2'];
- $collection = $this->getMockBuilder(Collection::class)
- ->setMethods(
- [
- 'getItems',
- 'addFieldToFilter',
- 'setPageSize',
- 'getLastPageNumber',
- 'setCurPage',
- 'clear'
- ]
- )
- ->disableOriginalConstructor()
- ->getMock();
+
$product = $this->getMockBuilder(ProductInterface::class)
->setMethods(['getSku', 'getData', 'getTypeId'])
->disableOriginalConstructor()
->getMockForAbstractClass();
- $metaDataInterface = $this->getMockBuilder(EntityMetadataInterface::class)
- ->setMethods(['getLinkField'])
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
- $this->collectionFactory->expects($this->once())->method('create')->willReturn($collection);
- $collection->expects($this->once())->method('addFieldToFilter')
- ->with(ProductInterface::SKU, ['in' => $skus])->willReturnSelf();
- $collection->expects($this->atLeastOnce())->method('getItems')->willReturn([$product]);
- $collection->expects($this->atLeastOnce())->method('setPageSize')->willReturnSelf();
- $collection->expects($this->atLeastOnce())->method('getLastPageNumber')->willReturn(1);
- $collection->expects($this->atLeastOnce())->method('setCurPage')->with(1)->willReturnSelf();
- $collection->expects($this->atLeastOnce())->method('clear')->willReturnSelf();
- $this->metadataPool
- ->expects($this->once())
- ->method('getMetadata')
- ->with(ProductInterface::class)
- ->willReturn($metaDataInterface);
- $metaDataInterface->expects($this->once())->method('getLinkField')->willReturn('entity_id');
- $product->expects($this->once())->method('getSku')->willReturn('sku_1');
- $product->expects($this->once())->method('getData')->with('entity_id')->willReturn(1);
- $product->expects($this->once())->method('getTypeId')->willReturn('simple');
+ $product->method('getSku')
+ ->willReturn('sku_1');
+ $product->method('getData')
+ ->with($this->linkField)
+ ->willReturn(1);
+ $product->method('getTypeId')
+ ->willReturn('simple');
+
+ $this->collection->expects($this->once())
+ ->method('addFieldToFilter')
+ ->with(ProductInterface::SKU, ['in' => $skus])
+ ->willReturnSelf();
+ $this->collection->expects($this->atLeastOnce())
+ ->method('getItems')
+ ->willReturn([$product]);
+ $this->collection->expects($this->atLeastOnce())
+ ->method('setPageSize')
+ ->willReturnSelf();
+ $this->collection->expects($this->atLeastOnce())
+ ->method('getLastPageNumber')
+ ->willReturn(1);
+ $this->collection->expects($this->atLeastOnce())
+ ->method('setCurPage')
+ ->with(1)
+ ->willReturnSelf();
+ $this->collection->expects($this->atLeastOnce())
+ ->method('clear')
+ ->willReturnSelf();
+
$this->assertEquals(
['sku_1' => [1 => 'simple']],
$this->model->retrieveProductIdsBySkus($skus)
);
}
+
+ public function testRetrieveProductIdsWithNumericSkus()
+ {
+ $skus = ['111', '222', '333', '444', '555'];
+ $products = [];
+ foreach ($skus as $sku) {
+ $product = $this->getMockBuilder(ProductInterface::class)
+ ->setMethods(['getSku', 'getData', 'getTypeId'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $product->method('getSku')
+ ->willReturn($sku);
+ $product->method('getData')
+ ->with($this->linkField)
+ ->willReturn((int) $sku);
+ $product->method('getTypeId')
+ ->willReturn('simple');
+ $products[] = $product;
+ }
+
+ $this->collection->expects($this->atLeastOnce())
+ ->method('addFieldToFilter')
+ ->withConsecutive([ProductInterface::SKU, ['in' => $skus]], [ProductInterface::SKU, ['in' => ['1']]])
+ ->willReturnSelf();
+ $this->collection->expects($this->atLeastOnce())
+ ->method('getItems')
+ ->willReturnOnConsecutiveCalls($products, []);
+ $this->collection->expects($this->atLeastOnce())
+ ->method('setPageSize')
+ ->willReturnSelf();
+ $this->collection->expects($this->atLeastOnce())
+ ->method('getLastPageNumber')
+ ->willReturn(1);
+ $this->collection->expects($this->atLeastOnce())
+ ->method('setCurPage')
+ ->with(1)
+ ->willReturnSelf();
+ $this->collection->expects($this->atLeastOnce())
+ ->method('clear')
+ ->willReturnSelf();
+
+ $this->assertEquals(
+ [
+ '111' => [111 => 'simple'],
+ '222' => [222 => 'simple'],
+ '333' => [333 => 'simple'],
+ '444' => [444 => 'simple'],
+ '555' => [555 => 'simple'],
+ ],
+ $this->model->retrieveProductIdsBySkus($skus)
+ );
+ $this->assertEmpty($this->model->retrieveProductIdsBySkus(['1']));
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php
index 129873a067d9..6d690d79ac1e 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php
@@ -220,7 +220,8 @@ protected function setUp(): void
'getStoreId',
'getMediaGalleryEntries',
'getExtensionAttributes',
- 'getCategoryIds'
+ 'getCategoryIds',
+ 'getAttributes'
]
)
->disableOriginalConstructor()
@@ -243,7 +244,8 @@ protected function setUp(): void
'save',
'getMediaGalleryEntries',
'getExtensionAttributes',
- 'getCategoryIds'
+ 'getCategoryIds',
+ 'getAttributes'
]
)
->disableOriginalConstructor()
@@ -852,6 +854,9 @@ public function testDeleteById()
public function testGetList()
{
$searchCriteriaMock = $this->getMockForAbstractClass(SearchCriteriaInterface::class);
+ $searchCriteriaMock->expects($this->once())
+ ->method('getFilterGroups')
+ ->willReturn([]);
$collectionMock = $this->createMock(Collection::class);
$this->collectionFactory->expects($this->once())->method('create')->willReturn($collectionMock);
$this->product->method('getSku')->willReturn('simple');
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php
index 42d0778daa4a..91313d4cd199 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php
@@ -508,28 +508,17 @@ public function testGetStoreIds()
/**
* @dataProvider getSingleStoreIds
* @param bool $isObjectNew
+ * @return void
*/
- public function testGetStoreSingleSiteModelIds(
- bool $isObjectNew
- ) {
+ public function testGetStoreSingleSiteModelIds(bool $isObjectNew): void
+ {
$websiteIDs = [0 => 2];
- $this->model->setWebsiteIds(
- !$isObjectNew ? $websiteIDs : array_flip($websiteIDs)
- );
+ $this->model->setWebsiteIds(!$isObjectNew ? $websiteIDs : array_flip($websiteIDs));
$this->model->isObjectNew($isObjectNew);
- $this->storeManager->expects(
- $this->exactly(
- (int)!$isObjectNew
- )
- )
- ->method('isSingleStoreMode')
- ->willReturn(true);
-
- $this->website->expects(
- $this->once()
- )->method('getStoreIds')
+ $this->website->expects($this->once())
+ ->method('getStoreIds')
->willReturn($websiteIDs);
$this->assertEquals($websiteIDs, $this->model->getStoreIds());
@@ -1095,6 +1084,35 @@ public function testSaveAndDuplicate()
$this->model->afterSave();
}
+ /**
+ * Test for save method behavior with type options
+ */
+ public function testSaveWithoutTypeOptions()
+ {
+ $this->model->setCanSaveCustomOptions(false);
+ $this->model->setTypeHasOptions(true);
+ $this->model->setTypeHasRequiredOptions(true);
+ $this->configureSaveTest();
+ $this->model->beforeSave();
+ $this->model->afterSave();
+ $this->assertTrue($this->model->getTypeHasOptions());
+ $this->assertTrue($this->model->getTypeHasRequiredOptions());
+ }
+
+ /**
+ * Test for save method with provided options data
+ */
+ public function testSaveWithProvidedRequiredOptions()
+ {
+ $this->model->setData("has_options", "1");
+ $this->model->setData("required_options", "1");
+ $this->configureSaveTest();
+ $this->model->beforeSave();
+ $this->model->afterSave();
+ $this->assertTrue($this->model->getHasOptions());
+ $this->assertTrue($this->model->getRequiredOptions());
+ }
+
public function testGetIsSalableSimple()
{
$typeInstanceMock =
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/MediaImageDeleteProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/MediaImageDeleteProcessorTest.php
new file mode 100644
index 000000000000..bfc113ce4160
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/MediaImageDeleteProcessorTest.php
@@ -0,0 +1,200 @@
+objectManager = new ObjectManager($this);
+
+ $this->productMock = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getId', 'getMediaGalleryImages'])
+ ->getMock();
+
+ $this->imageConfig = $this->getMockBuilder(MediaConfig::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getBaseMediaUrl', 'getMediaUrl', 'getBaseMediaPath', 'getMediaPath'])
+ ->getMock();
+
+ $this->mediaDirectory = $this->getMockBuilder(Filesystem::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getRelativePath', 'isFile', 'delete'])
+ ->getMock();
+
+ $this->imageProcessor = $this->getMockBuilder(Processor::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['removeImage'])
+ ->getMock();
+
+ $this->productGallery = $this->getMockBuilder(Gallery::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['deleteGallery', 'countImageUses'])
+ ->getMock();
+
+ $this->mediaImageDeleteProcessor = $this->objectManager->getObject(
+ MediaImageDeleteProcessor::class,
+ [
+ 'imageConfig' => $this->imageConfig,
+ 'mediaDirectory' => $this->mediaDirectory,
+ 'imageProcessor' => $this->imageProcessor,
+ 'productGallery' => $this->productGallery
+ ]
+ );
+ }
+
+ /**
+ * Test mediaImageDeleteProcessor execute method
+ *
+ * @dataProvider executeCategoryProductMediaDeleteDataProvider
+ * @param int $productId
+ * @param array $productImages
+ * @param bool $isValidFile
+ * @param bool $imageUsedBefore
+ */
+ public function testExecuteCategoryProductMediaDelete(
+ int $productId,
+ array $productImages,
+ bool $isValidFile,
+ bool $imageUsedBefore
+ ): void {
+ $this->productMock->expects($this->any())
+ ->method('getId')
+ ->willReturn($productId);
+
+ $this->productMock->expects($this->any())
+ ->method('getMediaGalleryImages')
+ ->willReturn($productImages);
+
+ $this->mediaDirectory->expects($this->any())
+ ->method('isFile')
+ ->willReturn($isValidFile);
+
+ $this->mediaDirectory->expects($this->any())
+ ->method('getRelativePath')
+ ->withConsecutive([$productImages[0]->getFile()], [$productImages[1]->getFile()])
+ ->willReturnOnConsecutiveCalls($productImages[0]->getPath(), $productImages[1]->getPath());
+
+ $this->productGallery->expects($this->any())
+ ->method('countImageUses')
+ ->willReturn($imageUsedBefore);
+
+ $this->productGallery->expects($this->any())
+ ->method('deleteGallery')
+ ->willReturnSelf();
+
+ $this->imageProcessor->expects($this->any())
+ ->method('removeImage')
+ ->willReturnSelf();
+
+ $this->mediaImageDeleteProcessor->execute($this->productMock);
+ }
+
+ /**
+ * @return array
+ */
+ public function executeCategoryProductMediaDeleteDataProvider(): array
+ {
+ $imageDirectoryPath = '/media/dir1/dir2/catalog/product/';
+ $image1FilePath = '/test/test1.jpg';
+ $image2FilePath = '/test/test2.jpg';
+ $productImages = [
+ new DataObject([
+ 'value_id' => 1,
+ 'file' => $image1FilePath,
+ 'media_type' => 'image',
+ 'path' => $imageDirectoryPath.$image1FilePath
+ ]),
+ new DataObject([
+ 'value_id' => 2,
+ 'file' => $image2FilePath,
+ 'media_type' => 'image',
+ 'path' => $imageDirectoryPath.$image2FilePath
+ ])
+ ];
+ return [
+ 'test image can be deleted with existing product and product images' =>
+ [
+ 12,
+ $productImages,
+ true,
+ false
+ ],
+ 'test image can not be deleted without valid product id' =>
+ [
+ 0,
+ $productImages,
+ true,
+ false
+ ],
+ 'test image can not be deleted without valid product images' =>
+ [
+ 12,
+ [new DataObject(['file' => null]), new DataObject(['file' => null])],
+ true,
+ false
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
index 2bf504369b8a..9a55e48cfb1b 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CollectionTest.php
@@ -38,6 +38,7 @@
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Framework\Validator\UniversalFactory;
+use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -93,6 +94,11 @@ class CollectionTest extends TestCase
*/
private $storeManager;
+ /**
+ * @var ProductLimitation|MockObject
+ */
+ private $productLimitationMock;
+
/**
* @var EntityFactory|MockObject
*/
@@ -192,7 +198,7 @@ protected function setUp(): void
$this->entityMock->expects($this->any())->method('getTable')->willReturnArgument(0);
$this->connectionMock->expects($this->atLeastOnce())->method('select')->willReturn($this->selectMock);
- $productLimitationMock = $this->createMock(
+ $this->productLimitationMock = $this->createMock(
ProductLimitation::class
);
$productLimitationFactoryMock = $this->getMockBuilder(
@@ -201,7 +207,7 @@ protected function setUp(): void
->setMethods(['create'])->getMock();
$productLimitationFactoryMock->method('create')
- ->willReturn($productLimitationMock);
+ ->willReturn($this->productLimitationMock);
$this->collection = $this->objectManager->getObject(
Collection::class,
[
@@ -432,4 +438,44 @@ public function testGetNewEmptyItem()
$secondItem = $this->collection->getNewEmptyItem();
$this->assertEquals($firstItem, $secondItem);
}
+
+ /**
+ * Test to add website filter in admin area
+ */
+ public function testAddWebsiteFilterOnAdminStore(): void
+ {
+ $websiteIds = [2];
+ $websiteTable = 'catalog_product_website';
+ $joinCondition = 'join condition';
+ $this->productLimitationMock->expects($this->atLeastOnce())
+ ->method('offsetSet')
+ ->with('website_ids', $websiteIds);
+ $this->productLimitationMock->method('offsetExists')
+ ->with('website_ids')
+ ->willReturn(true);
+ $this->productLimitationMock->method('offsetGet')
+ ->with('website_ids')
+ ->willReturn($websiteIds);
+ $this->connectionMock->expects($this->once())
+ ->method('quoteInto')
+ ->with('product_website.website_id IN(?)', $websiteIds, 'int')
+ ->willReturn($joinCondition);
+ $this->selectMock->method('getPart')->with(Select::FROM)->willReturn([]);
+ /** @var AbstractEntity|MockObject $eavEntity */
+ $eavEntity = $this->createMock(AbstractEntity::class);
+ $eavEntity->method('getTable')
+ ->with('catalog_product_website')
+ ->willReturn($websiteTable);
+ $this->selectMock->expects($this->once())
+ ->method('join')
+ ->with(
+ ['product_website' => $websiteTable],
+ 'product_website.product_id = e.entity_id AND ' . $joinCondition,
+ []
+ );
+
+ $this->collection->setEntity($eavEntity);
+ $this->collection->setStoreId(Store::DEFAULT_STORE_ID);
+ $this->collection->addWebsiteFilter($websiteIds);
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculatorTest.php
index 589bdf8dda07..d813bb0e5caa 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculatorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/BatchSizeCalculatorTest.php
@@ -12,6 +12,7 @@
use Magento\Framework\Indexer\BatchSizeManagementInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Magento\Framework\App\DeploymentConfig;
class BatchSizeCalculatorTest extends TestCase
{
@@ -30,6 +31,11 @@ class BatchSizeCalculatorTest extends TestCase
*/
private $batchRowsCount;
+ /**
+ * @var DeploymentConfig|MockObject
+ */
+ private $deploymentConfigMock;
+
protected function setUp(): void
{
$this->estimatorMock = $this->getMockForAbstractClass(BatchSizeManagementInterface::class);
@@ -37,7 +43,8 @@ protected function setUp(): void
$this->model = new BatchSizeCalculator(
['default' => $this->batchRowsCount],
['default' => $this->estimatorMock],
- []
+ [],
+ $this->createMock(DeploymentConfig::class)
);
}
diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Attribute/AbstractRepository.php b/app/code/Magento/Catalog/Ui/Component/Listing/Attribute/AbstractRepository.php
index c7b339d5fac9..b1218c88264c 100644
--- a/app/code/Magento/Catalog/Ui/Component/Listing/Attribute/AbstractRepository.php
+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Attribute/AbstractRepository.php
@@ -11,6 +11,11 @@
*/
abstract class AbstractRepository implements RepositoryInterface
{
+ /**
+ * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface
+ */
+ private $productAttributeRepository;
+
/**
* @var null|\Magento\Catalog\Api\Data\ProductAttributeInterface[]
*/
diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php
index 1e056d9f8d51..88edfc7b36cb 100644
--- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php
+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php
@@ -23,6 +23,11 @@ class Columns extends \Magento\Ui\Component\Listing\Columns
*/
protected $attributeRepository;
+ /**
+ * @var \Magento\Catalog\Ui\Component\ColumnFactory
+ */
+ private $columnFactory;
+
/**
* @var array
*/
diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Price.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Price.php
index 53347db23f5a..c35dad5e37bd 100644
--- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Price.php
+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Price.php
@@ -24,6 +24,11 @@ class Price extends \Magento\Ui\Component\Listing\Columns\Column
*/
protected $localeCurrency;
+ /**
+ * @var \Magento\Store\Model\StoreManagerInterface
+ */
+ private $storeManager;
+
/**
* @param ContextInterface $context
* @param UiComponentFactory $uiComponentFactory
diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php
index 09c9782fc0e3..0ef5adf7a188 100644
--- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php
+++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php
@@ -20,6 +20,16 @@ class Thumbnail extends \Magento\Ui\Component\Listing\Columns\Column
const ALT_FIELD = 'name';
+ /**
+ * @var \Magento\Catalog\Helper\Image
+ */
+ private $imageHelper;
+
+ /**
+ * @var \Magento\Framework\UrlInterface
+ */
+ private $urlBuilder;
+
/**
* @param ContextInterface $context
* @param UiComponentFactory $uiComponentFactory
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php
index 430b6c004e77..6915265b4881 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php
@@ -165,7 +165,7 @@ protected function getFieldsForFieldset()
$websitesList = $this->getWebsitesList();
$isNewProduct = !$this->locator->getProduct()->getId();
$tooltip = [
- 'link' => 'https://docs.magento.com/m2/ce/user_guide/configuration/scope.html',
+ 'link' => 'https://docs.magento.com/user-guide/configuration/scope.html',
'description' => __(
'If your Magento installation has multiple websites, ' .
'you can edit the scope to use the product on specific sites.'
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php
index 298595b3d0f6..f4334bc25efd 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php
@@ -25,58 +25,4 @@ protected function _productLimitationJoinPrice()
$this->_productLimitationFilters->setUsePriceIndex(false);
return $this->_productLimitationPrice(true);
}
-
- /**
- * Return approximately amount if too much entities.
- *
- * @return int|mixed
- */
- public function getSize()
- {
- $sql = $this->getSelectCountSql();
- $possibleCount = $this->analyzeCount($sql);
-
- if ($possibleCount > 20000) {
- return $possibleCount;
- }
-
- return parent::getSize();
- }
-
- /**
- * Analyze amount of entities in DB.
- *
- * @param $sql
- * @return int|mixed
- * @throws \Zend_Db_Statement_Exception
- */
- private function analyzeCount($sql)
- {
- $results = $this->getConnection()->query('EXPLAIN ' . $sql)->fetchAll();
- $alias = $this->getMainTableAlias();
-
- foreach ($results as $result) {
- if ($result['table'] == $alias) {
- return $result['rows'];
- }
- }
-
- return 0;
- }
-
- /**
- * Identify main table alias or its name if alias is not defined.
- *
- * @return string
- * @throws \LogicException
- */
- private function getMainTableAlias()
- {
- foreach ($this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM) as $tableAlias => $tableMetadata) {
- if ($tableMetadata['joinType'] == 'from') {
- return $tableAlias;
- }
- }
- throw new \LogicException("Main table cannot be identified.");
- }
}
diff --git a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php
index 00bac7e61b5b..f486eed91da5 100644
--- a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php
+++ b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php
@@ -7,10 +7,10 @@
namespace Magento\Catalog\ViewModel\Product\Checker;
-use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Catalog\Api\Data\ProductInterface;
-use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
+use Magento\CatalogInventory\Api\StockConfigurationInterface;
+use Magento\Framework\View\Element\Block\ArgumentInterface;
/**
* Check is available add to compare.
@@ -39,25 +39,9 @@ public function __construct(StockConfigurationInterface $stockConfiguration)
public function isAvailableForCompare(ProductInterface $product): bool
{
if ((int)$product->getStatus() !== Status::STATUS_DISABLED) {
- return $this->isInStock($product) || $this->stockConfiguration->isShowOutOfStock();
+ return $product->isSalable() || $this->stockConfiguration->isShowOutOfStock();
}
return false;
}
-
- /**
- * Get is in stock status.
- *
- * @param ProductInterface $product
- * @return bool
- */
- private function isInStock(ProductInterface $product): bool
- {
- $quantityAndStockStatus = $product->getQuantityAndStockStatus();
- if (!$quantityAndStockStatus) {
- return $product->isSalable();
- }
-
- return $quantityAndStockStatus['is_in_stock'] ?? false;
- }
}
diff --git a/app/code/Magento/Catalog/ViewModel/Product/OptionsData.php b/app/code/Magento/Catalog/ViewModel/Product/OptionsData.php
new file mode 100644
index 000000000000..a4b77ca89ee7
--- /dev/null
+++ b/app/code/Magento/Catalog/ViewModel/Product/OptionsData.php
@@ -0,0 +1,29 @@
+
+
+
+
+ - sku
+ - media_gallery
+
+
+
diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml
index 4e10453f542b..a1b2202309d6 100644
--- a/app/code/Magento/Catalog/etc/adminhtml/system.xml
+++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml
@@ -218,7 +218,7 @@
Catalog media URL format
Magento\Catalog\Model\Config\Source\Web\CatalogMediaUrlFormat
- Learn more about catalog URL formats.Warning! If you switch back to legacy mode, you must use the CLI to regenerate images .]]>
+ Learn more about catalog URL formats.Warning! If you switch back to legacy mode, you must use the CLI to regenerate images .]]>
diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml
index ce34914a2f5d..f6a1dbe5e41a 100644
--- a/app/code/Magento/Catalog/etc/db_schema.xml
+++ b/app/code/Magento/Catalog/etc/db_schema.xml
@@ -13,7 +13,7 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json
index efc45112920e..fd332606bb22 100644
--- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json
@@ -1020,6 +1020,7 @@
"entity_id": true
},
"constraint": {
+ "PRIMARY": true,
"FK_A6C6C8FAA386736921D3A7C4B50B1185": true,
"CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID": true,
"CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_VAL_ID_ENTT_ID": true
diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml
index 8a116282e257..b509debe7bae 100644
--- a/app/code/Magento/Catalog/etc/di.xml
+++ b/app/code/Magento/Catalog/etc/di.xml
@@ -1319,4 +1319,13 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml
index 1fd47fde304e..a33c3a19e1e4 100644
--- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml
+++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml
@@ -40,4 +40,8 @@
Magento\Catalog\Model\Product\Webapi\Rest\RequestTypeBasedDeserializer
+
+
+
+
diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml
index a709f23d8c12..03671ba0bb2e 100644
--- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml
+++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml
@@ -40,4 +40,8 @@
Magento\Framework\Webapi\Rest\Request\Deserializer\Xml
+
+
+
+
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
index e5f8a360c334..7ae3a2ade655 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml
@@ -4,6 +4,7 @@
* See COPYING.txt for license details.
*/
+// phpcs:disable PHPCompatibility.Miscellaneous.RemovedAlternativePHPTags.MaybeASPOpenTagFound
/** @var $block \Magento\Eav\Block\Adminhtml\Attribute\Edit\Options\Options */
$stores = $block->getStoresSortedBySortOrder();
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml
index 261de795f719..e4f3dba6f984 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml
@@ -5,6 +5,7 @@
*/
// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis
+// phpcs:disable PHPCompatibility.Miscellaneous.RemovedAlternativePHPTags.MaybeASPOpenTagFound
/* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\AttributeSet */
/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml
index ce7dac70010b..6848e6f269bc 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml
@@ -5,6 +5,7 @@
*/
// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis
+// phpcs:disable PHPCompatibility.Miscellaneous.RemovedAlternativePHPTags.MaybeASPOpenTagFound
/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */
/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */
diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml
index 2063609bf056..748f7d229dbb 100644
--- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml
+++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/type/date.phtml
@@ -4,6 +4,7 @@
* See COPYING.txt for license details.
*/
+// phpcs:disable PHPCompatibility.Miscellaneous.RemovedAlternativePHPTags.MaybeASPOpenTagFound
?>
-
+ }
+
= $block->escapeHtml(__('You have no items to compare.')) ?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml
index 6a47978f1e5c..afd804591fec 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml
@@ -87,6 +87,12 @@ $_helper = $block->getData('outputHelper');
data-product-sku="= $escaper->escapeHtml($_product->getSku()) ?>"
action="= $escaper->escapeUrl($postParams['action']) ?>"
method="post">
+ getData('viewModel')->getOptionsData($_product); ?>
+
+
+
@@ -153,16 +159,14 @@ $_helper = $block->getData('outputHelper');
- = $block->getToolbarHtml() ?>
- isRedirectToCartEnabled()): ?>
-
-
+ }
+
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml
index e426b940deab..6fd619de7fd6 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml
@@ -287,7 +287,7 @@ $_item = null;
- getIsSalable()):?>
+ isAvailable()):?>
= $block->escapeHtml(__('In stock')) ?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml
index 76ef6baf4993..3c8687d090ba 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/toolbar.phtml
@@ -10,27 +10,23 @@
*
* @var $block \Magento\Catalog\Block\Product\ProductList\Toolbar
*/
-
-// phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile
-// phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket
?>
getCollection()->getSize()) :?>
helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode($block->getWidgetOptionsJson());
$widgetOptions = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($widget['productListToolbarForm']);
?>
- isExpanded()) :?>
- getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?>
-
-
- getTemplateFile('Magento_Catalog::product/list/toolbar/amount.phtml')) ?>
-
- = $block->getPagerHtml() ?>
-
- getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?>
-
- isExpanded()) :?>
- getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?>
-
+ getIsBottom()): ?>
+ = $block->getPagerHtml() ?>
+ = $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/limiter.phtml')) ?>
+
+ isExpanded()): ?>
+ = $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/viewmode.phtml')) ?>
+
+ = $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/amount.phtml')) ?>
+ isExpanded()): ?>
+ = $block->fetchView($block->getTemplateFile('Magento_Catalog::product/list/toolbar/sorter.phtml')) ?>
+
+
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml
index 6cebd51284f4..49dd702a6e39 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml
@@ -63,7 +63,7 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class);
. ' data-mage-init=\'{ "redirectUrl": { "event": "click", url: "' . $block->escapeUrl($block->getAddToCartUrl($_product)) . '"} }\'>'
. '
' . $block->escapeHtml(__('Add to Cart')) . ' ';
} else {
- $info['button'] = $_product->getIsSalable() ? '
' . $block->escapeHtml(__('In stock')) . '
' :
+ $info['button'] = $_product->isAvailable() ? '
' . $block->escapeHtml(__('In stock')) . '
' :
'
' . $block->escapeHtml(__('Out of stock')) . '
';
}
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml
index f5fd1c5aa64e..4385829a5bdc 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml
@@ -51,7 +51,7 @@
id="= /* @noEscape */ $_fileName ?>"
class="product-custom-option= $_option->getIsRequire() ? ' required' : '' ?>"
= $_fileExists ? 'disabled="disabled"' : '' ?> />
-
getFileExtension()):?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml
index 53a0682311b1..fce91564c96a 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml
@@ -52,7 +52,7 @@
- getIsSalable()) :?>
+ isAvailable()) :?>
= $block->escapeHtml(__('In stock')) ?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml
index 5108c488aec1..66683ef328e0 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml
@@ -89,7 +89,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()->
- getIsSalable()) :?>
+ isAvailable()) :?>
= $block->escapeHtml(__('In stock')) ?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml
index 378cd49493a6..ceb32e78c7e4 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml
@@ -88,7 +88,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()->
- getIsSalable()) :?>
+ isAvailable()) :?>
= $block->escapeHtml(__('In stock')) ?>
= $block->escapeHtml(__('Out of stock')) ?>
diff --git a/app/code/Magento/Catalog/view/frontend/web/js/zoom.js b/app/code/Magento/Catalog/view/frontend/web/js/zoom.js
index 1c68818ed9c4..1305a9acac16 100644
--- a/app/code/Magento/Catalog/view/frontend/web/js/zoom.js
+++ b/app/code/Magento/Catalog/view/frontend/web/js/zoom.js
@@ -92,7 +92,7 @@ define([
}, this));
// Window resize will change offset for draggable
- $(window).resize(this._draggableImage());
+ $(window).on('resize', this._draggableImage);
},
/**
diff --git a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html
index 05dbf0270328..8755c7ec9103 100644
--- a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html
+++ b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocart-button.html
@@ -11,14 +11,14 @@
'data-post': getDataPost($row()),
title: getLabel()"
type="button">
-
+
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocompare-button.html b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocompare-button.html
index 915fc1b3ac2f..dfab8c3af84a 100644
--- a/app/code/Magento/Catalog/view/frontend/web/template/product/addtocompare-button.html
+++ b/app/code/Magento/Catalog/view/frontend/web/template/product/addtocompare-button.html
@@ -8,6 +8,6 @@
-
+
diff --git a/app/code/Magento/CatalogAnalytics/README.md b/app/code/Magento/CatalogAnalytics/README.md
index 0c4ee155c4f2..bfea74e7ddd8 100644
--- a/app/code/Magento/CatalogAnalytics/README.md
+++ b/app/code/Magento/CatalogAnalytics/README.md
@@ -1,3 +1,3 @@
# Magento_CatalogAnalytics module
-The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.3/advanced-reporting/modules.html).
+The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/modules.html).
diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
index efba88ff154b..3c6cc849081e 100644
--- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
@@ -19,7 +19,6 @@
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Pricing\PriceCurrencyInterface;
-use Magento\Store\Api\Data\StoreInterface;
/**
* Resolver for price_tiers
@@ -125,6 +124,10 @@ public function resolve(
return [];
}
+ if (!$product->getTierPrices()) {
+ return [];
+ }
+
$productId = (int)$product->getId();
$this->tiers->addProductFilter($productId);
@@ -152,7 +155,8 @@ private function formatAndFilterTierPrices(
array $tierPrices,
string $currencyCode
): array {
-
+ $this->formatAndFilterTierPrices = [];
+ $this->tierPricesQty = [];
foreach ($tierPrices as $key => $tierPrice) {
$tierPrice->setValue($this->priceCurrency->convertAndRound($tierPrice->getValue()));
$this->formatTierPrices($productPrice, $currencyCode, $tierPrice);
diff --git a/app/code/Magento/CatalogCustomerGraphQl/composer.json b/app/code/Magento/CatalogCustomerGraphQl/composer.json
index a7c887af0379..ce80ee602327 100644
--- a/app/code/Magento/CatalogCustomerGraphQl/composer.json
+++ b/app/code/Magento/CatalogCustomerGraphQl/composer.json
@@ -7,8 +7,7 @@
"magento/framework": "*",
"magento/module-catalog": "*",
"magento/module-customer": "*",
- "magento/module-catalog-graph-ql": "*",
- "magento/module-store": "*"
+ "magento/module-catalog-graph-ql": "*"
},
"license": [
"OSL-3.0",
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
index d46776bfe498..978e4f6b406d 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\Framework\DB\Select;
use Magento\Store\Model\Store;
/**
@@ -62,6 +63,7 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode
'attribute_id' => 'a.attribute_id',
'attribute_code' => 'a.attribute_code',
'attribute_label' => 'a.frontend_label',
+ 'position' => 'attribute_configuration.position'
]
)
->joinLeft(
@@ -71,6 +73,11 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode
'attribute_store_label' => 'attribute_label.value',
]
)
+ ->joinLeft(
+ ['attribute_configuration' => $this->resourceConnection->getTableName('catalog_eav_attribute')],
+ 'a.attribute_id = attribute_configuration.attribute_id',
+ []
+ )
->joinLeft(
['options' => $this->resourceConnection->getTableName('eav_attribute_option')],
'a.attribute_id = options.attribute_id',
@@ -95,6 +102,8 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode
)->where(
'a.attribute_id = options.attribute_id AND option_value.store_id = ?',
Store::DEFAULT_STORE_ID
+ )->order(
+ 'options.sort_order ' . Select::SQL_ASC
);
$select->where('option_value.option_id IN (?)', $optionIds);
@@ -112,11 +121,11 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode
/**
* Format result
*
- * @param \Magento\Framework\DB\Select $select
+ * @param Select $select
* @return array
* @throws \Zend_Db_Statement_Exception
*/
- private function formatResult(\Magento\Framework\DB\Select $select): array
+ private function formatResult(Select $select): array
{
$statement = $this->resourceConnection->getConnection()->query($select);
@@ -128,10 +137,13 @@ private function formatResult(\Magento\Framework\DB\Select $select): array
'attribute_code' => $option['attribute_code'],
'attribute_label' => $option['attribute_store_label']
? $option['attribute_store_label'] : $option['attribute_label'],
+ 'position' => $option['position'],
'options' => [],
];
}
- $result[$option['attribute_code']]['options'][$option['option_id']] = $option['option_label'];
+ if (!empty($option['option_id'])) {
+ $result[$option['attribute_code']]['options'][$option['option_id']] = $option['option_label'];
+ }
}
return $result;
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 5fce0fcdf3ca..afef26aad604 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
@@ -83,15 +83,16 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array
$result[$bucketName] = $this->layerFormatter->buildLayer(
$attribute['attribute_label'] ?? $bucketName,
\count($bucket->getValues()),
- $attribute['attribute_code'] ?? $bucketName
+ $attribute['attribute_code'] ?? $bucketName,
+ isset($attribute['position']) ? $attribute['position'] : null
);
- foreach ($bucket->getValues() as $value) {
- $metrics = $value->getMetrics();
+ $options = $this->getSortedOptions($bucket, isset($attribute['options']) ? $attribute['options'] : []);
+ foreach ($options as $option) {
$result[$bucketName]['options'][] = $this->layerFormatter->buildItem(
- $attribute['options'][$metrics['value']] ?? $metrics['value'],
- $metrics['value'],
- $metrics['count']
+ $option['label'],
+ $option['value'],
+ $option['count']
);
}
}
@@ -161,4 +162,36 @@ function (AggregationValueInterface $value) {
$attributes
);
}
+
+ /**
+ * Get sorted options
+ *
+ * @param BucketInterface $bucket
+ * @param array $optionLabels
+ * @return array
+ */
+ private function getSortedOptions(BucketInterface $bucket, array $optionLabels): array
+ {
+ /**
+ * Option labels array has been sorted
+ */
+ $options = $optionLabels;
+ foreach ($bucket->getValues() as $value) {
+ $metrics = $value->getMetrics();
+ $optionValue = $metrics['value'];
+ $optionLabel = $optionLabels[$optionValue] ?? $optionValue;
+ $options[$optionValue] = $metrics + ['label' => $optionLabel];
+ }
+
+ /**
+ * Delete options without bucket values
+ */
+ foreach ($options as $optionId => $option) {
+ if (!is_array($options[$optionId])) {
+ unset($options[$optionId]);
+ }
+ }
+
+ return array_values($options);
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
index 48a1265b10fc..6df29fa25692 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
@@ -18,14 +18,16 @@ class LayerFormatter
* @param string $layerName
* @param string $itemsCount
* @param string $requestName
+ * @param int $position
* @return array
*/
- public function buildLayer($layerName, $itemsCount, $requestName): array
+ public function buildLayer($layerName, $itemsCount, $requestName, $position = null): array
{
return [
'label' => $layerName,
'count' => $itemsCount,
- 'attribute_code' => $requestName
+ 'attribute_code' => $requestName,
+ 'position' => isset($position) ? (int)$position : null
];
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
index ab100c7272ba..356ff17183a5 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogGraphQl\Model\Category;
+use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeKind;
@@ -26,22 +27,35 @@ class DepthCalculator
*/
public function calculate(ResolveInfo $resolveInfo, FieldNode $fieldNode) : int
{
- $selections = $fieldNode->selectionSet->selections ?? [];
+ return $this->calculateRecursive($resolveInfo, $fieldNode);
+ }
+
+ /**
+ * Calculate recursive the total depth of a category tree inside a GraphQL request
+ *
+ * @param ResolveInfo $resolveInfo
+ * @param Node $node
+ * @return int
+ */
+ private function calculateRecursive(ResolveInfo $resolveInfo, Node $node) : int
+ {
+ if ($node->kind === NodeKind::FRAGMENT_SPREAD) {
+ $selections = isset($resolveInfo->fragments[$node->name->value]) ?
+ $resolveInfo->fragments[$node->name->value]->selectionSet->selections : [];
+ } else {
+ $selections = $node->selectionSet->selections ?? [];
+ }
$depth = count($selections) ? 1 : 0;
$childrenDepth = [0];
- foreach ($selections as $node) {
- if (isset($node->alias) && null !== $node->alias) {
+ foreach ($selections as $subNode) {
+ if (isset($subNode->alias) && null !== $subNode->alias) {
continue;
}
- 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);
- }
+ if ($subNode->kind === NodeKind::INLINE_FRAGMENT) {
+ $childrenDepth[] = $this->addInlineFragmentDepth($resolveInfo, $subNode);
} else {
- $childrenDepth[] = $this->calculate($resolveInfo, $node);
+ $childrenDepth[] = $this->calculateRecursive($resolveInfo, $subNode);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/ParentCategoryUidsArgsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Category/ParentCategoryUidsArgsProcessor.php
new file mode 100644
index 000000000000..700b95d79990
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Category/ParentCategoryUidsArgsProcessor.php
@@ -0,0 +1,69 @@
+uidEncoder = $uidEncoder;
+ }
+
+ /**
+ * Composite processor that loops through available processors for arguments that come from graphql input
+ *
+ * @param string $fieldName,
+ * @param array $args
+ * @return array
+ * @throws GraphQlInputException
+ */
+ public function process(
+ string $fieldName,
+ array $args
+ ): array {
+ $filterKey = 'filters';
+ $parentUidFilter = $args[$filterKey][self::UID] ?? [];
+ $parentIdFilter = $args[$filterKey][self::ID] ?? [];
+ if (!empty($parentIdFilter)
+ && !empty($parentUidFilter)
+ && ($fieldName === 'categories' || $fieldName === 'categoryList')) {
+ throw new GraphQlInputException(
+ __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID])
+ );
+ } elseif (!empty($parentUidFilter)) {
+ if (isset($parentUidFilter['eq'])) {
+ $args[$filterKey][self::ID]['eq'] = $this->uidEncoder->decode(
+ $parentUidFilter['eq']
+ );
+ } elseif (!empty($parentUidFilter['in'])) {
+ foreach ($parentUidFilter['in'] as $parentUids) {
+ $args[$filterKey][self::ID]['in'][] = $this->uidEncoder->decode($parentUids);
+ }
+ }
+ unset($args[$filterKey][self::UID]);
+ }
+ return $args;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/CategoryTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/CategoryTypeResolver.php
new file mode 100755
index 000000000000..d4b7c644fe82
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/CategoryTypeResolver.php
@@ -0,0 +1,30 @@
+getMessage()));
}
- $rootCategoryIds = $filterResult['category_ids'];
- $filterResult['items'] = $this->fetchCategories($rootCategoryIds, $info);
+ $rootCategoryIds = $filterResult['category_ids'] ?? [];
+
+ $filterResult['items'] = $this->fetchCategories($rootCategoryIds, $info, (int) $store->getId());
return $filterResult;
}
@@ -95,13 +96,14 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
*
* @param array $categoryIds
* @param ResolveInfo $info
+ * @param int $storeId
* @return array
*/
- private function fetchCategories(array $categoryIds, ResolveInfo $info)
+ private function fetchCategories(array $categoryIds, ResolveInfo $info, int $storeId)
{
$fetchedCategories = [];
foreach ($categoryIds as $categoryId) {
- $categoryTree = $this->categoryTree->getTree($info, $categoryId);
+ $categoryTree = $this->categoryTree->getTree($info, $categoryId, $storeId);
if (empty($categoryTree)) {
continue;
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php
index 747e05806a82..ee0ec69aaea7 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php
@@ -81,8 +81,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
} catch (InputException $e) {
throw new GraphQlInputException(__($e->getMessage()));
}
-
- return $this->fetchCategories($rootCategoryIds, $info);
+ return $this->fetchCategories($rootCategoryIds, $info, (int) $store->getId());
}
/**
@@ -90,13 +89,14 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
*
* @param array $categoryIds
* @param ResolveInfo $info
+ * @param int $storeId
* @return array
*/
- private function fetchCategories(array $categoryIds, ResolveInfo $info)
+ private function fetchCategories(array $categoryIds, ResolveInfo $info, int $storeId)
{
$fetchedCategories = [];
foreach ($categoryIds as $categoryId) {
- $categoryTree = $this->categoryTree->getTree($info, $categoryId);
+ $categoryTree = $this->categoryTree->getTree($info, $categoryId, $storeId);
if (empty($categoryTree)) {
continue;
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php
index 4284aed61084..cddba2e91f70 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php
@@ -71,7 +71,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
if ($rootCategoryId !== Category::TREE_ROOT_ID) {
$this->checkCategoryIsActive->execute($rootCategoryId);
}
- $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId);
+ $store = $context->getExtensionAttributes()->getStore();
+ $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId, (int)$store->getId());
if (empty($categoriesTree) || ($categoriesTree->count() == 0)) {
throw new GraphQlNoSuchEntityException(__('Category doesn\'t exist'));
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableDateTypeOptionValue.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableDateTypeOptionValue.php
new file mode 100644
index 000000000000..1b9583cc7239
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableDateTypeOptionValue.php
@@ -0,0 +1,42 @@
+getPriceInfo()->getPrice(PricingSpecialPrice::PRICE_CODE);
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 c553d4486f9e..86ee717d1ba3 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
@@ -80,9 +80,11 @@ public function __construct(
*
* @param ResolveInfo $resolveInfo
* @param int $rootCategoryId
+ * @param int $storeId
* @return \Iterator
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
- public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterator
+ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId, int $storeId): \Iterator
{
$categoryQuery = $resolveInfo->fieldNodes[0];
$collection = $this->collectionFactory->create();
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php
index 22bbc991a78e..8218c22b8056 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php
@@ -132,7 +132,7 @@ private function fetch() : array
$this->searchCriteriaBuilder->create(),
$this->attributeCodes,
false,
- true
+ false
);
/** @var \Magento\Catalog\Model\Product $product */
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php
index b38a2c9bb04d..e212f33a35d2 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php
@@ -67,7 +67,8 @@ public function execute(\Iterator $iterator): array
$tree = $this->mergeCategoriesTrees($currentLevelTree, $tree);
}
}
- return $tree;
+
+ return $this->sortTree($tree);
}
/**
@@ -141,4 +142,24 @@ private function explodePathToArray(array $pathElements, int $index): array
}
return $tree;
}
+
+ /**
+ * Recursive method to sort tree
+ *
+ * @param array $tree
+ * @return array
+ */
+ private function sortTree(array $tree): array
+ {
+ foreach ($tree as &$node) {
+ if ($node['children']) {
+ uasort($node['children'], function ($element1, $element2) {
+ return $element1['position'] > $element2['position'];
+ });
+ $node['children'] = $this->sortTree($node['children']);
+ }
+ }
+
+ return $tree;
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
index 3e955ae30345..30be41072242 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
@@ -89,7 +89,7 @@ public function getList(
$this->collectionPreProcessor->process($collection, $searchCriteria, $attributes, $context);
- if (!$isChildSearch) {
+ if ($isChildSearch) {
$visibilityIds = $isSearch
? $this->visibility->getVisibleInSearchIds()
: $this->visibility->getVisibleInCatalogIds();
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php
index b545047d0154..721441cd08d6 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php
@@ -36,8 +36,8 @@ public function process(
ContextInterface $context = null
): Collection {
$collection->addAttributeToSelect('special_price');
- $collection->addAttributeToSelect('special_price_from');
- $collection->addAttributeToSelect('special_price_to');
+ $collection->addAttributeToSelect('special_from_date');
+ $collection->addAttributeToSelect('special_to_date');
$collection->addAttributeToSelect('tax_class_id');
return $collection;
diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml
index 8c6fac0fe621..bb2a069d738e 100644
--- a/app/code/Magento/CatalogGraphQl/etc/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/di.xml
@@ -72,6 +72,7 @@
- Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUidArgsProcessor
- Magento\CatalogGraphQl\Model\Category\CategoryUidsArgsProcessor
+ - Magento\CatalogGraphQl\Model\Category\ParentCategoryUidsArgsProcessor
diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
index 79281ff42cf2..b1790f4bc628 100644
--- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
@@ -49,6 +49,12 @@ enum PriceTypeEnum @doc(description: "This enumeration the price type.") {
DYNAMIC
}
+enum CustomizableDateTypeEnum @doc(description: "This enumeration customizable date type.") {
+ DATE
+ DATE_TIME
+ TIME
+}
+
type ProductPrices @doc(description: "ProductPrices is deprecated, replaced by PriceRange. The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") {
minimalPrice: Price @deprecated(reason: "Use PriceRange.minimum_price.") @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.")
maximalPrice: Price @deprecated(reason: "Use PriceRange.maximum_price.") @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.")
@@ -136,7 +142,7 @@ type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the
uid: ID! @doc(description: "The unique ID for a `CustomizableAreaValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid")
}
-type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") {
+type CategoryTree implements CategoryInterface, RoutableInterface @doc(description: "Category tree implementation") {
children: [CategoryTree] @doc(description: "Child categories tree.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree")
}
@@ -154,6 +160,7 @@ type CustomizableDateOption implements CustomizableOptionInterface @doc(descript
type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price and sku of a product whose page contains a customized date picker.") {
price: Float @doc(description: "The price assigned to this option.")
price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.")
+ type: CustomizableDateTypeEnum @doc(description: "DATE, DATE_TIME or TIME") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableDateTypeOptionValue")
sku: String @doc(description: "The Stock Keeping Unit for this option.")
uid: ID! @doc(description: "The unique ID for a `CustomizableDateValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid")
}
@@ -301,10 +308,10 @@ type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defi
uid: ID! @doc(description: "The unique ID for a `CustomizableCheckboxValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid")
}
-type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") {
+type VirtualProduct implements ProductInterface, RoutableInterface, CustomizableProductInterface @doc(description: "A virtual product is a non-tangible product that does not require shipping and is not kept in inventory") {
}
-type SimpleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "A simple product is tangible and are usually sold as single units or in fixed quantities.")
+type SimpleProduct implements ProductInterface, RoutableInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "A simple product is tangible and is usually sold in single units or in fixed quantities")
{
}
@@ -332,7 +339,8 @@ input CategoryFilterInput @doc(description: "CategoryFilterInput defines the fi
{
ids: FilterEqualTypeInput @deprecated(reason: "Use the `category_uid` argument instead.") @doc(description: "Deprecated: use 'category_uid' to filter uniquely identifiers of categories.")
category_uid: FilterEqualTypeInput @doc(description: "Filter by the unique category ID for a `CategoryInterface` object.")
- parent_id: FilterEqualTypeInput @doc(description: "Filter by the unique parent category ID for a `CategoryInterface` object.")
+ parent_id: FilterEqualTypeInput @deprecated @doc(description: "Filter by the unique parent category ID for a `CategoryInterface` object.")
+ parent_category_uid: FilterEqualTypeInput @doc(description: "Filter by the unique parent category ID for a `CategoryInterface` object.")
url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category.")
name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.")
url_path: FilterEqualTypeInput @doc(description: "Filter by the URL path for the category.")
@@ -466,6 +474,7 @@ type Aggregation @doc(description: "A bucket that contains information for each
label: String @doc(description: "The aggregation display name.")
attribute_code: String! @doc(description: "Attribute code of the aggregation group.")
options: [AggregationOption] @doc(description: "Array of options for the aggregation.")
+ position: Int @doc(description: "The relative position of the attribute in a layered navigation block")
}
interface AggregationOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\AggregationOptionTypeResolverComposite") {
diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product/WebsiteFilter.php b/app/code/Magento/CatalogImportExport/Model/Export/Product/WebsiteFilter.php
new file mode 100644
index 000000000000..11ef78a8ca81
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Model/Export/Product/WebsiteFilter.php
@@ -0,0 +1,33 @@
+addWebsiteFilter($filters[self::NAME]);
+
+ return $collection;
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
index 673dbcb3b3c9..d0c93658fc28 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
@@ -1855,7 +1855,7 @@ protected function _saveProducts()
}
$productTypeModel = $this->_productTypeModels[$productType];
- if (!empty($rowData['tax_class_name'])) {
+ if (isset($rowData['tax_class_name']) && strlen($rowData['tax_class_name'])) {
$rowData['tax_class_id'] =
$this->taxClassProcessor->upsertTaxClass($rowData['tax_class_name'], $productTypeModel);
}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
index 22a83671f630..84cdc4608148 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
@@ -220,7 +220,7 @@ private function deleteProductsLinks(
if (!empty($linksToDelete) && Import::BEHAVIOR_APPEND === $importEntity->getBehavior()) {
foreach ($linksToDelete as $linkTypeId => $productIds) {
if (!empty($productIds)) {
- $whereLinkId = $importEntity->getConnection()->quoteInto('link_type_id', $linkTypeId);
+ $whereLinkId = $importEntity->getConnection()->quoteInto('link_type_id = ?', $linkTypeId);
$whereProductId = $importEntity->getConnection()->quoteInto(
'product_id IN (?)',
array_unique($productIds)
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php
index af102cc50b8b..e068a0740482 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/TaxClassProcessor.php
@@ -7,9 +7,25 @@
use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType;
use Magento\Tax\Model\ClassModel;
+use Magento\Tax\Model\ClassModelFactory;
+use Magento\Tax\Model\ResourceModel\TaxClass\Collection;
+use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory;
+/**
+ * Imported products tax class processor
+ */
class TaxClassProcessor
{
+ /**
+ * Empty tax class name
+ */
+ private const CLASS_NONE_NAME = 'none';
+
+ /**
+ * Empty tax class ID
+ */
+ private const CLASS_NONE_ID = 0;
+
/**
* Tax attribute code.
*/
@@ -25,24 +41,24 @@ class TaxClassProcessor
/**
* Instance of tax class collection factory.
*
- * @var \Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory
+ * @var CollectionFactory
*/
protected $collectionFactory;
/**
* Instance of tax model factory.
*
- * @var \Magento\Tax\Model\ClassModelFactory
+ * @var ClassModelFactory
*/
protected $classModelFactory;
/**
- * @param \Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory $collectionFactory
- * @param \Magento\Tax\Model\ClassModelFactory $classModelFactory
+ * @param CollectionFactory $collectionFactory
+ * @param ClassModelFactory $classModelFactory
*/
public function __construct(
- \Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory $collectionFactory,
- \Magento\Tax\Model\ClassModelFactory $classModelFactory
+ CollectionFactory $collectionFactory,
+ ClassModelFactory $classModelFactory
) {
$this->collectionFactory = $collectionFactory;
$this->classModelFactory = $classModelFactory;
@@ -59,9 +75,9 @@ protected function initTaxClasses()
if (empty($this->taxClasses)) {
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('class_type', ClassModel::TAX_CLASS_TYPE_PRODUCT);
- /* @var $collection \Magento\Tax\Model\ResourceModel\TaxClass\Collection */
+ /* @var $collection Collection */
foreach ($collection as $taxClass) {
- $this->taxClasses[$taxClass->getClassName()] = $taxClass->getId();
+ $this->taxClasses[mb_strtolower($taxClass->getClassName())] = $taxClass->getId();
}
}
return $this;
@@ -76,7 +92,7 @@ protected function initTaxClasses()
*/
protected function createTaxClass($taxClassName, AbstractType $productTypeModel)
{
- /** @var \Magento\Tax\Model\ClassModelFactory $taxClass */
+ /** @var ClassModelFactory $taxClass */
$taxClass = $this->classModelFactory->create();
$taxClass->setClassType(ClassModel::TAX_CLASS_TYPE_PRODUCT);
$taxClass->setClassName($taxClassName);
@@ -98,10 +114,22 @@ protected function createTaxClass($taxClassName, AbstractType $productTypeModel)
*/
public function upsertTaxClass($taxClassName, AbstractType $productTypeModel)
{
- if (!isset($this->taxClasses[$taxClassName])) {
- $this->taxClasses[$taxClassName] = $this->createTaxClass($taxClassName, $productTypeModel);
+ $normalizedTaxClassName = mb_strtolower($taxClassName);
+
+ if ($normalizedTaxClassName === (string) self::CLASS_NONE_ID) {
+ $normalizedTaxClassName = self::CLASS_NONE_NAME;
+ }
+
+ if (!isset($this->taxClasses[$normalizedTaxClassName])) {
+ $this->taxClasses[$normalizedTaxClassName] = $normalizedTaxClassName === self::CLASS_NONE_NAME
+ ? self::CLASS_NONE_ID
+ : $this->createTaxClass($taxClassName, $productTypeModel);
+ }
+ if ($normalizedTaxClassName === self::CLASS_NONE_NAME) {
+ // Add None option to tax_class_id options.
+ $productTypeModel->addAttributeOption(self::ATRR_CODE, self::CLASS_NONE_ID, self::CLASS_NONE_ID);
}
- return $this->taxClasses[$taxClassName];
+ return $this->taxClasses[$normalizedTaxClassName];
}
}
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportAllProductsActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportAllProductsActionGroup.xml
index 3edbb1b82184..bceac07f2a64 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportAllProductsActionGroup.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportAllProductsActionGroup.xml
@@ -12,13 +12,18 @@
Exports the unfiltered Products list. Validates that the Success Message is present.
-
+
+
+
+
-
+
+
+
-
+
-
-
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportProductsFilterByAttributeActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportProductsFilterByAttributeActionGroup.xml
index f3ca89420289..f34236b49843 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportProductsFilterByAttributeActionGroup.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/ExportProductsFilterByAttributeActionGroup.xml
@@ -17,8 +17,9 @@
-
+
+
@@ -26,6 +27,7 @@
-
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Data/ImportData.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Data/ImportData.xml
new file mode 100644
index 000000000000..3ca2da4f1b89
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Data/ImportData.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ 0
+ import_attribute1
+
+
+ import_attribute1
+ ProductAttributeFrontendLabelImport1
+
+
+ option3
+
+
+
+
+ test_image.jpg
+
+
+ adobe-base.jpg
+
+
+ Test Image
+ TestImageImageContentExportImport
+
+
+ Adobe Base
+ AdobeBaseContentExportImport
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml
index 785d19c000af..e72542df7c65 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml
@@ -11,14 +11,21 @@
-
-
-
+
+
+
+
@@ -81,7 +88,9 @@
+
+
@@ -92,33 +101,201 @@
+
+ var/export
+
-
-
-
-
+
+
-
+
+
-
+
+
+
+
+
+
+
+ var/export/{$grabNameFile}
+
+
+ var/export/{$grabNameFile}
+ $$firstSimpleProductForDynamic.name$$
+
+
+ var/export/{$grabNameFile}
+ $$secondSimpleProductForDynamic.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createDynamicBundleProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ name=$createFirstBundleOption.option[title]$,type=$createFirstBundleOption.option[type]$,required=$createFirstBundleOption.option[required]$,sku=$$firstSimpleProductForDynamic.sku$$
+
+
+ var/export/{$grabNameFile}
+ name=$createFirstBundleOption.option[title]$,type=$createFirstBundleOption.option[type]$,required=$createFirstBundleOption.option[required]$,sku=$$secondSimpleProductForDynamic.sku$$
+
+
+ var/export/{$grabNameFile}
+ 0.000000,,,,$$createDynamicBundleProduct.sku$$
+
-
+
+
+ var/export/{$grabNameFile}
+ $$firstSimpleProductForFixed.name$$
+
+
+ var/export/{$grabNameFile}
+ $$secondSimpleProductForFixed.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createFixedBundleProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ name=$createSecondBundleOption.option[title]$,type=$createSecondBundleOption.option[type]$,required=$createSecondBundleOption.option[required]$,sku=$$firstSimpleProductForFixed.sku$$
+
+
+ var/export/{$grabNameFile}
+ name=$createSecondBundleOption.option[title]$,type=$createSecondBundleOption.option[type]$,required=$createSecondBundleOption.option[required]$,sku=$$secondSimpleProductForFixed.sku$$
+
+
+ var/export/{$grabNameFile}
+ $$createFixedBundleProduct.price$$0000,,,,$$createFixedBundleProduct.sku$$
+
+
+
+
+ var/export/{$grabNameFile}
+ $$firstSimpleProductForFixedWithAttribute.name$$
+
+
+ var/export/{$grabNameFile}
+ $$secondSimpleProductForFixedWithAttribute.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createFixedBundleProductWithAttribute.name$$
+
+
+ var/export/{$grabNameFile}
+ name=$createBundleOptionWithAttribute.option[title]$,type=$createBundleOptionWithAttribute.option[type]$,required=$createBundleOptionWithAttribute.option[required]$,sku=$$firstSimpleProductForFixedWithAttribute.sku$$
+
+
+ var/export/{$grabNameFile}
+ name=$createBundleOptionWithAttribute.option[title]$,type=$createBundleOptionWithAttribute.option[type]$,required=$createBundleOptionWithAttribute.option[required]$,sku=$$secondSimpleProductForFixedWithAttribute.sku$$
+
+
+ var/export/{$grabNameFile}
+ $$createFixedBundleProductWithAttribute.price$$0000,,,,$$createFixedBundleProductWithAttribute.sku$$
+
+
+
-
+
-
+
+
+
+ {$grabExportUrl}
+ $$firstSimpleProductForDynamic.name$$
+
+
+ {$grabExportUrl}
+ $$secondSimpleProductForDynamic.name$$
+
+
+ {$grabExportUrl}
+ $$createDynamicBundleProduct.name$$
+
+
+ {$grabExportUrl}
+ name=$createFirstBundleOption.option[title]$,type=$createFirstBundleOption.option[type]$,required=$createFirstBundleOption.option[required]$,sku=$$firstSimpleProductForDynamic.sku$$
+
+
+ {$grabExportUrl}
+ name=$createFirstBundleOption.option[title]$,type=$createFirstBundleOption.option[type]$,required=$createFirstBundleOption.option[required]$,sku=$$secondSimpleProductForDynamic.sku$$
+
+
+ {$grabExportUrl}
+ 0.000000,,,,$$createDynamicBundleProduct.sku$$
+
+
+
+
+ {$grabExportUrl}
+ $$firstSimpleProductForFixed.name$$
+
+
+ {$grabExportUrl}
+ $$secondSimpleProductForFixed.name$$
+
+
+ {$grabExportUrl}
+ $$createFixedBundleProduct.name$$
+
+
+ {$grabExportUrl}
+ name=$createSecondBundleOption.option[title]$,type=$createSecondBundleOption.option[type]$,required=$createSecondBundleOption.option[required]$,sku=$$firstSimpleProductForFixed.sku$$
+
+
+ {$grabExportUrl}
+ name=$createSecondBundleOption.option[title]$,type=$createSecondBundleOption.option[type]$,required=$createSecondBundleOption.option[required]$,sku=$$secondSimpleProductForFixed.sku$$
+
+
+ {$grabExportUrl}
+ $$createFixedBundleProduct.price$$0000,,,,$$createFixedBundleProduct.sku$$
+
+
+
+
+ {$grabExportUrl}
+ $$firstSimpleProductForFixedWithAttribute.name$$
+
+
+ {$grabExportUrl}
+ $$secondSimpleProductForFixedWithAttribute.name$$
+
+
+ {$grabExportUrl}
+ $$createFixedBundleProductWithAttribute.name$$
+
+
+ {$grabExportUrl}
+ name=$createBundleOptionWithAttribute.option[title]$,type=$createBundleOptionWithAttribute.option[type]$,required=$createBundleOptionWithAttribute.option[required]$,sku=$$firstSimpleProductForFixedWithAttribute.sku$$
+
+
+ {$grabExportUrl}
+ name=$createBundleOptionWithAttribute.option[title]$,type=$createBundleOptionWithAttribute.option[type]$,required=$createBundleOptionWithAttribute.option[required]$,sku=$$secondSimpleProductForFixedWithAttribute.sku$$
+
+
+ {$grabExportUrl}
+ $$createFixedBundleProductWithAttribute.price$$0000,,,,$$createFixedBundleProductWithAttribute.sku$$
+
+
+
-
+
+
+ var/export/{$grabNameFile}
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportFilenameTimezoneTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportFilenameTimezoneTest.xml
new file mode 100644
index 000000000000..a38a43bc7217
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportFilenameTimezoneTest.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ var/export/{$grabNameFile}
+
+
+
+
+
+
+ getFilename
+ expectedFilename
+
+
+
+
+ {$grabExportUrl}
+ Content-Disposition: attachment; filename="export/{$expectedFilename}"
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml
index 9f8d65968d74..dba833c95b21 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml
@@ -11,14 +11,20 @@
-
-
-
+
+
+
+
@@ -50,40 +56,97 @@
+
+
+
+ var/export
+
-
-
-
-
-
-
+
+
+
-
+
+
-
+
+
+
+
+
+
+
+ var/export/{$grabNameFile}
+
+
+ var/export/{$grabNameFile}
+ $$createFirstSimpleProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createSecondSimpleProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createGroupedProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createFirstSimpleProduct.sku$$=$$createFirstSimpleProduct.quantity$$
+
+
+ var/export/{$grabNameFile}
+ $$createSecondSimpleProduct.sku$$=$$createSecondSimpleProduct.quantity$$
+
-
+
-
+
-
+
+
+
+ {$grabExportUrl}
+ $$createFirstSimpleProduct.name$$
+
+
+ {$grabExportUrl}
+ $$createSecondSimpleProduct.name$$
+
+
+ {$grabExportUrl}
+ $$createGroupedProduct.name$$
+
+
+ {$grabExportUrl}
+ $$createFirstSimpleProduct.sku$$=$$createFirstSimpleProduct.quantity$$
+
+
+ {$grabExportUrl}
+ $$createSecondSimpleProduct.sku$$=$$createSecondSimpleProduct.quantity$$
+
+
+
-
+
+
+ var/export/{$grabNameFile}
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
index f0e6e12204a9..44a7558d6054 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
@@ -171,11 +171,14 @@
-
+
+
+
+
-
+
@@ -209,9 +212,7 @@
-
-
-
+
@@ -219,14 +220,12 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml
index 94478e63aa92..d60ed5e1c448 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml
@@ -11,14 +11,21 @@
-
-
-
+
+
+
+
@@ -76,6 +83,7 @@
+
@@ -83,37 +91,93 @@
+
+ var/export
+
-
-
-
-
-
+
+
+
-
+
+
-
+
+
+
+
+
+
+
+ var/export/{$grabNameFile}
+
+
+ var/export/{$grabNameFile}
+ $$createConfigProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ sku=$$createConfigFirstChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option1
+
+
+ var/export/{$grabNameFile}
+ sku=$$createConfigSecondChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option2
+
+
+ var/export/{$grabNameFile}
+ $$createConfigFirstChildProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createConfigSecondChildProduct.name$$
+
-
+
-
+
-
+
+
+
+ {$grabExportUrl}
+ $$createConfigProduct.name$$
+
+
+ {$grabExportUrl}
+ sku=$$createConfigFirstChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option1
+
+
+ {$grabExportUrl}
+ sku=$$createConfigSecondChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option2
+
+
+ {$grabExportUrl}
+ $$createConfigFirstChildProduct.name$$
+
+
+ {$grabExportUrl}
+ $$createConfigSecondChildProduct.name$$
+
+
+
-
+
+
+ var/export/{$grabNameFile}
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml
index 95cfe2c87bff..ab0d00527cbb 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml
@@ -11,14 +11,22 @@
-
+
-
+
+
@@ -92,43 +100,108 @@
+
-
+
+
+ var/export
+
-
-
-
-
+
+
-
+
+
-
+
+
+
+
+
+
+
+ var/export/{$grabNameFile}
+
+
+ var/export/{$grabNameFile}
+ $$createConfigProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ sku=$$createConfigFirstChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option1
+
+
+ var/export/{$grabNameFile}
+ sku=$$createConfigSecondChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option2
+
+
+ var/export/{$grabNameFile}
+ $createConfigProductImage.entry[content][name]$,"$createConfigProductImage.entry[label]$"
+
+
+ var/export/{$grabNameFile}
+ $$createConfigFirstChildProduct.name$$
+
+
+ var/export/{$grabNameFile}
+ $$createConfigSecondChildProduct.name$$
+
-
+
-
+
-
+
+
+
+ {$grabExportUrl}
+ $$createConfigProduct.name$$
+
+
+ {$grabExportUrl}
+ sku=$$createConfigFirstChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option1
+
+
+ {$grabExportUrl}
+ sku=$$createConfigSecondChildProduct.sku$$,$$createConfigProductAttribute.attribute_code$$=option2
+
+
+ {$grabExportUrl}
+ $createConfigProductImage.entry[content][name]$,"$createConfigProductImage.entry[label]$"
+
+
+ {$grabExportUrl}
+ $$createConfigFirstChildProduct.name$$
+
+
+ {$grabExportUrl}
+ $$createConfigSecondChildProduct.name$$
+
+
+
-
+
+
+ var/export/{$grabNameFile}
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml
index 2f57d94113d3..bbfc65d18c7e 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml
@@ -11,7 +11,7 @@
-
+
@@ -103,16 +103,19 @@
-
+
+
+
+
-
+
-
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml
index dac97a61a967..d9e42e65b44f 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml
@@ -11,7 +11,7 @@
-
+
@@ -56,16 +56,19 @@
-
+
+
+
+
-
+
-
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php
index ba65ffa37c8c..80ee5e92f2e7 100644
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/TaxClassProcessorTest.php
@@ -101,4 +101,22 @@ public function testUpsertTaxClassNotExist()
$taxClassId = $this->taxClassProcessor->upsertTaxClass('noExistClassName', $this->product);
$this->assertEquals(self::TEST_JUST_CREATED_TAX_CLASS_ID, $taxClassId);
}
+
+ public function testUpsertTaxClassExistCaseInsensitive()
+ {
+ $taxClassId = $this->taxClassProcessor->upsertTaxClass(strtoupper(self::TEST_TAX_CLASS_NAME), $this->product);
+ $this->assertEquals(self::TEST_TAX_CLASS_ID, $taxClassId);
+ }
+
+ public function testUpsertTaxClassNone()
+ {
+ $taxClassId = $this->taxClassProcessor->upsertTaxClass('none', $this->product);
+ $this->assertEquals(0, $taxClassId);
+ }
+
+ public function testUpsertTaxClassZero()
+ {
+ $taxClassId = $this->taxClassProcessor->upsertTaxClass(0, $this->product);
+ $this->assertEquals(0, $taxClassId);
+ }
}
diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml
index 040e9dcda132..c35bcbd84951 100644
--- a/app/code/Magento/CatalogImportExport/etc/di.xml
+++ b/app/code/Magento/CatalogImportExport/etc/di.xml
@@ -48,6 +48,7 @@
- Magento\CatalogImportExport\Model\Export\Product\CategoryFilter
- Magento\CatalogImportExport\Model\Export\Product\StockStatusFilter
+ - Magento\CatalogImportExport\Model\Export\Product\WebsiteFilter
diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php
index 07b8429ddf18..4b9383b9eb10 100644
--- a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php
@@ -17,8 +17,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockCollectionInterface extends SearchResultsInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php
index 581081f2924e..e0375471acf1 100644
--- a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php
@@ -13,8 +13,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockInterface extends ExtensibleDataInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php
index 2d5f980a5703..d280df7e9fe1 100644
--- a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php
@@ -17,8 +17,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockItemCollectionInterface extends SearchResultsInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php
index 759cc9883be9..4b42c6498c94 100644
--- a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php
@@ -13,8 +13,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockItemInterface extends ExtensibleDataInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php
index a7d70a943d40..c3649496f2be 100644
--- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php
@@ -13,8 +13,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockStatusCollectionInterface extends SearchResultsInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php
index 35e56b0e3e7b..10123c9c5a10 100644
--- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php
@@ -13,8 +13,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockStatusInterface extends ExtensibleDataInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php
index ddb3fce22a85..e530b0d83c9c 100644
--- a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php
@@ -14,8 +14,8 @@
* @api
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
* @since 100.3.0
*/
interface RegisterProductSaleInterface
diff --git a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php
index 83f7d73deaed..5d5f22580b1e 100644
--- a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php
@@ -11,8 +11,8 @@
* @api
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
* @since 100.3.0
*/
interface RevertProductSaleInterface
diff --git a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php
index ab52580988c5..4436f3b220c2 100644
--- a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockConfigurationInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php
index 92f2290ec08a..5c3c82701339 100644
--- a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php
index 24dbaf5bb6d5..e3288d355f74 100644
--- a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockIndexInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php
index b72289ee0927..19c5f597d4b3 100644
--- a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockItemCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php
index 4269569f9da1..41b96b0d5ccd 100644
--- a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockItemRepositoryInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php
index 3c1c7ea137c8..a3fca303236b 100644
--- a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockManagementInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php
index bab5f9b457c4..07bf2746338d 100644
--- a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockRegistryInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php
index a7d64ec9eedb..f38d4a2ca91b 100644
--- a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockRepositoryInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php
index d404e885d78d..ad7291281ed3 100644
--- a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockStateInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php
index be1c9642826a..cd26a575b676 100644
--- a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockStatusCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface
{
diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php
index 91efd5576133..b120b93c9193 100644
--- a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php
+++ b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockStatusRepositoryInterface
{
diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php
index bc63114d9980..e7918e32f78a 100644
--- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php
+++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php
@@ -14,8 +14,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class Minsaleqty extends \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray
{
diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php
index 3c1a6e798270..0063eefeb12b 100644
--- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php
+++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php
@@ -16,8 +16,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class Stock extends \Magento\Framework\Data\Form\Element\Select
{
@@ -256,8 +256,8 @@ protected function _getJs($quantityFieldId, $inStockFieldId)
};
$.each(fieldsAssociations, function(generalTabField, advancedTabField) {
$('#' + generalTabField + ', #' + advancedTabField)
- .bind('focus blur change keyup click', filler)
- .bind('keyup change blur', disabler)
+ .on('focus blur change keyup click', filler)
+ .on('keyup change blur', disabler)
.trigger('change');
});
diff --git a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php
index dd8c987fe5da..909ec9346ebf 100644
--- a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php
+++ b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php
@@ -16,8 +16,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class Qtyincrements extends Template implements IdentityInterface
{
diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php
index c19dc5fb34bf..cb7d68c92ef6 100644
--- a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php
+++ b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php
@@ -13,8 +13,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class DefaultStockqty extends AbstractStockqty implements \Magento\Framework\DataObject\IdentityInterface
{
diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php
index 87a0e3c32ad0..e79d2098be68 100644
--- a/app/code/Magento/CatalogInventory/Helper/Stock.php
+++ b/app/code/Magento/CatalogInventory/Helper/Stock.php
@@ -20,8 +20,8 @@
* @api
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
* @since 100.0.2
*/
class Stock
diff --git a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php
index 04e54acad5c0..c2715241fbe1 100644
--- a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php
+++ b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php
@@ -22,8 +22,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class Item extends \Magento\CatalogInventory\Model\Stock\Item implements IdentityInterface
{
diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php
index f85f3f357627..d8bf4f858047 100644
--- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php
+++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php
@@ -28,6 +28,8 @@
use Magento\Framework\Exception\LocalizedException;
use Magento\CatalogInventory\Model\Indexer\Stock\AbstractAction;
use Magento\CatalogInventory\Model\ResourceModel\Indexer\Stock\StockInterface;
+use Magento\Framework\App\DeploymentConfig;
+use Magento\CatalogInventory\Model\Indexer\Stock\Processor;
/**
* Class Full reindex action
@@ -71,6 +73,18 @@ class Full extends AbstractAction
*/
private $batchQueryGenerator;
+ /**
+ * @var DeploymentConfig|null
+ */
+ private $deploymentConfig;
+
+ /**
+ * Deployment config path
+ *
+ * @var string
+ */
+ private const DEPLOYMENT_CONFIG_INDEXER_BATCHES = 'indexer/batch_size/';
+
/**
* @param ResourceConnection $resource
* @param StockFactory $indexerFactory
@@ -83,6 +97,7 @@ class Full extends AbstractAction
* @param array $batchRowsCount
* @param ActiveTableSwitcher|null $activeTableSwitcher
* @param QueryGenerator|null $batchQueryGenerator
+ * @param DeploymentConfig|null $deploymentConfig
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -96,7 +111,8 @@ public function __construct(
BatchProviderInterface $batchProvider = null,
array $batchRowsCount = [],
ActiveTableSwitcher $activeTableSwitcher = null,
- QueryGenerator $batchQueryGenerator = null
+ QueryGenerator $batchQueryGenerator = null,
+ ?DeploymentConfig $deploymentConfig = null
) {
parent::__construct(
$resource,
@@ -115,6 +131,7 @@ public function __construct(
$this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()
->get(ActiveTableSwitcher::class);
$this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class);
+ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class);
}
/**
@@ -141,9 +158,18 @@ public function execute($ids = null): void
$connection = $indexer->getConnection();
$tableName = $this->activeTableSwitcher->getAdditionalTableName($indexer->getMainTable());
- $batchRowCount = isset($this->batchRowsCount[$indexer->getTypeId()])
- ? $this->batchRowsCount[$indexer->getTypeId()]
- : $this->batchRowsCount['default'];
+ $batchRowCount = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . Processor::INDEXER_ID . '/' . $indexer->getTypeId(),
+ $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . Processor::INDEXER_ID . '/' . 'default'
+ )
+ );
+
+ if (is_null($batchRowCount)) {
+ $batchRowCount = isset($this->batchRowsCount[$indexer->getTypeId()])
+ ? $this->batchRowsCount[$indexer->getTypeId()]
+ : $this->batchRowsCount['default'];
+ }
$this->batchSizeManagement->ensureBatchSize($connection, $batchRowCount);
diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php
index 005ffd11ac7a..c871a8dee65f 100644
--- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php
+++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php
@@ -6,6 +6,7 @@
namespace Magento\CatalogInventory\Model\Indexer\Stock;
+use Magento\Catalog\Model\Category;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\App\ObjectManager;
@@ -88,6 +89,11 @@ public function clean(array $productIds, callable $reindex)
if ($productIds) {
$this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds));
$this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
+ $categoryIds = $this->getCategoryIdsByProductIds($productIds);
+ if ($categoryIds){
+ $this->cacheContext->registerEntities(Category::CACHE_TAG, array_unique($categoryIds));
+ $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
+ }
}
}
@@ -159,6 +165,22 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array
return $productIds;
}
+ /**
+ * Get category ids for products
+ *
+ * @param array $productIds
+ * @return array
+ */
+ private function getCategoryIdsByProductIds(array $productIds): array
+ {
+ $categoryProductTable = $this->resource->getTableName('catalog_category_product');
+ $select = $this->getConnection()->select()
+ ->from(['catalog_category_product' => $categoryProductTable], ['category_id'])
+ ->where('product_id IN (?)', $productIds);
+
+ return $this->getConnection()->fetchCol($select);
+ }
+
/**
* Get database connection.
*
diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php
index 12a48caf6241..3304e39f2cb2 100644
--- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php
+++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php
@@ -27,8 +27,8 @@
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class QuantityValidator
{
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php
index dec18044b699..05b645652093 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php
@@ -20,8 +20,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class DefaultStock extends AbstractIndexer implements StockInterface
{
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php
index 665ebf2db2f3..4a78babd0320 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php
@@ -13,8 +13,8 @@
* @since 100.1.0
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface QueryProcessorInterface
{
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php
index 9a1945d5aefa..e111a5267da7 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockInterface
{
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php
index 49e4889c8ede..f109643bc09c 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php
@@ -14,8 +14,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class StockFactory
{
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php
index afb7d51335df..adf62b75b2ad 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php
@@ -24,8 +24,8 @@
* @api
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
* @since 100.0.2
*/
class Status extends AbstractDb
diff --git a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php
index d28da4e5b349..59d359433c26 100644
--- a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php
+++ b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php
@@ -11,8 +11,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class Backorders implements \Magento\Framework\Option\ArrayInterface
{
diff --git a/app/code/Magento/CatalogInventory/Model/Source/Stock.php b/app/code/Magento/CatalogInventory/Model/Source/Stock.php
index 69e80658ecd7..c0ffb619e36c 100644
--- a/app/code/Magento/CatalogInventory/Model/Source/Stock.php
+++ b/app/code/Magento/CatalogInventory/Model/Source/Stock.php
@@ -13,8 +13,8 @@
* @since 100.0.2
*
* @deprecated 100.3.0 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
class Stock extends AbstractSource
{
diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php
index b2dfe532ffbe..bbba3498ab03 100644
--- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php
+++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php
@@ -9,8 +9,8 @@
* Interface StockRegistryProviderInterface
*
* @deprecated 100.3.2 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockRegistryProviderInterface
{
diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php
index 5bb78e1489b3..2cc69513f31b 100644
--- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php
+++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php
@@ -11,8 +11,8 @@
* Interface StockStateProviderInterface
*
* @deprecated 100.3.2 Replaced with Multi Source Inventory
- * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html
- * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html
+ * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html
*/
interface StockStateProviderInterface
{
diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/Stock/Status.php
index 4941d5d333bd..7066d06c7f1a 100644
--- a/app/code/Magento/CatalogInventory/Model/Stock/Status.php
+++ b/app/code/Magento/CatalogInventory/Model/Stock/Status.php
@@ -27,6 +27,11 @@ class Status extends AbstractExtensibleModel implements StockStatusInterface
const KEY_STOCK_STATUS = 'stock_status';
/**#@-*/
+ /**
+ * @var StockRegistryInterface
+ */
+ private $stockRegistry;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
diff --git a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php
index 515080d56541..936cafb60f33 100644
--- a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php
+++ b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php
@@ -25,6 +25,7 @@
use Magento\Framework\Stdlib\DateTime\DateTime;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
+use Psr\Log\LoggerInterface as PsrLogger;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -98,8 +99,11 @@ class StockItemRepository implements StockItemRepositoryInterface
protected $productCollectionFactory;
/**
- * Constructor
- *
+ * @var PsrLogger
+ */
+ private $psrLogger;
+
+ /**
* @param StockConfigurationInterface $stockConfiguration
* @param StockStateProviderInterface $stockStateProvider
* @param StockItemResource $resource
@@ -111,7 +115,8 @@ class StockItemRepository implements StockItemRepositoryInterface
* @param TimezoneInterface $localeDate
* @param Processor $indexProcessor
* @param DateTime $dateTime
- * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory|null $collectionFactory
+ * @param CollectionFactory|null $productCollectionFactory
+ * @param PsrLogger|null $psrLogger
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -126,7 +131,8 @@ public function __construct(
TimezoneInterface $localeDate,
Processor $indexProcessor,
DateTime $dateTime,
- \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory = null
+ \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory = null,
+ PsrLogger $psrLogger = null
) {
$this->stockConfiguration = $stockConfiguration;
$this->stockStateProvider = $stockStateProvider;
@@ -141,6 +147,8 @@ public function __construct(
$this->dateTime = $dateTime;
$this->productCollectionFactory = $productCollectionFactory ?: ObjectManager::getInstance()
->get(CollectionFactory::class);
+ $this->psrLogger = $psrLogger ?: ObjectManager::getInstance()
+ ->get(PsrLogger::class);
}
/**
@@ -184,6 +192,7 @@ public function save(\Magento\CatalogInventory\Api\Data\StockItemInterface $stoc
$this->resource->save($stockItem);
} catch (\Exception $exception) {
+ $this->psrLogger->error($exception->getMessage());
throw new CouldNotSaveException(__('The stock item was unable to be saved. Please try again.'), $exception);
}
return $stockItem;
diff --git a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php
index b57518b681aa..bfa854edeaaf 100644
--- a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php
+++ b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php
@@ -105,47 +105,46 @@ public function verifyNotification(StockItemInterface $stockItem)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQty, $origQty = 0)
{
$result = $this->objectFactory->create();
$result->setHasError(false);
-
$qty = $this->getNumber($qty);
-
- /**
- * Check quantity type
- */
- $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal());
- if (!$stockItem->getIsQtyDecimal()) {
- $result->setHasQtyOptionUpdate(true);
- $qty = (int) $qty ?: 1;
- /**
- * Adding stock data to quote item
- */
- $result->setItemQty($qty);
- $result->setOrigQty((int)$this->getNumber($origQty) ?: 1);
- }
+ $quoteMessage = __('Please correct the quantity for some products.');
if ($stockItem->getMinSaleQty() && $qty < $stockItem->getMinSaleQty()) {
$result->setHasError(true)
->setMessage(__('The fewest you may purchase is %1.', $stockItem->getMinSaleQty() * 1))
->setErrorCode('qty_min')
- ->setQuoteMessage(__('Please correct the quantity for some products.'))
+ ->setQuoteMessage($quoteMessage)
->setQuoteMessageIndex('qty');
return $result;
}
if ($stockItem->getMaxSaleQty() && $qty > $stockItem->getMaxSaleQty()) {
$result->setHasError(true)
- ->setMessage(__('The most you may purchase is %1.', $stockItem->getMaxSaleQty() * 1))
+ ->setMessage(__('The requested qty exceeds the maximum qty allowed in shopping cart'))
->setErrorCode('qty_max')
- ->setQuoteMessage(__('Please correct the quantity for some products.'))
+ ->setQuoteMessage($quoteMessage)
->setQuoteMessageIndex('qty');
return $result;
}
$result->addData($this->checkQtyIncrements($stockItem, $qty)->getData());
+
+ $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal());
+ if (!$stockItem->getIsQtyDecimal() && (floor($qty) !== $qty)) {
+ $result->setHasError(true)
+ ->setMessage(__('You cannot use decimal quantity for this product.'))
+ ->setErrorCode('qty_decimal')
+ ->setQuoteMessage($quoteMessage)
+ ->setQuoteMessageIndex('qty');
+
+ return $result;
+ }
+
if ($result->getHasError()) {
return $result;
}
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml
index 2c38f14f5337..b93c2af43e64 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryChangeManageStockActionGroup.xml
@@ -16,6 +16,8 @@
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml
index a5e4d3e9c2af..e8871365dabe 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductSetMaxQtyAllowedInShoppingCartActionGroup.xml
@@ -13,11 +13,11 @@
+
+
-
-
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml
index e7387ddd5d67..eb076d23919b 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml
@@ -109,8 +109,8 @@
-
-
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php
index f65ccaf806c1..794f5d92da1e 100644
--- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogInventory\Test\Unit\Model\Indexer\Stock;
+use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Product;
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\CatalogInventory\Model\Indexer\Stock\CacheCleaner;
@@ -20,6 +21,9 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+/**
+ * Test for CacheCleaner
+ */
class CacheCleanerTest extends TestCase
{
/**
@@ -70,14 +74,16 @@ protected function setUp(): void
$this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
->getMock();
$this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class)
- ->setMethods(['getStockThresholdQty'])->getMockForAbstractClass();
+ ->setMethods(['getStockThresholdQty'])
+ ->getMockForAbstractClass();
$this->cacheContextMock = $this->getMockBuilder(CacheContext::class)
->disableOriginalConstructor()
->getMock();
$this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class)
->getMock();
$this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class)
- ->setMethods(['getMetadata', 'getLinkField'])->disableOriginalConstructor()
+ ->setMethods(['getMetadata', 'getLinkField'])
+ ->disableOriginalConstructor()
->getMock();
$this->selectMock = $this->getMockBuilder(Select::class)
->disableOriginalConstructor()
@@ -100,37 +106,63 @@ protected function setUp(): void
}
/**
+ * Test clean cache by product ids and category ids
+ *
* @param bool $stockStatusBefore
* @param bool $stockStatusAfter
* @param int $qtyAfter
* @param bool|int $stockThresholdQty
* @dataProvider cleanDataProvider
+ * @return void
*/
- public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $stockThresholdQty)
+ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $stockThresholdQty): void
{
$productId = 123;
- $this->selectMock->expects($this->any())->method('from')->willReturnSelf();
- $this->selectMock->expects($this->any())->method('where')->willReturnSelf();
- $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf();
- $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock);
- $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls(
- [
- ['product_id' => $productId, 'stock_status' => $stockStatusBefore],
- ],
- [
- ['product_id' => $productId, 'stock_status' => $stockStatusAfter, 'qty' => $qtyAfter],
- ]
- );
- $this->stockConfigurationMock->expects($this->once())->method('getStockThresholdQty')
+ $categoryId = 3;
+ $this->selectMock->expects($this->any())
+ ->method('from')
+ ->willReturnSelf();
+ $this->selectMock->expects($this->any())
+ ->method('where')
+ ->willReturnSelf();
+ $this->selectMock->expects($this->any())
+ ->method('joinLeft')
+ ->willReturnSelf();
+ $this->connectionMock->expects($this->exactly(3))
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $this->connectionMock->expects($this->exactly(2))
+ ->method('fetchAll')
+ ->willReturnOnConsecutiveCalls(
+ [
+ ['product_id' => $productId, 'stock_status' => $stockStatusBefore],
+ ],
+ [
+ ['product_id' => $productId, 'stock_status' => $stockStatusAfter, 'qty' => $qtyAfter],
+ ]
+ );
+ $this->connectionMock->expects($this->exactly(1))
+ ->method('fetchCol')
+ ->willReturn([$categoryId]);
+ $this->stockConfigurationMock->expects($this->once())
+ ->method('getStockThresholdQty')
->willReturn($stockThresholdQty);
- $this->cacheContextMock->expects($this->once())->method('registerEntities')
- ->with(Product::CACHE_TAG, [$productId]);
- $this->eventManagerMock->expects($this->once())->method('dispatch')
+ $this->cacheContextMock->expects($this->exactly(2))
+ ->method('registerEntities')
+ ->withConsecutive(
+ [Product::CACHE_TAG, [$productId]],
+ [Category::CACHE_TAG, [$categoryId]],
+ );
+ $this->eventManagerMock->expects($this->exactly(2))
+ ->method('dispatch')
->with('clean_cache_by_tags', ['object' => $this->cacheContextMock]);
- $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata')
+ $this->metadataPoolMock->expects($this->exactly(2))
+ ->method('getMetadata')
->willReturnSelf();
- $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField')
+ $this->metadataPoolMock->expects($this->exactly(2))
+ ->method('getLinkField')
->willReturn('row_id');
+
$callback = function () {
};
$this->unit->clean([], $callback);
@@ -139,7 +171,7 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto
/**
* @return array
*/
- public function cleanDataProvider()
+ public function cleanDataProvider(): array
{
return [
[true, false, 1, false],
@@ -155,29 +187,42 @@ public function cleanDataProvider()
* @param int $qtyAfter
* @param bool|int $stockThresholdQty
* @dataProvider notCleanCacheDataProvider
+ * @return void
*/
- public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAfter, $stockThresholdQty)
+ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAfter, $stockThresholdQty): void
{
$productId = 123;
- $this->selectMock->expects($this->any())->method('from')->willReturnSelf();
- $this->selectMock->expects($this->any())->method('where')->willReturnSelf();
- $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf();
- $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock);
- $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls(
- [
- ['product_id' => $productId, 'stock_status' => $stockStatusBefore],
- ],
- [
- ['product_id' => $productId, 'stock_status' => $stockStatusAfter, 'qty' => $qtyAfter],
- ]
- );
- $this->stockConfigurationMock->expects($this->once())->method('getStockThresholdQty')
+ $this->selectMock->expects($this->any())->method('from')
+ ->willReturnSelf();
+ $this->selectMock->expects($this->any())->method('where')
+ ->willReturnSelf();
+ $this->selectMock->expects($this->any())->method('joinLeft')
+ ->willReturnSelf();
+ $this->connectionMock->expects($this->exactly(2))
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $this->connectionMock->expects($this->exactly(2))
+ ->method('fetchAll')
+ ->willReturnOnConsecutiveCalls(
+ [
+ ['product_id' => $productId, 'stock_status' => $stockStatusBefore],
+ ],
+ [
+ ['product_id' => $productId, 'stock_status' => $stockStatusAfter, 'qty' => $qtyAfter],
+ ]
+ );
+ $this->stockConfigurationMock->expects($this->once())
+ ->method('getStockThresholdQty')
->willReturn($stockThresholdQty);
- $this->cacheContextMock->expects($this->never())->method('registerEntities');
- $this->eventManagerMock->expects($this->never())->method('dispatch');
- $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata')
+ $this->cacheContextMock->expects($this->never())
+ ->method('registerEntities');
+ $this->eventManagerMock->expects($this->never())
+ ->method('dispatch');
+ $this->metadataPoolMock->expects($this->exactly(2))
+ ->method('getMetadata')
->willReturnSelf();
- $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField')
+ $this->metadataPoolMock->expects($this->exactly(2))
+ ->method('getLinkField')
->willReturn('row_id');
$callback = function () {
@@ -188,7 +233,7 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft
/**
* @return array
*/
- public function notCleanCacheDataProvider()
+ public function notCleanCacheDataProvider(): array
{
return [
[true, true, 1, false],
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php
index 9d2fb66dc716..0d900bde6015 100644
--- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Spi/StockStateProviderTest.php
@@ -21,6 +21,8 @@
use PHPUnit\Framework\TestCase;
/**
+ * Unit tests for \Magento\CatalogInventory\Model\StockStateProvider class.
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class StockStateProviderTest extends TestCase
@@ -115,6 +117,9 @@ class StockStateProviderTest extends TestCase
'getProductName',
];
+ /**
+ * @inheritDoc
+ */
protected function setUp(): void
{
$this->objectManagerHelper = new ObjectManagerHelper($this);
@@ -383,7 +388,7 @@ protected function getVariations()
'suggestQty' => 51,
'getStockQty' => $stockQty,
'checkQtyIncrements' => false,
- 'checkQuoteItemQty' => false,
+ 'checkQuoteItemQty' => true,
],
],
[
@@ -411,7 +416,7 @@ protected function getVariations()
'getStockQty' => $stockQty,
'checkQtyIncrements' => false,
'checkQuoteItemQty' => true,
- ]
+ ],
],
[
'values' => [
@@ -438,8 +443,8 @@ protected function getVariations()
'getStockQty' => null,
'checkQtyIncrements' => false,
'checkQuoteItemQty' => true,
- ]
- ]
+ ],
+ ],
];
}
diff --git a/app/code/Magento/CatalogInventory/i18n/en_US.csv b/app/code/Magento/CatalogInventory/i18n/en_US.csv
index af989dc06d47..4bac24ed998b 100644
--- a/app/code/Magento/CatalogInventory/i18n/en_US.csv
+++ b/app/code/Magento/CatalogInventory/i18n/en_US.csv
@@ -71,3 +71,5 @@ Stock,Stock
"Qty Uses Decimals","Qty Uses Decimals"
"Allow Multiple Boxes for Shipping","Allow Multiple Boxes for Shipping"
"Done","Done"
+"The requested qty exceeds the maximum qty allowed in shopping cart","The requested qty exceeds the maximum qty allowed in shopping cart"
+"You cannot use decimal quantity for this product.","You cannot use decimal quantity for this product."
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
index df167d171e00..38b48e05c55c 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
@@ -7,13 +7,23 @@
namespace Magento\CatalogRule\Model\Indexer;
use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\ProductFactory;
+use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
+use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader;
+use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper;
use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection;
use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory as RuleCollectionFactory;
use Magento\CatalogRule\Model\Rule;
+use Magento\Eav\Model\Config;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Pricing\PriceCurrencyInterface;
-use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader;
-use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper;
+use Magento\Framework\Stdlib\DateTime;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Store\Model\ScopeInterface;
+use Magento\Store\Model\StoreManagerInterface;
+use Psr\Log\LoggerInterface;
/**
* Catalog rule index builder
@@ -46,12 +56,12 @@ class IndexBuilder
protected $_catalogRuleGroupWebsiteColumnsList = ['rule_id', 'customer_group_id', 'website_id'];
/**
- * @var \Magento\Framework\App\ResourceConnection
+ * @var ResourceConnection
*/
protected $resource;
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $storeManager;
@@ -61,7 +71,7 @@ class IndexBuilder
protected $ruleCollectionFactory;
/**
- * @var \Psr\Log\LoggerInterface
+ * @var LoggerInterface
*/
protected $logger;
@@ -71,22 +81,22 @@ class IndexBuilder
protected $priceCurrency;
/**
- * @var \Magento\Eav\Model\Config
+ * @var Config
*/
protected $eavConfig;
/**
- * @var \Magento\Framework\Stdlib\DateTime
+ * @var DateTime
*/
protected $dateFormat;
/**
- * @var \Magento\Framework\Stdlib\DateTime\DateTime
+ * @var DateTime\DateTime
*/
protected $dateTime;
/**
- * @var \Magento\Catalog\Model\ProductFactory
+ * @var ProductFactory
*/
protected $productFactory;
@@ -136,7 +146,12 @@ class IndexBuilder
private $pricesPersistor;
/**
- * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher
+ * @var TimezoneInterface|mixed
+ */
+ private $localeDate;
+
+ /**
+ * @var ActiveTableSwitcher|mixed
*/
private $activeTableSwitcher;
@@ -146,20 +161,20 @@ class IndexBuilder
private $tableSwapper;
/**
- * @var ProductLoader
+ * @var ProductLoader|mixed
*/
private $productLoader;
/**
* @param RuleCollectionFactory $ruleCollectionFactory
* @param PriceCurrencyInterface $priceCurrency
- * @param \Magento\Framework\App\ResourceConnection $resource
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Psr\Log\LoggerInterface $logger
- * @param \Magento\Eav\Model\Config $eavConfig
- * @param \Magento\Framework\Stdlib\DateTime $dateFormat
- * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime
- * @param \Magento\Catalog\Model\ProductFactory $productFactory
+ * @param ResourceConnection $resource
+ * @param StoreManagerInterface $storeManager
+ * @param LoggerInterface $logger
+ * @param Config $eavConfig
+ * @param DateTime $dateFormat
+ * @param DateTime\DateTime $dateTime
+ * @param ProductFactory $productFactory
* @param int $batchCount
* @param ProductPriceCalculator|null $productPriceCalculator
* @param ReindexRuleProduct|null $reindexRuleProduct
@@ -167,21 +182,23 @@ class IndexBuilder
* @param RuleProductsSelectBuilder|null $ruleProductsSelectBuilder
* @param ReindexRuleProductPrice|null $reindexRuleProductPrice
* @param RuleProductPricesPersistor|null $pricesPersistor
- * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher
+ * @param ActiveTableSwitcher|null $activeTableSwitcher
* @param ProductLoader|null $productLoader
* @param TableSwapper|null $tableSwapper
+ * @param TimezoneInterface|null $localeDate
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
RuleCollectionFactory $ruleCollectionFactory,
PriceCurrencyInterface $priceCurrency,
- \Magento\Framework\App\ResourceConnection $resource,
- \Magento\Store\Model\StoreManagerInterface $storeManager,
- \Psr\Log\LoggerInterface $logger,
- \Magento\Eav\Model\Config $eavConfig,
- \Magento\Framework\Stdlib\DateTime $dateFormat,
- \Magento\Framework\Stdlib\DateTime\DateTime $dateTime,
- \Magento\Catalog\Model\ProductFactory $productFactory,
+ ResourceConnection $resource,
+ StoreManagerInterface $storeManager,
+ LoggerInterface $logger,
+ Config $eavConfig,
+ DateTime $dateFormat,
+ DateTime\DateTime $dateTime,
+ ProductFactory $productFactory,
$batchCount = 1000,
ProductPriceCalculator $productPriceCalculator = null,
ReindexRuleProduct $reindexRuleProduct = null,
@@ -189,9 +206,10 @@ public function __construct(
RuleProductsSelectBuilder $ruleProductsSelectBuilder = null,
ReindexRuleProductPrice $reindexRuleProductPrice = null,
RuleProductPricesPersistor $pricesPersistor = null,
- \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null,
+ ActiveTableSwitcher $activeTableSwitcher = null,
ProductLoader $productLoader = null,
- TableSwapper $tableSwapper = null
+ TableSwapper $tableSwapper = null,
+ TimezoneInterface $localeDate = null
) {
$this->resource = $resource;
$this->connection = $resource->getConnection();
@@ -224,19 +242,22 @@ public function __construct(
RuleProductPricesPersistor::class
);
$this->activeTableSwitcher = $activeTableSwitcher ?? ObjectManager::getInstance()->get(
- \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class
+ ActiveTableSwitcher::class
);
$this->productLoader = $productLoader ?? ObjectManager::getInstance()->get(
ProductLoader::class
);
$this->tableSwapper = $tableSwapper ??
ObjectManager::getInstance()->get(TableSwapper::class);
+ $this->localeDate = $localeDate ??
+ ObjectManager::getInstance()->get(TimezoneInterface::class);
}
/**
* Reindex by id
*
* @param int $id
+ * @throws LocalizedException
* @return void
* @api
*/
@@ -254,7 +275,7 @@ public function reindexById($id)
$this->reindexRuleGroupWebsite->execute();
} catch (\Exception $e) {
$this->critical($e);
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__('Catalog rule indexing failed. See details in exception log.')
);
}
@@ -264,7 +285,7 @@ public function reindexById($id)
* Reindex by ids
*
* @param array $ids
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
* @return void
* @api
*/
@@ -274,7 +295,7 @@ public function reindexByIds(array $ids)
$this->doReindexByIds($ids);
} catch (\Exception $e) {
$this->critical($e);
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__("Catalog rule indexing failed. See details in exception log.")
);
}
@@ -308,7 +329,7 @@ protected function doReindexByIds($ids)
/**
* Full reindex
*
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
* @return void
* @api
*/
@@ -318,7 +339,7 @@ public function reindexFull()
$this->doReindexFull();
} catch (\Exception $e) {
$this->critical($e);
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__("Catalog rule indexing failed. See details in exception log.")
);
}
@@ -404,9 +425,6 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we
);
$customerGroupIds = $rule->getCustomerGroupIds();
- $fromTime = strtotime($rule->getFromDate());
- $toTime = strtotime($rule->getToDate());
- $toTime = $toTime ? $toTime + self::SECONDS_IN_DAY - 1 : 0;
$sortOrder = (int)$rule->getSortOrder();
$actionOperator = $rule->getSimpleAction();
$actionAmount = $rule->getDiscountAmount();
@@ -414,6 +432,15 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we
$rows = [];
foreach ($websiteIds as $websiteId) {
+ $scopeTz = new \DateTimeZone(
+ $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId)
+ );
+ $fromTime = $rule->getFromDate()
+ ? (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp()
+ : 0;
+ $toTime = $rule->getToDate()
+ ? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1
+ : 0;
foreach ($customerGroupIds as $customerGroupId) {
$rows[] = [
'rule_id' => $ruleId,
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php
index f99f8c50a7f9..0ddae74ff0a5 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php
@@ -122,4 +122,14 @@ public function swapIndexTables(array $originalTablesNames)
$this->resourceConnection->getConnection()->dropTable($tableName);
}
}
+
+ /**
+ * Cleanup leftover temporary tables
+ */
+ public function __destruct()
+ {
+ foreach ($this->temporaryTables as $tableName) {
+ $this->resourceConnection->getConnection()->dropTable($tableName);
+ }
+ }
}
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php
index 944710773123..ff9893ae1b90 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php
@@ -6,8 +6,8 @@
namespace Magento\CatalogRule\Model\Indexer;
-use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper;
use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
+use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper;
use Magento\CatalogRule\Model\Rule;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
@@ -18,6 +18,8 @@
*/
class ReindexRuleProduct
{
+ private const ADMIN_WEBSITE_ID = 0;
+
/**
* @var ResourceConnection
*/
@@ -38,22 +40,30 @@ class ReindexRuleProduct
*/
private $localeDate;
+ /**
+ * @var bool
+ */
+ private $useWebsiteTimezone;
+
/**
* @param ResourceConnection $resource
* @param ActiveTableSwitcher $activeTableSwitcher
* @param TableSwapper $tableSwapper
* @param TimezoneInterface $localeDate
+ * @param bool $useWebsiteTimezone
*/
public function __construct(
ResourceConnection $resource,
ActiveTableSwitcher $activeTableSwitcher,
TableSwapper $tableSwapper,
- TimezoneInterface $localeDate
+ TimezoneInterface $localeDate,
+ bool $useWebsiteTimezone = true
) {
$this->resource = $resource;
$this->activeTableSwitcher = $activeTableSwitcher;
$this->tableSwapper = $tableSwapper;
$this->localeDate = $localeDate;
+ $this->useWebsiteTimezone = $useWebsiteTimezone;
}
/**
@@ -95,18 +105,23 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false)
$actionOperator = $rule->getSimpleAction();
$actionAmount = $rule->getDiscountAmount();
$actionStop = $rule->getStopRulesProcessing();
+ $fromTimeInAdminTz = $this->parseDateByWebsiteTz((string)$rule->getFromDate(), self::ADMIN_WEBSITE_ID);
+ $toTimeInAdminTz = $this->parseDateByWebsiteTz((string)$rule->getToDate(), self::ADMIN_WEBSITE_ID);
+ $excludedWebsites = [];
+ $ruleExtensionAttributes = $rule->getExtensionAttributes();
+ if ($ruleExtensionAttributes && $ruleExtensionAttributes->getExcludeWebsiteIds()) {
+ $excludedWebsites = $ruleExtensionAttributes->getExcludeWebsiteIds();
+ }
$rows = [];
foreach ($websiteIds as $websiteId) {
- $scopeTz = new \DateTimeZone(
- $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId)
- );
- $fromTime = $rule->getFromDate()
- ? (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp()
- : 0;
- $toTime = $rule->getToDate()
- ? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1
- : 0;
+ $fromTime = $this->useWebsiteTimezone
+ ? $this->parseDateByWebsiteTz((string)$rule->getFromDate(), (int)$websiteId)
+ : $fromTimeInAdminTz;
+ $toTime = $this->useWebsiteTimezone
+ ? $this->parseDateByWebsiteTz((string)$rule->getToDate(), (int)$websiteId)
+ + ($rule->getToDate() ? IndexBuilder::SECONDS_IN_DAY - 1 : 0)
+ : $toTimeInAdminTz;
foreach ($productIds as $productId => $validationByWebsite) {
if (empty($validationByWebsite[$websiteId])) {
@@ -114,22 +129,26 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false)
}
foreach ($customerGroupIds as $customerGroupId) {
- $rows[] = [
- 'rule_id' => $ruleId,
- 'from_time' => $fromTime,
- 'to_time' => $toTime,
- 'website_id' => $websiteId,
- 'customer_group_id' => $customerGroupId,
- 'product_id' => $productId,
- 'action_operator' => $actionOperator,
- 'action_amount' => $actionAmount,
- 'action_stop' => $actionStop,
- 'sort_order' => $sortOrder,
- ];
-
- if (count($rows) == $batchCount) {
- $connection->insertMultiple($indexTable, $rows);
- $rows = [];
+ if (!array_key_exists($customerGroupId, $excludedWebsites)
+ || !in_array((int)$websiteId, array_values($excludedWebsites[$customerGroupId]), true)
+ ) {
+ $rows[] = [
+ 'rule_id' => $ruleId,
+ 'from_time' => $fromTime,
+ 'to_time' => $toTime,
+ 'website_id' => $websiteId,
+ 'customer_group_id' => $customerGroupId,
+ 'product_id' => $productId,
+ 'action_operator' => $actionOperator,
+ 'action_amount' => $actionAmount,
+ 'action_stop' => $actionStop,
+ 'sort_order' => $sortOrder,
+ ];
+
+ if (count($rows) === $batchCount) {
+ $connection->insertMultiple($indexTable, $rows);
+ $rows = [];
+ }
}
}
}
@@ -140,4 +159,23 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false)
return true;
}
+
+ /**
+ * Parse date value by the timezone of the website
+ *
+ * @param string $date
+ * @param int $websiteId
+ * @return int
+ */
+ private function parseDateByWebsiteTz(string $date, int $websiteId): int
+ {
+ if (empty($date)) {
+ return 0;
+ }
+
+ $websiteTz = $this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId);
+ $dateTime = new \DateTime($date, new \DateTimeZone($websiteTz));
+
+ return $dateTime->getTimestamp();
+ }
}
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php
index 51869f1accbb..ccc5352567ff 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogRule\Model\Indexer;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -39,25 +40,33 @@ class ReindexRuleProductPrice
*/
private $pricesPersistor;
+ /**
+ * @var bool
+ */
+ private $useWebsiteTimezone;
+
/**
* @param StoreManagerInterface $storeManager
* @param RuleProductsSelectBuilder $ruleProductsSelectBuilder
* @param ProductPriceCalculator $productPriceCalculator
* @param TimezoneInterface $localeDate
* @param RuleProductPricesPersistor $pricesPersistor
+ * @param bool $useWebsiteTimezone
*/
public function __construct(
StoreManagerInterface $storeManager,
RuleProductsSelectBuilder $ruleProductsSelectBuilder,
ProductPriceCalculator $productPriceCalculator,
TimezoneInterface $localeDate,
- RuleProductPricesPersistor $pricesPersistor
+ RuleProductPricesPersistor $pricesPersistor,
+ bool $useWebsiteTimezone = true
) {
$this->storeManager = $storeManager;
$this->ruleProductsSelectBuilder = $ruleProductsSelectBuilder;
$this->productPriceCalculator = $productPriceCalculator;
$this->localeDate = $localeDate;
$this->pricesPersistor = $pricesPersistor;
+ $this->useWebsiteTimezone = $useWebsiteTimezone;
}
/**
@@ -82,11 +91,9 @@ public function execute(int $batchCount, ?int $productId = null, bool $useAdditi
$prevKey = null;
$storeGroup = $this->storeManager->getGroup($website->getDefaultGroupId());
- $currentDate = $this->localeDate->scopeDate($storeGroup->getDefaultStoreId(), null, true);
- $previousDate = (clone $currentDate)->modify('-1 day');
- $previousDate->setTime(23, 59, 59);
- $nextDate = (clone $currentDate)->modify('+1 day');
- $nextDate->setTime(0, 0, 0);
+ $dateInterval = $this->useWebsiteTimezone
+ ? $this->getDateInterval((int)$storeGroup->getDefaultStoreId())
+ : $this->getDateInterval(Store::DEFAULT_STORE_ID);
while ($ruleData = $productsStmt->fetch()) {
$ruleProductId = $ruleData['product_id'];
@@ -107,7 +114,7 @@ public function execute(int $batchCount, ?int $productId = null, bool $useAdditi
/**
* Build prices for each day
*/
- foreach ([$previousDate, $currentDate, $nextDate] as $date) {
+ foreach ($dateInterval as $date) {
$time = $date->getTimestamp();
if (($ruleData['from_time'] == 0 ||
$time >= $ruleData['from_time']) && ($ruleData['to_time'] == 0 ||
@@ -157,4 +164,21 @@ public function execute(int $batchCount, ?int $productId = null, bool $useAdditi
return true;
}
+
+ /**
+ * Retrieve date sequence in store time zone
+ *
+ * @param int $storeId
+ * @return \DateTime[]
+ */
+ private function getDateInterval(int $storeId): array
+ {
+ $currentDate = $this->localeDate->scopeDate($storeId, null, true);
+ $previousDate = (clone $currentDate)->modify('-1 day');
+ $previousDate->setTime(23, 59, 59);
+ $nextDate = (clone $currentDate)->modify('+1 day');
+ $nextDate->setTime(0, 0, 0);
+
+ return [$previousDate, $currentDate, $nextDate];
+ }
}
diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Rule/Collection.php
index fdc039067cc3..f7a01de2d277 100644
--- a/app/code/Magento/CatalogRule/Model/ResourceModel/Rule/Collection.php
+++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Rule/Collection.php
@@ -5,8 +5,8 @@
*/
namespace Magento\CatalogRule\Model\ResourceModel\Rule;
-use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Serialize\Serializer\Json;
class Collection extends \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection
{
@@ -22,6 +22,16 @@ class Collection extends \Magento\Rule\Model\ResourceModel\Rule\Collection\Abstr
*/
protected $serializer;
+ /**
+ * @var string
+ */
+ protected $_eventPrefix = 'catalog_rule_collection';
+
+ /**
+ * @var string
+ */
+ protected $_eventObject = 'catalog_rule';
+
/**
* Collection constructor.
* @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory
diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php
index 2d9219236896..da32801ace47 100644
--- a/app/code/Magento/CatalogRule/Model/Rule.php
+++ b/app/code/Magento/CatalogRule/Model/Rule.php
@@ -589,7 +589,7 @@ protected function _invalidateCache()
*/
public function afterSave()
{
- if (!$this->getIsActive()) {
+ if (!$this->getIsActive() && !$this->getOrigData(self::IS_ACTIVE)) {
return parent::afterSave();
}
@@ -601,6 +601,7 @@ public function afterSave()
} else {
$this->_ruleProductProcessor->getIndexer()->invalidate();
}
+
return parent::afterSave();
}
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml
index 27edab962033..d16255e0237b 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml
@@ -16,7 +16,7 @@
-
+
{{AdminDataGridTableSection.firstNotEmptyRow}}
{{AdminConfirmationModalSection.ok}}
{{AdminMainActionsSection.delete}}
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndContinueEditCatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndContinueEditCatalogPriceRuleActionGroup.xml
new file mode 100644
index 000000000000..cf8499ca1b46
--- /dev/null
+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/SaveAndContinueEditCatalogPriceRuleActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Clicks on Save and Continue Edit. Validates that the Success Message is present and correct on the Admin Catalog Price Rule creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml
index 3109c56122d1..c49b31ad4734 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml
@@ -78,16 +78,14 @@
-
-
-
+
-
+
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml
index 97e93c8f762c..26e1966ece36 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml
@@ -127,9 +127,7 @@
-
-
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml
index 946a25d721cb..843bc4e722e6 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml
@@ -25,9 +25,7 @@
-
-
-
+
@@ -49,13 +47,13 @@
-
+
-
+
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 e55cabd50646..a6a735bb81de 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
@@ -49,9 +49,7 @@
-
-
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml
index 831470e0d64c..c6a3291561fa 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml
@@ -72,9 +72,7 @@
-
-
-
+
@@ -115,12 +113,10 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml
index 5a62b1a373f9..c6452612f82a 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml
@@ -60,18 +60,16 @@
-
-
-
+
-
+
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml
index d03ca6b22d66..333c4ab06aad 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml
@@ -38,6 +38,15 @@
+
+
+
+
+
+
+
+
+
@@ -52,7 +61,7 @@
-
+
@@ -86,12 +95,10 @@
-
-
-
+
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml
index da62981c9920..1951aa6c0f6a 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml
@@ -80,9 +80,7 @@
-
-
-
+
@@ -128,9 +126,7 @@
-
-
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml
index 6de7bba59c34..0e1059f2f5f1 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml
@@ -234,19 +234,17 @@
-
-
-
+
-
+
-
+
@@ -259,7 +257,7 @@
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml
index c3690e72e084..b1d6935bec28 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml
@@ -113,12 +113,10 @@
-
-
-
+
-
+
@@ -156,7 +154,7 @@
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml
index ece8dc4bacf2..6d8de2cb1d9b 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml
@@ -64,15 +64,11 @@
-
-
-
-
-
-
+
+
-
+
@@ -93,7 +89,7 @@
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml
index 3b7c9c181f1b..ba446380a4f6 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml
@@ -40,7 +40,7 @@
-
+
@@ -74,7 +74,7 @@
-
+
@@ -95,7 +95,7 @@
-
+
@@ -113,7 +113,7 @@
-
+
@@ -134,7 +134,7 @@
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml
index 45e97f179a11..77b10a09d6ce 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml
@@ -71,15 +71,11 @@
-
-
-
-
-
-
+
+
-
+
@@ -100,7 +96,7 @@
-
+
@@ -115,7 +111,7 @@
-
+
@@ -130,7 +126,7 @@
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php
index 7f22634f9343..230a6e386095 100644
--- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php
+++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php
@@ -64,7 +64,8 @@ protected function setUp(): void
$this->ruleProductsSelectBuilderMock,
$this->productPriceCalculatorMock,
$this->localeDate,
- $this->pricesPersistorMock
+ $this->pricesPersistorMock,
+ true
);
}
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php
index ddb6a85ed614..fff8a80c88e3 100644
--- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php
+++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php
@@ -5,7 +5,6 @@
*/
declare(strict_types=1);
-
namespace Magento\CatalogRule\Test\Unit\Model\Indexer;
use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
@@ -21,6 +20,8 @@
class ReindexRuleProductTest extends TestCase
{
+ private const ADMIN_WEBSITE_ID = 0;
+
/**
* @var ReindexRuleProduct
*/
@@ -31,11 +32,6 @@ class ReindexRuleProductTest extends TestCase
*/
private $resourceMock;
- /**
- * @var ActiveTableSwitcher|MockObject
- */
- private $activeTableSwitcherMock;
-
/**
* @var IndexerTableSwapperInterface|MockObject
*/
@@ -46,45 +42,59 @@ class ReindexRuleProductTest extends TestCase
*/
private $localeDateMock;
+ /**
+ * @var AdapterInterface|MockObject
+ */
+ private $connectionMock;
+
+ /**
+ * @var Rule|MockObject
+ */
+ private $ruleMock;
+
protected function setUp(): void
{
$this->resourceMock = $this->createMock(ResourceConnection::class);
- $this->activeTableSwitcherMock = $this->createMock(ActiveTableSwitcher::class);
+ $activeTableSwitcherMock = $this->createMock(ActiveTableSwitcher::class);
$this->tableSwapperMock = $this->getMockForAbstractClass(IndexerTableSwapperInterface::class);
$this->localeDateMock = $this->getMockForAbstractClass(TimezoneInterface::class);
+ $this->connectionMock = $this->getMockForAbstractClass(AdapterInterface::class);
+ $this->ruleMock = $this->createMock(Rule::class);
$this->model = new ReindexRuleProduct(
$this->resourceMock,
- $this->activeTableSwitcherMock,
+ $activeTableSwitcherMock,
$this->tableSwapperMock,
- $this->localeDateMock
+ $this->localeDateMock,
+ true
);
}
- public function testExecuteIfRuleInactive()
+ public function testExecuteIfRuleInactive(): void
{
$ruleMock = $this->createMock(Rule::class);
- $ruleMock->expects($this->once())
+ $ruleMock->expects(self::once())
->method('getIsActive')
->willReturn(false);
- $this->assertFalse($this->model->execute($ruleMock, 100, true));
+ self::assertFalse($this->model->execute($ruleMock, 100, true));
}
- public function testExecuteIfRuleWithoutWebsiteIds()
+ public function testExecuteIfRuleWithoutWebsiteIds(): void
{
$ruleMock = $this->createMock(Rule::class);
- $ruleMock->expects($this->once())
+ $ruleMock->expects(self::once())
->method('getIsActive')
->willReturn(true);
- $ruleMock->expects($this->once())
+ $ruleMock->expects(self::once())
->method('getWebsiteIds')
->willReturn(null);
- $this->assertFalse($this->model->execute($ruleMock, 100, true));
+ self::assertFalse($this->model->execute($ruleMock, 100, true));
}
- public function testExecute()
+ public function testExecute(): void
{
$websiteId = 3;
+ $adminTimeZone = 'America/Chicago';
$websiteTz = 'America/Los_Angeles';
$productIds = [
4 => [$websiteId => 1],
@@ -92,41 +102,14 @@ public function testExecute()
6 => [$websiteId => 1],
];
- $this->tableSwapperMock->expects($this->once())
- ->method('getWorkingTableName')
- ->with('catalogrule_product')
- ->willReturn('catalogrule_product_replica');
-
- $connectionMock = $this->getMockForAbstractClass(AdapterInterface::class);
- $this->resourceMock->expects($this->at(0))
- ->method('getConnection')
- ->willReturn($connectionMock);
- $this->resourceMock->expects($this->at(1))
- ->method('getTableName')
- ->with('catalogrule_product')
- ->willReturn('catalogrule_product');
- $this->resourceMock->expects($this->at(2))
- ->method('getTableName')
- ->with('catalogrule_product_replica')
- ->willReturn('catalogrule_product_replica');
+ $this->prepareResourceMock();
+ $this->prepareRuleMock([3], $productIds, [10]);
- $ruleMock = $this->createMock(Rule::class);
- $ruleMock->expects($this->once())->method('getIsActive')->willReturn(true);
- $ruleMock->expects($this->exactly(2))->method('getWebsiteIds')->willReturn([$websiteId]);
- $ruleMock->expects($this->once())->method('getMatchingProductIds')->willReturn($productIds);
- $ruleMock->expects($this->once())->method('getId')->willReturn(100);
- $ruleMock->expects($this->once())->method('getCustomerGroupIds')->willReturn([10]);
- $ruleMock->expects($this->atLeastOnce())->method('getFromDate')->willReturn('2017-06-21');
- $ruleMock->expects($this->atLeastOnce())->method('getToDate')->willReturn('2017-06-30');
- $ruleMock->expects($this->once())->method('getSortOrder')->willReturn(1);
- $ruleMock->expects($this->once())->method('getSimpleAction')->willReturn('simple_action');
- $ruleMock->expects($this->once())->method('getDiscountAmount')->willReturn(43);
- $ruleMock->expects($this->once())->method('getStopRulesProcessing')->willReturn(true);
-
- $this->localeDateMock->expects($this->once())
- ->method('getConfigTimezone')
- ->with(ScopeInterface::SCOPE_WEBSITE, $websiteId)
- ->willReturn($websiteTz);
+ $this->localeDateMock->method('getConfigTimezone')
+ ->willReturnMap([
+ [ScopeInterface::SCOPE_WEBSITE, self::ADMIN_WEBSITE_ID, $adminTimeZone],
+ [ScopeInterface::SCOPE_WEBSITE, $websiteId, $websiteTz],
+ ]);
$batchRows = [
[
@@ -170,13 +153,141 @@ public function testExecute()
]
];
- $connectionMock->expects($this->at(0))
+ $this->connectionMock->expects(self::at(0))
->method('insertMultiple')
->with('catalogrule_product_replica', $batchRows);
- $connectionMock->expects($this->at(1))
+ $this->connectionMock->expects(self::at(1))
->method('insertMultiple')
->with('catalogrule_product_replica', $rowsNotInBatch);
- $this->assertTrue($this->model->execute($ruleMock, 2, true));
+ self::assertTrue($this->model->execute($this->ruleMock, 2, true));
+ }
+
+ public function testExecuteWithExcludedWebsites(): void
+ {
+ $websitesIds = [1, 2, 3];
+ $adminTimeZone = 'America/Chicago';
+ $websiteTz = 'America/Los_Angeles';
+ $productIds = [
+ 1 => [1 => 1],
+ 2 => [2 => 1],
+ 3 => [3 => 1],
+ ];
+
+ $this->prepareResourceMock();
+ $this->prepareRuleMock($websitesIds, $productIds, [10, 20]);
+
+ $extensionAttributes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class)
+ ->setMethods(['getExtensionAttributes', 'getExcludeWebsiteIds'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->ruleMock->expects(self::once())->method('getExtensionAttributes')
+ ->willReturn($extensionAttributes);
+ $extensionAttributes->expects(self::exactly(2))->method('getExcludeWebsiteIds')
+ ->willReturn([10 => [1, 2]]);
+
+ $this->localeDateMock->method('getConfigTimezone')
+ ->willReturnMap([
+ [ScopeInterface::SCOPE_WEBSITE, self::ADMIN_WEBSITE_ID, $adminTimeZone],
+ [ScopeInterface::SCOPE_WEBSITE, 1, $websiteTz],
+ [ScopeInterface::SCOPE_WEBSITE, 2, $websiteTz],
+ [ScopeInterface::SCOPE_WEBSITE, 3, $websiteTz],
+ ]);
+
+ $batchRows = [
+ [
+ 'rule_id' => 100,
+ 'from_time' => 1498028400,
+ 'to_time' => 1498892399,
+ 'website_id' => 1,
+ 'customer_group_id' => 20,
+ 'product_id' => 1,
+ 'action_operator' => 'simple_action',
+ 'action_amount' => 43,
+ 'action_stop' => true,
+ 'sort_order' => 1,
+ ],
+ [
+ 'rule_id' => 100,
+ 'from_time' => 1498028400,
+ 'to_time' => 1498892399,
+ 'website_id' => 2,
+ 'customer_group_id' => 20,
+ 'product_id' => 2,
+ 'action_operator' => 'simple_action',
+ 'action_amount' => 43,
+ 'action_stop' => true,
+ 'sort_order' => 1,
+ ],
+ [
+ 'rule_id' => 100,
+ 'from_time' => 1498028400,
+ 'to_time' => 1498892399,
+ 'website_id' => 3,
+ 'customer_group_id' => 10,
+ 'product_id' => 3,
+ 'action_operator' => 'simple_action',
+ 'action_amount' => 43,
+ 'action_stop' => true,
+ 'sort_order' => 1,
+ ],
+ [
+ 'rule_id' => 100,
+ 'from_time' => 1498028400,
+ 'to_time' => 1498892399,
+ 'website_id' => 3,
+ 'customer_group_id' => 20,
+ 'product_id' => 3,
+ 'action_operator' => 'simple_action',
+ 'action_amount' => 43,
+ 'action_stop' => true,
+ 'sort_order' => 1,
+ ]
+ ];
+
+ $this->connectionMock->expects(self::at(0))
+ ->method('insertMultiple')
+ ->with('catalogrule_product_replica', $batchRows);
+
+ self::assertTrue($this->model->execute($this->ruleMock, 100, true));
+ }
+
+ private function prepareResourceMock(): void
+ {
+ $this->tableSwapperMock->expects(self::once())
+ ->method('getWorkingTableName')
+ ->with('catalogrule_product')
+ ->willReturn('catalogrule_product_replica');
+ $this->resourceMock->expects(self::at(0))
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->resourceMock->expects(self::at(1))
+ ->method('getTableName')
+ ->with('catalogrule_product')
+ ->willReturn('catalogrule_product');
+ $this->resourceMock->expects(self::at(2))
+ ->method('getTableName')
+ ->with('catalogrule_product_replica')
+ ->willReturn('catalogrule_product_replica');
+ }
+
+ /**
+ * @param array $websiteId
+ * @param array $productIds
+ * @param array $customerGroupIds
+ */
+ private function prepareRuleMock(array $websiteId, array $productIds, array $customerGroupIds): void
+ {
+ $this->ruleMock->expects(self::once())->method('getIsActive')->willReturn(true);
+ $this->ruleMock->expects(self::exactly(2))->method('getWebsiteIds')->willReturn($websiteId);
+ $this->ruleMock->expects(self::once())->method('getMatchingProductIds')->willReturn($productIds);
+ $this->ruleMock->expects(self::once())->method('getId')->willReturn(100);
+ $this->ruleMock->expects(self::once())->method('getCustomerGroupIds')->willReturn($customerGroupIds);
+ $this->ruleMock->expects(self::atLeastOnce())->method('getFromDate')->willReturn('2017-06-21');
+ $this->ruleMock->expects(self::atLeastOnce())->method('getToDate')->willReturn('2017-06-30');
+ $this->ruleMock->expects(self::once())->method('getSortOrder')->willReturn(1);
+ $this->ruleMock->expects(self::once())->method('getSimpleAction')->willReturn('simple_action');
+ $this->ruleMock->expects(self::once())->method('getDiscountAmount')->willReturn(43);
+ $this->ruleMock->expects(self::once())->method('getStopRulesProcessing')->willReturn(true);
}
}
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php
index e6d2fc78b2ae..0649cfc6112c 100644
--- a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php
+++ b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
+use Magento\CatalogRule\Api\Data\RuleInterface;
use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor;
use Magento\CatalogRule\Model\Rule;
use Magento\CatalogRule\Model\Rule\Condition\CombineFactory;
@@ -31,46 +32,60 @@
*/
class RuleTest extends TestCase
{
- /** @var Rule */
- protected $rule;
+ /**
+ * @var Rule
+ */
+ private $rule;
- /** @var ObjectManager */
+ /**
+ * @var ObjectManager
+ */
private $objectManager;
- /** @var StoreManagerInterface|MockObject */
- protected $storeManager;
+ /**
+ * @var StoreManagerInterface|MockObject
+ */
+ private $storeManager;
- /** @var MockObject */
- protected $combineFactory;
+ /**
+ * @var CombineFactory|MockObject
+ */
+ private $combineFactory;
- /** @var Store|MockObject */
- protected $storeModel;
+ /**
+ * @var Store|MockObject
+ */
+ private $storeModel;
- /** @var Website|MockObject */
- protected $websiteModel;
+ /**
+ * @var Website|MockObject
+ */
+ private $websiteModel;
- /** @var Combine|MockObject */
- protected $condition;
+ /**
+ * @var Combine|MockObject
+ */
+ private $condition;
/**
* @var RuleProductProcessor|MockObject
*/
- protected $_ruleProductProcessor;
+ private $_ruleProductProcessor;
/**
* @var CollectionFactory|MockObject
*/
- protected $_productCollectionFactory;
+ private $_productCollectionFactory;
/**
* @var Iterator|MockObject
*/
- protected $_resourceIterator;
+ private $_resourceIterator;
/**
* @var Product|MockObject
*/
- protected $productModel;
+ private $productModel;
/**
* Set up before test
@@ -85,7 +100,7 @@ protected function setUp(): void
$this->combineFactory = $this->createPartialMock(
CombineFactory::class,
[
- 'create'
+ 'create',
]
);
$this->productModel = $this->createPartialMock(
@@ -93,7 +108,7 @@ protected function setUp(): void
[
'__wakeup',
'getId',
- 'setData'
+ 'setData',
]
);
$this->condition = $this->getMockBuilder(Combine::class)
@@ -106,7 +121,7 @@ protected function setUp(): void
[
'__wakeup',
'getId',
- 'getDefaultStore'
+ 'getDefaultStore',
]
);
$this->_ruleProductProcessor = $this->createMock(
@@ -192,7 +207,7 @@ public function testCallbackValidateProduct($validate)
'has_options' => '0',
'required_options' => '0',
'created_at' => '2014-06-25 13:14:30',
- 'updated_at' => '2014-06-25 14:37:15'
+ 'updated_at' => '2014-06-25 14:37:15',
];
$this->storeManager->expects($this->any())->method('getWebsites')->with(false)
->willReturn([$this->websiteModel, $this->websiteModel]);
@@ -263,14 +278,14 @@ public function validateDataDataProvider()
'simple_action' => 'by_fixed',
'discount_amount' => '123',
],
- true
+ true,
],
[
[
'simple_action' => 'by_percent',
'discount_amount' => '9,99',
],
- true
+ true,
],
[
[
@@ -279,7 +294,7 @@ public function validateDataDataProvider()
],
[
'Percentage discount should be between 0 and 100.',
- ]
+ ],
],
[
[
@@ -288,7 +303,7 @@ public function validateDataDataProvider()
],
[
'Percentage discount should be between 0 and 100.',
- ]
+ ],
],
[
[
@@ -297,7 +312,7 @@ public function validateDataDataProvider()
],
[
'Discount value should be 0 or greater.',
- ]
+ ],
],
[
[
@@ -306,7 +321,7 @@ public function validateDataDataProvider()
],
[
'Unknown action.',
- ]
+ ],
],
];
}
@@ -325,33 +340,48 @@ public function testAfterDelete()
}
/**
- * Test after update action for inactive rule
+ * Test after update action for active and deactivated rule.
*
+ * @dataProvider afterUpdateDataProvider
+ * @param int $active
* @return void
*/
- public function testAfterUpdateInactive()
+ public function testAfterUpdate(int $active)
{
$this->rule->isObjectNew(false);
- $this->rule->setIsActive(0);
- $this->_ruleProductProcessor->expects($this->never())->method('getIndexer');
+ $this->rule->setIsActive($active);
+ $this->rule->setOrigData(RuleInterface::IS_ACTIVE, 1);
+ $indexer = $this->getMockForAbstractClass(IndexerInterface::class);
+ $indexer->expects($this->once())->method('invalidate');
+ $this->_ruleProductProcessor->expects($this->once())->method('getIndexer')->willReturn($indexer);
$this->rule->afterSave();
}
/**
- * Test after update action for active rule
+ * Test after update action for inactive rule.
*
* @return void
*/
- public function testAfterUpdateActive()
+ public function testAfterUpdateInactiveRule()
{
$this->rule->isObjectNew(false);
- $this->rule->setIsActive(1);
- $indexer = $this->getMockForAbstractClass(IndexerInterface::class);
- $indexer->expects($this->once())->method('invalidate');
- $this->_ruleProductProcessor->expects($this->once())->method('getIndexer')->willReturn($indexer);
+ $this->rule->setIsActive(0);
+ $this->rule->setOrigData(RuleInterface::IS_ACTIVE, 0);
+ $this->_ruleProductProcessor->expects($this->never())->method('getIndexer');
$this->rule->afterSave();
}
+ /**
+ * @return array
+ */
+ public function afterUpdateDataProvider(): array
+ {
+ return [
+ ['active' => 0],
+ ['active' => 1],
+ ];
+ }
+
/**
* Test isRuleBehaviorChanged action
*
diff --git a/app/code/Magento/CatalogRule/etc/extension_attributes.xml b/app/code/Magento/CatalogRule/etc/extension_attributes.xml
new file mode 100644
index 000000000000..78568d8961ad
--- /dev/null
+++ b/app/code/Magento/CatalogRule/etc/extension_attributes.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml
index 59e3c4668e8a..64bc5efe8e38 100644
--- a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml
+++ b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml
@@ -133,7 +133,7 @@
number
- https://docs.magento.com/m2/ce/user_guide/configuration/scope.html
+ https://docs.magento.com/user-guide/configuration/scope.html
What is this?
Websites
diff --git a/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Indexer/ProductRuleReindex.php b/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Indexer/ProductRuleReindex.php
index 9c6671933463..ad2a7c3c2b21 100644
--- a/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Indexer/ProductRuleReindex.php
+++ b/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Indexer/ProductRuleReindex.php
@@ -6,21 +6,22 @@
*/
namespace Magento\CatalogRuleConfigurable\Plugin\CatalogRule\Model\Indexer;
+use Magento\CatalogRule\Model\Indexer\Product\ProductRuleIndexer;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\CatalogRuleConfigurable\Plugin\CatalogRule\Model\ConfigurableProductsProvider;
/**
- * Class ReindexProduct. Add configurable sub-products to reindex
+ * Add configurable sub-products to reindex
*/
class ProductRuleReindex
{
/**
- * @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable
+ * @var Configurable
*/
private $configurable;
/**
- * @var \Magento\CatalogRuleConfigurable\Plugin\CatalogRule\Model\ConfigurableProductsProvider
+ * @var ConfigurableProductsProvider
*/
private $configurableProductsProvider;
@@ -37,61 +38,47 @@ public function __construct(
}
/**
- * @param \Magento\CatalogRule\Model\Indexer\Product\ProductRuleIndexer $subject
+ * Reindex configurable product with sub-products
+ *
+ * @param ProductRuleIndexer $subject
* @param \Closure $proceed
* @param int $id
- *
* @return void
*/
- public function aroundExecuteRow(
- \Magento\CatalogRule\Model\Indexer\Product\ProductRuleIndexer $subject,
- \Closure $proceed,
- $id
- ) {
+ public function aroundExecuteRow(ProductRuleIndexer $subject, \Closure $proceed, $id)
+ {
+ $isReindexed = false;
+
$configurableProductIds = $this->configurableProductsProvider->getIds([$id]);
- $this->reindexSubProducts($configurableProductIds, $subject);
- if (!$configurableProductIds) {
- $proceed($id);
+ if ($configurableProductIds) {
+ $subProducts = array_values($this->configurable->getChildrenIds($id)[0]);
+ if ($subProducts) {
+ $subject->executeList(array_merge([$id], $subProducts));
+ $isReindexed = true;
+ }
}
- }
- /**
- * @param \Magento\CatalogRule\Model\Indexer\Product\ProductRuleIndexer $subject
- * @param \Closure $proceed
- * @param array $ids
- *
- * @return void
- */
- public function aroundExecuteList(
- \Magento\CatalogRule\Model\Indexer\Product\ProductRuleIndexer $subject,
- \Closure $proceed,
- array $ids
- ) {
- $configurableProductIds = $this->configurableProductsProvider->getIds($ids);
- $subProducts = $this->reindexSubProducts($configurableProductIds, $subject);
- $ids = array_diff($ids, $configurableProductIds, $subProducts);
- if ($ids) {
- $proceed($ids);
+ if (!$isReindexed) {
+ $proceed($id);
}
}
/**
- * @param array $configurableIds
- * @param \Magento\CatalogRule\Model\Indexer\Product\ProductRuleIndexer $subject
+ * Add sub-products to reindex
*
+ * @param ProductRuleIndexer $subject
+ * @param array $ids
* @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
- private function reindexSubProducts(
- array $configurableIds,
- \Magento\CatalogRule\Model\Indexer\Product\ProductRuleIndexer $subject
- ) {
- $subProducts = [];
- if ($configurableIds) {
- $subProducts = array_values($this->configurable->getChildrenIds($configurableIds)[0]);
- if ($subProducts) {
- $subject->executeList($subProducts);
- }
+ public function beforeExecuteList(ProductRuleIndexer $subject, array $ids): array
+ {
+ $configurableProductIds = $this->configurableProductsProvider->getIds($ids);
+ if ($configurableProductIds) {
+ $subProducts = array_values($this->configurable->getChildrenIds($configurableProductIds)[0]);
+ $ids = array_unique(array_merge($ids, $subProducts));
}
- return $subProducts;
+
+ return [$ids];
}
}
diff --git a/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php b/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php
index fa42cadb8a2f..840eb147a86f 100644
--- a/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php
+++ b/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php
@@ -15,6 +15,11 @@
*/
class SearchWeight
{
+ /**
+ * @var \Magento\Framework\Search\Request\Config
+ */
+ private $config;
+
/**
* @param \Magento\Framework\Search\Request\Config $config
*/
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php
index f72516d28c46..603a5c8a4793 100644
--- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php
@@ -15,6 +15,7 @@
use Magento\Framework\Indexer\SaveHandler\IndexerInterface;
use Magento\Store\Model\StoreDimensionProvider;
use Magento\Indexer\Model\ProcessManager;
+use Magento\Framework\App\DeploymentConfig;
/**
* Provide functionality for Fulltext Search indexing.
@@ -88,6 +89,18 @@ class Fulltext implements
*/
private $batchSize;
+ /**
+ * @var DeploymentConfig|null
+ */
+ private $deploymentConfig;
+
+ /**
+ * Deployment config path
+ *
+ * @var string
+ */
+ private const DEPLOYMENT_CONFIG_INDEXER_BATCHES = 'indexer/batch_size/';
+
/**
* @param FullFactory $fullActionFactory
* @param IndexerHandlerFactory $indexerHandlerFactory
@@ -96,8 +109,9 @@ class Fulltext implements
* @param StateFactory $indexScopeStateFactory
* @param DimensionProviderInterface $dimensionProvider
* @param array $data
- * @param ProcessManager $processManager
+ * @param ProcessManager|null $processManager
* @param int|null $batchSize
+ * @param DeploymentConfig|null $deploymentConfig
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
@@ -109,7 +123,8 @@ public function __construct(
DimensionProviderInterface $dimensionProvider,
array $data,
ProcessManager $processManager = null,
- ?int $batchSize = null
+ ?int $batchSize = null,
+ ?DeploymentConfig $deploymentConfig = null
) {
$this->fullAction = $fullActionFactory->create(['data' => $data]);
$this->indexerHandlerFactory = $indexerHandlerFactory;
@@ -120,6 +135,7 @@ public function __construct(
$this->dimensionProvider = $dimensionProvider;
$this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class);
$this->batchSize = $batchSize ?? self::BATCH_SIZE;
+ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class);
}
/**
@@ -165,6 +181,10 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds =
$currentBatch = [];
$i = 0;
+ $this->batchSize = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . self::INDEXER_ID . '/partial_reindex'
+ ) ?? $this->batchSize;
+
foreach ($entityIds as $entityId) {
$currentBatch[] = $entityId;
if (++$i === $this->batchSize) {
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php
index 3ce8a96fb507..59bdd0e47327 100644
--- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php
@@ -7,6 +7,7 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\CatalogSearch\Model\Indexer\Fulltext;
+use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
@@ -199,6 +200,19 @@ class Full
private $batchSize;
/**
+ * @var DeploymentConfig|null
+ */
+ private $deploymentConfig;
+
+ /**
+ * Deployment config path
+ *
+ * @var string
+ */
+ private const DEPLOYMENT_CONFIG_INDEXER_BATCHES = 'indexer/batch_size/';
+
+ /**
+ * Full constructor.
* @param ResourceConnection $resource
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
* @param \Magento\Eav\Model\Config $eavConfig
@@ -216,10 +230,12 @@ class Full
* @param \Magento\CatalogSearch\Model\ResourceModel\Fulltext $fulltextResource
* @param \Magento\Framework\Search\Request\DimensionFactory $dimensionFactory
* @param \Magento\Framework\Indexer\ConfigInterface $indexerConfig
- * @param mixed $indexIteratorFactory
+ * @param null $indexIteratorFactory
* @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool
* @param DataProvider|null $dataProvider
* @param int $batchSize
+ * @param DeploymentConfig|null $deploymentConfig
+ *
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -244,7 +260,8 @@ public function __construct(
$indexIteratorFactory = null,
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
DataProvider $dataProvider = null,
- $batchSize = 500
+ $batchSize = 500,
+ ?DeploymentConfig $deploymentConfig = null
) {
$this->resource = $resource;
$this->connection = $resource->getConnection();
@@ -268,6 +285,7 @@ public function __construct(
->get(\Magento\Framework\EntityManager\MetadataPool::class);
$this->dataProvider = $dataProvider ?: ObjectManager::getInstance()->get(DataProvider::class);
$this->batchSize = $batchSize;
+ $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class);
}
/**
@@ -360,6 +378,9 @@ public function rebuildStoreIndex($storeId, $productIds = null)
];
$lastProductId = 0;
+ $this->batchSize = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . Fulltext::INDEXER_ID . '/mysql_get'
+ ) ?? $this->batchSize;
$products = $this->dataProvider
->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $this->batchSize);
while (count($products) > 0) {
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php
index ed841996ea07..5c5b4591f8a5 100644
--- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Model\ResourceModel\Category as Resource;
use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor;
+use Magento\Framework\DataObject;
/**
* Perform indexer invalidation after a category delete.
@@ -33,12 +34,15 @@ public function __construct(Processor $fulltextIndexerProcessor)
*
* @param Resource $subjectCategory
* @param Resource $resultCategory
+ * @param DataObject $object
* @return Resource
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
- public function afterDelete(Resource $subjectCategory, Resource $resultCategory) : Resource
+ public function afterDelete(Resource $subjectCategory, Resource $resultCategory, DataObject $object) : Resource
{
- $this->fulltextIndexerProcessor->markIndexerAsInvalid();
+ if ($object->getIsActive() || $object->getDeletedChildrenIds()) {
+ $this->fulltextIndexerProcessor->markIndexerAsInvalid();
+ }
return $resultCategory;
}
diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
index d37f0f8a5153..7e9be408a385 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
@@ -17,6 +17,11 @@
class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection implements
\Magento\Search\Model\SearchCollectionInterface
{
+ /**
+ * @var array
+ */
+ private $indexUsageEnforcements;
+
/**
* Attribute collection
*
@@ -61,6 +66,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
* @param \Magento\Customer\Api\GroupManagementInterface $groupManagement
* @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory
* @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
+ * @param array $indexUsageEnforcements
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -84,7 +90,8 @@ public function __construct(
\Magento\Framework\Stdlib\DateTime $dateTime,
\Magento\Customer\Api\GroupManagementInterface $groupManagement,
\Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory,
- \Magento\Framework\DB\Adapter\AdapterInterface $connection = null
+ \Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
+ array $indexUsageEnforcements = []
) {
$this->_attributeCollectionFactory = $attributeCollectionFactory;
parent::__construct(
@@ -109,6 +116,7 @@ public function __construct(
$groupManagement,
$connection
);
+ $this->indexUsageEnforcements = $indexUsageEnforcements;
}
/**
@@ -197,6 +205,35 @@ protected function _hasAttributeOptionsAndSearchable($attribute)
return false;
}
+ /**
+ * Prepare table names for the index enforcements
+ *
+ * @return array
+ */
+ private function prepareIndexEnforcements() : array
+ {
+ $result = [];
+ foreach ($this->indexUsageEnforcements as $table => $index) {
+ $table = $this->getTable($table);
+ if ($this->isIndexExists($table, $index)) {
+ $result[$table] = $index;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Check if index exists in the table
+ *
+ * @param string $table
+ * @param string $index
+ * @return bool
+ */
+ private function isIndexExists(string $table, string $index) : bool
+ {
+ return array_key_exists($index, $this->_conn->getIndexList($table));
+ }
+
/**
* Retrieve SQL for search entities
*
@@ -208,6 +245,7 @@ protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = tr
{
$tables = [];
$selects = [];
+ $preparedIndexEnforcements = $this->prepareIndexEnforcements();
$likeOptions = ['position' => 'any'];
@@ -249,23 +287,56 @@ protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = tr
$ifValueId = $this->getConnection()->getIfNullSql('t2.value', 't1.value');
foreach ($tables as $table => $attributeIds) {
- $selects[] = $this->getConnection()->select()->from(
- ['t1' => $table],
- $linkField
- )->joinLeft(
- ['t2' => $table],
- $joinCondition,
- []
- )->where(
- 't1.attribute_id IN (?)',
- $attributeIds,
- \Zend_Db::INT_TYPE
- )->where(
- 't1.store_id = ?',
- 0
- )->where(
- $this->_resourceHelper->getCILike($ifValueId, $this->_searchQuery, $likeOptions)
- );
+ if (!empty($preparedIndexEnforcements[$table])) {
+ $condition1 = $this->_conn->quoteInto(
+ '`t1`.`attribute_id` IN (?)',
+ $attributeIds,
+ \Zend_Db::INT_TYPE
+ );
+ $condition2 = '`t1`.`store_id` = 0';
+ $quotedField = $this->_conn->quoteIdentifier($ifValueId);
+ $condition3 = $this->_conn->quoteInto(
+ $quotedField . ' LIKE ?',
+ $this->_resourceHelper->addLikeEscape($this->_searchQuery, $likeOptions)
+ );
+
+ //force index statement not implemented in framework
+ // phpcs:ignore Magento2.SQL.RawQuery
+ $select = sprintf(
+ 'SELECT `t1`.`%s` FROM `%s` AS `t1` FORCE INDEX(%s)
+ LEFT JOIN `%s` AS `t2` FORCE INDEX(%s)
+ ON %s WHERE %s AND %s AND (%s)',
+ $linkField,
+ $table,
+ $preparedIndexEnforcements[$table],
+ $table,
+ $preparedIndexEnforcements[$table],
+ $joinCondition,
+ $condition1,
+ $condition2,
+ $condition3
+ );
+ } else {
+ $select = $this->getConnection()->select();
+ $select->from(
+ ['t1' => $table],
+ $linkField
+ )->joinLeft(
+ ['t2' => $table],
+ $joinCondition,
+ []
+ )->where(
+ 't1.attribute_id IN (?)',
+ $attributeIds,
+ \Zend_Db::INT_TYPE
+ )->where(
+ 't1.store_id = ?',
+ 0
+ )->where(
+ $this->_resourceHelper->getCILike($ifValueId, $this->_searchQuery, $likeOptions)
+ );
+ }
+ $selects[] = $select;
}
$sql = $this->_getSearchInOptionSql($query);
diff --git a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php
index 2f6a402b2040..4adb9c2299e9 100644
--- a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php
+++ b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php
@@ -5,6 +5,9 @@
*/
namespace Magento\CatalogSearch\Model\Search;
+use Magento\CatalogSearch\Model\Search\Request\ModifierInterface;
+use Magento\Framework\Config\ReaderInterface;
+
/**
* @deprecated 101.0.0
* @see \Magento\ElasticSearch
@@ -12,34 +15,34 @@
class ReaderPlugin
{
/**
- * @var \Magento\CatalogSearch\Model\Search\RequestGenerator
+ * @var ModifierInterface
*/
- private $requestGenerator;
+ private $requestModifier;
/**
- * @param \Magento\CatalogSearch\Model\Search\RequestGenerator $requestGenerator
+ * @param ModifierInterface $requestModifier
*/
public function __construct(
- \Magento\CatalogSearch\Model\Search\RequestGenerator $requestGenerator
+ ModifierInterface $requestModifier
) {
- $this->requestGenerator = $requestGenerator;
+ $this->requestModifier = $requestModifier;
}
/**
* Merge reader's value with generated
*
- * @param \Magento\Framework\Config\ReaderInterface $subject
+ * @param ReaderInterface $subject
* @param array $result
* @param string|null $scope
* @return array
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterRead(
- \Magento\Framework\Config\ReaderInterface $subject,
+ ReaderInterface $subject,
array $result,
$scope = null
) {
- $result = array_merge_recursive($result, $this->requestGenerator->generate());
+ $result = $this->requestModifier->modify($result);
return $result;
}
}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/Request/MatchQueriesModifier.php b/app/code/Magento/CatalogSearch/Model/Search/Request/MatchQueriesModifier.php
new file mode 100644
index 000000000000..8d1675884c3b
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/Request/MatchQueriesModifier.php
@@ -0,0 +1,62 @@
+queries = $queries;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modify(array $requests): array
+ {
+ foreach ($requests as &$request) {
+ foreach ($this->queries as $query => $fields) {
+ if (!empty($request[self::NODE_QUERIES][$query][self::NODE_MATCH])) {
+ foreach ($request[self::NODE_QUERIES][$query][self::NODE_MATCH] as $index => $match) {
+ $field = $match[self::NODE_MATCH_ATTRIBUTE_FIELD] ?? null;
+ if ($field !== null && isset($fields[$field])) {
+ $request[self::NODE_QUERIES][$query][self::NODE_MATCH][$index] += $fields[$field];
+ }
+ }
+ }
+ }
+ }
+ return $requests;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/Request/ModifierComposite.php b/app/code/Magento/CatalogSearch/Model/Search/Request/ModifierComposite.php
new file mode 100644
index 000000000000..6290b5bd7ce4
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/Request/ModifierComposite.php
@@ -0,0 +1,46 @@
+modifiers = $modifiers;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modify(array $requests): array
+ {
+ foreach ($this->modifiers as $modifier) {
+ $requests = $modifier->modify($requests);
+ }
+ return $requests;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/Request/ModifierInterface.php b/app/code/Magento/CatalogSearch/Model/Search/Request/ModifierInterface.php
new file mode 100644
index 000000000000..68421d6cb1d4
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/Request/ModifierInterface.php
@@ -0,0 +1,22 @@
+collectionFactory = $collectionFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modify(array $requests): array
+ {
+ $attributes = $this->getSearchableAttributes();
+ foreach ($requests as $code => $request) {
+ $matches = $request['queries']['partial_search']['match'] ?? [];
+ if ($matches) {
+ foreach ($matches as $index => $match) {
+ $field = $match['field'] ?? null;
+ if ($field && $field !== '*' && !isset($attributes[$field])) {
+ unset($matches[$index]);
+ }
+ }
+ $requests[$code]['queries']['partial_search']['match'] = array_values($matches);
+ }
+ }
+ return $requests;
+ }
+
+ /**
+ * Retrieve searchable attributes
+ *
+ * @return Attribute[]
+ */
+ private function getSearchableAttributes(): array
+ {
+ $attributes = [];
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+ $collection->addFieldToFilter(
+ ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'],
+ [1, 1, [1, 2], 1]
+ );
+
+ /** @var Attribute $attribute */
+ foreach ($collection->getItems() as $attribute) {
+ $attributes[$attribute->getAttributeCode()] = $attribute;
+ }
+
+ return $attributes;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/Request/SearchModifier.php b/app/code/Magento/CatalogSearch/Model/Search/Request/SearchModifier.php
new file mode 100644
index 000000000000..54082dd28ec0
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/Request/SearchModifier.php
@@ -0,0 +1,39 @@
+requestGenerator = $requestGenerator;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function modify(array $requests): array
+ {
+ $requests = array_merge_recursive($requests, $this->requestGenerator->generate());
+ return $requests;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php
index caa49d0f0c4a..94922c9ce772 100644
--- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php
+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php
@@ -186,6 +186,7 @@ private function generateAdvancedSearchRequest()
[
'field' => $attribute->getAttributeCode(),
'boost' => $attribute->getSearchWeight() ?: 1,
+ 'matchCondition' => 'match_phrase_prefix',
],
],
];
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AssertStorefrontNoResultsMessageOnSearchPageActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AssertStorefrontNoResultsMessageOnSearchPageActionGroup.xml
new file mode 100644
index 000000000000..6001708b41a1
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AssertStorefrontNoResultsMessageOnSearchPageActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Check if search returned no results
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AssertStorefrontProductNotOnSearchPageActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AssertStorefrontProductNotOnSearchPageActionGroup.xml
new file mode 100644
index 000000000000..c35be6f46e7d
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AssertStorefrontProductNotOnSearchPageActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml
index 1afdb6e5e46f..81a283b9fcf3 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml
@@ -23,6 +23,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
index c02ef4957ad3..395cfd187013 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
@@ -12,9 +12,8 @@
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
index 0c8e192f9366..99448fd730d9 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
@@ -13,9 +13,8 @@
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
index 99c09b5ba93a..af70c3ba1c3a 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
@@ -13,9 +13,8 @@
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
index 1e18c5ea4d0a..38399f2729d3 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
@@ -13,9 +13,8 @@
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
index 34e0a73e91fe..e12505299c76 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
@@ -13,9 +13,8 @@
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml
index 28eb53542ad9..f787b160e768 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml
@@ -23,7 +23,7 @@
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml
index d6a5aa8b9357..8b9b78b67bc7 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml
@@ -38,10 +38,14 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml
index 09f7ee455ebb..a6cec9480373 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml
@@ -43,10 +43,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml
index 98d1b3412360..e587840f3bff 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml
@@ -54,10 +54,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml
index 3298eff34759..1a25ee6bc57a 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml
@@ -25,10 +25,7 @@
-
-
-
-
+
@@ -38,6 +35,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml
index 85e3c4665450..2201b17c31a0 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml
@@ -27,10 +27,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml
index 3488a6314080..efe1018c29e5 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml
@@ -27,10 +27,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml
index 26f4cd77b60b..b0137f145bff 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml
@@ -23,10 +23,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml
index 9277baba94aa..b2e86ae896ab 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml
@@ -23,10 +23,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml
index a6654db91eff..02f6ffd5db8e 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml
@@ -24,10 +24,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml
index 83436ebb44c6..4bde337ab78d 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml
@@ -23,10 +23,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml
index d8f8924c4db0..d85b17348b0b 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml
@@ -10,17 +10,15 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
+
-
+
-
-
-
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
index 14ae988e6ce7..1e27f9f6d05a 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
@@ -31,10 +31,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchNotSearchableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchNotSearchableTest.xml
new file mode 100644
index 000000000000..b8ca2a16e751
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchNotSearchableTest.xml
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchStemmingTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchStemmingTest.xml
new file mode 100644
index 000000000000..b1528b169cbd
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchStemmingTest.xml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5127SS YALE JUNIOR KNOB ENTRANCE SET 5 PINS SATIN STAI
+
+
+
+
+ 5127AC YALE JUNIOR KNOB ENTRANCE SET 5 PINS ANTIQUE CO
+
+
+
+
+ 5127AB YALE JUNIOR KNOB ENTRANCE SET 5 PINS ANTIQUE BRASS
+
+
+
+
+ 5127SS-YALE
+
+
+
+
+ 5127AC-CO
+
+
+
+
+ 5127AB-BRASS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml
index 67e8bc6bf183..fe30acc17424 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml
@@ -72,10 +72,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml
index 26280ed67d18..634ee57f1723 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml
@@ -25,10 +25,7 @@
-
-
-
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/ReaderPluginTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/ReaderPluginTest.php
index 5f342f1735de..3b19c98aca3c 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/ReaderPluginTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/ReaderPluginTest.php
@@ -8,7 +8,7 @@
namespace Magento\CatalogSearch\Test\Unit\Model\Search;
use Magento\CatalogSearch\Model\Search\ReaderPlugin;
-use Magento\CatalogSearch\Model\Search\RequestGenerator;
+use Magento\CatalogSearch\Model\Search\Request\ModifierInterface;
use Magento\Framework\Config\ReaderInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use PHPUnit\Framework\MockObject\MockObject;
@@ -16,8 +16,8 @@
class ReaderPluginTest extends TestCase
{
- /** @var RequestGenerator|MockObject */
- protected $requestGenerator;
+ /** @var ModifierInterface|MockObject */
+ protected $requestModifier;
/** @var ObjectManager */
protected $objectManagerHelper;
@@ -27,22 +27,20 @@ class ReaderPluginTest extends TestCase
protected function setUp(): void
{
- $this->requestGenerator = $this->getMockBuilder(RequestGenerator::class)
+ $this->requestModifier = $this->getMockBuilder(ModifierInterface::class)
->disableOriginalConstructor()
->getMock();
$this->objectManagerHelper = new ObjectManager($this);
- $this->object = $this->objectManagerHelper->getObject(
- ReaderPlugin::class,
- ['requestGenerator' => $this->requestGenerator]
- );
+ $this->object = new ReaderPlugin($this->requestModifier);
}
public function testAfterRead()
{
$readerConfig = ['test' => 'b', 'd' => 'e'];
- $this->requestGenerator->expects($this->once())
- ->method('generate')
+ $this->requestModifier->expects($this->once())
+ ->method('modify')
+ ->with($readerConfig)
->willReturn(['test' => 'a']);
$result = $this->object->afterRead(
@@ -53,6 +51,6 @@ public function testAfterRead()
null
);
- $this->assertEquals(['test' => ['b', 'a'], 'd' => 'e'], $result);
+ $this->assertEquals(['test' => 'a'], $result);
}
}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/MatchQueriesModifierTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/MatchQueriesModifierTest.php
new file mode 100644
index 000000000000..00576f9ad029
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/MatchQueriesModifierTest.php
@@ -0,0 +1,130 @@
+assertEquals($expected, $model->modify($requests));
+ }
+
+ /**
+ * @return array
+ */
+ public function modifyDataProvider(): array
+ {
+ return [
+ [
+ [
+ 'partial_search' => [
+ 'name' => [
+ 'analyzer' => 'standard',
+ 'max_expansions' => 20,
+ ]
+ ],
+ ],
+ [
+ 'search_1' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ],
+ 'queries' => [
+ 'partial_search' => [
+ 'name' => 'partial_search',
+ 'value' => '$search_term$',
+ 'match' => [
+ [
+ 'field' => '*'
+ ],
+ [
+ 'field' => 'sku',
+ 'matchCondition' => 'match_phrase_prefix',
+ ],
+ [
+ 'field' => 'name',
+ 'matchCondition' => 'match_phrase_prefix',
+ ],
+ ]
+ ]
+ ]
+ ],
+ 'search_2' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ]
+ ]
+ ],
+ [
+ 'search_1' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ],
+ 'queries' => [
+ 'partial_search' => [
+ 'name' => 'partial_search',
+ 'value' => '$search_term$',
+ 'match' => [
+ [
+ 'field' => '*'
+ ],
+ [
+ 'field' => 'sku',
+ 'matchCondition' => 'match_phrase_prefix',
+ ],
+ [
+ 'field' => 'name',
+ 'matchCondition' => 'match_phrase_prefix',
+ 'analyzer' => 'standard',
+ 'max_expansions' => 20,
+ ],
+ ]
+ ]
+ ]
+ ],
+ 'search_2' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/ModifierCompositeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/ModifierCompositeTest.php
new file mode 100644
index 000000000000..936ad83b8a63
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/ModifierCompositeTest.php
@@ -0,0 +1,86 @@
+modifier1 = $this->getMockForAbstractClass(ModifierInterface::class);
+ $this->modifier2 = $this->getMockForAbstractClass(ModifierInterface::class);
+ $this->model = new ModifierComposite(
+ [
+ $this->modifier1,
+ $this->modifier2
+ ]
+ );
+ }
+
+ /**
+ * Test that all modifiers are executed
+ */
+ public function testModify(): void
+ {
+ $requests = ['a', 'b', 'c'];
+ $this->modifier1->expects($this->once())
+ ->method('modify')
+ ->with($requests)
+ ->willReturn(['a', 'b', 'c', 'd']);
+
+ $this->modifier2->expects($this->once())
+ ->method('modify')
+ ->with(['a', 'b', 'c', 'd'])
+ ->willReturn(['a', 'c', 'd']);
+
+ $this->assertEquals(['a', 'c', 'd'], $this->model->modify($requests));
+ }
+
+ /**
+ * Test that exception is thrown if modifier is not instance of ModifierInterface
+ */
+ public function testInvalidModifier(): void
+ {
+ $exception = new \InvalidArgumentException(
+ 'Magento\Framework\DataObject must implement Magento\CatalogSearch\Model\Search\Request\ModifierInterface'
+ );
+ $this->expectExceptionObject($exception);
+ $this->model = new ModifierComposite(
+ [
+ new DataObject()
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/PartialSearchModifierTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/PartialSearchModifierTest.php
new file mode 100644
index 000000000000..2fabec670a57
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/PartialSearchModifierTest.php
@@ -0,0 +1,157 @@
+createMock(CollectionFactory::class);
+ $this->collection = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['load', 'addFieldToFilter'])
+ ->getMock();
+ $collectionFactory->method('create')
+ ->willReturn($this->collection);
+ $this->model = new PartialSearchModifier($collectionFactory);
+ }
+
+ /**
+ * Test that not searchable attributes are removed from the request
+ *
+ * @param array $attributes
+ * @param array $requests
+ * @param array $expected
+ * @dataProvider modifyDataProvider
+ */
+ public function testModify(array $attributes, array $requests, array $expected): void
+ {
+ $items = [];
+ foreach ($attributes as $attribute) {
+ $item = $this->getMockForAbstractClass(\Magento\Eav\Api\Data\AttributeInterface::class);
+ $item->method('getAttributeCode')
+ ->willReturn($attribute);
+ $items[] = $item;
+ }
+ $reflectionProperty = new \ReflectionProperty($this->collection, '_items');
+ $reflectionProperty->setAccessible(true);
+ $reflectionProperty->setValue($this->collection, $items);
+ $this->assertEquals($expected, $this->model->modify($requests));
+ }
+
+ /**
+ * @return array
+ */
+ public function modifyDataProvider(): array
+ {
+ return [
+ [
+ [
+ 'name',
+ ],
+ [
+ 'search_1' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ],
+ 'queries' => [
+ 'partial_search' => [
+ 'name' => 'partial_search',
+ 'value' => '$search_term$',
+ 'match' => [
+ [
+ 'field' => '*'
+ ],
+ [
+ 'field' => 'sku',
+ 'matchCondition' => 'match_phrase_prefix',
+ ],
+ [
+ 'field' => 'name',
+ 'matchCondition' => 'match_phrase_prefix',
+ ],
+ ]
+ ]
+ ]
+ ],
+ 'search_2' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ]
+ ]
+ ],
+ [
+ 'search_1' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ],
+ 'queries' => [
+ 'partial_search' => [
+ 'name' => 'partial_search',
+ 'value' => '$search_term$',
+ 'match' => [
+ [
+ 'field' => '*'
+ ],
+ [
+ 'field' => 'name',
+ 'matchCondition' => 'match_phrase_prefix',
+ ],
+ ]
+ ]
+ ]
+ ],
+ 'search_2' => [
+ 'filters' => [
+ 'category_filter' => [
+ 'name' => 'category_filter',
+ 'field' => 'category_ids',
+ 'value' => '$category_ids$',
+ ]
+ ]
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/SearchModifierTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/SearchModifierTest.php
new file mode 100644
index 000000000000..2c7ce97751f1
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/SearchModifierTest.php
@@ -0,0 +1,52 @@
+requestGenerator = $this->createMock(RequestGenerator::class);
+ $this->model = new SearchModifier($this->requestGenerator);
+ }
+
+ /**
+ * Test that the result is merged into the initial requests
+ */
+ public function testModifier(): void
+ {
+ $requests = ['a' => ['x', 'y'], 'b' => ['k']];
+ $expected = ['a' => ['x', 'y', 'z'], 'b' => ['k'], 'c' => ['n']];
+ $this->requestGenerator->expects($this->once())
+ ->method('generate')
+ ->willReturn(['a' => ['z'], 'c' => ['n']]);
+ $this->assertEquals($expected, $this->model->modify($requests));
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml
index f8e2a262d73c..43ba82f047e4 100644
--- a/app/code/Magento/CatalogSearch/etc/di.xml
+++ b/app/code/Magento/CatalogSearch/etc/di.xml
@@ -14,6 +14,7 @@
+
@@ -248,4 +249,27 @@
+
+
+
+ - Magento\CatalogSearch\Model\Search\Request\SearchModifier
+ - Magento\CatalogSearch\Model\Search\Request\PartialSearchModifier
+ - Magento\CatalogSearch\Model\Search\Request\MatchQueriesModifier
+
+
+
+
+
+
+ -
+
-
+
- prefix_search
+
+ -
+
- sku_prefix_search
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_advanced_result.xml b/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_advanced_result.xml
index 9c20f614076b..67958af8b54c 100644
--- a/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_advanced_result.xml
+++ b/app/code/Magento/CatalogSearch/view/frontend/layout/catalogsearch_advanced_result.xml
@@ -36,5 +36,10 @@
+
+
+ Magento\Catalog\ViewModel\Product\OptionsData
+
+