diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2839ac5ee9d32..dae954a0970b7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,34 +1,34 @@ # Contributing to Magento 2 code Contributions to the Magento 2 codebase are done using the fork & pull model. -This contribution model has contributors maintaining their own copy of the forked codebase (which can easily be synced with the main copy). The forked repository is then used to submit a request to the base repository to “pull” a set of changes (hence the phrase “pull request”). +This contribution model has contributors maintaining their own copy of the forked codebase (which can easily be synced with the main copy). The forked repository is then used to submit a request to the base repository to “pull” a set of changes. For more information on pull requests please refer to [GitHub Help](https://help.github.com/articles/about-pull-requests/). -Contributions can take the form of new components/features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations or just good suggestions. +Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes or optimizations. -The Magento 2 development team will review all issues and contributions submitted by the community of developers in the first in, first out order. During the review we might require clarifications from the contributor. If there is no response from the contributor for two weeks, the issue is closed. +The Magento 2 development team will review all issues and contributions submitted by the community of developers in the first in, first out order. During the review we might require clarifications from the contributor. If there is no response from the contributor within two weeks, the pull request will be closed. ## Contribution requirements -1. Contributions must adhere to [Magento coding standards](http://devdocs.magento.com/guides/v2.0/coding-standards/bk-coding-standards.html). -2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request to be merged quickly and without additional clarification requests. -3. Commits must be accompanied by meaningful commit messages. -4. PRs which include bug fixing, must be accompanied with step-by-step description of how to reproduce the bug. +1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.2/coding-standards/bk-coding-standards.html). +2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request being merged quickly and without additional clarification requests. +3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/2.2-develop/.github/PULL_REQUEST_TEMPLATE.md) for more information. +4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug. 3. PRs which include new logic or new features must be submitted along with: -* Unit/integration test coverage (we will be releasing more information on writing test coverage in the near future). -* Proposed [documentation](http://devdocs.magento.com) update. Documentation contributions can be submitted [here](https://github.com/magento/devdocs). -4. For large features or changes, please [open an issue](https://github.com/magento/magento2/issues) and discuss first. This may prevent duplicate or unnecessary effort, and it may gain you some additional contributors. -5. All automated tests are passed successfully (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). +* Unit/integration test coverage +* Proposed [documentation](http://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). +4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. +5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). ## Contribution process -If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). By doing that, you will be able to collaborate with the Magento 2 development team, “fork” the Magento 2 project and be able to easily send “pull requests”. +If you are a new GitHub user, we recommend that you create your own [free github account](https://github.com/signup/free). This will allow you to collaborate with the Magento 2 development team, fork the Magento 2 project and send pull requests. 1. Search current [listed issues](https://github.com/magento/magento2/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. 2. Review the [Contributor License Agreement](https://magento.com/legaldocuments/mca) if this is your first time contributing. 3. Create and test your work. -4. Fork the Magento 2 repository according to [Fork a repository instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow [Create a pull request instructions](http://devdocs.magento.com/guides/v2.0/contributor-guide/contributing.html#pull_request). -5. Once your contribution is received, Magento 2 development team will review the contribution and collaborate with you as needed to improve the quality of the contribution. +4. Fork the Magento 2 repository according to the [Fork A Repository instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#pull_request). +5. Once your contribution is received the Magento 2 development team will review the contribution and collaborate with you as needed. ## Code of Conduct diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3ac68076d4353..12ad4e452b1c7 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,24 +1,35 @@ - - + ### Preconditions - - + 1. 2. ### Steps to reproduce - + 1. 2. 3. ### Expected result -1. +1. [Screenshots, logs or description] ### Actual result -1. [Screenshot, logs] - - +1. [Screenshots, logs or description] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d1f01ba9f2640..5b0b9d74e453b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,32 @@ - + + + ### Description - + ### Fixed Issues (if relevant) - + 1. magento/magento2#: Issue title 2. ... ### Manual testing scenarios - + 1. ... 2. ... diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php index 303675b968256..d58a7ec31f77d 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php @@ -62,8 +62,10 @@ public function execute() if (empty($result)) { $result[] = [ 'severity' => (string)\Magento\Framework\Notification\MessageInterface::SEVERITY_NOTICE, - 'text' => 'You have viewed and resolved all recent system notices. ' - . 'Please refresh the web page to clear the notice alert.', + 'text' => __( + 'You have viewed and resolved all recent system notices. ' + . 'Please refresh the web page to clear the notice alert.' + ) ]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ diff --git a/app/code/Magento/AdminNotification/i18n/en_US.csv b/app/code/Magento/AdminNotification/i18n/en_US.csv index 16c5abb9db0d2..db5a4c9254814 100644 --- a/app/code/Magento/AdminNotification/i18n/en_US.csv +++ b/app/code/Magento/AdminNotification/i18n/en_US.csv @@ -48,3 +48,4 @@ Severity,Severity "Date Added","Date Added" Message,Message Actions,Actions +"You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert.","You have viewed and resolved all recent system notices. Please refresh the web page to clear the notice alert." diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php index 0f10fd633cb5b..5476fd05a0fed 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost.php +++ b/app/code/Magento/Authorizenet/Model/Directpost.php @@ -5,10 +5,9 @@ */ namespace Magento\Authorizenet\Model; -use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\App\ObjectManager; use Magento\Payment\Model\Method\ConfigInterface; use Magento\Payment\Model\Method\TransparentInterface; -use Magento\Sales\Model\Order\Email\Sender\OrderSender; /** * Authorize.net DirectPost payment method model. @@ -102,7 +101,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra protected $response; /** - * @var OrderSender + * @var \Magento\Sales\Model\Order\Email\Sender\OrderSender */ protected $orderSender; @@ -123,6 +122,16 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra */ private $psrLogger; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + + /** + * @var \Magento\Sales\Model\Order + */ + private $order; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -134,18 +143,19 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra * @param \Magento\Framework\Module\ModuleListInterface $moduleList * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Authorizenet\Helper\Data $dataHelper - * @param Directpost\Request\Factory $requestFactory - * @param Directpost\Response\Factory $responseFactory + * @param \Magento\Authorizenet\Model\Directpost\Request\Factory $requestFactory + * @param \Magento\Authorizenet\Model\Directpost\Response\Factory $responseFactory * @param \Magento\Authorizenet\Model\TransactionService $transactionService * @param \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory * @param \Magento\Sales\Model\OrderFactory $orderFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository - * @param OrderSender $orderSender + * @param \Magento\Sales\Model\Order\Email\Sender\OrderSender $orderSender * @param \Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -161,8 +171,8 @@ public function __construct( \Magento\Authorizenet\Helper\Data $dataHelper, \Magento\Authorizenet\Model\Directpost\Request\Factory $requestFactory, \Magento\Authorizenet\Model\Directpost\Response\Factory $responseFactory, - TransactionService $transactionService, - ZendClientFactory $httpClientFactory, + \Magento\Authorizenet\Model\TransactionService $transactionService, + \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory, \Magento\Sales\Model\OrderFactory $orderFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Quote\Api\CartRepositoryInterface $quoteRepository, @@ -170,7 +180,8 @@ public function __construct( \Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { $this->orderFactory = $orderFactory; $this->storeManager = $storeManager; @@ -179,6 +190,8 @@ public function __construct( $this->orderSender = $orderSender; $this->transactionRepository = $transactionRepository; $this->_code = static::METHOD_CODE; + $this->paymentFailures = $paymentFailures ? : ObjectManager::getInstance() + ->get(\Magento\Sales\Api\PaymentFailuresInterface::class); parent::__construct( $context, @@ -561,13 +574,10 @@ public function process(array $responseData) $this->validateResponse(); $response = $this->getResponse(); - //operate with order - $orderIncrementId = $response->getXInvoiceNum(); $responseText = $this->dataHelper->wrapGatewayError($response->getXResponseReasonText()); $isError = false; - if ($orderIncrementId) { - /* @var $order \Magento\Sales\Model\Order */ - $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + if ($this->getOrderIncrementId()) { + $order = $this->getOrderFromResponse(); //check payment method $payment = $order->getPayment(); if (!$payment || $payment->getMethod() != $this->getCode()) { @@ -632,9 +642,10 @@ public function checkResponseCode() return true; case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: - throw new \Magento\Framework\Exception\LocalizedException( - $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()) - ); + $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()); + $order = $this->getOrderFromResponse(); + $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMessage); + throw new \Magento\Framework\Exception\LocalizedException($errorMessage); default: throw new \Magento\Framework\Exception\LocalizedException( __('There was a payment authorization error.') @@ -988,12 +999,40 @@ protected function getTransactionResponse($transactionId) private function getPsrLogger() { if (null === $this->psrLogger) { - $this->psrLogger = \Magento\Framework\App\ObjectManager::getInstance() + $this->psrLogger = ObjectManager::getInstance() ->get(\Psr\Log\LoggerInterface::class); } return $this->psrLogger; } + /** + * Fetch order by increment id from response. + * + * @return \Magento\Sales\Model\Order + */ + private function getOrderFromResponse(): \Magento\Sales\Model\Order + { + if (!$this->order) { + $this->order = $this->orderFactory->create(); + + if ($incrementId = $this->getOrderIncrementId()) { + $this->order = $this->order->loadByIncrementId($incrementId); + } + } + + return $this->order; + } + + /** + * Fetch order increment id from response. + * + * @return string + */ + private function getOrderIncrementId(): string + { + return $this->getResponse()->getXInvoiceNum(); + } + /** * Checks if filter action is Report Only. Transactions that trigger this filter are processed as normal, * but are also reported in the Merchant Interface as triggering this filter. diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php index dbb6ac8333c14..95c67f67852da 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Authorizenet\Test\Unit\Model; +use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Framework\Simplexml\Element; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Authorizenet\Model\Directpost; @@ -74,6 +75,14 @@ class DirectpostTest extends \PHPUnit\Framework\TestCase */ protected $requestFactory; + /** + * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentFailures; + + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) @@ -104,6 +113,12 @@ protected function setUp() ->setMethods(['getTransactionDetails']) ->getMock(); + $this->paymentFailures = $this->getMockBuilder( + PaymentFailuresInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->requestFactory = $this->getRequestFactoryMock(); $httpClientFactoryMock = $this->getHttpClientFactoryMock(); @@ -117,7 +132,8 @@ protected function setUp() 'responseFactory' => $this->responseFactoryMock, 'transactionRepository' => $this->transactionRepositoryMock, 'transactionService' => $this->transactionServiceMock, - 'httpClientFactory' => $httpClientFactoryMock + 'httpClientFactory' => $httpClientFactoryMock, + 'paymentFailures' => $this->paymentFailures, ] ); } @@ -313,12 +329,16 @@ public function checkResponseCodeSuccessDataProvider() } /** - * @param bool $responseCode + * Checks response failures behaviour. + * + * @param int $responseCode + * @param int $failuresHandlerCalls + * @return void * * @expectedException \Magento\Framework\Exception\LocalizedException * @dataProvider checkResponseCodeFailureDataProvider */ - public function testCheckResponseCodeFailure($responseCode) + public function testCheckResponseCodeFailure(int $responseCode, int $failuresHandlerCalls): void { $reasonText = 'reason text'; @@ -333,18 +353,35 @@ public function testCheckResponseCodeFailure($responseCode) ->with($reasonText) ->willReturn(__('Gateway error: %1', $reasonText)); + $orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $orderMock->expects($this->exactly($failuresHandlerCalls)) + ->method('getQuoteId') + ->willReturn(1); + + $this->paymentFailures->expects($this->exactly($failuresHandlerCalls)) + ->method('handle') + ->with(1); + + $reflection = new \ReflectionClass($this->directpost); + $order = $reflection->getProperty('order'); + $order->setAccessible(true); + $order->setValue($this->directpost, $orderMock); + $this->directpost->checkResponseCode(); } /** * @return array */ - public function checkResponseCodeFailureDataProvider() + public function checkResponseCodeFailureDataProvider(): array { return [ - ['responseCode' => Directpost::RESPONSE_CODE_DECLINED], - ['responseCode' => Directpost::RESPONSE_CODE_ERROR], - ['responseCode' => 999999] + ['responseCode' => Directpost::RESPONSE_CODE_DECLINED, 1], + ['responseCode' => Directpost::RESPONSE_CODE_ERROR, 1], + ['responseCode' => 999999, 0], ]; } diff --git a/app/code/Magento/Backend/Block/Dashboard.php b/app/code/Magento/Backend/Block/Dashboard.php index 8d0a061621fe3..e1e87d8d4c5a3 100644 --- a/app/code/Magento/Backend/Block/Dashboard.php +++ b/app/code/Magento/Backend/Block/Dashboard.php @@ -20,7 +20,7 @@ class Dashboard extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'dashboard/index.phtml'; + protected $_template = 'Magento_Backend::dashboard/index.phtml'; /** * @return void diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index 301dffbdc4987..8e238ccab44cb 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Graph.php +++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php @@ -90,7 +90,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * @var string */ - protected $_template = 'dashboard/graph.phtml'; + protected $_template = 'Magento_Backend::dashboard/graph.phtml'; /** * Adminhtml dashboard data diff --git a/app/code/Magento/Backend/Block/Dashboard/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Grid.php index 602b5e414d538..f7f9a79f17eb0 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Grid.php +++ b/app/code/Magento/Backend/Block/Dashboard/Grid.php @@ -17,7 +17,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended /** * @var string */ - protected $_template = 'dashboard/grid.phtml'; + protected $_template = 'Magento_Backend::dashboard/grid.phtml'; /** * Setting default for every grid on dashboard diff --git a/app/code/Magento/Backend/Block/Dashboard/Sales.php b/app/code/Magento/Backend/Block/Dashboard/Sales.php index d0f056230bcd1..6d7a4d6458a8e 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Sales.php +++ b/app/code/Magento/Backend/Block/Dashboard/Sales.php @@ -15,7 +15,7 @@ class Sales extends \Magento\Backend\Block\Dashboard\Bar /** * @var string */ - protected $_template = 'dashboard/salebar.phtml'; + protected $_template = 'Magento_Backend::dashboard/salebar.phtml'; /** * @var \Magento\Framework\Module\Manager diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php index 96ae6dd636380..4dcda3677584c 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Totals.php +++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php @@ -16,7 +16,7 @@ class Totals extends \Magento\Backend\Block\Dashboard\Bar /** * @var string */ - protected $_template = 'dashboard/totalbar.phtml'; + protected $_template = 'Magento_Backend::dashboard/totalbar.phtml'; /** * @var \Magento\Framework\Module\Manager diff --git a/app/code/Magento/Backend/Block/Page/Copyright.php b/app/code/Magento/Backend/Block/Page/Copyright.php index 062497d6a8304..a1b61352930b5 100644 --- a/app/code/Magento/Backend/Block/Page/Copyright.php +++ b/app/code/Magento/Backend/Block/Page/Copyright.php @@ -18,5 +18,5 @@ class Copyright extends \Magento\Backend\Block\Template * * @var string */ - protected $_template = 'page/copyright.phtml'; + protected $_template = 'Magento_Backend::page/copyright.phtml'; } diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php index 368869b79e15c..3d1570e5ddfe7 100644 --- a/app/code/Magento/Backend/Block/Page/Footer.php +++ b/app/code/Magento/Backend/Block/Page/Footer.php @@ -17,7 +17,7 @@ class Footer extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'page/footer.phtml'; + protected $_template = 'Magento_Backend::page/footer.phtml'; /** * @var \Magento\Framework\App\ProductMetadataInterface diff --git a/app/code/Magento/Backend/Block/Page/Header.php b/app/code/Magento/Backend/Block/Page/Header.php index b7ed05ce58e95..c2c5f7472b370 100644 --- a/app/code/Magento/Backend/Block/Page/Header.php +++ b/app/code/Magento/Backend/Block/Page/Header.php @@ -18,7 +18,7 @@ class Header extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'page/header.phtml'; + protected $_template = 'Magento_Backend::page/header.phtml'; /** * Backend data diff --git a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php index 2f9b73f0ae037..6fe8416784c2e 100644 --- a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php +++ b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset.php @@ -25,7 +25,7 @@ class Fieldset extends \Magento\Backend\Block\Template implements RendererInterf /** * @var string */ - protected $_template = 'store/switcher/form/renderer/fieldset.phtml'; + protected $_template = 'Magento_Backend::store/switcher/form/renderer/fieldset.phtml'; /** * Retrieve an element diff --git a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php index ddd1f1a9178cd..71d4db6849bd2 100644 --- a/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php +++ b/app/code/Magento/Backend/Block/Store/Switcher/Form/Renderer/Fieldset/Element.php @@ -23,7 +23,7 @@ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Eleme /** * @var string */ - protected $_template = 'store/switcher/form/renderer/fieldset/element.phtml'; + protected $_template = 'Magento_Backend::store/switcher/form/renderer/fieldset/element.phtml'; /** * Retrieve an element diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php index ff0399e4f507f..b3f467ce37c88 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php @@ -68,10 +68,7 @@ public function __construct( $this->_storeManager = $storeManager; $this->_currencyLocator = $currencyLocator; $this->_localeCurrency = $localeCurrency; - $defaultBaseCurrencyCode = $this->_scopeConfig->getValue( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, - 'default' - ); + $defaultBaseCurrencyCode = $currencyLocator->getDefaultCurrency($this->_request); $this->_defaultBaseCurrency = $currencyFactory->create()->load($defaultBaseCurrencyCode); } diff --git a/app/code/Magento/Backend/etc/adminhtml/di.xml b/app/code/Magento/Backend/etc/adminhtml/di.xml index 1276033b33056..e08d4ac202756 100644 --- a/app/code/Magento/Backend/etc/adminhtml/di.xml +++ b/app/code/Magento/Backend/etc/adminhtml/di.xml @@ -139,14 +139,17 @@ - + Magento\Config\Model\Config\Structure\ElementVisibilityInterface::HIDDEN - Magento\Config\Model\Config\Structure\ElementVisibilityInterface::DISABLED - - + + + + + + Magento\Config\Model\Config\Structure\ElementVisibilityInterface::DISABLED diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml index 27127e54e5be2..a115777624e91 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml @@ -41,11 +41,11 @@ - + diff --git a/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php b/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php index 1d5057d83d6cf..f9fae8a469b1d 100644 --- a/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php +++ b/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php @@ -24,7 +24,7 @@ class AvsEmsCodeMapper implements PaymentVerificationInterface * * @var string */ - private static $unavailableCode = 'U'; + private static $unavailableCode = ''; /** * List of mapping AVS codes diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php index ebadc1703ecad..b3a7f8b9ee76a 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/PayPal/VaultDetailsHandlerTest.php @@ -88,7 +88,7 @@ protected function setUp() $this->paymentInfoMock = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() - ->setMethods(['__wakeup']) + ->setMethods(['__wakeup', 'getExtensionAttributes']) ->getMock(); $this->paymentTokenMock = $objectManager->getObject(PaymentToken::class); @@ -107,6 +107,10 @@ protected function setUp() ->setMethods(['create']) ->getMock(); + $this->paymentInfoMock->expects(self::any()) + ->method('getExtensionAttributes') + ->willReturn($this->paymentExtensionMock); + $this->subject = [ 'payment' => $this->paymentDataObjectMock, ]; @@ -184,7 +188,7 @@ public function testHandleWithoutToken() ->method('create'); $this->handler->handle($this->subject, $response); - self::assertNull($this->paymentInfoMock->getExtensionAttributes()); + self::assertNotNull($this->paymentInfoMock->getExtensionAttributes()); } /** diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php index 74592c6869ed3..c8ec52560be29 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php @@ -80,9 +80,11 @@ protected function setUp() $this->payment = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() - ->setMethods(['__wakeup']) + ->setMethods(['__wakeup', 'getExtensionAttributes']) ->getMock(); + $this->payment->expects(self::any())->method('getExtensionAttributes')->willReturn($this->paymentExtension); + $config = $this->getConfigMock(); $this->paymentHandler = new VaultDetailsHandler( diff --git a/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php b/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php index 9b80a2237a8fb..c82634d36db31 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php @@ -84,11 +84,11 @@ public function testGetCodeWithException() public function getCodeDataProvider() { return [ - ['avsZip' => null, 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => null, 'avsStreet' => 'M', 'expected' => 'U'], - ['avsZip' => 'M', 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => 'M', 'avsStreet' => 'Unknown', 'expected' => 'U'], - ['avsZip' => 'I', 'avsStreet' => 'A', 'expected' => 'U'], + ['avsZip' => null, 'avsStreet' => null, 'expected' => ''], + ['avsZip' => null, 'avsStreet' => 'M', 'expected' => ''], + ['avsZip' => 'M', 'avsStreet' => null, 'expected' => ''], + ['avsZip' => 'M', 'avsStreet' => 'Unknown', 'expected' => ''], + ['avsZip' => 'I', 'avsStreet' => 'A', 'expected' => ''], ['avsZip' => 'M', 'avsStreet' => 'M', 'expected' => 'Y'], ['avsZip' => 'N', 'avsStreet' => 'M', 'expected' => 'A'], ['avsZip' => 'M', 'avsStreet' => 'N', 'expected' => 'Z'], diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php index 39863e6561c43..76bf5b659bda3 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php @@ -13,6 +13,7 @@ use Magento\Braintree\Observer\DataAssignObserver; use Magento\Braintree\Gateway\Config\PayPal\Config; use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; +use Magento\Quote\Api\Data\CartExtensionInterface; /** * Class QuoteUpdaterTest @@ -281,7 +282,7 @@ private function updateQuoteStep(\PHPUnit_Framework_MockObject_MockObject $quote */ private function getQuoteMock() { - return $this->getMockBuilder(Quote::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->setMethods( [ 'getIsVirtual', @@ -291,9 +292,21 @@ private function getQuoteMock() 'collectTotals', 'getShippingAddress', 'getBillingAddress', + 'getExtensionAttributes' ] )->disableOriginalConstructor() ->getMock(); + + $cartExtensionMock = $this->getMockBuilder(CartExtensionInterface::class) + ->setMethods(['setShippingAssignments']) + ->disableOriginalConstructor() + ->getMock(); + + $quoteMock->expects(self::any()) + ->method('getExtensionAttributes') + ->willReturn($cartExtensionMock); + + return $quoteMock; } /** diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml index 5f4a345760f2d..2bb4cea6742d6 100644 --- a/app/code/Magento/Braintree/etc/di.xml +++ b/app/code/Magento/Braintree/etc/di.xml @@ -447,7 +447,7 @@ - Magento\Vault\Model\AccountPaymentTokenFactory + Magento\Vault\Api\Data\PaymentTokenFactoryInterface diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml index 13249cd8e0e80..535a5a852fe70 100644 --- a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml @@ -84,7 +84,7 @@ $ccType = $block->getInfoData('cc_type'); name="payment[is_active_payment_token_enabler]" class="admin__control-checkbox"/> diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php index 0b3a938255de1..b220e2c98d77c 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php @@ -17,7 +17,7 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/checkbox.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/checkbox.phtml'; /** * @param string $elementId diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php index 304b3a5cf34ed..a4b8c6bde73aa 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php @@ -17,7 +17,7 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/multi.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/multi.phtml'; /** * @param string $elementId diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php index e011ab36e8029..1519b3a67ac97 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php @@ -17,7 +17,7 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/radio.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/radio.phtml'; /** * @param string $elementId diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php index f1206db359b5c..502dfa32044a3 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php @@ -17,7 +17,7 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti /** * @var string */ - protected $_template = 'product/composite/fieldset/options/type/select.phtml'; + protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/select.phtml'; /** * @param string $elementId diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php index f124740a766ab..8be512a3e6348 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle.php @@ -20,7 +20,7 @@ class Bundle extends \Magento\Backend\Block\Widget implements \Magento\Backend\B /** * @var string */ - protected $_template = 'product/edit/bundle.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle.phtml'; /** * Core registry diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php index 13c5dcc81afb3..19da6bc6244e5 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php @@ -26,7 +26,7 @@ class Option extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'product/edit/bundle/option.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle/option.phtml'; /** * Core registry diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php index 5b73c22b5781a..cf4814d3cd778 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search.php @@ -15,7 +15,7 @@ class Search extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'product/edit/bundle/option/search.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle/option/search.phtml'; /** * @return void diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php index 353808dc66a72..cf88f9b93d32f 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Selection.php @@ -15,7 +15,7 @@ class Selection extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'product/edit/bundle/option/selection.phtml'; + protected $_template = 'Magento_Bundle::product/edit/bundle/option/selection.phtml'; /** * Catalog data diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php index 8ca0cf8a5159e..83730d4eae2bd 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Checkbox.php @@ -16,5 +16,5 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/checkbox.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/checkbox.phtml'; } diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php index 3319db8cff1d5..79e94a18a789e 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Multi.php @@ -16,7 +16,7 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/multi.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/multi.phtml'; /** * @inheritdoc diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php index 84a619dafab52..07c113bd8e4bb 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Radio.php @@ -16,5 +16,5 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/radio.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/radio.phtml'; } diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php index d7f1cf41057a8..63f0d35bda0f0 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option/Select.php @@ -16,5 +16,5 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti /** * @var string */ - protected $_template = 'catalog/product/view/type/bundle/option/select.phtml'; + protected $_template = 'Magento_Bundle::catalog/product/view/type/bundle/option/select.phtml'; } diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index c0624be8e7a97..98fd96c52ccd9 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -314,7 +314,8 @@ protected function getBundleOptions() 'template' => 'ui/dynamic-rows/templates/collapsible', 'additionalClasses' => 'admin__field-wide', 'dataScope' => 'data.bundle_options', - 'bundleSelectionsName' => 'product_bundle_container.bundle_selections' + 'isDefaultFieldScope' => 'is_default', + 'bundleSelectionsName' => 'product_bundle_container.bundle_selections', ], ], ], @@ -378,7 +379,10 @@ protected function getBundleOptions() 'selection_qty' => '', ], 'links' => ['insertData' => '${ $.provider }:${ $.dataProvider }'], - 'source' => 'product' + 'imports' => [ + 'inputType' => '${$.provider}:${$.dataScope}.type', + ], + 'source' => 'product', ], ], ], @@ -594,11 +598,14 @@ protected function getBundleSelections() 'config' => [ 'componentType' => Container::NAME, 'isTemplate' => true, - 'component' => 'Magento_Bundle/js/components/bundle-record', + 'component' => 'Magento_Ui/js/dynamic-rows/record', 'is_collection' => true, 'imports' => [ - 'onTypeChanged' => '${ $.provider }:${ $.bundleOptionsDataScope }.type' - ] + 'inputType' => '${$.parentName}:inputType', + ], + 'exports' => [ + 'isDefaultValue' => '${$.parentName}:isDefaultValue.${$.index}', + ], ], ], ], @@ -691,11 +698,15 @@ protected function getBundleSelections() 'componentType' => Form\Field::NAME, 'formElement' => Form\Element\Checkbox::NAME, 'dataType' => Form\Element\DataType\Price::NAME, + 'component' => 'Magento_Bundle/js/components/bundle-user-defined-checkbox', 'label' => __('User Defined'), 'dataScope' => 'selection_can_change_qty', 'value' => '1', 'valueMap' => ['true' => '1', 'false' => '0'], 'sortOrder' => 110, + 'imports' => [ + 'inputType' => '${$.parentName}:inputType', + ], ], ], ], diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js index b608cff85b067..09331d37bb3b6 100644 --- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-checkbox.js @@ -14,7 +14,10 @@ define([ clearing: false, parentContainer: '', parentSelections: '', - changer: '' + changer: '', + exports: { + value: '${$.parentName}:isDefaultValue' + } }, /** @@ -58,10 +61,6 @@ define([ this.prefer = typeMap[type]; this.elementTmpl(this.templates[typeMap[type]]); - - if (this.prefer === 'radio' && this.checked()) { - this.clearValues(); - } }, /** diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js index 428361f459544..a6fc84765cc65 100644 --- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js @@ -14,7 +14,57 @@ define([ label: '', columnsHeader: false, columnsHeaderAfterRender: true, - addButton: false + addButton: false, + isDefaultFieldScope: 'is_default', + defaultRecords: { + use: [], + moreThanOne: false, + state: {} + }, + listens: { + inputType: 'onInputTypeChange', + isDefaultValue: 'onIsDefaultValue' + } + }, + + /** + * Handler for type select. + * + * @param {String} inputType - changed. + */ + onInputTypeChange: function (inputType) { + if (this.defaultRecords.moreThanOne && (inputType === 'radio' || inputType === 'select')) { + _.each(this.defaultRecords.use, function (index, counter) { + this.source.set( + this.dataScope + '.bundle_selections.' + index + '.' + this.isDefaultFieldScope, + counter ? '0' : '1' + ); + }.bind(this)); + } + }, + + /** + * Handler for is_default field. + * + * @param {Object} data - changed data. + */ + onIsDefaultValue: function (data) { + var cb, + use = 0; + + this.defaultRecords.use = []; + + cb = function (elem, key) { + + if (~~elem) { + this.defaultRecords.use.push(key); + use++; + } + + this.defaultRecords.moreThanOne = use > 1; + }.bind(this); + + _.each(data, cb); }, /** @@ -29,7 +79,6 @@ define([ recordIndex; this.parsePagesData(data); - this.templates.record.bundleOptionsDataScope = this.dataScope; if (newData.length) { if (this.insertData().length) { diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js new file mode 100644 index 0000000000000..a7ceded02d0c3 --- /dev/null +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-user-defined-checkbox.js @@ -0,0 +1,30 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/single-checkbox' +], function (Checkbox) { + 'use strict'; + + return Checkbox.extend({ + defaults: { + listens: { + inputType: 'onInputTypeChange' + } + }, + + /** + * Handler for "inputType" property + * + * @param {String} data + */ + onInputTypeChange: function (data) { + data === 'checkbox' || data === 'multi' ? + this.clear() + .visible(false) : + this.visible(true); + } + }); +}); diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index 1d2ac3a87ab77..3ed7e144ddd5a 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -411,6 +411,7 @@ public function saveData() $this->populateExistingOptions(); $this->insertOptions(); $this->insertSelections(); + $this->insertParentChildRelations(); $this->clear(); } } @@ -659,6 +660,32 @@ protected function insertSelections() return $this; } + /** + * Insert parent/child product relations + * + * @return \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType + */ + private function insertParentChildRelations() + { + foreach ($this->_cachedOptions as $productId => $options) { + $childIds = []; + foreach ($options as $option) { + foreach ($option['selections'] as $selection) { + if (!isset($selection['parent_product_id'])) { + if (!isset($this->_cachedSkuToProducts[$selection['sku']])) { + continue; + } + $childIds[] = $this->_cachedSkuToProducts[$selection['sku']]; + } + } + + $this->relationsDataSaver->saveProductRelations($productId, $childIds); + } + } + + return $this; + } + /** * Initialize attributes parameters for all attributes' sets. * diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle/RelationsDataSaver.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle/RelationsDataSaver.php index a58195f823bf1..5409d12ac56d3 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle/RelationsDataSaver.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle/RelationsDataSaver.php @@ -5,6 +5,9 @@ */ namespace Magento\BundleImportExport\Model\Import\Product\Type\Bundle; +use Magento\Catalog\Model\ResourceModel\Product\Relation; +use Magento\Framework\App\ObjectManager; + /** * A bundle product relations (options, selections, etc.) data saver. * @@ -17,13 +20,22 @@ class RelationsDataSaver */ private $resource; + /** + * @var Relation + */ + private $productRelation; + /** * @param \Magento\Framework\App\ResourceConnection $resource + * @param Relation $productRelation */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource + \Magento\Framework\App\ResourceConnection $resource, + Relation $productRelation = null ) { - $this->resource = $resource; + $this->resource = $resource; + $this->productRelation = $productRelation + ?: ObjectManager::getInstance()->get(Relation::class); } /** @@ -92,4 +104,17 @@ public function saveSelections(array $selections) ); } } + + /** + * Saves given parent/child relations. + * + * @param int $parentId + * @param array $childIds + * + * @return void + */ + public function saveProductRelations($parentId, $childIds) + { + $this->productRelation->processRelations($parentId, $childIds); + } } diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/Bundle/RelationsDataSaverTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/Bundle/RelationsDataSaverTest.php index 42d508cdfb195..d50243b3656f3 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/Bundle/RelationsDataSaverTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/Bundle/RelationsDataSaverTest.php @@ -7,6 +7,7 @@ namespace Magento\BundleImportExport\Test\Unit\Model\Import\Product\Type\Bundle; use Magento\BundleImportExport\Model\Import\Product\Type\Bundle\RelationsDataSaver; +use Magento\Catalog\Model\ResourceModel\Product\Relation; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; @@ -30,6 +31,11 @@ class RelationsDataSaverTest extends \PHPUnit\Framework\TestCase */ private $connectionMock; + /** + * @var Relation|\PHPUnit_Framework_MockObject_MockObject + */ + private $productRelationMock; + protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -39,12 +45,16 @@ protected function setUp() $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->resourceMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); + + $this->productRelationMock = $this->getMockBuilder(Relation::class) + ->disableOriginalConstructor() + ->getMock(); $this->relationsDataSaver = $helper->getObject( RelationsDataSaver::class, [ - 'resource' => $this->resourceMock + 'resource' => $this->resourceMock, + 'productRelation' => $this->productRelationMock ] ); } @@ -53,7 +63,7 @@ public function testSaveOptions() { $options = [1, 2]; $table_name= 'catalog_product_bundle_option'; - + $this->resourceMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); $this->resourceMock->expects($this->once()) ->method('getTableName') ->with('catalog_product_bundle_option') @@ -78,6 +88,7 @@ public function testSaveOptionValues() $optionsValues = [1, 2]; $table_name= 'catalog_product_bundle_option_value'; + $this->resourceMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); $this->resourceMock->expects($this->once()) ->method('getTableName') ->with('catalog_product_bundle_option_value') @@ -98,6 +109,7 @@ public function testSaveSelections() $selections = [1, 2]; $table_name= 'catalog_product_bundle_selection'; + $this->resourceMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); $this->resourceMock->expects($this->once()) ->method('getTableName') ->with('catalog_product_bundle_selection') @@ -121,4 +133,16 @@ public function testSaveSelections() $this->relationsDataSaver->saveSelections($selections); } + + public function testSaveProductRelations() + { + $parentId = 1; + $children = [2, 3]; + + $this->productRelationMock->expects($this->once()) + ->method('processRelations') + ->with($parentId, $children); + + $this->relationsDataSaver->saveProductRelations($parentId, $children); + } } diff --git a/app/code/Magento/Captcha/i18n/en_US.csv b/app/code/Magento/Captcha/i18n/en_US.csv index 3c56d3f0d393d..480107df8adfe 100644 --- a/app/code/Magento/Captcha/i18n/en_US.csv +++ b/app/code/Magento/Captcha/i18n/en_US.csv @@ -20,11 +20,7 @@ Forms,Forms "Number of Symbols","Number of Symbols" "Please specify 8 symbols at the most. Range allowed (e.g. 3-5)","Please specify 8 symbols at the most. Range allowed (e.g. 3-5)" "Symbols Used in CAPTCHA","Symbols Used in CAPTCHA" -" - Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.
Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer. - "," - Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.
Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer. - " +"Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.
Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer.","Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.
Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer." "Case Sensitive","Case Sensitive" "Enable CAPTCHA on Storefront","Enable CAPTCHA on Storefront" "CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen.","CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen." diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index 4b0d233d3e77b..34da5bb1d4ca1 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -325,7 +325,7 @@ public function getBreadcrumbsJavascript($path, $javascriptVarName) * * @param Node|array $node * @param int $level - * @return string + * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php index 5e98313f95f0f..b5330ab66af71 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php @@ -144,7 +144,7 @@ function (node, e) { * * @param \Magento\Framework\Data\Tree\Node|array $node * @param int $level - * @return string + * @return array */ protected function _getNodeJson($node, $level = 0) { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php index 10214fc1d16fd..ad6df27b89334 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php @@ -21,7 +21,7 @@ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Eleme /** * Retrieve data object related with form * - * @return \Magento\Catalog\Model\Product || \Magento\Catalog\Model\Category + * @return \Magento\Catalog\Model\Product|\Magento\Catalog\Model\Category */ public function getDataObject() { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php index ab5026b1e69b9..66e04ef03f771 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php @@ -101,8 +101,7 @@ protected function _prepareColumns() 'type' => 'options', 'options' => ['1' => __('Yes'), '0' => __('No')], 'align' => 'center' - ], - 'is_user_defined' + ] ); $this->_eventManager->dispatch('product_attribute_grid_build', ['grid' => $this]); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php index d5f66231f1d82..e1b97f996c769 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php @@ -81,7 +81,7 @@ public function getSelectorOptions() * * @param string $labelPart * @param int $templateId - * @return \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection + * @return array */ public function getSuggestedAttributes($labelPart, $templateId = null) { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php b/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php index dbeff93683bc0..9d13d89d54b80 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Rss/Grid/Link.php @@ -69,7 +69,7 @@ public function isRssAllowed() } /** - * @return string + * @return array */ protected function getLinkParams() { diff --git a/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php index 03932151358ab..99399110505b7 100644 --- a/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php +++ b/app/code/Magento/Catalog/Block/Category/Plugin/PriceBoxTags.php @@ -71,7 +71,7 @@ public function afterGetCacheKey(PriceBox $subject, $result) '-', [ $result, - $this->priceCurrency->getCurrencySymbol(), + $this->priceCurrency->getCurrency()->getCode(), $this->dateTime->scopeDate($this->scopeResolver->getScope()->getId())->format('Ymd'), $this->scopeResolver->getScope()->getId(), $this->customerSession->getCustomerGroupId(), diff --git a/app/code/Magento/Catalog/Block/Category/Rss/Link.php b/app/code/Magento/Catalog/Block/Category/Rss/Link.php index 0599d5f4b989c..e40b81200574c 100644 --- a/app/code/Magento/Catalog/Block/Category/Rss/Link.php +++ b/app/code/Magento/Catalog/Block/Category/Rss/Link.php @@ -62,7 +62,7 @@ public function getLabel() } /** - * @return string + * @return array */ protected function getLinkParams() { diff --git a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php index f22edd91e7914..4102c82a0a316 100644 --- a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php +++ b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php @@ -195,7 +195,7 @@ public function getAddToCompareUrl() * Gets minimal sales quantity * * @param \Magento\Catalog\Model\Product $product - * @return int|null + * @return float|null */ public function getMinimalQty($product) { diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php index c561913dee4d3..6c54aa4e171ef 100644 --- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php @@ -213,6 +213,22 @@ public function getProductAttributeValue($product, $attribute) return (string)$value == '' ? __('No') : $value; } + /** + * Check if any of the products has a value set for the attribute + * + * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute + * @return bool + */ + public function hasAttributeValueForProducts($attribute) + { + foreach ($this->getItems() as $item) { + if ($item->hasData($attribute->getAttributeCode())) { + return true; + } + } + return false; + } + /** * Retrieve Print URL * diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php index 66bf5eafb156e..d582005f653ef 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -178,7 +178,7 @@ public function getPrice($price, $includingTax = null) * Returns price converted to current currency rate * * @param float $price - * @return float + * @return float|string */ public function getCurrencyPrice($price) { diff --git a/app/code/Magento/Catalog/Block/Rss/Product/Special.php b/app/code/Magento/Catalog/Block/Rss/Product/Special.php index c61bee4417cbc..a9107f14cc5e4 100644 --- a/app/code/Magento/Catalog/Block/Rss/Product/Special.php +++ b/app/code/Magento/Catalog/Block/Rss/Product/Special.php @@ -107,7 +107,7 @@ protected function _construct() } /** - * @return string + * @return array */ public function getRssData() { 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 6a9abe0a4c64e..e054a9d49b437 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -139,9 +139,7 @@ public function execute() ->setName($name) ->getAttributeSet(); } catch (AlreadyExistsException $alreadyExists) { - $this->messageManager->addErrorMessage( - __('A "%1" attribute set name already exists. Create a new name and try again.', $name) - ); + $this->messageManager->addErrorMessage(__('An attribute set named \'%1\' already exists.', $name)); $this->_session->setAttributeData($data); return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]); } catch (LocalizedException $e) { @@ -202,6 +200,8 @@ public function execute() } } + $data = $this->presentation->convertPresentationDataToInputType($data); + if ($attributeId) { if (!$model->getId()) { $this->messageManager->addErrorMessage(__('This attribute no longer exists.')); @@ -216,7 +216,7 @@ public function execute() $data['attribute_code'] = $model->getAttributeCode(); $data['is_user_defined'] = $model->getIsUserDefined(); - $data['frontend_input'] = $model->getFrontendInput(); + $data['frontend_input'] = $data['frontend_input'] ?? $model->getFrontendInput(); } else { /** * @todo add to helper and specify all relations for properties @@ -229,8 +229,6 @@ public function execute() ); } - $data = $this->presentation->convertPresentationDataToInputType($data); - $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0]; if ($model->getIsUserDefined() === null || $model->getIsUserDefined() != 0) { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index beb6f2b13bcfe..95339870b4d61 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeDefaultValueFilter; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; use Magento\Catalog\Model\Product\Link\Resolver as LinkResolver; +use Magento\Catalog\Model\Product\LinkTypeProvider; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; @@ -81,7 +85,7 @@ class Helper private $dateTimeFilter; /** - * @var \Magento\Catalog\Model\Product\LinkTypeProvider + * @var LinkTypeProvider */ private $linkTypeProvider; @@ -99,10 +103,10 @@ class Helper * @param ProductLinks $productLinks * @param \Magento\Backend\Helper\Js $jsHelper * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter - * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory|null $customOptionFactory - * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory - * @param \Magento\Catalog\Api\ProductRepositoryInterface|null $productRepository - * @param \Magento\Catalog\Model\Product\LinkTypeProvider|null $linkTypeProvider + * @param CustomOptionFactory|null $customOptionFactory + * @param ProductLinkFactory |null $productLinkFactory + * @param ProductRepositoryInterface|null $productRepository + * @param LinkTypeProvider|null $linkTypeProvider * @param AttributeFilter|null $attributeFilter * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -113,10 +117,10 @@ public function __construct( \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks, \Magento\Backend\Helper\Js $jsHelper, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory = null, - \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository = null, - \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider = null, + CustomOptionFactory $customOptionFactory = null, + ProductLinkFactory $productLinkFactory = null, + ProductRepositoryInterface $productRepository = null, + LinkTypeProvider $linkTypeProvider = null, AttributeFilter $attributeFilter = null ) { $this->request = $request; @@ -125,16 +129,13 @@ public function __construct( $this->productLinks = $productLinks; $this->jsHelper = $jsHelper; $this->dateFilter = $dateFilter; - $this->customOptionFactory = $customOptionFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); - $this->productLinkFactory = $productLinkFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); - $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->linkTypeProvider = $linkTypeProvider ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Product\LinkTypeProvider::class); - $this->attributeFilter = $attributeFilter ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(AttributeFilter::class); + + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $this->customOptionFactory = $customOptionFactory ?: $objectManager->get(CustomOptionFactory::class); + $this->productLinkFactory = $productLinkFactory ?: $objectManager->get(ProductLinkFactory::class); + $this->productRepository = $productRepository ?: $objectManager->get(ProductRepositoryInterface::class); + $this->linkTypeProvider = $linkTypeProvider ?: $objectManager->get(LinkTypeProvider::class); + $this->attributeFilter = $attributeFilter ?: $objectManager->get(AttributeFilter::class); } /** @@ -150,8 +151,7 @@ public function __construct( */ public function initializeFromData(\Magento\Catalog\Model\Product $product, array $productData) { - unset($productData['custom_attributes']); - unset($productData['extension_attributes']); + unset($productData['custom_attributes'], $productData['extension_attributes']); if ($productData) { $stockData = isset($productData['stock_data']) ? $productData['stock_data'] : []; @@ -199,28 +199,13 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra $productData['tier_price'] = isset($productData['tier_price']) ? $productData['tier_price'] : []; $useDefaults = (array)$this->request->getPost('use_default', []); - $productData = $this->attributeFilter->prepareProductAttributes($product, $productData, $useDefaults); - $product->addData($productData); if ($wasLockedMedia) { $product->lockAttribute('media'); } - /** - * Check "Use Default Value" checkboxes values - */ - foreach ($useDefaults as $attributeCode => $useDefaultState) { - if ($useDefaultState) { - $product->setData($attributeCode, null); - // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false - if ($product->hasData('use_config_' . $attributeCode)) { - $product->setData('use_config_' . $attributeCode, false); - } - } - } - $product = $this->setProductLinks($product); $product = $this->fillProductOptions($product, $productOptions); 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 ee3a6d491e92f..188b0b22f33bf 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 @@ -28,11 +28,17 @@ class AttributeFilter * @param array $useDefaults * @return array */ - public function prepareProductAttributes(Product $product, array $productData, array $useDefaults) + public function prepareProductAttributes(Product $product, array $productData, array $useDefaults): array { - foreach ($productData as $attribute => $value) { - if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attribute, $value)) { - unset($productData[$attribute]); + $attributeList = $product->getAttributes(); + foreach ($productData as $attributeCode => $attributeValue) { + if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attributeCode, $attributeValue)) { + unset($productData[$attributeCode]); + } + + if (isset($useDefaults[$attributeCode]) && $useDefaults[$attributeCode] === '1') { + $productData = $this->prepareDefaultData($attributeList, $attributeCode, $productData); + $productData = $this->prepareConfigData($product, $attributeCode, $productData); } } @@ -41,14 +47,54 @@ public function prepareProductAttributes(Product $product, array $productData, a /** * @param Product $product - * @param $useDefaults - * @param $attribute - * @param $value + * @param string $attributeCode + * @param array $productData + * @return array + */ + private function prepareConfigData(Product $product, string $attributeCode, array $productData): array + { + // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false + if ($product->hasData('use_config_' . $attributeCode)) { + $productData['use_config_' . $attributeCode] = false; + } + + return $productData; + } + + /** + * @param array $attributeList + * @param string $attributeCode + * @param array $productData + * @return array + */ + private function prepareDefaultData(array $attributeList, string $attributeCode, array $productData): array + { + if (isset($attributeList[$attributeCode])) { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + $attribute = $attributeList[$attributeCode]; + $attributeType = $attribute->getBackendType(); + // For non-numberic types set the attributeValue to 'false' to trigger their removal from the db + if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { + $attribute->setIsRequired(false); + $productData[$attributeCode] = false; + } else { + $productData[$attributeCode] = null; + } + } + + return $productData; + } + + /** + * @param Product $product + * @param array $useDefaults + * @param string $attribute + * @param mixed $value * @return bool */ - private function isAttributeShouldNotBeUpdated(Product $product, $useDefaults, $attribute, $value) : bool + private function isAttributeShouldNotBeUpdated(Product $product, array $useDefaults, $attribute, $value): bool { - $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === "1"; + $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === '1'; return ($value === '' && $considerUseDefaultsAttribute && !$product->getData($attribute)); } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php index d2f8925d09a7b..1278434fcad43 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/TableMaintainer.php @@ -13,6 +13,9 @@ use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver as TableResolver; +/** + * Class encapsulate logic of work with tables per store in Category Product indexer + */ class TableMaintainer { /** @@ -202,9 +205,14 @@ public function createMainTmpTable(int $storeId) * @param $storeId * * @return string + * + * @throws \Exception */ public function getMainTmpTable(int $storeId) { + if (!isset($this->mainTmpTable[$storeId])) { + throw new \Exception('Temporary table does not exist'); + } return $this->mainTmpTable[$storeId]; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php index 6a2642a8568f4..b6206f96b91e0 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Eav; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\AbstractEav; + /** * Abstract action reindex class */ @@ -51,7 +53,7 @@ abstract public function execute($ids); /** * Retrieve array of EAV type indexers * - * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\AbstractEav[] + * @return AbstractEav[] */ public function getIndexers() { @@ -69,7 +71,7 @@ public function getIndexers() * Retrieve indexer instance by type * * @param string $type - * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\AbstractEav + * @return AbstractEav * @throws \Magento\Framework\Exception\LocalizedException */ public function getIndexer($type) @@ -108,7 +110,7 @@ public function reindex($ids = null) /** * Synchronize data between index storage and original storage * - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\AbstractEav $indexer + * @param AbstractEav $indexer * @param string $destinationTable * @param array $ids * @throws \Exception @@ -134,16 +136,17 @@ protected function syncData($indexer, $destinationTable, $ids) /** * Retrieve product relations by children and parent * - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\AbstractEav $indexer + * @param AbstractEav $indexer * @param array $ids - * * @param bool $onlyParents * @return array $ids */ - protected function processRelations($indexer, $ids, $onlyParents = false) + protected function processRelations(AbstractEav $indexer, array $ids, bool $onlyParents = false) { $parentIds = $indexer->getRelationsByChild($ids); + $parentIds = array_unique(array_merge($parentIds, $ids)); $childIds = $onlyParents ? [] : $indexer->getRelationsByParent($parentIds); + return array_unique(array_merge($ids, $childIds, $parentIds)); } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index cfa5ec91a2e1b..7aed842713f5d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -336,9 +336,10 @@ protected function _reindexRows($changedIds = []) if (!empty($notCompositeIds)) { $parentProductsTypes = $this->getParentProductsTypes($notCompositeIds); $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes); - $parentProductsIds = array_keys($parentProductsTypes); - $compositeIds = $compositeIds + array_combine($parentProductsIds, $parentProductsIds); - $changedIds = array_merge($changedIds, $parentProductsIds); + foreach ($parentProductsTypes as $parentProductsIds) { + $compositeIds = $compositeIds + $parentProductsIds; + $changedIds = array_merge($changedIds, $parentProductsIds); + } } if (!empty($compositeIds)) { @@ -370,7 +371,8 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) ['child_id'] )->join( ['e' => $this->_defaultIndexerResource->getTable('catalog_product_entity')], - 'e.' . $linkField . ' = parent_id' + 'e.' . $linkField . ' = parent_id', + [] )->where( 'e.entity_id IN(?)', $parentIds diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index eb15833a7d0b2..ba04af8ec1f41 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -109,7 +109,7 @@ public function execute($ids = null) // Prepare replica table for indexation. $this->_defaultIndexerResource->getConnection()->truncateTable($replicaTable); - /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\AbstractIndexer $indexer */ + /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */ foreach ($this->getTypeIndexers() as $indexer) { $indexer->getTableStrategy()->setUseIdxTable(false); $connection = $indexer->getConnection(); diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index e58c9aab77665..f514e5c68769e 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -513,19 +513,6 @@ public function getStoreId() return $this->_storeManager->getStore()->getId(); } - /** - * Get collection instance - * - * @return object - * @deprecated 101.1.0 because collections should be used directly via factory - */ - public function getResourceCollection() - { - $collection = parent::getResourceCollection(); - $collection->setStoreId($this->getStoreId()); - return $collection; - } - /** * Get product url model * @@ -2108,6 +2095,8 @@ public function reset() /** * Get cache tags associated with object id * + * @deprecated + * @see \Magento\Catalog\Model\Product::getIdentities * @return string[] */ public function getCacheIdTags() @@ -2533,13 +2522,7 @@ public function setTypeId($typeId) */ public function getExtensionAttributes() { - $extensionAttributes = $this->_getExtensionAttributes(); - if (null === $extensionAttributes) { - /** @var \Magento\Catalog\Api\Data\ProductExtensionInterface $extensionAttributes */ - $extensionAttributes = $this->extensionAttributesFactory->create(ProductInterface::class); - $this->setExtensionAttributes($extensionAttributes); - } - return $extensionAttributes; + return $this->_getExtensionAttributes(); } /** diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Inputtype/Presentation.php b/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Inputtype/Presentation.php index 28e0f22fc6ec9..03b8c7aa1cadc 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Inputtype/Presentation.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Frontend/Inputtype/Presentation.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; @@ -19,9 +21,9 @@ class Presentation * Get input type for presentation layer from stored input type. * * @param Attribute $attribute - * @return string + * @return string|null */ - public function getPresentationInputType(Attribute $attribute) + public function getPresentationInputType(Attribute $attribute) :?string { $inputType = $attribute->getFrontendInput(); if ($inputType == 'textarea' && $attribute->getIsWysiwygEnabled()) { @@ -37,12 +39,12 @@ public function getPresentationInputType(Attribute $attribute) * * @return array */ - public function convertPresentationDataToInputType(array $data) + public function convertPresentationDataToInputType(array $data) : array { - if ($data['frontend_input'] === 'texteditor') { + if (isset($data['frontend_input']) && $data['frontend_input'] === 'texteditor') { $data['is_wysiwyg_enabled'] = 1; $data['frontend_input'] = 'textarea'; - } elseif ($data['frontend_input'] === 'textarea') { + } elseif (isset($data['frontend_input']) && $data['frontend_input'] === 'textarea') { $data['is_wysiwyg_enabled'] = 0; } return $data; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index 51480e849d9f3..2390a049fbeb6 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -341,7 +341,7 @@ public function getOptionPrice($optionValue, $basePrice) { $option = $this->getOption(); - return $this->_getChargableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); + return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); } /** @@ -395,14 +395,27 @@ public function getProductOptions() } /** - * Return final chargable price for option - * * @param float $price Price of option * @param boolean $isPercent Price type - percent or fixed * @param float $basePrice For percent price type * @return float + * @deprecated 102.0.4 typo in method name + * @see _getChargeableOptionPrice */ protected function _getChargableOptionPrice($price, $isPercent, $basePrice) + { + return $this->_getChargeableOptionPrice($price, $isPercent, $basePrice); + } + + /** + * Return final chargeable price for option + * + * @param float $price Price of option + * @param boolean $isPercent Price type - percent or fixed + * @param float $basePrice For percent price type + * @return float + */ + protected function _getChargeableOptionPrice($price, $isPercent, $basePrice) { if ($isPercent) { return $basePrice * $price / 100; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php index 0ace0372c43bb..4a257a4781063 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -231,7 +231,7 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $result += $this->_getChargableOptionPrice( + $result += $this->_getChargeableOptionPrice( $_result->getPrice(), $_result->getPriceType() == 'percent', $basePrice @@ -246,7 +246,7 @@ public function getOptionPrice($optionValue, $basePrice) } elseif ($this->_isSingleSelection()) { $_result = $option->getValueById($optionValue); if ($_result) { - $result = $this->_getChargableOptionPrice( + $result = $this->_getChargeableOptionPrice( $_result->getPrice(), $_result->getPriceType() == 'percent', $basePrice diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 3bb6bba69bfb4..1bddd2d07cd81 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -172,8 +172,10 @@ private function getExistingPrices(array $skus, $groupBySku = false) $rawPrices = $this->tierPricePersistence->get($ids); $prices = []; + $linkField = $this->tierPricePersistence->getEntityLinkField(); + $skuByIdLookup = $this->buildSkuByIdLookup($skus); foreach ($rawPrices as $rawPrice) { - $sku = $this->retrieveSkuById($rawPrice[$this->tierPricePersistence->getEntityLinkField()], $skus); + $sku = $skuByIdLookup[$rawPrice[$linkField]]; $price = $this->tierPriceFactory->create($rawPrice, $sku); if ($groupBySku) { $prices[$sku][] = $price; @@ -300,21 +302,21 @@ private function isCorrectPriceValue(array $existingPrice, array $price) } /** - * Retrieve SKU by product ID. + * Generate lookup to retrieve SKU by product ID. * - * @param int $id * @param array $skus - * @return string|null + * @return array */ - private function retrieveSkuById($id, $skus) + private function buildSkuByIdLookup($skus) { + $lookup = []; foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $sku => $ids) { - if (isset($ids[$id])) { - return $sku; + foreach (array_keys($ids) as $id) { + $lookup[$id] = $sku; } } - return null; + return $lookup; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index 0ab1fbab471e6..d3f0c8be6f649 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -252,7 +252,7 @@ public function getChildrenIds($parentId, $required = true) } /** - * Retrieve parent ids array by requered child + * Retrieve parent ids array by required child * * @param int|array $childId * @return array diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 6a82658342824..03ddab3d44547 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -233,7 +233,8 @@ public function __construct( public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey([$editMode, $storeId]); - if (!isset($this->instances[$sku][$cacheKey]) || $forceReload) { + $cachedProduct = $this->getProductFromLocalCache($sku, $cacheKey); + if ($cachedProduct === null || $forceReload) { $product = $this->productFactory->create(); $productId = $this->resourceModel->getIdBySku($sku); @@ -250,11 +251,10 @@ public function get($sku, $editMode = false, $storeId = null, $forceReload = fal } $product->load($productId); $this->cacheProduct($cacheKey, $product); + $cachedProduct = $product; } - if (!isset($this->instances[$sku])) { - $sku = trim($sku); - } - return $this->instances[$sku][$cacheKey]; + + return $cachedProduct; } /** @@ -312,7 +312,7 @@ protected function getCacheKey($data) private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterface $product) { $this->instancesById[$product->getId()][$cacheKey] = $product; - $this->instances[$product->getSku()][$cacheKey] = $product; + $this->saveProductInLocalCache($product, $cacheKey); if ($this->cacheLimit && count($this->instances) > $this->cacheLimit) { $offset = round($this->cacheLimit / -2); @@ -338,7 +338,7 @@ protected function initializeProductData(array $productData, $createNew) $product->setWebsiteIds([$this->storeManager->getStore(true)->getWebsiteId()]); } } else { - unset($this->instances[$productData['sku']]); + $this->removeProductFromLocalCache($productData['sku']); $product = $this->get($productData['sku']); } @@ -613,7 +613,7 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO if ($tierPrices !== null) { $product->setData('tier_price', $tierPrices); } - unset($this->instances[$product->getSku()]); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); $this->resourceModel->save($product); } catch (ConnectionException $exception) { @@ -650,8 +650,9 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO $e ); } - unset($this->instances[$product->getSku()]); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); + return $this->get($product->getSku(), false, $product->getStoreId()); } @@ -663,7 +664,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductInterface $product) $sku = $product->getSku(); $productId = $product->getId(); try { - unset($this->instances[$product->getSku()]); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); $this->resourceModel->delete($product); } catch (ValidatorException $e) { @@ -673,8 +674,9 @@ public function delete(\Magento\Catalog\Api\Data\ProductInterface $product) __('The "%1" product couldn\'t be removed.', $sku) ); } - unset($this->instances[$sku]); + $this->removeProductFromLocalCache($sku); unset($this->instancesById[$productId]); + return true; } @@ -796,4 +798,54 @@ private function getCollectionProcessor() } return $this->collectionProcessor; } + + /** + * Gets product from the local cache by SKU. + * + * @param string $sku + * @param string $cacheKey + * @return Product|null + */ + private function getProductFromLocalCache(string $sku, string $cacheKey) + { + $preparedSku = $this->prepareSku($sku); + + return $this->instances[$preparedSku][$cacheKey] ?? null; + } + + /** + * Removes product in the local cache. + * + * @param string $sku + * @return void + */ + private function removeProductFromLocalCache(string $sku) :void + { + $preparedSku = $this->prepareSku($sku); + unset($this->instances[$preparedSku]); + } + + /** + * Saves product in the local cache. + * + * @param Product $product + * @param string $cacheKey + * @return void + */ + private function saveProductInLocalCache(Product $product, string $cacheKey) : void + { + $preparedSku = $this->prepareSku($product->getSku()); + $this->instances[$preparedSku][$cacheKey] = $product; + } + + /** + * Converts SKU to lower case and trims. + * + * @param string $sku + * @return string + */ + private function prepareSku(string $sku): string + { + return mb_strtolower(trim($sku)); + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 6c9867359d40b..fa68ae3f865ef 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -11,6 +11,8 @@ */ namespace Magento\Catalog\Model\ResourceModel; +use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; /** @@ -82,6 +84,11 @@ class Category extends AbstractResource */ protected $aggregateCount; + /** + * @var Processor + */ + private $indexerProcessor; + /** * Category constructor. * @param \Magento\Eav\Model\Entity\Context $context @@ -90,6 +97,7 @@ class Category extends AbstractResource * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param Category\TreeFactory $categoryTreeFactory * @param Category\CollectionFactory $categoryCollectionFactory + * @param Processor $indexerProcessor * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer */ @@ -100,6 +108,7 @@ public function __construct( \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Catalog\Model\ResourceModel\Category\TreeFactory $categoryTreeFactory, \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, + Processor $indexerProcessor, $data = [], \Magento\Framework\Serialize\Serializer\Json $serializer = null ) { @@ -113,6 +122,7 @@ public function __construct( $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_eventManager = $eventManager; $this->connectionName = 'catalog'; + $this->indexerProcessor = $indexerProcessor; $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); } @@ -197,6 +207,18 @@ protected function _beforeDelete(\Magento\Framework\DataObject $object) $this->deleteChildren($object); } + /** + * Mark Category indexer as invalid to be picked up by cron. + * + * @param DataObject $object + * @return $this + */ + protected function _afterDelete(DataObject $object) + { + $this->indexerProcessor->markIndexerAsInvalid(); + return parent::_afterDelete($object); + } + /** * Delete children categories of specific category * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index d882ad078b97f..8f8e9f6bfedfa 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -895,18 +895,4 @@ public function setIsFilterableInGrid($isFilterableInGrid) $this->setData(self::IS_FILTERABLE_IN_GRID, $isFilterableInGrid); return $this; } - - /** - * @return \Magento\Eav\Api\Data\AttributeExtensionInterface - */ - public function getExtensionAttributes() - { - $extensionAttributes = $this->_getExtensionAttributes(); - if (null === $extensionAttributes) { - /** @var \Magento\Eav\Api\Data\AttributeExtensionInterface $extensionAttributes */ - $extensionAttributes = $this->eavAttributeFactory->create(); - $this->setExtensionAttributes($extensionAttributes); - } - return $extensionAttributes; - } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 0ce67a96a99cc..9b87515450a12 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -1570,7 +1570,7 @@ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $ent } /** - * Add requere tax percent flag for product collection + * Add require tax percent flag for product collection * * @return $this */ diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index c4e3fb1bf1e70..c33ea7c781aa3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -82,6 +82,7 @@ public function reindexEntities($processIds) $this->_prepareIndex($processIds); $this->_prepareRelationIndex($processIds); $this->_removeNotVisibleEntityFromIndex(); + return $this; } @@ -159,11 +160,12 @@ protected function _removeNotVisibleEntityFromIndex() * @param array $parentIds the parent entity ids limitation * @return \Magento\Framework\DB\Select */ - protected function _prepareRelationIndexSelect($parentIds = null) + protected function _prepareRelationIndexSelect(array $parentIds = null) { $connection = $this->getConnection(); $idxTable = $this->getIdxTable(); $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); + $select = $connection->select()->from( ['l' => $this->getTable('catalog_product_relation')], [] @@ -179,6 +181,14 @@ protected function _prepareRelationIndexSelect($parentIds = null) ['i' => $idxTable], 'l.child_id = i.entity_id AND cs.store_id = i.store_id', [] + )->join( + ['sw' => $this->getTable('store_website')], + "cs.website_id = sw.website_id", + [] + )->join( + ['cpw' => $this->getTable('catalog_product_website')], + 'i.entity_id = cpw.product_id AND sw.website_id = cpw.website_id', + [] )->group( ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id'] )->columns( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 591a26efbf615..285e1781e2f95 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -52,6 +52,16 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface */ private $hasEntity = null; + /** + * @var IndexTableStructureFactory + */ + private $indexTableStructureFactory; + + /** + * @var PriceModifierInterface[] + */ + private $priceModifiers = []; + /** * DefaultPrice constructor. * @@ -61,7 +71,8 @@ class DefaultPrice extends AbstractIndexer implements PriceInterface * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Module\Manager $moduleManager * @param string|null $connectionName - * @param null|\Magento\Indexer\Model\Indexer\StateFactory $stateFactory + * @param null|IndexTableStructureFactory $indexTableStructureFactory + * @param PriceModifierInterface[] $priceModifiers */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -69,11 +80,25 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Framework\Module\Manager $moduleManager, - $connectionName = null + $connectionName = null, + IndexTableStructureFactory $indexTableStructureFactory = null, + array $priceModifiers = [] ) { $this->_eventManager = $eventManager; $this->moduleManager = $moduleManager; parent::__construct($context, $tableStrategy, $eavConfig, $connectionName); + + $this->indexTableStructureFactory = $indexTableStructureFactory ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(IndexTableStructureFactory::class); + foreach ($priceModifiers as $priceModifier) { + if (!($priceModifier instanceof PriceModifierInterface)) { + throw new \InvalidArgumentException( + 'Argument \'priceModifiers\' must be of the type ' . PriceModifierInterface::class . '[]' + ); + } + + $this->priceModifiers[] = $priceModifier; + } } /** @@ -209,6 +234,8 @@ protected function _getDefaultFinalPriceTable() * Prepare final price temporary index table * * @return $this + * @deprecated + * @see prepareFinalPriceTable() */ protected function _prepareDefaultFinalPriceTable() { @@ -216,6 +243,32 @@ protected function _prepareDefaultFinalPriceTable() return $this; } + /** + * Create (if needed), clean and return structure of final price table + * + * @return IndexTableStructure + */ + private function prepareFinalPriceTable() + { + $tableName = $this->_getDefaultFinalPriceTable(); + $this->getConnection()->delete($tableName); + + $finalPriceTable = $this->indexTableStructureFactory->create([ + 'tableName' => $tableName, + 'entityField' => 'entity_id', + 'customerGroupField' => 'customer_group_id', + 'websiteField' => 'website_id', + 'taxClassField' => 'tax_class_id', + 'originalPriceField' => 'orig_price', + 'finalPriceField' => 'price', + 'minPriceField' => 'min_price', + 'maxPriceField' => 'max_price', + 'tierPriceField' => 'tier_price', + ]); + + return $finalPriceTable; + } + /** * Retrieve website current dates table name * @@ -248,11 +301,14 @@ protected function _prepareFinalPriceData($entityIds = null) */ protected function prepareFinalPriceDataForType($entityIds, $type) { - $this->_prepareDefaultFinalPriceTable(); + $finalPriceTable = $this->prepareFinalPriceTable(); $select = $this->getSelect($entityIds, $type); - $query = $select->insertFromSelect($this->_getDefaultFinalPriceTable(), [], false); + $query = $select->insertFromSelect($finalPriceTable->getTableName(), [], false); $this->getConnection()->query($query); + + $this->applyDiscountPrices($finalPriceTable); + return $this; } @@ -359,7 +415,7 @@ protected function getSelect($entityIds = null, $type = null) 'e.' . $metadata->getLinkField(), 'cs.store_id' ); - $currentDate = $connection->getDatePartSql('cwd.website_date'); + $currentDate = 'cwd.website_date'; $maxUnsignedBigint = '~0'; $specialFromDate = $connection->getDatePartSql($specialFrom); @@ -409,6 +465,7 @@ protected function getSelect($entityIds = null, $type = null) 'store_field' => new \Zend_Db_Expr('cs.store_id'), ] ); + return $select; } @@ -454,6 +511,19 @@ protected function _prepareCustomOptionPriceTable() return $this; } + /** + * Apply discount prices to final price index table. + * + * @param IndexTableStructure $finalPriceTable + * @return void + */ + private function applyDiscountPrices(IndexTableStructure $finalPriceTable) : void + { + foreach ($this->priceModifiers as $priceModifier) { + $priceModifier->modifyPrice($finalPriceTable); + } + } + /** * Apply custom option minimal and maximal price to temporary final price index table * @@ -463,6 +533,7 @@ protected function _prepareCustomOptionPriceTable() protected function _applyCustomOption() { $connection = $this->getConnection(); + $finalPriceTable = $this->_getDefaultFinalPriceTable(); $coaTable = $this->_getCustomOptionAggregateTable(); $copTable = $this->_getCustomOptionPriceTable(); @@ -470,7 +541,7 @@ protected function _applyCustomOption() $this->_prepareCustomOptionPriceTable(); $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], + ['i' => $finalPriceTable], ['entity_id', 'customer_group_id', 'website_id'] )->join( ['cw' => $this->getTable('store_website')], @@ -537,7 +608,7 @@ protected function _applyCustomOption() $connection->query($query); $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], + ['i' => $finalPriceTable], ['entity_id', 'customer_group_id', 'website_id'] )->join( ['cw' => $this->getTable('store_website')], @@ -606,7 +677,7 @@ protected function _applyCustomOption() $query = $select->insertFromSelect($copTable); $connection->query($query); - $table = ['i' => $this->_getDefaultFinalPriceTable()]; + $table = ['i' => $finalPriceTable]; $select = $connection->select()->join( ['io' => $copTable], 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php new file mode 100644 index 0000000000000..fb3eef2bf38eb --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/IndexTableStructure.php @@ -0,0 +1,181 @@ +tableName = $tableName; + $this->entityField = $entityField; + $this->customerGroupField = $customerGroupField; + $this->websiteField = $websiteField; + $this->taxClassField = $taxClassField; + $this->originalPriceField = $originalPriceField; + $this->finalPriceField = $finalPriceField; + $this->minPriceField = $minPriceField; + $this->maxPriceField = $maxPriceField; + $this->tierPriceField = $tierPriceField; + } + + /** + * @return string + */ + public function getTableName(): string + { + return $this->tableName; + } + + /** + * @return string + */ + public function getEntityField(): string + { + return $this->entityField; + } + + /** + * @return string + */ + public function getCustomerGroupField(): string + { + return $this->customerGroupField; + } + + /** + * @return string + */ + public function getWebsiteField(): string + { + return $this->websiteField; + } + + /** + * @return string + */ + public function getTaxClassField(): string + { + return $this->taxClassField; + } + + /** + * @return string + */ + public function getOriginalPriceField(): string + { + return $this->originalPriceField; + } + + /** + * @return string + */ + public function getFinalPriceField(): string + { + return $this->finalPriceField; + } + + /** + * @return string + */ + public function getMinPriceField(): string + { + return $this->minPriceField; + } + + /** + * @return string + */ + public function getMaxPriceField(): string + { + return $this->maxPriceField; + } + + /** + * @return string + */ + public function getTierPriceField(): string + { + return $this->tierPriceField; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php new file mode 100644 index 0000000000000..6ecb6aba89933 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/PriceModifierInterface.php @@ -0,0 +1,23 @@ + $this->catalogCategory->getCategoryUrl($category), 'has_active' => in_array((string)$category->getId(), explode('/', $currentCategory->getPath()), true), 'is_active' => $category->getId() == $currentCategory->getId(), + 'is_category' => true, 'is_parent_active' => $isParentActive ]; } diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php index 55402eb1f6fd2..3f388d00eaf9f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Category/Plugin/PriceBoxTagsTest.php @@ -16,6 +16,11 @@ class PriceBoxTagsTest extends \PHPUnit\Framework\TestCase */ private $priceCurrencyInterface; + /** + * @var \Magento\Directory\Model\Currency | \PHPUnit_Framework_MockObject_MockObject + */ + private $currency; + /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface | \PHPUnit_Framework_MockObject_MockObject */ @@ -46,6 +51,9 @@ protected function setUp() $this->priceCurrencyInterface = $this->getMockBuilder( \Magento\Framework\Pricing\PriceCurrencyInterface::class )->getMock(); + $this->currency = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) + ->disableOriginalConstructor() + ->getMock(); $this->timezoneInterface = $this->getMockBuilder( \Magento\Framework\Stdlib\DateTime\TimezoneInterface::class )->getMock(); @@ -82,7 +90,7 @@ protected function setUp() public function testAfterGetCacheKey() { $date = date('Ymd'); - $currencySymbol = '$'; + $currencyCode = 'USD'; $result = 'result_string'; $billingAddress = ['billing_address']; $shippingAddress = ['shipping_address']; @@ -95,7 +103,7 @@ public function testAfterGetCacheKey() '-', [ $result, - $currencySymbol, + $currencyCode, $date, $scopeId, $customerGroupId, @@ -104,7 +112,8 @@ public function testAfterGetCacheKey() ); $priceBox = $this->getMockBuilder(\Magento\Framework\Pricing\Render\PriceBox::class) ->disableOriginalConstructor()->getMock(); - $this->priceCurrencyInterface->expects($this->once())->method('getCurrencySymbol')->willReturn($currencySymbol); + $this->priceCurrencyInterface->expects($this->once())->method('getCurrency')->willReturn($this->currency); + $this->currency->expects($this->once())->method('getCode')->willReturn($currencyCode); $scope = $this->getMockBuilder(\Magento\Framework\App\ScopeInterface::class)->getMock(); $this->scopeResolverInterface->expects($this->any())->method('getScope')->willReturn($scope); $scope->expects($this->any())->method('getId')->willReturn($scopeId); 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 28617addc6d27..424427b871456 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 @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class AttributeFilterTest extends \PHPUnit\Framework\TestCase { @@ -16,12 +19,12 @@ class AttributeFilterTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $objectManagerMock; /** - * @var Product|\PHPUnit_Framework_MockObject_MockObject + * @var Product|MockObject */ protected $productMock; @@ -44,15 +47,25 @@ public function testPrepareProductAttributes( $expectedProductData, $initialProductData ) { + /** @var MockObject | Product $productMockMap */ $productMockMap = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getData']) + ->setMethods(['getData', 'getAttributes']) ->getMock(); if (!empty($initialProductData)) { $productMockMap->expects($this->any())->method('getData')->willReturnMap($initialProductData); } + if ($useDefaults) { + $productMockMap + ->expects($this->once()) + ->method('getAttributes') + ->willReturn( + $this->getProductAttributesMock($useDefaults) + ); + } + $actualProductData = $this->model->prepareProductAttributes($productMockMap, $requestProductData, $useDefaults); $this->assertEquals($expectedProductData, $actualProductData); } @@ -69,15 +82,15 @@ public function setupInputDataProvider() 'name' => 'testName', 'sku' => 'testSku', 'price' => '100', - 'description' => '' + 'description' => '', ], 'useDefaults' => [], 'expectedProductData' => [ 'name' => 'testName', 'sku' => 'testSku', - 'price' => '100' + 'price' => '100', ], - 'initialProductData' => [] + 'initialProductData' => [], ], 'update_product_without_use_defaults' => [ 'productData' => [ @@ -85,21 +98,21 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => '', - 'special_price' => null + 'special_price' => null, ], 'useDefaults' => [], 'expectedProductData' => [ 'name' => 'testName2', 'sku' => 'testSku2', 'price' => '101', - 'special_price' => null + 'special_price' => null, ], 'initialProductData' => [ ['name', 'testName2'], ['sku', 'testSku2'], ['price', '101'], - ['special_price', null] - ] + ['special_price', null], + ], ], 'update_product_without_use_defaults_2' => [ 'productData' => [ @@ -107,7 +120,7 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => 'updated description', - 'special_price' => null + 'special_price' => null, ], 'useDefaults' => [], 'expectedProductData' => [ @@ -115,14 +128,14 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => 'updated description', - 'special_price' => null + 'special_price' => null, ], 'initialProductData' => [ ['name', 'testName2'], ['sku', 'testSku2'], ['price', '101'], - ['special_price', null] - ] + ['special_price', null], + ], ], 'update_product_with_use_defaults' => [ 'productData' => [ @@ -130,25 +143,25 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => '', - 'special_price' => null + 'special_price' => null, ], 'useDefaults' => [ - 'description' => '0' + 'description' => '0', ], 'expectedProductData' => [ 'name' => 'testName2', 'sku' => 'testSku2', 'price' => '101', 'special_price' => null, - 'description' => '' + 'description' => '', ], 'initialProductData' => [ ['name', 'testName2'], ['sku', 'testSku2'], ['price', '101'], ['special_price', null], - ['description', 'descr text'] - ] + ['description', 'descr text'], + ], ], 'update_product_with_use_defaults_2' => [ 'requestProductData' => [ @@ -156,48 +169,73 @@ public function setupInputDataProvider() 'sku' => 'testSku3', 'price' => '103', 'description' => 'descr modified', - 'special_price' => '100' + 'special_price' => '100', ], 'useDefaults' => [ - 'description' => '0' + 'description' => '0', ], 'expectedProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', 'price' => '103', 'special_price' => '100', - 'description' => 'descr modified' + 'description' => 'descr modified', ], 'initialProductData' => [ - ['name', null,'testName2'], + ['name', null, 'testName2'], ['sku', null, 'testSku2'], ['price', null, '101'], - ['description', null, 'descr text'] - ] + ['description', null, 'descr text'], + ], ], 'update_product_with_use_defaults_3' => [ 'requestProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', 'price' => '103', - 'special_price' => '100' + 'special_price' => '100', + 'description' => 'descr modified', ], 'useDefaults' => [ - 'description' => '1' + 'description' => '1', ], 'expectedProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', 'price' => '103', 'special_price' => '100', + 'description' => false, ], 'initialProductData' => [ - ['name', null,'testName2'], + ['name', null, 'testName2'], ['sku', null, 'testSku2'], ['price', null, '101'], - ['description', null, 'descr text'] - ] + ['description', null, 'descr text'], + ], ], ]; } + + /** + * @param array $useDefaults + * @return array + */ + private function getProductAttributesMock(array $useDefaults): array + { + $returnArray = []; + foreach ($useDefaults as $attributecode => $isDefault) { + if ($isDefault === '1') { + /** @var Attribute | MockObject $attribute */ + $attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + $attribute->expects($this->any()) + ->method('getBackendType') + ->willReturn('varchar'); + + $returnArray[$attributecode] = $attribute; + } + } + return $returnArray; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CustomOptions/CustomOptionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CustomOptions/CustomOptionTest.php index 33b7892462f4c..fb90eaaaf1ec5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CustomOptions/CustomOptionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CustomOptions/CustomOptionTest.php @@ -7,6 +7,8 @@ use Magento\Catalog\Model\CustomOptions\CustomOption; use Magento\Catalog\Model\Webapi\Product\Option\Type\File\Processor as FileProcessor; +use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Catalog\Api\Data\CustomOptionExtensionInterface; class CustomOptionTest extends \PHPUnit\Framework\TestCase { @@ -15,6 +17,12 @@ class CustomOptionTest extends \PHPUnit\Framework\TestCase */ protected $model; + /** @var \Magento\Framework\Api\ExtensionAttributesFactory | \PHPUnit_Framework_MockObject_MockObject */ + private $extensionAttributesFactoryMock; + + /** @var \Magento\Catalog\Api\Data\CustomOptionExtensionInterface | \PHPUnit_Framework_MockObject_MockObject */ + private $extensionMock; + /** * @var FileProcessor | \PHPUnit_Framework_MockObject_MockObject */ @@ -30,7 +38,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $extensionAttributesFactory = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesFactory::class) + $this->extensionAttributesFactoryMock = $this->getMockBuilder(ExtensionAttributesFactory::class) ->disableOriginalConstructor() ->getMock(); @@ -52,10 +60,17 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->extensionMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\CustomOptionExtensionInterface::class) + ->setMethods(['getFileInfo']) + ->getMockForAbstractClass(); + + $this->extensionAttributesFactoryMock->expects(self::any()) + ->method('create')->willReturn($this->extensionMock); + $this->model = new CustomOption( $context, $registry, - $extensionAttributesFactory, + $this->extensionAttributesFactoryMock, $attributeValueFactory, $this->fileProcessor, $resource, @@ -84,14 +99,10 @@ public function testGetOptionValue() public function testGetOptionValueWithFileInfo() { - $customOption = $this->getMockBuilder(\Magento\Catalog\Api\Data\CustomOptionExtensionInterface::class) - ->setMethods(['getFileInfo']) - ->getMockForAbstractClass(); - $imageContent = $this->getMockBuilder(\Magento\Framework\Api\Data\ImageContentInterface::class) ->getMockForAbstractClass(); - $customOption->expects($this->once()) + $this->extensionMock->expects($this->once()) ->method('getFileInfo') ->willReturn($imageContent); @@ -112,7 +123,6 @@ public function testGetOptionValueWithFileInfo() ->with($imageContent) ->willReturn($imageResult); - $this->model->setExtensionAttributes($customOption); $this->model->setData(\Magento\Catalog\Api\Data\CustomOptionInterface::OPTION_VALUE, 'file'); $this->assertEquals($imageResult, $this->model->getOptionValue()); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php index 58654136ab5a8..9d58822fb6073 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/AbstractActionTest.php @@ -113,11 +113,20 @@ public function testReindexWithoutArgumentsExecutesReindexAll() $this->_model->reindex(); } - public function testReindexWithNotNullArgumentExecutesReindexEntities() - { - $childIds = [1, 2, 3]; - $parentIds = [4]; - $reindexIds = array_merge($childIds, $parentIds); + /** + * @param array $ids + * @param array $parentIds + * @param array $childIds + * @return void + * @dataProvider reindexEntitiesDataProvider + */ + public function testReindexWithNotNullArgumentExecutesReindexEntities( + array $ids, + array $parentIds, + array $childIds + ) : void { + $reindexIds = array_unique(array_merge($ids, $parentIds, $childIds)); + $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->getMockForAbstractClass(); @@ -129,11 +138,23 @@ public function testReindexWithNotNullArgumentExecutesReindexEntities() ->disableOriginalConstructor() ->getMock(); - $eavSource->expects($this->once())->method('getRelationsByChild')->with($childIds)->willReturn($childIds); - $eavSource->expects($this->once())->method('getRelationsByParent')->with($childIds)->willReturn($parentIds); + $eavSource->expects($this->once()) + ->method('getRelationsByChild') + ->with($ids) + ->willReturn($parentIds); + $eavSource->expects($this->once()) + ->method('getRelationsByParent') + ->with(array_unique(array_merge($parentIds, $ids))) + ->willReturn($childIds); - $eavDecimal->expects($this->once())->method('getRelationsByChild')->with($reindexIds)->willReturn($reindexIds); - $eavDecimal->expects($this->once())->method('getRelationsByParent')->with($reindexIds)->willReturn([]); + $eavDecimal->expects($this->once()) + ->method('getRelationsByChild') + ->with($reindexIds) + ->willReturn($parentIds); + $eavDecimal->expects($this->once()) + ->method('getRelationsByParent') + ->with(array_unique(array_merge($parentIds, $reindexIds))) + ->willReturn($childIds); $eavSource->expects($this->once())->method('getConnection')->willReturn($connectionMock); $eavDecimal->expects($this->once())->method('getConnection')->willReturn($connectionMock); @@ -153,6 +174,18 @@ public function testReindexWithNotNullArgumentExecutesReindexEntities() ->method('create') ->will($this->returnValue($eavDecimal)); - $this->_model->reindex($childIds); + $this->_model->reindex($ids); + } + + /** + * @return array + */ + public function reindexEntitiesDataProvider() : array + { + return [ + [[4], [], [1, 2, 3]], + [[3], [4], []], + [[5], [], []], + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/InputType/PresentationTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/InputType/PresentationTest.php new file mode 100644 index 0000000000000..16dff2d210f27 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Frontend/InputType/PresentationTest.php @@ -0,0 +1,80 @@ +presentation = new \Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation(); + $this->attributeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @param string $inputType + * @param boolean $isWysiwygEnabled + * @param string $expectedResult + * @dataProvider getPresentationInputTypeDataProvider + */ + public function testGetPresentationInputType(string $inputType, bool $isWysiwygEnabled, string $expectedResult) + { + $this->attributeMock->expects($this->once())->method('getFrontendInput')->willReturn($inputType); + $this->attributeMock->expects($this->any())->method('getIsWysiwygEnabled')->willReturn($isWysiwygEnabled); + $this->assertEquals($expectedResult, $this->presentation->getPresentationInputType($this->attributeMock)); + } + + public function getPresentationInputTypeDataProvider() + { + return [ + 'attribute_is_textarea_and_wysiwyg_enabled' => ['textarea', true, 'texteditor'], + 'attribute_is_input_and_wysiwyg_enabled' => ['input', true, 'input'], + 'attribute_is_textarea_and_wysiwyg_disabled' => ['textarea', false, 'textarea'], + ]; + } + + /** + * @param array $data + * @param array $expectedResult + * @dataProvider convertPresentationDataToInputTypeDataProvider + */ + public function testConvertPresentationDataToInputType(array $data, array $expectedResult) + { + $this->assertEquals($expectedResult, $this->presentation->convertPresentationDataToInputType($data)); + } + + public function convertPresentationDataToInputTypeDataProvider() + { + return [ + [['key' => 'value'], ['key' => 'value']], + [ + ['frontend_input' => 'texteditor'], + ['frontend_input' => 'textarea', 'is_wysiwyg_enabled' => 1] + ], + [ + ['frontend_input' => 'textarea'], + ['frontend_input' => 'textarea', 'is_wysiwyg_enabled' => 0] + ], + [ + ['frontend_input' => 'input'], + ['frontend_input' => 'input'] + ] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index 8d65153d7ba20..bf5c3d8276295 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -381,11 +381,11 @@ public function testGetByIdAbsentProduct() public function testGetByIdProductInEditMode() { $productId = 123; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->once())->method('setData')->with('_edit_mode', true); - $this->productMock->expects($this->once())->method('load')->with($productId); + $this->productFactoryMock->method('create')->willReturn($this->productMock); + $this->productMock->method('setData')->with('_edit_mode', true); + $this->productMock->method('load')->with($productId); $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($productId, true)); } @@ -411,6 +411,7 @@ public function testGetByIdForCacheKeyGenerate($identifier, $editMode, $storeId) } $this->productMock->expects($this->once())->method('load')->with($identifier); $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($identifier); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); //Second invocation should just return from cache $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); @@ -433,6 +434,7 @@ public function testGetByIdForcedReload() $this->serializerMock->expects($this->exactly(3))->method('serialize'); $this->productMock->expects($this->exactly(4))->method('getId')->willReturn($identifier); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); //second invocation should just return from cache $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); @@ -532,6 +534,7 @@ public function testGetByIdWithSetStoreId() $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId); $this->productMock->expects($this->once())->method('load')->with($productId); $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($productId, false, $storeId)); } @@ -585,7 +588,8 @@ public function testSaveNew() ->expects($this->once()) ->method('toNestedArray') ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->save($this->productMock)); } @@ -597,7 +601,8 @@ public function testSaveNew() public function testSaveUnableToSaveException() { $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); + $this->resourceModelMock->expects($this->exactly(1)) + ->method('getIdBySku')->willReturn(null); $this->productFactoryMock->expects($this->exactly(2)) ->method('create') ->will($this->returnValue($this->productMock)); @@ -610,7 +615,8 @@ public function testSaveUnableToSaveException() ->expects($this->once()) ->method('toNestedArray') ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -637,6 +643,7 @@ public function testSaveException() ->method('toNestedArray') ->will($this->returnValue($this->productData)); $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -661,6 +668,7 @@ public function testSaveInvalidProductException() ->method('toNestedArray') ->will($this->returnValue($this->productData)); $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -692,6 +700,7 @@ public function testSaveThrowsTemporaryStateExceptionIfDatabaseConnectionErrorOc $this->productMock->expects($this->once()) ->method('getWebsiteIds') ->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -734,9 +743,8 @@ public function testGetList() { $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class); $collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); - $this->collectionFactoryMock->expects($this->once())->method('create')->willReturn($collectionMock); - + $this->productMock->method('getSku')->willReturn('simple'); $collectionMock->expects($this->once())->method('addAttributeToSelect')->with('*'); $collectionMock->expects($this->exactly(2))->method('joinAttribute')->withConsecutive( ['status', 'catalog_product/status', 'entity_id', null, 'inner'], @@ -1299,6 +1307,7 @@ public function testSaveWithDifferentWebsites() ]); $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([1,2,3]); $this->productMock->expects($this->once())->method('setWebsiteIds')->willReturn([2,3]); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->save($this->productMock)); } @@ -1336,7 +1345,6 @@ public function testSaveExistingWithMediaGalleryEntries() $expectedResult = [ [ - 'value_id' => 5, 'value_id' => 5, "label" => "new_label_text", 'file' => 'filename1', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php index 4812751792f18..b7d05fd2b70ee 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/CategoryTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel; use Magento\Catalog\Model\Factory; +use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Catalog\Model\ResourceModel\Category; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Eav\Model\Config; @@ -91,6 +92,11 @@ class CategoryTest extends \PHPUnit\Framework\TestCase */ private $serializerMock; + /** + * @var Processor|\PHPUnit_Framework_MockObject_MockObject + */ + private $indexerProcessorMock; + /** * {@inheritDoc} */ @@ -121,6 +127,9 @@ protected function setUp() $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->indexerProcessorMock = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); $this->serializerMock = $this->getMockBuilder(Json::class)->getMock(); @@ -131,6 +140,7 @@ protected function setUp() $this->managerMock, $this->treeFactoryMock, $this->collectionFactoryMock, + $this->indexerProcessorMock, [], $this->serializerMock ); diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index a29379647b9e1..0426e389d9aeb 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -5,11 +5,10 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface; use Magento\Framework\App\RequestInterface; -use Magento\Framework\EntityManager\EventManager; use Magento\Framework\Phrase; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Api\Data\StoreInterface; @@ -257,7 +256,15 @@ protected function setUp() $this->searchResultsMock = $this->getMockBuilder(SearchResultsInterface::class) ->getMockForAbstractClass(); $this->eavAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods(['load', 'getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode']) + ->setMethods([ + 'load', + 'getAttributeGroupCode', + 'getApplyTo', + 'getFrontendInput', + 'getAttributeCode', + 'usesSource', + 'getSource', + ]) ->disableOriginalConstructor() ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) @@ -451,64 +458,61 @@ public function testModifyData() } /** - * @param int $productId + * @param int|null $productId * @param bool $productRequired - * @param string $attrValue - * @param string $note + * @param string|null $attrValue * @param array $expected + * @return void * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::isProductExists * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::setupAttributeMeta * @dataProvider setupAttributeMetaDataProvider */ - public function testSetupAttributeMetaDefaultAttribute($productId, $productRequired, $attrValue, $note, $expected) - { - $configPath = 'arguments/data/config'; + public function testSetupAttributeMetaDefaultAttribute( + $productId, + bool $productRequired, + $attrValue, + array $expected + ) : void { + $configPath = 'arguments/data/config'; $groupCode = 'product-details'; $sortOrder = '0'; + $attributeOptions = [ + ['value' => 1, 'label' => 'Int label'], + ['value' => 1.5, 'label' => 'Float label'], + ['value' => true, 'label' => 'Boolean label'], + ['value' => 'string', 'label' => 'String label'], + ['value' => ['test1', 'test2'], 'label' => 'Array label'], + ]; + $attributeOptionsExpected = [ + ['value' => '1', 'label' => 'Int label'], + ['value' => '1.5', 'label' => 'Float label'], + ['value' => '1', 'label' => 'Boolean label'], + ['value' => 'string', 'label' => 'String label'], + ['value' => ['test1', 'test2'], 'label' => 'Array label'], + ]; - $this->productMock->expects($this->any()) - ->method('getId') - ->willReturn($productId); - - $this->productAttributeMock->expects($this->any()) - ->method('getIsRequired') - ->willReturn($productRequired); - - $this->productAttributeMock->expects($this->any()) - ->method('getDefaultValue') - ->willReturn('required_value'); - - $this->productAttributeMock->expects($this->any()) - ->method('getAttributeCode') - ->willReturn('code'); - - $this->productAttributeMock->expects($this->any()) - ->method('getValue') - ->willReturn('value'); - - $this->productAttributeMock->expects($this->any()) - ->method('getNote') - ->willReturn($note); - - $this->productAttributeMock->expects($this->any()) - ->method('getDefaultFrontendLabel') - ->willReturn(new Phrase('mylabel')); + $this->productMock->method('getId')->willReturn($productId); + $this->productAttributeMock->method('getIsRequired')->willReturn($productRequired); + $this->productAttributeMock->method('getDefaultValue')->willReturn('required_value'); + $this->productAttributeMock->method('getAttributeCode')->willReturn('code'); + $this->productAttributeMock->method('getValue')->willReturn('value'); $attributeMock = $this->getMockBuilder(AttributeInterface::class) ->setMethods(['getValue']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $attributeMock->expects($this->any()) - ->method('getValue') - ->willReturn($attrValue); + $attributeMock->method('getValue')->willReturn($attrValue); - $this->productMock->expects($this->any()) - ->method('getCustomAttribute') - ->willReturn($attributeMock); + $this->productMock->method('getCustomAttribute')->willReturn($attributeMock); + $this->eavAttributeMock->method('usesSource')->willReturn(true); + + $attributeSource = $this->getMockBuilder(SourceInterface::class)->getMockForAbstractClass(); + $attributeSource->method('getAllOptions')->willReturn($attributeOptions); - $this->arrayManagerMock->expects($this->any()) - ->method('set') + $this->eavAttributeMock->method('getSource')->willReturn($attributeSource); + + $this->arrayManagerMock->method('set') ->with( $configPath, [], @@ -516,16 +520,21 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi ) ->willReturn($expected); - $this->arrayManagerMock->expects($this->any()) + $this->arrayManagerMock->expects($this->once()) ->method('merge') + ->with( + $this->anything(), + $this->anything(), + $this->callback( + function ($value) use ($attributeOptionsExpected) { + return $value['options'] === $attributeOptionsExpected; + } + ) + ) ->willReturn($expected); - $this->arrayManagerMock->expects($this->any()) - ->method('get') - ->willReturn([]); - - $this->arrayManagerMock->expects($this->any()) - ->method('exists'); + $this->arrayManagerMock->method('get')->willReturn([]); + $this->arrayManagerMock->method('exists')->willReturn(true); $this->assertEquals( $expected, @@ -539,147 +548,82 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi public function setupAttributeMetaDataProvider() { return [ - 'default_null_prod_not_new_and_required' => $this->defaultNullProdNotNewAndRequired(), - 'default_null_prod_not_new_and_not_required' => $this->defaultNullProdNotNewAndNotRequired(), - 'default_null_prod_new_and_not_required' => $this->defaultNullProdNewAndNotRequired(), - 'default_null_prod_new_and_required' => $this->defaultNullProdNewAndRequired(), - 'default_null_prod_new_and_required_and_filled_notice' => - $this->defaultNullProdNewAndRequiredAndFilledNotice() - ]; - } - - /** - * @return array - */ - private function defaultNullProdNotNewAndRequired() - { - return [ - 'productId' => 1, - 'productRequired' => true, - 'attrValue' => 'val', - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => true, - 'notice' => null, - 'default' => null, - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 - ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNotNewAndNotRequired() - { - return [ - 'productId' => 1, - 'productRequired' => false, - 'attrValue' => 'val', - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => null, - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'default_null_prod_not_new_and_required' => [ + 'productId' => 1, + 'productRequired' => true, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => true, + 'notice' => null, + 'default' => null, + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNewAndNotRequired() - { - return [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => 'required_value', - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'default_null_prod_not_new_and_not_required' => [ + 'productId' => 1, + 'productRequired' => false, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => null, + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNewAndRequired() - { - return [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => 'required_value', - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 - ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNewAndRequiredAndFilledNotice() - { - return [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'note' => 'example notice', - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => __('example notice'), - 'default' => 'required_value', - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'default_null_prod_new_and_not_required' => [ + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], ], + 'default_null_prod_new_and_required' => [ + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + ] ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php index b4460b314513b..78502ae297b52 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GeneralTest.php @@ -5,8 +5,11 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\General; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Stdlib\ArrayManager; /** * Class GeneralTest @@ -15,6 +18,35 @@ */ class GeneralTest extends AbstractModifierTest { + /** + * @var AttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepositoryMock; + + /** + * @var General + */ + private $generalModifier; + + protected function setUp() + { + parent::setUp(); + + $this->attributeRepositoryMock = $this->getMockBuilder(AttributeRepositoryInterface::class) + ->getMockForAbstractClass(); + + $arrayManager = $this->objectManager->getObject(ArrayManager::class); + + $this->generalModifier = $this->objectManager->getObject( + General::class, + [ + 'attributeRepository' => $this->attributeRepositoryMock, + 'locator' => $this->locatorMock, + 'arrayManager' => $arrayManager, + ] + ); + } + /** * {@inheritdoc} */ @@ -40,4 +72,59 @@ public function testModifyMeta() ] ])); } + + /** + * @param array $data + * @param int $defaultStatusValue + * @param array $expectedResult + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @dataProvider modifyDataDataProvider + */ + public function testModifyDataNewProduct(array $data, int $defaultStatusValue, array $expectedResult) + { + $attributeMock = $this->getMockBuilder(AttributeInterface::class) + ->getMockForAbstractClass(); + $attributeMock + ->method('getDefaultValue') + ->willReturn($defaultStatusValue); + $this->attributeRepositoryMock + ->method('get') + ->with( + ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::CODE_STATUS + ) + ->willReturn($attributeMock); + $this->assertSame($expectedResult, $this->generalModifier->modifyData($data)); + } + + /** + * @return array + */ + public function modifyDataDataProvider(): array + { + return [ + 'With default status value' => [ + 'data' => [], + 'defaultStatusAttributeValue' => 5, + 'expectedResult' => [ + null => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 5, + ], + ], + ], + ], + 'Without default status value' => [ + 'data' => [], + 'defaultStatusAttributeValue' => 0, + 'expectedResult' => [ + null => [ + General::DATA_SOURCE_DEFAULT => [ + ProductAttributeInterface::CODE_STATUS => 1, + ], + ], + ], + ], + ]; + } } diff --git a/app/code/Magento/Catalog/Ui/Component/UrlInput/Category.php b/app/code/Magento/Catalog/Ui/Component/UrlInput/Category.php index 836bc5058777d..01d93de577927 100644 --- a/app/code/Magento/Catalog/Ui/Component/UrlInput/Category.php +++ b/app/code/Magento/Catalog/Ui/Component/UrlInput/Category.php @@ -46,6 +46,7 @@ public function getConfig(): array 'sortOrder' => 30, 'missingValuePlaceholder' => __('Category with ID: %s doesn\'t exist'), 'isDisplayMissingValuePlaceholder' => true, + 'isRemoveSelectedIcon' => true, ]; } } diff --git a/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php b/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php index efa8417e4686a..be73940237db4 100644 --- a/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php +++ b/app/code/Magento/Catalog/Ui/Component/UrlInput/Product.php @@ -50,6 +50,7 @@ public function getConfig(): array 'emptyOptionsHtml' => __('Start typing to find products'), 'missingValuePlaceholder' => __('Product with ID: %s doesn\'t exist'), 'isDisplayMissingValuePlaceholder' => true, + 'isRemoveSelectedIcon' => true, 'validationUrl' => $this->urlBuilder->getUrl('catalog/product/getSelected'), ]; } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index b216ee8c9c547..0e6f17d761bc3 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -611,8 +611,9 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { + $options = $attributeModel->getSource()->getAllOptions(); $meta = $this->arrayManager->merge($configPath, $meta, [ - 'options' => $attributeModel->getSource()->getAllOptions(), + 'options' => $this->convertOptionsValueToString($options), ]); } @@ -683,6 +684,23 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute) return $attribute->getDefaultValue(); } + /** + * Convert options value to string. + * + * @param array $options + * @return array + */ + private function convertOptionsValueToString(array $options) : array + { + array_walk($options, function (&$value) { + if (isset($value['value']) && is_scalar($value['value'])) { + $value['value'] = (string)$value['value']; + } + }); + + return $options; + } + /** * @param ProductAttributeInterface $attribute * @param array $meta diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php index ea69ebf4dda24..03d4dde311491 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\Ui\Component\Form; use Magento\Framework\Stdlib\ArrayManager; @@ -35,21 +36,31 @@ class General extends AbstractModifier */ private $localeCurrency; + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + /** * @param LocatorInterface $locator * @param ArrayManager $arrayManager + * @param AttributeRepositoryInterface|null $attributeRepository */ public function __construct( LocatorInterface $locator, - ArrayManager $arrayManager + ArrayManager $arrayManager, + AttributeRepositoryInterface $attributeRepository = null ) { $this->locator = $locator; $this->arrayManager = $arrayManager; + $this->attributeRepository = $attributeRepository + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(AttributeRepositoryInterface::class); } /** * {@inheritdoc} * @since 101.0.0 + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function modifyData(array $data) { @@ -58,7 +69,12 @@ public function modifyData(array $data) $modelId = $this->locator->getProduct()->getId(); if (!isset($data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS])) { - $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] = '1'; + $attributeStatus = $this->attributeRepository->get( + ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::CODE_STATUS + ); + $data[$modelId][static::DATA_SOURCE_DEFAULT][ProductAttributeInterface::CODE_STATUS] = + $attributeStatus->getDefaultValue() ?: 1; } return $data; diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index ca8390a6c8f8a..10251d35dffcd 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -220,7 +220,4 @@ Magento\Catalog\Ui\DataProvider\Product\AddSearchKeyConditionToCollection
- - - diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 875c3fecf37c6..9f1fb020ef95a 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -865,6 +865,7 @@ Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductCategoryFilter Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductStoreFilter + Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductStoreFilter Magento\Catalog\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor\ProductWebsiteFilter diff --git a/app/code/Magento/Catalog/etc/widget.xml b/app/code/Magento/Catalog/etc/widget.xml index ef9009549da24..a11d206e2ce42 100644 --- a/app/code/Magento/Catalog/etc/widget.xml +++ b/app/code/Magento/Catalog/etc/widget.xml @@ -64,7 +64,11 @@ - 86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache. + + If not set, equals to 86400 seconds (24 hours). To update widget instantly, go to Cache Management and clear Blocks HTML Output cache. +
Widget will not show products that begin to match the specified conditions until cache is refreshed.]]> +
diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index b9012c030dace..0727d03df1340 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -705,7 +705,13 @@ Template,Template "New Products Names Only Template","New Products Names Only Template" "New Products Images Only Template","New Products Images Only Template" "Cache Lifetime (Seconds)","Cache Lifetime (Seconds)" -"86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache.","86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache." +"Time in seconds between the widget updates. +
If not set, equals to 86400 seconds (24 hours). To update widget instantly, go to Cache Management and clear Blocks HTML Output cache. +
Widget will not show products that begin to match the specified conditions until cache is refreshed." +, +"Time in seconds between the widget updates. +
If not set, equals to 86400 seconds (24 hours). To update widget instantly, go to Cache Management and clear Blocks HTML Output cache. +
Widget will not show products that begin to match the specified conditions until cache is refreshed." "Catalog Product Link","Catalog Product Link" "Link to a Specified Product","Link to a Specified Product" "Select Product...","Select Product..." diff --git a/app/code/Magento/Catalog/view/base/web/js/price-options.js b/app/code/Magento/Catalog/view/base/web/js/price-options.js index ceeea4c878622..e18abe3af38a6 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-options.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-options.js @@ -20,8 +20,10 @@ define([ optionConfig: {}, optionHandlers: {}, optionTemplate: '<%= data.label %>' + - '<% if (data.finalPrice.value) { %>' + + '<% if (data.finalPrice.value > 0) { %>' + ' +<%- data.finalPrice.formatted %>' + + '<% } else if (data.finalPrice.value < 0) { %>' + + ' <%- data.finalPrice.formatted %>' + '<% } %>', controlContainer: 'dd' }; diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml index a5d193afd2af4..528b2b5c59f23 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/breadcrumbs.phtml @@ -11,7 +11,7 @@ $viewModel = $block->getData('viewModel'); "breadcrumbs": { "categoryUrlSuffix": "escapeHtml($viewModel->getCategoryUrlSuffix()); ?>", "useCategoryPathInUrl": isCategoryUsedInProductUrl(); ?>, - "product": "escapeHtml($viewModel->getProductName()); ?>" + "product": "escapeHtml($block->escapeJsQuote($viewModel->getProductName(), '"')); ?>" } }'> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml index 6a2dd1f27d4a9..949d365e7899a 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml @@ -8,9 +8,9 @@ /* @var $block \Magento\Catalog\Block\Product\Compare\ListCompare */ ?> -getItems()->getSize() ?> - - +getItems()->getSize() ?> + +
@@ -24,14 +24,14 @@ - - getItems() as $_item): ?> - + + getItems() as $item): ?> + - + helper('Magento\Catalog\Helper\Product\Compare');?> - @@ -41,35 +41,35 @@ - - helper('Magento\Catalog\Helper\Output'); ?> - - getItems() as $_item): ?> - + + helper('Magento\Catalog\Helper\Output'); ?> + + getItems() as $item): ?> + - - getImage($_item, 'product_comparison_list')->toHtml() ?> + + getImage($item, 'product_comparison_list')->toHtml() ?> - - productAttribute($_item, $_item->getName(), 'name') ?> + + productAttribute($item, $item->getName(), 'name') ?> - getReviewsSummaryHtml($_item, 'short') ?> - getProductPrice($_item, '-compare-list-top') ?> -
+ getReviewsSummaryHtml($item, 'short') ?> + getProductPrice($item, '-compare-list-top') ?> +
- isSaleable()): ?> -
+ isSaleable()): ?> + getBlockHtml('formkey') ?>
- getIsSalable()): ?> + getIsSalable()): ?>
@@ -78,7 +78,7 @@
helper('Magento\Wishlist\Helper\Data')->isAllow()) : ?> @@ -89,39 +89,41 @@ - getAttributes() as $_attribute): ?> - - - getItems() as $_item): ?> - - - - escapeHtml($_attribute->getStoreLabel() ? $_attribute->getStoreLabel() : __($_attribute->getFrontendLabel())) ?> - - - - -
- getAttributeCode()) { - case "price": ?> - getProductPrice( - $_item, - '-compare-list-' . $_attribute->getCode() - ) - ?> - - getImage($_item, 'product_small_image')->toHtml(); ?> + getAttributes() as $attribute): ?> + + hasAttributeValueForProducts($attribute)): ?> + + getItems() as $item): ?> + + + + escapeHtml($attribute->getStoreLabel() ? $attribute->getStoreLabel() : __($attribute->getFrontendLabel())) ?> + + + + +
+ getAttributeCode()) { + case "price": ?> + getProductPrice( + $item, + '-compare-list-' . $attribute->getCode() + ) + ?> + + getImage($item, 'product_small_image')->toHtml(); ?> + + productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> - productAttribute($_item, $block->getProductAttributeValue($_item, $_attribute), $_attribute->getAttributeCode()) ?> - -
- - - + } ?> +
+ + + + diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js index b2da91c3b55c1..8fcac2f9f1d65 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js @@ -38,6 +38,11 @@ define([ _bindSubmit: function () { var self = this; + if (this.element.data('catalog-addtocart-initialized')) { + return; + } + + this.element.data('catalog-addtocart-initialized', 1); this.element.on('submit', function (e) { e.preventDefault(); self.submitForm($(this)); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js b/app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js index 7bf92fa2c089d..032b8541939c3 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/breadcrumbs.js @@ -16,26 +16,10 @@ define([ categoryUrlSuffix: '', useCategoryPathInUrl: false, product: '', + categoryItemSelector: '.category-item', menuContainer: '[data-action="navigation"] > ul' }, - /** @inheritdoc */ - _init: function () { - var menu, - originalInit = this._super.bind(this); - - // render breadcrumbs after navigation menu is loaded. - menu = $(this.options.menuContainer).data('mageMenu'); - - if (typeof menu === 'undefined') { - $(this.options.menuContainer).on('menucreate', function () { - originalInit(); - }); - } else { - this._super(); - } - }, - /** @inheritdoc */ _render: function () { this._appendCatalogCrumbs(); @@ -88,14 +72,10 @@ define([ * @private */ _getCategoryCrumb: function (menuItem) { - var categoryId = /(\d+)/i.exec(menuItem.attr('id'))[0], - categoryName = menuItem.text(), - categoryUrl = menuItem.attr('href'); - return { - 'name': 'category' + categoryId, - 'label': categoryName, - 'link': categoryUrl, + 'name': 'category', + 'label': menuItem.text(), + 'link': menuItem.attr('href'), 'title': '' }; }, @@ -163,7 +143,10 @@ define([ categoryMenuItem = null; if (categoryUrl && menu.length) { - categoryMenuItem = menu.find('a[href="' + categoryUrl + '"]'); + categoryMenuItem = menu.find( + this.options.categoryItemSelector + + ' > a[href="' + categoryUrl + '"]' + ); } return categoryMenuItem; diff --git a/app/code/Magento/CatalogGraphQl/Model/Layer/CollectionProvider.php b/app/code/Magento/CatalogGraphQl/Model/Layer/CollectionProvider.php index 1e6fdf0e60e66..86645b0d36fdb 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Layer/CollectionProvider.php +++ b/app/code/Magento/CatalogGraphQl/Model/Layer/CollectionProvider.php @@ -29,6 +29,10 @@ class CollectionProvider implements \Magento\Catalog\Model\Layer\ItemCollectionP */ private $collectionProcessor; + /** + * @param \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface $collectionProcessor + * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory + */ public function __construct( \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface $collectionProcessor, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php new file mode 100644 index 0000000000000..5927e747c2238 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -0,0 +1,107 @@ +productRepository = $productRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterQuery = $filterQuery; + $this->valueFactory = $valueFactory; + } + + /** + * {@inheritdoc} + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): Value { + $args['filter'] = [ + 'category_ids' => [ + 'eq' => $value['id'] + ] + ]; + $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); + $searchCriteria->setCurrentPage($args['currentPage']); + $searchCriteria->setPageSize($args['pageSize']); + $searchResult = $this->filterQuery->getResult($searchCriteria, $info); + + //possible division by 0 + if ($searchCriteria->getPageSize()) { + $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize()); + } else { + $maxPages = 0; + } + + $currentPage = $searchCriteria->getCurrentPage(); + if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { + $currentPage = new GraphQlInputException( + __( + 'currentPage value %1 specified is greater than the number of pages available.', + [$maxPages] + ) + ); + } + + $data = [ + 'total_count' => $searchResult->getTotalCount(), + 'items' => $searchResult->getProductsSearchResult(), + 'page_info' => [ + 'page_size' => $searchCriteria->getPageSize(), + 'current_page' => $currentPage + ] + ]; + + $result = function () use ($data) { + return $data; + }; + + return $this->valueFactory->create($result); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php new file mode 100644 index 0000000000000..ca68b29910118 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php @@ -0,0 +1,82 @@ +valueFactory = $valueFactory; + $this->catalogConfig = $catalogConfig; + $this->storeManager = $storeManager; + $this->sortbyAttributeSource = $sortbyAttributeSource; + } + + /** + * {@inheritDoc} + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + { + $sortFieldsOptions = $this->sortbyAttributeSource->getAllOptions(); + array_walk( + $sortFieldsOptions, + function (&$option) { + $option['label'] = (string)$option['label']; + } + ); + $data = [ + 'default' => $this->catalogConfig->getProductListDefaultSortBy($this->storeManager->getStore()->getId()), + 'options' => $sortFieldsOptions, + ]; + + $result = function () use ($data) { + return $data; + }; + + return $this->valueFactory->create($result); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php new file mode 100644 index 0000000000000..d2675848c2d2a --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php @@ -0,0 +1,62 @@ +valueFactory = $valueFactory; + } + + /** + * {@inheritdoc} + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): Value { + if (!isset($value['model'])) { + $result = function () { + return null; + }; + return $this->valueFactory->create($result); + } + + /* @var $product Product */ + $product = $value['model']; + $url = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]); + $result = function () use ($url) { + return $url; + }; + + return $this->valueFactory->create($result); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php new file mode 100644 index 0000000000000..ae01c67eb5224 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php @@ -0,0 +1,92 @@ +getMockBuilder(\Magento\Framework\GraphQl\Config\Element\Field::class) + ->disableOriginalConstructor() + ->getMock(); + $mockInfo = $this->getMockBuilder(\Magento\Framework\GraphQl\Schema\Type\ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mockValueFactory->method('create')->with( + $this->callback( + function ($param) { + return $param() === null; + } + ) + ); + + $this->subject->resolve($mockField, '', $mockInfo, [], []); + } + + protected function setUp() + { + parent::setUp(); + $this->objectManager = new ObjectManager($this); + $this->mockStoreManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->mockValueFactory = $this->getMockBuilder(ValueFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mockValueFactory->method('create')->willReturn( + $this->objectManager->getObject( + Value::class, + ['callback' => function () { + return ''; + }] + ) + ); + + $mockProductUrlPathGenerator = $this->getMockBuilder(ProductUrlPathGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $mockProductUrlPathGenerator->method('getUrlPathWithSuffix')->willReturn('product_url.html'); + + $this->subject = $this->objectManager->getObject( + CanonicalUrl::class, + [ + 'valueFactory' => $this->mockValueFactory, + 'storeManager' => $this->mockStoreManager, + 'productUrlPathGenerator' => $mockProductUrlPathGenerator + ] + ); + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index ca1ff78654319..5d9e174f169d1 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -279,6 +279,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ gift_message_available: String @doc(description: "Indicates whether a gift message is available") manufacturer: Int @doc(description: "A number representing the product's manufacturer") categories: [CategoryInterface] @doc(description: "The categories assigned to a product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category") + canonical_url: String @doc(description: "Canonical URL") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") } interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "PhysicalProductInterface contains attributes specific to tangible products") { @@ -375,6 +376,11 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model updated_at: String @doc(description: "Timestamp indicating when the category was updated") product_count: Int @doc(description: "The number of products in the category") default_sort_by: String @doc(description: "The attribute to use for sorting") + products( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), + sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") + ): CategoryProducts @doc(description: "The list of products assigned to the category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") } type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option") { @@ -402,6 +408,13 @@ type Products @doc(description: "The Products object is the top-level object ret page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") total_count: Int @doc(description: "The number of products returned") filters: [LayerFilter] @doc(description: "Layered navigation filters array") + sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") +} + +type CategoryProducts @doc(description: "The category products object returned in the Category query") { + items: [ProductInterface] @doc(description: "An array of products that are assigned to the category") + page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") + total_count: Int @doc(description: "The number of products returned") } input ProductFilterInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { @@ -521,3 +534,13 @@ interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl type LayerFilterItem implements LayerFilterItemInterface { } + +type SortField { + value: String @doc(description: "Attribute code of sort field") + label: String @doc(description: "Label of sort field") +} + +type SortFields @doc(description: "SortFields contains a default value for sort fields and all available sort fields") { + default: String @doc(description: "Default value of sort fields") + options: [SortField] @doc(description: "Available sort fields") +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 7d175d524e287..4d42330cd00bf 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -15,6 +15,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; +use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; use Magento\Framework\Stdlib\DateTime; @@ -724,6 +725,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $mediaProcessor; + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -767,7 +773,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param ImageTypeProcessor $imageTypeProcessor * @param MediaGalleryProcessor $mediaProcessor * @param StockItemImporterInterface|null $stockItemImporter - * + * @param DateTimeFactory $dateTimeFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -812,7 +818,8 @@ public function __construct( CatalogConfig $catalogConfig = null, ImageTypeProcessor $imageTypeProcessor = null, MediaGalleryProcessor $mediaProcessor = null, - StockItemImporterInterface $stockItemImporter = null + StockItemImporterInterface $stockItemImporter = null, + DateTimeFactory $dateTimeFactory = null ) { $this->_eventManager = $eventManager; $this->stockRegistry = $stockRegistry; @@ -858,16 +865,14 @@ public function __construct( $string, $errorAggregator ); - $this->_optionEntity = isset( - $data['option_entity'] - ) ? $data['option_entity'] : $optionFactory->create( - ['data' => ['product_entity' => $this]] - ); + $this->_optionEntity = $data['option_entity'] ?? + $optionFactory->create(['data' => ['product_entity' => $this]]); $this->_initAttributeSets() ->_initTypeModels() ->_initSkus() ->initImagesArrayKeys(); $this->validator->init($this); + $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class); } /** @@ -2151,40 +2156,8 @@ protected function _saveStockItem() $row = []; $sku = $rowData[self::COL_SKU]; if ($this->skuProcessor->getNewSku($sku) !== null) { - $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id']; + $row = $this->formatStockDataForRow($rowData); $productIdsToReindex[] = $row['product_id']; - - $row['website_id'] = $this->stockConfiguration->getDefaultScopeId(); - $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId(); - - $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']); - $existStockData = $stockItemDo->getData(); - - $row = array_merge( - $this->defaultStockData, - array_intersect_key($existStockData, $this->defaultStockData), - array_intersect_key($rowData, $this->defaultStockData), - $row - ); - $row['sku'] = $sku; - - if ($this->stockConfiguration->isQty( - $this->skuProcessor->getNewSku($sku)['type_id'] - ) - ) { - $stockItemDo->setData($row); - $row['is_in_stock'] = $this->stockStateProvider->verifyStock($stockItemDo); - if ($this->stockStateProvider->verifyNotification($stockItemDo)) { - $row['low_stock_date'] = gmdate( - 'Y-m-d H:i:s', - (new \DateTime())->getTimestamp() - ); - } - $row['stock_status_changed_auto'] = - (int)!$this->stockStateProvider->verifyStock($stockItemDo); - } else { - $row['qty'] = 0; - } } if (!isset($stockData[$sku])) { @@ -2875,4 +2848,44 @@ private function getExistingSku($sku) { return $this->_oldSku[strtolower($sku)]; } + + /** + * Format row data to DB compatible values. + * + * @param array $rowData + * @return array + */ + private function formatStockDataForRow(array $rowData): array + { + $sku = $rowData[self::COL_SKU]; + $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id']; + $row['website_id'] = $this->stockConfiguration->getDefaultScopeId(); + $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId(); + + $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']); + $existStockData = $stockItemDo->getData(); + + $row = array_merge( + $this->defaultStockData, + array_intersect_key($existStockData, $this->defaultStockData), + array_intersect_key($rowData, $this->defaultStockData), + $row + ); + + if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) { + $stockItemDo->setData($row); + $row['is_in_stock'] = isset($row['is_in_stock']) && $stockItemDo->getBackorders() + ? $row['is_in_stock'] + : $this->stockStateProvider->verifyStock($stockItemDo); + if ($this->stockStateProvider->verifyNotification($stockItemDo)) { + $date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT); + } + $row['stock_status_changed_auto'] = (int)!$this->stockStateProvider->verifyStock($stockItemDo); + } else { + $row['qty'] = 0; + } + + return $row; + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index cbaf401f32982..adb660dd118f9 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -1223,6 +1223,7 @@ protected function _importData() $typeTitles = []; $parentCount = []; $childCount = []; + $optionsToRemove = []; foreach ($bunch as $rowNumber => $rowData) { if (isset($optionId, $valueId) && empty($rowData[PRODUCT::COL_STORE_VIEW_CODE])) { @@ -1232,14 +1233,18 @@ protected function _importData() $optionId = $nextOptionId; $valueId = $nextValueId; $multiRowData = $this->_getMultiRowFormat($rowData); - + if (!empty($rowData[self::COLUMN_SKU]) && isset($this->_productsSkuToId[$rowData[self::COLUMN_SKU]])) { + $this->_rowProductId = $this->_productsSkuToId[$rowData[self::COLUMN_SKU]]; + if (array_key_exists('custom_options', $rowData) && trim($rowData['custom_options']) === '') { + $optionsToRemove[] = $this->_rowProductId; + } + } foreach ($multiRowData as $optionData) { $combinedData = array_merge($rowData, $optionData); - if (!$this->isRowAllowedToImport($combinedData, $rowNumber)) { - continue; - } - if (!$this->_parseRequiredData($combinedData)) { + if (!$this->isRowAllowedToImport($combinedData, $rowNumber) + || !$this->_parseRequiredData($combinedData) + ) { continue; } $optionData = $this->_collectOptionMainData( @@ -1266,38 +1271,45 @@ protected function _importData() } } - // Save prepared custom options data !!! - if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { - $this->_deleteEntities(array_keys($products)); - } - - if ($this->_isReadyForSaving($options, $titles, $typeValues)) { - if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { - $this->_compareOptionsWithExisting($options, $titles, $prices, $typeValues); - $this->restoreOriginalOptionTypeIds($typeValues, $typePrices, $typeTitles); - } + $this->removeExistingOptions($products, $optionsToRemove); - $this->_saveOptions( - $options - )->_saveTitles( - $titles - )->_savePrices( - $prices - )->_saveSpecificTypeValues( - $typeValues - )->_saveSpecificTypePrices( - $typePrices - )->_saveSpecificTypeTitles( - $typeTitles - )->_updateProducts( - $products - ); - } + $types = [ + 'values' => $typeValues, + 'prices' => $typePrices, + 'titles' => $typeTitles, + ]; + //Save prepared custom options data. + $this->savePreparedCustomOptions( + $products, + $options, + $titles, + $prices, + $types + ); } return true; } + /** + * Remove all existing options if import behaviour is APPEND + * in other case remove options for products with empty "custom_options" row only. + * + * @param array $products + * @param array $optionsToRemove + * + * @return void + */ + private function removeExistingOptions(array $products, array $optionsToRemove): void + { + if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + $this->_deleteEntities(array_keys($products)); + } elseif (!empty($optionsToRemove)) { + // Remove options for products with empty "custom_options" row + $this->_deleteEntities($optionsToRemove); + } + } + /** * Load data of existed products * @@ -1537,9 +1549,7 @@ private function getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle) */ protected function _parseRequiredData(array $rowData) { - if ($rowData[self::COLUMN_SKU] != '' && isset($this->_productsSkuToId[$rowData[self::COLUMN_SKU]])) { - $this->_rowProductId = $this->_productsSkuToId[$rowData[self::COLUMN_SKU]]; - } elseif (!isset($this->_rowProductId)) { + if ($this->_rowProductId === null) { return false; } @@ -1991,4 +2001,38 @@ private function getProductIdentifierField() } return $this->productEntityIdentifierField; } + + /** + * Save prepared custom options. + * + * @param array $products + * @param array $options + * @param array $titles + * @param array $prices + * @param array $types + * + * @return void + */ + private function savePreparedCustomOptions( + array $products, + array $options, + array $titles, + array $prices, + array $types + ): void { + if ($this->_isReadyForSaving($options, $titles, $types['values'])) { + if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']); + $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']); + } + + $this->_saveOptions($options) + ->_saveTitles($titles) + ->_savePrices($prices) + ->_saveSpecificTypeValues($types['values']) + ->_saveSpecificTypePrices($types['prices']) + ->_saveSpecificTypeTitles($types['titles']) + ->_updateProducts($products); + } + } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php new file mode 100644 index 0000000000000..a60b05dc7c9bc --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php @@ -0,0 +1,75 @@ +priceResourceModel = $priceResourceModel; + } + + /** + * @inheritdoc + */ + public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []) : void + { + $connection = $this->priceResourceModel->getConnection(); + $select = $connection->select(); + + $select->join( + ['cpiw' => $this->priceResourceModel->getTable('catalog_product_index_website')], + 'cpiw.website_id = i.' . $priceTable->getWebsiteField(), + [] + ); + $select->join( + ['cpp' => $this->priceResourceModel->getMainTable()], + 'cpp.product_id = i.' . $priceTable->getEntityField() + . ' AND cpp.customer_group_id = i.' . $priceTable->getCustomerGroupField() + . ' AND cpp.website_id = i.' . $priceTable->getWebsiteField() + . ' AND cpp.rule_date = cpiw.website_date', + [] + ); + if ($entityIds) { + $select->where('i.entity_id IN (?)', $entityIds); + } + + $finalPrice = $priceTable->getFinalPriceField(); + $finalPriceExpr = $select->getConnection()->getLeastSql([ + $priceTable->getFinalPriceField(), + $select->getConnection()->getIfNullSql('cpp.rule_price', 'i.' . $finalPrice), + ]); + $minPrice = $priceTable->getMinPriceField(); + $minPriceExpr = $select->getConnection()->getLeastSql([ + $priceTable->getMinPriceField(), + $select->getConnection()->getIfNullSql('cpp.rule_price', 'i.' . $minPrice), + ]); + $select->columns([ + $finalPrice => $finalPriceExpr, + $minPrice => $minPriceExpr, + ]); + + $query = $connection->updateFromSelect($select, ['i' => $priceTable->getTableName()]); + $connection->query($query); + } +} diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index 7696569cb26da..d927d6f4d0c82 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -606,7 +606,10 @@ public function afterSave() */ public function reindex() { - $this->_ruleProductProcessor->reindexList($this->_productIds); + $productIds = $this->_productIds ? array_keys(array_filter($this->_productIds, function (array $data) { + return array_filter($data); + })) : []; + $this->_ruleProductProcessor->reindexList($productIds); } /** diff --git a/app/code/Magento/CatalogRule/etc/di.xml b/app/code/Magento/CatalogRule/etc/di.xml index 40893592c3d0f..8ed88dd4f3fdb 100644 --- a/app/code/Magento/CatalogRule/etc/di.xml +++ b/app/code/Magento/CatalogRule/etc/di.xml @@ -150,4 +150,11 @@ + + + + Magento\CatalogRule\Model\Indexer\ProductPriceIndexModifier + + + diff --git a/app/code/Magento/CatalogRule/etc/indexer.xml b/app/code/Magento/CatalogRule/etc/indexer.xml index 08ed456457bfe..e648ea567631c 100644 --- a/app/code/Magento/CatalogRule/etc/indexer.xml +++ b/app/code/Magento/CatalogRule/etc/indexer.xml @@ -14,4 +14,9 @@ Catalog Product Rule Indexed product/rule association + + + + + 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 new file mode 100644 index 0000000000000..ed841996ea07b --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Model/Plugin/Category.php @@ -0,0 +1,45 @@ +fulltextIndexerProcessor = $fulltextIndexerProcessor; + } + + /** + * Mark fulltext indexer as invalid post-deletion of category. + * + * @param Resource $subjectCategory + * @param Resource $resultCategory + * @return Resource + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDelete(Resource $subjectCategory, Resource $resultCategory) : Resource + { + $this->fulltextIndexerProcessor->markIndexerAsInvalid(); + + return $resultCategory; + } +} diff --git a/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml b/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml index d6c72d883fedf..2d41d17889e49 100644 --- a/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml +++ b/app/code/Magento/CatalogSearch/etc/adminhtml/di.xml @@ -31,4 +31,7 @@ Magento\CatalogSearch\Ui\DataProvider\Product\AddFulltextFilterToCollection + + + diff --git a/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml b/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..c7293783dc609 --- /dev/null +++ b/app/code/Magento/CatalogSearch/etc/webapi_rest/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml b/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..c7293783dc609 --- /dev/null +++ b/app/code/Magento/CatalogSearch/etc/webapi_soap/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Store/Block/Switcher.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Store/Block/Switcher.php new file mode 100644 index 0000000000000..44213c007551c --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Store/Block/Switcher.php @@ -0,0 +1,86 @@ +postHelper = $postHelper; + $this->urlFinder = $urlFinder; + $this->request = $request; + } + + /** + * @param \Magento\Store\Block\Switcher $subject + * @param string $result + * @param Store $store + * @param array $data + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetTargetStorePostData( + \Magento\Store\Block\Switcher $subject, + string $result, + Store $store, + array $data = [] + ): string { + $data[StoreResolverInterface::PARAM_NAME] = $store->getCode(); + $currentUrl = $store->getCurrentUrl(true); + $baseUrl = $store->getBaseUrl(); + $urlPath = parse_url($currentUrl, PHP_URL_PATH); + $urlToSwitch = $currentUrl; + + //check only catalog pages + if ($this->request->getFrontName() === 'catalog') { + $currentRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::REQUEST_PATH => ltrim($urlPath, '/'), + UrlRewrite::STORE_ID => $store->getId(), + ]); + if (null === $currentRewrite) { + $urlToSwitch = $baseUrl; + } + } + + return $this->postHelper->getPostData($urlToSwitch, $data); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml new file mode 100644 index 0000000000000..3a9122b2f748d --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/etc/frontend/di.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/code/Magento/CatalogWidget/etc/widget.xml b/app/code/Magento/CatalogWidget/etc/widget.xml index 3d54c314c6622..bcc1b623da02e 100644 --- a/app/code/Magento/CatalogWidget/etc/widget.xml +++ b/app/code/Magento/CatalogWidget/etc/widget.xml @@ -40,7 +40,11 @@ - 86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache. + + If not set, equals to 86400 seconds (24 hours). To update widget instantly, go to Cache Management and clear Blocks HTML Output cache. +
Widget will not show products that begin to match the specified conditions until cache is refreshed.]]> +
diff --git a/app/code/Magento/CatalogWidget/i18n/en_US.csv b/app/code/Magento/CatalogWidget/i18n/en_US.csv index 9ecde5cb1a062..4cccbdd926282 100644 --- a/app/code/Magento/CatalogWidget/i18n/en_US.csv +++ b/app/code/Magento/CatalogWidget/i18n/en_US.csv @@ -16,5 +16,11 @@ Title,Title Template,Template "Products Grid Template","Products Grid Template" "Cache Lifetime (Seconds)","Cache Lifetime (Seconds)" -"86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache.","86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache." +"Time in seconds between the widget updates. +
If not set, equals to 86400 seconds (24 hours). To update widget instantly, go to Cache Management and clear Blocks HTML Output cache. +
Widget will not show products that begin to match the specified conditions until cache is refreshed." +, +"Time in seconds between the widget updates. +
If not set, equals to 86400 seconds (24 hours). To update widget instantly, go to Cache Management and clear Blocks HTML Output cache. +
Widget will not show products that begin to match the specified conditions until cache is refreshed." Conditions,Conditions diff --git a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php index 45e885d0dbd46..cad1c100c7e5b 100644 --- a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php +++ b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php @@ -9,7 +9,7 @@ * Interface PaymentDetailsInterface * @api */ -interface PaymentDetailsInterface +interface PaymentDetailsInterface extends \Magento\Framework\Api\ExtensibleDataInterface { /**#@+ * Constants defined for keys of array, makes typos less likely diff --git a/app/code/Magento/Checkout/Block/Registration.php b/app/code/Magento/Checkout/Block/Registration.php index 91ec85c1db0ed..e880230f50a74 100644 --- a/app/code/Magento/Checkout/Block/Registration.php +++ b/app/code/Magento/Checkout/Block/Registration.php @@ -91,7 +91,7 @@ public function getEmailAddress() */ public function getCreateAccountUrl() { - return $this->getUrl('checkout/account/create'); + return $this->getUrl('checkout/account/delegateCreate'); } /** diff --git a/app/code/Magento/Checkout/Controller/Account/Create.php b/app/code/Magento/Checkout/Controller/Account/Create.php index 2ee5d6d5528c3..dae0bb98be453 100644 --- a/app/code/Magento/Checkout/Controller/Account/Create.php +++ b/app/code/Magento/Checkout/Controller/Account/Create.php @@ -9,6 +9,10 @@ use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\NoSuchEntityException; +/** + * @deprecated + * @see DelegateCreate + */ class Create extends \Magento\Framework\App\Action\Action { /** diff --git a/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php new file mode 100644 index 0000000000000..6c4c8b053e2ae --- /dev/null +++ b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php @@ -0,0 +1,58 @@ +delegateService = $customerDelegation; + $this->session = $session; + } + + /** + * {@inheritdoc} + */ + public function execute() + { + /** @var string|null $orderId */ + $orderId = $this->session->getLastOrderId(); + if (!$orderId) { + return $this->resultRedirectFactory->create()->setPath('/'); + } + + return $this->delegateService->delegateNew((int)$orderId); + } +} diff --git a/app/code/Magento/Checkout/Helper/Data.php b/app/code/Magento/Checkout/Helper/Data.php index b3c2e17e5d678..636d4aaca21f0 100644 --- a/app/code/Magento/Checkout/Helper/Data.php +++ b/app/code/Magento/Checkout/Helper/Data.php @@ -9,6 +9,7 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\Store\Model\Store; use Magento\Store\Model\ScopeInterface; +use Magento\Sales\Api\PaymentFailuresInterface; /** * Checkout default helper @@ -52,6 +53,11 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper */ protected $priceCurrency; + /** + * @var PaymentFailuresInterface + */ + private $paymentFailures; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -60,6 +66,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param PriceCurrencyInterface $priceCurrency + * @param PaymentFailuresInterface|null $paymentFailures * @codeCoverageIgnore */ public function __construct( @@ -69,7 +76,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder, \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, - PriceCurrencyInterface $priceCurrency + PriceCurrencyInterface $priceCurrency, + PaymentFailuresInterface $paymentFailures = null ) { $this->_storeManager = $storeManager; $this->_checkoutSession = $checkoutSession; @@ -77,6 +85,8 @@ public function __construct( $this->_transportBuilder = $transportBuilder; $this->inlineTranslation = $inlineTranslation; $this->priceCurrency = $priceCurrency; + $this->paymentFailures = $paymentFailures ? : \Magento\Framework\App\ObjectManager::getInstance() + ->get(PaymentFailuresInterface::class); parent::__construct($context); } @@ -202,126 +212,13 @@ public function getBaseSubtotalInclTax($item) * @param string $message * @param string $checkoutType * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function sendPaymentFailedEmail($checkout, $message, $checkoutType = 'onepage') - { - $this->inlineTranslation->suspend(); - - $template = $this->scopeConfig->getValue( - 'checkout/payment_failed/template', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - - $copyTo = $this->_getEmails('checkout/payment_failed/copy_to', $checkout->getStoreId()); - $copyMethod = $this->scopeConfig->getValue( - 'checkout/payment_failed/copy_method', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - $bcc = []; - if ($copyTo && $copyMethod == 'bcc') { - $bcc = $copyTo; - } - - $_receiver = $this->scopeConfig->getValue( - 'checkout/payment_failed/receiver', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - $sendTo = [ - [ - 'email' => $this->scopeConfig->getValue( - 'trans_email/ident_' . $_receiver . '/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ), - 'name' => $this->scopeConfig->getValue( - 'trans_email/ident_' . $_receiver . '/name', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ), - ], - ]; - - if ($copyTo && $copyMethod == 'copy') { - foreach ($copyTo as $email) { - $sendTo[] = ['email' => $email, 'name' => null]; - } - } - $shippingMethod = ''; - if ($shippingInfo = $checkout->getShippingAddress()->getShippingMethod()) { - $data = explode('_', $shippingInfo); - $shippingMethod = $data[0]; - } - - $paymentMethod = ''; - if ($paymentInfo = $checkout->getPayment()) { - $paymentMethod = $paymentInfo->getMethod(); - } - - $items = ''; - foreach ($checkout->getAllVisibleItems() as $_item) { - /* @var $_item \Magento\Quote\Model\Quote\Item */ - $items .= - $_item->getProduct()->getName() . ' x ' . $_item->getQty() . ' ' . $checkout->getStoreCurrencyCode() - . ' ' . $_item->getProduct()->getFinalPrice( - $_item->getQty() - ) . "\n"; - } - $total = $checkout->getStoreCurrencyCode() . ' ' . $checkout->getGrandTotal(); - - foreach ($sendTo as $recipient) { - $transport = $this->_transportBuilder->setTemplateIdentifier( - $template - )->setTemplateOptions( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => Store::DEFAULT_STORE_ID - ] - )->setTemplateVars( - [ - 'reason' => $message, - 'checkoutType' => $checkoutType, - 'dateAndTime' => $this->_localeDate->formatDateTime( - new \DateTime(), - \IntlDateFormatter::MEDIUM, - \IntlDateFormatter::MEDIUM - ), - 'customer' => $checkout->getCustomerFirstname() . ' ' . $checkout->getCustomerLastname(), - 'customerEmail' => $checkout->getCustomerEmail(), - 'billingAddress' => $checkout->getBillingAddress(), - 'shippingAddress' => $checkout->getShippingAddress(), - 'shippingMethod' => $this->scopeConfig->getValue( - 'carriers/' . $shippingMethod . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ), - 'paymentMethod' => $this->scopeConfig->getValue( - 'payment/' . $paymentMethod . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ), - 'items' => nl2br($items), - 'total' => $total, - ] - )->setFrom( - $this->scopeConfig->getValue( - 'checkout/payment_failed/identity', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ) - )->addTo( - $recipient['email'], - $recipient['name'] - )->addBcc( - $bcc - )->getTransport(); - - $transport->sendMessage(); - } - - $this->inlineTranslation->resume(); + public function sendPaymentFailedEmail( + \Magento\Quote\Model\Quote $checkout, + string $message, + string $checkoutType = 'onepage' + ): \Magento\Checkout\Helper\Data { + $this->paymentFailures->handle((int)$checkout->getId(), $message, $checkoutType); return $this; } diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index a17cf41585649..333226b7d216f 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Model; @@ -10,6 +11,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Model\Quote; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -135,13 +137,19 @@ public function savePaymentInformation( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + /** @var Quote $quote */ + $quote = $this->cartRepository->getActive($quoteIdMask->getQuoteId()); + if ($billingAddress) { $billingAddress->setEmail($email); - $this->billingAddressManagement->assign($cartId, $billingAddress); + $quote->removeAddress($quote->getBillingAddress()->getId()); + $quote->setBillingAddress($billingAddress); + $quote->setDataChanges(true); } else { - $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); - $this->cartRepository->getActive($quoteIdMask->getQuoteId())->getBillingAddress()->setEmail($email); + $quote->getBillingAddress()->setEmail($email); } + $this->limitShippingCarrier($quote); $this->paymentMethodManagement->set($cartId, $paymentMethod); return true; @@ -169,4 +177,22 @@ private function getLogger() } return $this->logger; } + + /** + * Limits shipping rates request by carrier from shipping address. + * + * @param Quote $quote + * + * @return void + * @see \Magento\Shipping\Model\Shipping::collectRates + */ + private function limitShippingCarrier(Quote $quote) : void + { + $shippingAddress = $quote->getShippingAddress(); + if ($shippingAddress && $shippingAddress->getShippingMethod()) { + $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); + $shippingCarrier = array_shift($shippingDataArray); + $shippingAddress->setLimitCarrier($shippingCarrier); + } + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php index c403156dc13e9..53132ffaa748b 100644 --- a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php @@ -6,8 +6,7 @@ namespace Magento\Checkout\Test\Unit\Helper; -use \Magento\Checkout\Helper\Data; - +use Magento\Checkout\Helper\Data; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\ScopeInterface; @@ -24,38 +23,36 @@ class DataTest extends \PHPUnit\Framework\TestCase /** * @var Data */ - private $_helper; + private $helper; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $_transportBuilder; + private $transportBuilder; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $_translator; + private $translator; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_checkoutSession; + private $checkoutSession; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_scopeConfig; + private $scopeConfig; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_collectionFactory; + private $eventManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @inheritdoc */ - protected $_eventManager; - protected function setUp() { $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -63,191 +60,88 @@ protected function setUp() $arguments = $objectManagerHelper->getConstructArguments($className); /** @var \Magento\Framework\App\Helper\Context $context */ $context = $arguments['context']; - $this->_translator = $arguments['inlineTranslation']; - $this->_eventManager = $context->getEventManager(); - $this->_scopeConfig = $context->getScopeConfig(); - $this->_scopeConfig->expects($this->any()) + $this->translator = $arguments['inlineTranslation']; + $this->eventManager = $context->getEventManager(); + $this->scopeConfig = $context->getScopeConfig(); + $this->scopeConfig->expects($this->any()) ->method('getValue') - ->will( - $this->returnValueMap( + ->willReturnMap( + [ + [ + 'checkout/payment_failed/template', + ScopeInterface::SCOPE_STORE, + 8, + 'fixture_email_template_payment_failed', + ], + [ + 'checkout/payment_failed/receiver', + ScopeInterface::SCOPE_STORE, + 8, + 'sysadmin', + ], + [ + 'trans_email/ident_sysadmin/email', + ScopeInterface::SCOPE_STORE, + 8, + 'sysadmin@example.com', + ], + [ + 'trans_email/ident_sysadmin/name', + ScopeInterface::SCOPE_STORE, + 8, + 'System Administrator', + ], + [ + 'checkout/payment_failed/identity', + ScopeInterface::SCOPE_STORE, + 8, + 'noreply@example.com', + ], + [ + 'carriers/ground/title', + ScopeInterface::SCOPE_STORE, + null, + 'Ground Shipping', + ], [ - [ - 'checkout/payment_failed/template', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'fixture_email_template_payment_failed' - ], - [ - 'checkout/payment_failed/receiver', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'sysadmin' - ], - [ - 'trans_email/ident_sysadmin/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'sysadmin@example.com' - ], - [ - 'trans_email/ident_sysadmin/name', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'System Administrator' - ], - [ - 'checkout/payment_failed/identity', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'noreply@example.com' - ], - [ - 'carriers/ground/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'Ground Shipping' - ], - [ - 'payment/fixture-payment-method/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'Check Money Order' - ], - [ - 'checkout/options/onepage_checkout_enabled', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'One Page Checkout' - ] - ] - ) + 'payment/fixture-payment-method/title', + ScopeInterface::SCOPE_STORE, + null, + 'Check Money Order', + ], + [ + 'checkout/options/onepage_checkout_enabled', + ScopeInterface::SCOPE_STORE, + null, + 'One Page Checkout', + ], + ] ); - $this->_checkoutSession = $arguments['checkoutSession']; + $this->checkoutSession = $arguments['checkoutSession']; $arguments['localeDate']->expects($this->any()) ->method('formatDateTime') ->willReturn('Oct 02, 2013'); - $this->_transportBuilder = $arguments['transportBuilder']; + $this->transportBuilder = $arguments['transportBuilder']; $this->priceCurrency = $arguments['priceCurrency']; - $this->_helper = $objectManagerHelper->getObject($className, $arguments); + $this->helper = $objectManagerHelper->getObject($className, $arguments); } /** * @return void - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSendPaymentFailedEmail() { - $shippingAddress = new \Magento\Framework\DataObject(['shipping_method' => 'ground_transportation']); - $billingAddress = new \Magento\Framework\DataObject(['street' => 'Fixture St']); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateOptions' - )->with( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - ] - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateIdentifier' - )->with( - 'fixture_email_template_payment_failed' - )->will( - $this->returnSelf() - ); + $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $quoteMock->expects($this->any())->method('getId')->willReturn(1); - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setFrom' - )->with( - 'noreply@example.com' - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'addTo' - )->with( - 'sysadmin@example.com', - 'System Administrator' - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateVars' - )->with( - [ - 'reason' => 'test message', - 'checkoutType' => 'onepage', - 'dateAndTime' => 'Oct 02, 2013', - 'customer' => 'John Doe', - 'customerEmail' => 'john.doe@example.com', - 'billingAddress' => $billingAddress, - 'shippingAddress' => $shippingAddress, - 'shippingMethod' => 'Ground Shipping', - 'paymentMethod' => 'Check Money Order', - 'items' => "Product One x 2 USD 10
\nProduct Two x 3 USD 60
\n", - 'total' => 'USD 70' - ] - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects($this->once())->method('addBcc')->will($this->returnSelf()); - $this->_transportBuilder->expects( - $this->once() - )->method( - 'getTransport' - )->will( - $this->returnValue($this->createMock(\Magento\Framework\Mail\TransportInterface::class)) - ); - - $this->_translator->expects($this->at(1))->method('suspend'); - $this->_translator->expects($this->at(1))->method('resume'); - - $productOne = $this->createMock(\Magento\Catalog\Model\Product::class); - $productOne->expects($this->once())->method('getName')->will($this->returnValue('Product One')); - $productOne->expects($this->once())->method('getFinalPrice')->with(2)->will($this->returnValue(10)); - - $productTwo = $this->createMock(\Magento\Catalog\Model\Product::class); - $productTwo->expects($this->once())->method('getName')->will($this->returnValue('Product Two')); - $productTwo->expects($this->once())->method('getFinalPrice')->with(3)->will($this->returnValue(60)); - - $quote = new \Magento\Framework\DataObject( - [ - 'store_id' => 8, - 'store_currency_code' => 'USD', - 'grand_total' => 70, - 'customer_firstname' => 'John', - 'customer_lastname' => 'Doe', - 'customer_email' => 'john.doe@example.com', - 'billing_address' => $billingAddress, - 'shipping_address' => $shippingAddress, - 'payment' => new \Magento\Framework\DataObject(['method' => 'fixture-payment-method']), - 'all_visible_items' => [ - new \Magento\Framework\DataObject(['product' => $productOne, 'qty' => 2]), - new \Magento\Framework\DataObject(['product' => $productTwo, 'qty' => 3]) - ] - ] - ); - $this->assertSame($this->_helper, $this->_helper->sendPaymentFailedEmail($quote, 'test message')); + $this->assertSame($this->helper, $this->helper->sendPaymentFailedEmail($quoteMock, 'test message')); } /** @@ -255,14 +149,14 @@ public function testSendPaymentFailedEmail() */ public function testGetCheckout() { - $this->assertEquals($this->_checkoutSession, $this->_helper->getCheckout()); + $this->assertEquals($this->checkoutSession, $this->helper->getCheckout()); } public function testGetQuote() { $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->_checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); - $this->assertEquals($quoteMock, $this->_helper->getQuote()); + $this->checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); + $this->assertEquals($quoteMock, $this->helper->getQuote()); } public function testFormatPrice() @@ -270,26 +164,26 @@ public function testFormatPrice() $price = 5.5; $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, ['formatPrice', '__wakeup']); - $this->_checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); + $this->checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); $quoteMock->expects($this->once())->method('getStore')->will($this->returnValue($storeMock)); $this->priceCurrency->expects($this->once())->method('format')->will($this->returnValue('5.5')); - $this->assertEquals('5.5', $this->_helper->formatPrice($price)); + $this->assertEquals('5.5', $this->helper->formatPrice($price)); } public function testConvertPrice() { $price = 5.5; $this->priceCurrency->expects($this->once())->method('convertAndFormat')->willReturn($price); - $this->assertEquals(5.5, $this->_helper->convertPrice($price)); + $this->assertEquals(5.5, $this->helper->convertPrice($price)); } public function testCanOnepageCheckout() { - $this->_scopeConfig->expects($this->once())->method('getValue')->with( + $this->scopeConfig->expects($this->once())->method('getValue')->with( 'checkout/options/onepage_checkout_enabled', 'store' )->will($this->returnValue(true)); - $this->assertTrue($this->_helper->canOnepageCheckout()); + $this->assertTrue($this->helper->canOnepageCheckout()); } public function testIsContextCheckout() @@ -310,18 +204,18 @@ public function testIsContextCheckout() public function testIsCustomerMustBeLogged() { - $this->_scopeConfig->expects($this->once())->method('isSetFlag')->with( + $this->scopeConfig->expects($this->once())->method('isSetFlag')->with( 'checkout/options/customer_must_be_logged', \Magento\Store\Model\ScopeInterface::SCOPE_STORE )->will($this->returnValue(true)); - $this->assertTrue($this->_helper->isCustomerMustBeLogged()); + $this->assertTrue($this->helper->isCustomerMustBeLogged()); } public function testGetPriceInclTax() { $itemMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getPriceInclTax']); $itemMock->expects($this->exactly(2))->method('getPriceInclTax')->will($this->returnValue(5.5)); - $this->assertEquals(5.5, $this->_helper->getPriceInclTax($itemMock)); + $this->assertEquals(5.5, $this->helper->getPriceInclTax($itemMock)); } public function testGetPriceInclTaxWithoutTax() @@ -362,7 +256,7 @@ public function testGetSubtotalInclTax() $expected = 5.5; $itemMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getRowTotalInclTax']); $itemMock->expects($this->exactly(2))->method('getRowTotalInclTax')->will($this->returnValue($rowTotalInclTax)); - $this->assertEquals($expected, $this->_helper->getSubtotalInclTax($itemMock)); + $this->assertEquals($expected, $this->helper->getSubtotalInclTax($itemMock)); } public function testGetSubtotalInclTaxNegative() @@ -380,7 +274,7 @@ public function testGetSubtotalInclTaxNegative() $itemMock->expects($this->once()) ->method('getDiscountTaxCompensation')->will($this->returnValue($discountTaxCompensation)); $itemMock->expects($this->once())->method('getRowTotal')->will($this->returnValue($rowTotal)); - $this->assertEquals($expected, $this->_helper->getSubtotalInclTax($itemMock)); + $this->assertEquals($expected, $this->helper->getSubtotalInclTax($itemMock)); } public function testGetBasePriceInclTaxWithoutQty() @@ -427,7 +321,7 @@ public function testGetBaseSubtotalInclTax() $itemMock->expects($this->once())->method('getBaseTaxAmount'); $itemMock->expects($this->once())->method('getBaseDiscountTaxCompensation'); $itemMock->expects($this->once())->method('getBaseRowTotal'); - $this->_helper->getBaseSubtotalInclTax($itemMock); + $this->helper->getBaseSubtotalInclTax($itemMock); } public function testIsAllowedGuestCheckoutWithoutStore() @@ -435,9 +329,9 @@ public function testIsAllowedGuestCheckoutWithoutStore() $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $store = null; $quoteMock->expects($this->once())->method('getStoreId')->will($this->returnValue(1)); - $this->_scopeConfig->expects($this->once()) + $this->scopeConfig->expects($this->once()) ->method('isSetFlag') ->will($this->returnValue(true)); - $this->assertTrue($this->_helper->isAllowedGuestCheckout($quoteMock, $store)); + $this->assertTrue($this->helper->isAllowedGuestCheckout($quoteMock, $store)); } } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index da0de5d4f0a3d..853ae0157e64a 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Test\Unit\Model; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\QuoteIdMask; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -96,6 +100,7 @@ public function testSavePaymentInformationAndPlaceOrder() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); @@ -119,10 +124,6 @@ public function testSavePaymentInformationAndPlaceOrder() ->willReturn($adapterMockForCheckout); $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); $adapterMockForCheckout->expects($this->once())->method('commit'); - - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $this->cartManagementMock->expects($this->once())->method('placeOrder')->with($cartId)->willReturn($orderId); @@ -142,6 +143,7 @@ public function testSavePaymentInformationAndPlaceOrderException() $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) @@ -164,12 +166,9 @@ public function testSavePaymentInformationAndPlaceOrderException() ->willReturn($adapterMockForCheckout); $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); $adapterMockForCheckout->expects($this->once())->method('rollback'); - - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $exception = new \Exception(__('DB exception')); + $exception = new \Magento\Framework\Exception\CouldNotSaveException(__('DB exception')); $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); @@ -185,11 +184,9 @@ public function testSavePaymentInformation() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $this->assertTrue($this->model->savePaymentInformation($cartId, $email, $paymentMock, $billingAddressMock)); @@ -201,13 +198,13 @@ public function testSavePaymentInformationWithoutBillingAddress() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); $this->billingAddressManagementMock->expects($this->never())->method('assign'); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $quoteIdMaskMock = $this->createPartialMock(\Magento\Quote\Model\QuoteIdMask::class, ['getQuoteId', 'load']); + $quoteIdMaskMock = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); $this->quoteIdMaskFactoryMock->expects($this->once())->method('create')->willReturn($quoteIdMaskMock); $quoteIdMaskMock->expects($this->once())->method('load')->with($cartId, 'masked_id')->willReturnSelf(); $quoteIdMaskMock->expects($this->once())->method('getQuoteId')->willReturn($cartId); @@ -228,6 +225,15 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $quoteMock = $this->createMock(Quote::class); + $quoteMock->method('getBillingAddress')->willReturn($billingAddressMock); + $this->cartRepositoryMock->method('getActive')->with($cartId)->willReturn($quoteMock); + + $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); + $this->quoteIdMaskFactoryMock->method('create')->willReturn($quoteIdMask); + $quoteIdMask->method('load')->with($cartId, 'masked_id')->willReturnSelf(); + $quoteIdMask->method('getQuoteId')->willReturn($cartId); + $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) @@ -250,10 +256,7 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() ->willReturn($adapterMockForCheckout); $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); $adapterMockForCheckout->expects($this->once())->method('rollback'); - - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $phrase = new \Magento\Framework\Phrase(__('DB exception')); $exception = new \Magento\Framework\Exception\LocalizedException($phrase); @@ -262,4 +265,57 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); } + + /** + * @param int $cartId + * @param \PHPUnit_Framework_MockObject_MockObject $billingAddressMock + * @return void + */ + private function getMockForAssignBillingAddress( + int $cartId, + \PHPUnit_Framework_MockObject_MockObject $billingAddressMock + ) : void { + $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); + $this->quoteIdMaskFactoryMock->method('create') + ->willReturn($quoteIdMask); + $quoteIdMask->method('load') + ->with($cartId, 'masked_id') + ->willReturnSelf(); + $quoteIdMask->method('getQuoteId') + ->willReturn($cartId); + + $billingAddressId = 1; + $quote = $this->createMock(Quote::class); + $quoteBillingAddress = $this->createMock(Address::class); + $quoteShippingAddress = $this->createPartialMock( + Address::class, + ['setLimitCarrier', 'getShippingMethod'] + ); + $this->cartRepositoryMock->method('getActive') + ->with($cartId) + ->willReturn($quote); + $quote->expects($this->once()) + ->method('getBillingAddress') + ->willReturn($quoteBillingAddress); + $quote->expects($this->once()) + ->method('getShippingAddress') + ->willReturn($quoteShippingAddress); + $quoteBillingAddress->expects($this->once()) + ->method('getId') + ->willReturn($billingAddressId); + $quote->expects($this->once()) + ->method('removeAddress') + ->with($billingAddressId); + $quote->expects($this->once()) + ->method('setBillingAddress') + ->with($billingAddressMock); + $quote->expects($this->once()) + ->method('setDataChanges') + ->willReturnSelf(); + $quoteShippingAddress->method('getShippingMethod') + ->willReturn('flatrate_flatrate'); + $quoteShippingAddress->expects($this->once()) + ->method('setLimitCarrier') + ->with('flatrate'); + } } diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 5f695adc9f4b4..540565345bd9b 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -7,7 +7,6 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-catalog-inventory": "*", "magento/module-config": "*", diff --git a/app/code/Magento/Checkout/etc/adminhtml/routes.xml b/app/code/Magento/Checkout/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..e537861059870 --- /dev/null +++ b/app/code/Magento/Checkout/etc/adminhtml/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Magento/Checkout/etc/webapi.xml b/app/code/Magento/Checkout/etc/webapi.xml index 7b435db200f19..26c601a4e9f38 100644 --- a/app/code/Magento/Checkout/etc/webapi.xml +++ b/app/code/Magento/Checkout/etc/webapi.xml @@ -104,7 +104,7 @@ - + diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js index c707792111c82..0f2b0f4e26869 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js @@ -182,6 +182,15 @@ define([ }); }, + /** + * Sets window location hash. + * + * @param {String} hash + */ + setHash: function (hash) { + window.location.hash = hash; + }, + /** * Next step. */ @@ -199,7 +208,7 @@ define([ if (steps().length > activeIndex + 1) { code = steps()[activeIndex + 1].code; steps()[activeIndex + 1].isVisible(true); - window.location = window.checkoutConfig.checkoutUrl + '#' + code; + this.setHash(code); document.body.scrollTop = document.documentElement.scrollTop = 0; } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index 399321bd2f67d..3ea49cd981d90 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -12,7 +12,7 @@ define([ $.widget('mage.shoppingCart', { /** @inheritdoc */ _create: function () { - var items, i; + var items, i, reload; $(this.options.emptyCartButton).on('click', $.proxy(function () { $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); @@ -36,6 +36,27 @@ define([ $(this.options.continueShoppingButton).on('click', $.proxy(function () { location.href = this.options.continueShoppingUrl; }, this)); + + $(document).on('ajax:removeFromCart', $.proxy(function () { + reload = true; + $('div.block.block-minicart').on('dropdowndialogclose', $.proxy(function () { + if (reload === true) { + location.reload(); + reload = false; + } + $('div.block.block-minicart').off('dropdowndialogclose'); + })); + }, this)); + $(document).on('ajax:updateItemQty', $.proxy(function () { + reload = true; + $('div.block.block-minicart').on('dropdowndialogclose', $.proxy(function () { + if (reload === true) { + location.reload(); + reload = false; + } + $('div.block.block-minicart').off('dropdowndialogclose'); + })); + }, this)); } }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index dab40f026645d..3fb8743e951c8 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -220,6 +220,7 @@ define([ */ _updateItemQtyAfter: function (elem) { this._hideItemButton(elem); + $(document).trigger('ajax:updateItemQty'); }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js index 72cf4e3d479c3..683a18d0e4ead 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js @@ -25,6 +25,11 @@ define([ initialize: function () { this._super(); window.addEventListener('hashchange', _.bind(stepNavigator.handleHash, stepNavigator)); + + if (!window.location.hash) { + stepNavigator.setHash(stepNavigator.steps().sort(stepNavigator.sortItems)[0].code); + } + stepNavigator.handleHash(); }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js b/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js index c715b5c4d45ce..a7b3e18c06088 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/registration.js @@ -38,7 +38,16 @@ define([ }, /** - * Create new user account + * @return String + */ + getUrl: function () { + return this.registrationUrl; + }, + + /** + * Create new user account. + * + * @deprecated */ createAccount: function () { this.creationStarted(true); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index 8bf1a87d34e6e..2daca51a2f5da 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -30,8 +30,12 @@ - - + + + + + +
diff --git a/app/code/Magento/Checkout/view/frontend/web/template/registration.html b/app/code/Magento/Checkout/view/frontend/web/template/registration.html index 256fc1968abfc..ea94726e5443e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/registration.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/registration.html @@ -11,11 +11,8 @@

:

-
- + +
- - -

- +
diff --git a/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml b/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml index 1249ea44b991e..5a708f49a7034 100644 --- a/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml +++ b/app/code/Magento/CheckoutAgreements/etc/adminhtml/routes.xml @@ -7,7 +7,7 @@ --> - + diff --git a/app/code/Magento/Cms/Model/Wysiwyg/DefaultConfigProvider.php b/app/code/Magento/Cms/Model/Wysiwyg/DefaultConfigProvider.php index dee37c8e901ec..2ff2aa3f82ba8 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/DefaultConfigProvider.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/DefaultConfigProvider.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Model\Wysiwyg; /** @@ -27,7 +29,7 @@ public function __construct(\Magento\Framework\View\Asset\Repository $assetRepo) /** * {@inheritdoc} */ - public function getConfig($config) + public function getConfig(\Magento\Framework\DataObject $config) : \Magento\Framework\DataObject { $config->addData([ 'tinymce4' => [ diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Gallery/DefaultConfigProvider.php b/app/code/Magento/Cms/Model/Wysiwyg/Gallery/DefaultConfigProvider.php index 2301cf9950ecc..822f9ce2b1cb5 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Gallery/DefaultConfigProvider.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Gallery/DefaultConfigProvider.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Model\Wysiwyg\Gallery; class DefaultConfigProvider implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface @@ -49,7 +51,7 @@ public function __construct( /** * {@inheritdoc} */ - public function getConfig($config) + public function getConfig(\Magento\Framework\DataObject $config) : \Magento\Framework\DataObject { $pluginData = (array) $config->getData('plugins'); $imageData = [ diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 0c8ff7d0b2b78..4b7cd239a66f5 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -739,7 +739,7 @@ protected function _validatePath($path) */ protected function _sanitizePath($path) { - return rtrim(preg_replace('~[/\\\]+~', '/', $this->_directory->getDriver()->getRealPath($path)), '/'); + return rtrim(preg_replace('~[/\\\]+~', '/', $this->_directory->getDriver()->getRealPathSafety($path)), '/'); } /** diff --git a/app/code/Magento/Cms/Model/WysiwygDefaultConfig.php b/app/code/Magento/Cms/Model/WysiwygDefaultConfig.php index c03629188798b..b0f7260d209ea 100644 --- a/app/code/Magento/Cms/Model/WysiwygDefaultConfig.php +++ b/app/code/Magento/Cms/Model/WysiwygDefaultConfig.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Model; class WysiwygDefaultConfig implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface @@ -10,7 +12,7 @@ class WysiwygDefaultConfig implements \Magento\Framework\Data\Wysiwyg\ConfigProv /** * {@inheritdoc} */ - public function getConfig($config) + public function getConfig(\Magento\Framework\DataObject $config) : \Magento\Framework\DataObject { return $config; } diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 2fb9649fc61de..25134451d5a56 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -114,16 +114,9 @@ class StorageTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class); - $this->driverMock = $this->getMockForAbstractClass( - \Magento\Framework\Filesystem\DriverInterface::class, - [], - '', - false, - false, - true, - ['getRealPath'] - ); - $this->driverMock->expects($this->any())->method('getRealPath')->will($this->returnArgument(0)); + $this->driverMock = $this->getMockBuilder(\Magento\Framework\Filesystem\DriverInterface::class) + ->setMethods(['getRealPathSafety']) + ->getMockForAbstractClass(); $this->directoryMock = $this->createPartialMock( \Magento\Framework\Filesystem\Directory\Write::class, @@ -243,6 +236,7 @@ public function testDeleteDirectoryOverRoot() $this->expectExceptionMessage( sprintf('Directory %s is not under storage root path.', self::INVALID_DIRECTORY_OVER_ROOT) ); + $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->will($this->returnArgument(0)); $this->imagesStorage->deleteDirectory(self::INVALID_DIRECTORY_OVER_ROOT); } @@ -253,7 +247,7 @@ public function testDeleteRootDirectory() { $this->expectException(\Magento\Framework\Exception\LocalizedException::class); $this->expectExceptionMessage(sprintf('We can\'t delete root directory %s right now.', self::STORAGE_ROOT_DIR)); - + $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->will($this->returnArgument(0)); $this->imagesStorage->deleteDirectory(self::STORAGE_ROOT_DIR); } diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/HomePageUrlLocator.php b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/HomePageUrlLocator.php new file mode 100644 index 0000000000000..6cc669e46d080 --- /dev/null +++ b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/HomePageUrlLocator.php @@ -0,0 +1,47 @@ +scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function locateUrl($urlKey): ?string + { + if ($urlKey === '/') { + $homePageUrl = $this->scopeConfig->getValue( + Page::XML_PATH_HOME_PAGE, + ScopeInterface::SCOPE_STORE + ); + return $homePageUrl; + } + return null; + } +} diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json index 1e0c836389340..c57e4cdc92a83 100644 --- a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json @@ -4,13 +4,14 @@ "type": "magento2-module", "require": { "php": "~7.1.3||~7.2.0", - "magento/framework": "*" - + "magento/framework": "*", + "magento/module-url-rewrite-graph-ql": "*", + "magento/module-store": "*", + "magento/module-cms": "*" }, "suggest": { "magento/module-cms-url-rewrite": "*", - "magento/module-catalog-graph-ql": "*", - "magento/module-url-rewrite-graph-ql": "*" + "magento/module-catalog-graph-ql": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/etc/di.xml b/app/code/Magento/CmsUrlRewriteGraphQl/etc/di.xml new file mode 100644 index 0000000000000..d384c898acb62 --- /dev/null +++ b/app/code/Magento/CmsUrlRewriteGraphQl/etc/di.xml @@ -0,0 +1,16 @@ + + + + + + + Magento\CmsUrlRewriteGraphQl\Model\Resolver\UrlRewrite\HomePageUrlLocator + + + + diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index c17df229cf549..81e39a83296d7 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -709,7 +709,7 @@ protected function _getAdditionalElementTypes() } /** - * Temporary moved those $this->getRequest()->getParam('blabla') from the code accross this block + * Temporary moved those $this->getRequest()->getParam('blabla') from the code across this block * to getBlala() methods to be later set from controller with setters */ diff --git a/app/code/Magento/Config/Model/Config/Importer.php b/app/code/Magento/Config/Model/Config/Importer.php index e65a90c593e84..a54af2ead5048 100644 --- a/app/code/Magento/Config/Model/Config/Importer.php +++ b/app/code/Magento/Config/Model/Config/Importer.php @@ -124,7 +124,7 @@ public function import(array $data) $this->scopeConfig->clean(); } - $this->state->emulateAreaCode(Area::AREA_ADMINHTML, function () use ($changedData, $data) { + $this->state->emulateAreaCode(Area::AREA_ADMINHTML, function () use ($changedData) { $this->scope->setCurrentScope(Area::AREA_ADMINHTML); // Invoke saving of new values. diff --git a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php index 92bc61b3d65e5..252042a41cc1d 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php +++ b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php @@ -11,7 +11,8 @@ * Defines status of visibility of form elements on Stores > Settings > Configuration page * in Admin Panel in Production mode. * @api - * @since 100.2.0 + * @deprecated class location was changed + * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction */ class ConcealInProductionConfigList implements ElementVisibilityInterface { @@ -42,64 +43,32 @@ class ConcealInProductionConfigList implements ElementVisibilityInterface */ private $state; - /** - * - * The list of form element paths which ignore visibility status. - * - * E.g. - * - * ```php - * [ - * 'general/country/default' => '', - * ]; - * ``` - * - * It means that: - * - field 'default' in group Country Options (in section General) will be showed, even if all group(section) - * will be hidden. - * - * @var array - */ - private $exemptions = []; - /** * @param State $state The object that has information about the state of the system * @param array $configs The list of form element paths with concrete visibility status. - * @param array $exemptions The list of form element paths which ignore visibility status. */ - public function __construct(State $state, array $configs = [], array $exemptions = []) + public function __construct(State $state, array $configs = []) { $this->state = $state; $this->configs = $configs; - $this->exemptions = $exemptions; } /** * @inheritdoc - * @since 100.2.0 + * @deprecated */ public function isHidden($path) { - $result = false; $path = $this->normalizePath($path); - if ($this->state->getMode() === State::MODE_PRODUCTION - && preg_match('/(?(?
.*?)\/.*?)\/.*?/', $path, $match)) { - $group = $match['group']; - $section = $match['section']; - $exemptions = array_keys($this->exemptions); - foreach ($this->configs as $configPath => $value) { - if ($value === static::HIDDEN && strpos($path, $configPath) !==false) { - $result = empty(array_intersect([$section, $group, $path], $exemptions)); - } - } - } - return $result; + return $this->state->getMode() === State::MODE_PRODUCTION + && !empty($this->configs[$path]) + && $this->configs[$path] === static::HIDDEN; } /** * @inheritdoc - * @since 100.2.0 + * @deprecated */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php new file mode 100755 index 0000000000000..d5ded9292864a --- /dev/null +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php @@ -0,0 +1,138 @@ + Settings > Configuration page + * in Admin Panel in Production mode. + * @api + */ +class ConcealInProduction implements ElementVisibilityInterface +{ + /** + * The list of form element paths with concrete visibility status. + * + * E.g. + * + * ```php + * [ + * 'general/locale/code' => ElementVisibilityInterface::DISABLED, + * 'general/country' => ElementVisibilityInterface::HIDDEN, + * ]; + * ``` + * + * It means that: + * - field Locale (in group Locale Options in section General) will be disabled + * - group Country Options (in section General) will be hidden + * + * @var array + */ + private $configs = []; + + /** + * The object that has information about the state of the system. + * + * @var State + */ + private $state; + + /** + * + * The list of form element paths which ignore visibility status. + * + * E.g. + * + * ```php + * [ + * 'general/country/default' => '', + * ]; + * ``` + * + * It means that: + * - field 'default' in group Country Options (in section General) will be showed, even if all group(section) + * will be hidden. + * + * @var array + */ + private $exemptions = []; + + /** + * @param State $state The object that has information about the state of the system + * @param array $configs The list of form element paths with concrete visibility status. + * @param array $exemptions The list of form element paths which ignore visibility status. + */ + public function __construct(State $state, array $configs = [], array $exemptions = []) + { + $this->state = $state; + $this->configs = $configs; + $this->exemptions = $exemptions; + } + + /** + * @inheritdoc + * @since 100.2.0 + */ + public function isHidden($path) + { + $path = $this->normalizePath($path); + if ($this->state->getMode() === State::MODE_PRODUCTION + && preg_match('/(?(?
.*?)\/.*?)\/.*?/', $path, $match)) { + $group = $match['group']; + $section = $match['section']; + $exemptions = array_keys($this->exemptions); + $checkedItems = []; + foreach ([$path, $group, $section] as $itemPath) { + $checkedItems[] = $itemPath; + if (!empty($this->configs[$itemPath])) { + return $this->configs[$itemPath] === static::HIDDEN + && empty(array_intersect($checkedItems, $exemptions)); + } + } + } + + return false; + } + + /** + * @inheritdoc + * @since 100.2.0 + */ + public function isDisabled($path) + { + $path = $this->normalizePath($path); + if ($this->state->getMode() === State::MODE_PRODUCTION) { + while (true) { + if (!empty($this->configs[$path])) { + return $this->configs[$path] === static::DISABLED; + } + + $position = strripos($path, '/'); + if ($position === false) { + break; + } + $path = substr($path, 0, $position); + } + } + + return false; + } + + /** + * Returns normalized path. + * + * @param string $path The path to be normalized + * @return string The normalized path + */ + private function normalizePath($path) + { + return trim($path, '/'); + } +} diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php new file mode 100755 index 0000000000000..29148a244dcc6 --- /dev/null +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php @@ -0,0 +1,72 @@ + Settings > Configuration page + * when Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION is enabled + * otherwise rule from Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction is used + * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction + * + * @api + */ +class ConcealInProductionWithoutScdOnDemand implements ElementVisibilityInterface +{ + /** + * @var ConcealInProduction Element visibility rules in the Production mode + */ + private $concealInProduction; + + /** + * @var DeploymentConfig The application deployment configuration + */ + private $deploymentConfig; + + /** + * @param ConcealInProductionFactory $concealInProductionFactory + * @param DeploymentConfig $deploymentConfig Deployment configuration reader + * @param array $configs The list of form element paths with concrete visibility status. + * @param array $exemptions The list of form element paths which ignore visibility status. + */ + public function __construct( + ConcealInProductionFactory $concealInProductionFactory, + DeploymentConfig $deploymentConfig, + array $configs = [], + array $exemptions = [] + ) { + $this->concealInProduction = $concealInProductionFactory + ->create(['configs' => $configs, 'exemptions' => $exemptions]); + $this->deploymentConfig = $deploymentConfig; + } + + /** + * @inheritdoc + */ + public function isHidden($path): bool + { + if (!$this->deploymentConfig->getConfigData(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION)) { + return $this->concealInProduction->isHidden($path); + } + return false; + } + + /** + * @inheritdoc + */ + public function isDisabled($path): bool + { + if (!$this->deploymentConfig->getConfigData(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION)) { + return $this->concealInProduction->isDisabled($path); + } + return false; + } +} diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php index 1679ac75ad02c..8a005a52ab614 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php @@ -72,7 +72,7 @@ public function testGetElementHtmlWithValue() 'showInWebsite' => '1', 'showInStore' => '1', 'label' => null, - 'backend_model' => \Magento\BackendModelConfig\Backend\Image::class, + 'backend_model' => \Magento\Config\Model\Config\Backend\Image::class, 'upload_dir' => [ 'config' => 'system/filesystem/media', 'scope_info' => '1', diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php index fa78d5dde652c..ba74b93d9ad76 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ConcealInProductionConfigListTest.php @@ -8,6 +8,11 @@ use Magento\Config\Model\Config\Structure\ConcealInProductionConfigList; use Magento\Framework\App\State; +/** + * @deprecated Original class has changed the location + * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction + * @see \Magento\Config\Test\Unit\Model\Config\Structure\ElementVisibility\ConcealInProductionTest + */ class ConcealInProductionConfigListTest extends \PHPUnit\Framework\TestCase { /** @@ -33,13 +38,9 @@ protected function setUp() 'third/path' => 'no', 'third/path/field' => ConcealInProductionConfigList::DISABLED, 'first/path/field' => 'no', - 'fourth' => ConcealInProductionConfigList::HIDDEN, - ]; - $exemptions = [ - 'fourth/path/value' => '', ]; - $this->model = new ConcealInProductionConfigList($this->stateMock, $configs, $exemptions); + $this->model = new ConcealInProductionConfigList($this->stateMock, $configs); } /** @@ -47,6 +48,8 @@ protected function setUp() * @param string $mageMode * @param bool $expectedResult * @dataProvider disabledDataProvider + * + * @deprecated */ public function testIsDisabled($path, $mageMode, $expectedResult) { @@ -58,6 +61,8 @@ public function testIsDisabled($path, $mageMode, $expectedResult) /** * @return array + * + * @deprecated */ public function disabledDataProvider() { @@ -82,6 +87,8 @@ public function disabledDataProvider() * @param string $mageMode * @param bool $expectedResult * @dataProvider hiddenDataProvider + * + * @deprecated */ public function testIsHidden($path, $mageMode, $expectedResult) { @@ -93,6 +100,8 @@ public function testIsHidden($path, $mageMode, $expectedResult) /** * @return array + * + * @deprecated */ public function hiddenDataProvider() { @@ -100,10 +109,8 @@ public function hiddenDataProvider() ['first/path', State::MODE_PRODUCTION, false], ['first/path', State::MODE_DEFAULT, false], ['some/path', State::MODE_PRODUCTION, false], - ['second/path/field', State::MODE_PRODUCTION, true], + ['second/path', State::MODE_PRODUCTION, true], ['second/path', State::MODE_DEVELOPER, false], - ['fourth/path/value', State::MODE_PRODUCTION, false], - ['fourth/path/test', State::MODE_PRODUCTION, true], ]; } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionTest.php new file mode 100644 index 0000000000000..5fc689f911c1c --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionTest.php @@ -0,0 +1,106 @@ +stateMock = $this->getMockBuilder(State::class) + ->disableOriginalConstructor() + ->getMock(); + + $configs = [ + 'section1/group1/field1' => ElementVisibilityInterface::DISABLED, + 'section1/group1' => ElementVisibilityInterface::HIDDEN, + 'section1' => ElementVisibilityInterface::DISABLED, + 'section1/group2' => 'no', + 'section2/group1' => ElementVisibilityInterface::DISABLED, + 'section2/group2' => ElementVisibilityInterface::HIDDEN, + 'section3' => ElementVisibilityInterface::HIDDEN, + 'section3/group1/field1' => 'no', + ]; + $exemptions = [ + 'section1/group1/field3' => '', + 'section1/group2/field1' => '', + 'section2/group2/field1' => '', + 'section3/group2' => '', + ]; + + $this->model = new ConcealInProduction($this->stateMock, $configs, $exemptions); + } + + /** + * @param string $path + * @param string $mageMode + * @param bool $isDisabled + * @param bool $isHidden + * @dataProvider disabledDataProvider + */ + public function testCheckVisibility(string $path, string $mageMode, bool $isHidden, bool $isDisabled): void + { + $this->stateMock->expects($this->any()) + ->method('getMode') + ->willReturn($mageMode); + + $this->assertSame($isHidden, $this->model->isHidden($path)); + $this->assertSame($isDisabled, $this->model->isDisabled($path)); + } + + /** + * @return array + */ + public function disabledDataProvider(): array + { + return [ + //visibility of field 'section1/group1/field1' should be applied + ['section1/group1/field1', State::MODE_PRODUCTION, false, true], + ['section1/group1/field1', State::MODE_DEFAULT, false, false], + ['section1/group1/field1', State::MODE_DEVELOPER, false, false], + //visibility of group 'section1/group1' should be applied + ['section1/group1/field2', State::MODE_PRODUCTION, true, false], + ['section1/group1/field2', State::MODE_DEFAULT, false, false], + ['section1/group1/field2', State::MODE_DEVELOPER, false, false], + //exemption should be applied for section1/group2/field1 + ['section1/group2/field1', State::MODE_PRODUCTION, false, false], + ['section1/group2/field1', State::MODE_DEFAULT, false, false], + ['section1/group2/field1', State::MODE_DEVELOPER, false, false], + //as 'section1/group2' has neither Disable nor Hidden rule, this field should be visible + ['section1/group2/field2', State::MODE_PRODUCTION, false, false], + //exemption should be applied for section1/group1/field3 + ['section1/group1/field3', State::MODE_PRODUCTION, false, false], + //visibility of group 'section2/group1' should be applied + ['section2/group1/field1', State::MODE_PRODUCTION, false, true], + //exemption should be applied for section2/group2/field1 + ['section2/group2/field1', State::MODE_PRODUCTION, false, false], + //any rule should not be applied + ['section2/group3/field1', State::MODE_PRODUCTION, false, false], + //any rule should not be applied + ['section3/group1/field1', State::MODE_PRODUCTION, false, false], + //visibility of section 'section3' should be applied + ['section3/group1/field2', State::MODE_PRODUCTION, true, false], + //exception from 'section3/group2' should be applied + ['section3/group2/field1', State::MODE_PRODUCTION, false, false], + + ]; + } +} diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemandTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemandTest.php new file mode 100644 index 0000000000000..9d69a587f695d --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemandTest.php @@ -0,0 +1,148 @@ +createMock(ConcealInProductionFactory::class); + + $this->concealInProductionMock = $this->createMock(ConcealInProduction::class); + + $this->deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + + $configs = [ + 'section1/group1/field1' => ElementVisibilityInterface::DISABLED, + 'section1/group1' => ElementVisibilityInterface::HIDDEN, + 'section1' => ElementVisibilityInterface::DISABLED, + 'section1/group2' => 'no', + 'section2/group1' => ElementVisibilityInterface::DISABLED, + 'section2/group2' => ElementVisibilityInterface::HIDDEN, + 'section3' => ElementVisibilityInterface::HIDDEN, + 'section3/group1/field1' => 'no', + ]; + $exemptions = [ + 'section1/group1/field3' => '', + 'section1/group2/field1' => '', + 'section2/group2/field1' => '', + 'section3/group2' => '', + ]; + + $concealInProductionFactoryMock->expects($this->any()) + ->method('create') + ->with(['configs' => $configs, 'exemptions' => $exemptions]) + ->willReturn($this->concealInProductionMock); + + $this->model = new ConcealInProductionWithoutScdOnDemand( + $concealInProductionFactoryMock, + $this->deploymentConfigMock, + $configs, + $exemptions + ); + } + + public function testIsHiddenScdOnDemandEnabled(): void + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(true); + $this->concealInProductionMock->expects($this->never()) + ->method('isHidden'); + + $this->assertFalse($this->model->isHidden($path)); + } + + public function testIsDisabledScdOnDemandEnabled(): void + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(true); + $this->concealInProductionMock->expects($this->never()) + ->method('isDisabled'); + + $this->assertFalse($this->model->isDisabled($path)); + } + + /** + * @param bool $isHidden + * + * @dataProvider visibilityDataProvider + */ + public function testIsHiddenScdOnDemandDisabled(bool $isHidden): void + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(false); + $this->concealInProductionMock->expects($this->once()) + ->method('isHidden') + ->with($path) + ->willReturn($isHidden); + + $this->assertSame($isHidden, $this->model->isHidden($path)); + } + + /** + * @param bool $isDisabled + * + * @dataProvider visibilityDataProvider + */ + public function testIsDisabledScdOnDemandDisabled(bool $isDisabled): void + { + $path = 'section1/group1/field1'; + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Constants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(false); + $this->concealInProductionMock->expects($this->once()) + ->method('isDisabled') + ->with($path) + ->willReturn($isDisabled); + + $this->assertSame($isDisabled, $this->model->isDisabled($path)); + } + + /** + * @return array + */ + public function visibilityDataProvider(): array + { + return [ + [true], + [false], + ]; + } +} diff --git a/app/code/Magento/Config/etc/adminhtml/di.xml b/app/code/Magento/Config/etc/adminhtml/di.xml index c21c06c7f3e1f..5e54f177776ba 100644 --- a/app/code/Magento/Config/etc/adminhtml/di.xml +++ b/app/code/Magento/Config/etc/adminhtml/di.xml @@ -15,6 +15,8 @@ Magento\Config\Model\Config\Structure\ConcealInProductionConfigList + Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction + Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionWithoutScdOnDemand diff --git a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php index 64a0c23139c01..151bf5aa9263e 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php @@ -629,7 +629,7 @@ protected function _insertData() } /** - * Get new supper attribute id. + * Get new super attribute id. * * @return int */ diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php new file mode 100644 index 0000000000000..0c3fc6fba6005 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php @@ -0,0 +1,56 @@ +configurableType = $configurableType; + $this->productRepository = $productRepository; + } + + /** + * Add parent identities to product identities + * + * @param Product $subject + * @param array $identities + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetIdentities(Product $subject, array $identities): array + { + foreach ($this->configurableType->getParentIdsByChild($subject->getId()) as $parentId) { + $parentProduct = $this->productRepository->getById($parentId); + $identities = array_merge($identities, $parentProduct->getIdentities()); + } + + return array_unique($identities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Cache/Tag/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Cache/Tag/Configurable.php deleted file mode 100644 index ac42e320f3ad9..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Cache/Tag/Configurable.php +++ /dev/null @@ -1,51 +0,0 @@ -catalogProductTypeConfigurable = $catalogProductTypeConfigurable; - } - - /** - * {@inheritdoc} - */ - public function getTags($object) - { - if (!is_object($object)) { - throw new \InvalidArgumentException('Provided argument is not an object'); - } - - if (!($object instanceof \Magento\Catalog\Model\Product)) { - throw new \InvalidArgumentException('Provided argument must be a product'); - } - - $result = $object->getIdentities(); - - foreach ($this->catalogProductTypeConfigurable->getParentIdsByChild($object->getId()) as $parentId) { - $result[] = \Magento\Catalog\Model\Product::CACHE_TAG . '_' . $parentId; - } - return $result; - } -} diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 326310cc3c802..087931ebe5dcc 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -1,7 +1,5 @@ storeResolver = $storeResolver ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - StoreResolverInterface::class - ); - } - /** * @param null|int|array $entityIds * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable @@ -58,6 +25,7 @@ protected function reindex($entityIds = null) $this->_applyConfigurableOption($entityIds); $this->_movePriceDataToIndexTable($entityIds); } + return $this; } @@ -109,67 +77,49 @@ protected function _prepareConfigurableOptionPriceTable() * * @param array|null $entityIds * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _applyConfigurableOption($entityIds = null) { $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); $connection = $this->getConnection(); - $coaTable = $this->_getConfigurableOptionAggregateTable(); $copTable = $this->_getConfigurableOptionPriceTable(); + $finalPriceTable = $this->_getDefaultFinalPriceTable(); $linkField = $metadata->getLinkField(); - $this->_prepareConfigurableOptionAggregateTable(); $this->_prepareConfigurableOptionPriceTable(); - $subSelect = $this->getSelect(); - $subSelect->join( + $select = $connection->select()->from( + ['i' => $this->getIdxTable()], + [] + )->join( ['l' => $this->getTable('catalog_product_super_link')], - 'l.product_id = e.entity_id', + 'l.product_id = i.entity_id', [] )->join( ['le' => $this->getTable('catalog_product_entity')], 'le.' . $linkField . ' = l.parent_id', - ['parent_id' => 'entity_id'] - ); - - if ($entityIds !== null) { - $subSelect->where('le.entity_id IN (?)', $entityIds); - } - - $select = $connection->select(); - $select - ->from(['sub' => new \Zend_Db_Expr('(' . (string)$subSelect . ')')], '') - ->columns([ - 'sub.parent_id', - 'sub.entity_id', - 'sub.customer_group_id', - 'sub.website_id', - 'sub.price', - 'sub.tier_price' - ]); - - $query = $select->insertFromSelect($coaTable); - $connection->query($query); - - $select = $connection->select()->from( - [$coaTable], + [] + )->columns( [ - 'parent_id', + 'le.entity_id', 'customer_group_id', 'website_id', - 'MIN(price)', - 'MAX(price)', + 'MIN(final_price)', + 'MAX(final_price)', 'MIN(tier_price)', + ] )->group( - ['parent_id', 'customer_group_id', 'website_id'] + ['le.entity_id', 'customer_group_id', 'website_id'] ); + if ($entityIds !== null) { + $select->where('le.entity_id IN (?)', $entityIds); + } $query = $select->insertFromSelect($copTable); $connection->query($query); - $table = ['i' => $this->_getDefaultFinalPriceTable()]; + $table = ['i' => $finalPriceTable]; $select = $connection->select()->join( ['io' => $copTable], 'i.entity_id = io.entity_id AND i.customer_group_id = io.customer_group_id' . @@ -188,7 +138,6 @@ protected function _applyConfigurableOption($entityIds = null) $query = $select->crossUpdateFromSelect($table); $connection->query($query); - $connection->delete($coaTable); $connection->delete($copTable); return $this; diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php index 95afba984d57d..ccff85dd9717f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php @@ -189,7 +189,6 @@ public function getChildrenIds($parentId, $required = true) */ public function getParentIdsByChild($childId) { - $parentIds = []; $select = $this->getConnection() ->select() ->from(['l' => $this->getMainTable()], []) @@ -198,10 +197,7 @@ public function getParentIdsByChild($childId) 'e.' . $this->optionProvider->getProductEntityLinkField() . ' = l.parent_id', ['e.entity_id'] )->where('l.product_id IN(?)', $childId); - - foreach ($this->getConnection()->fetchAll($select) as $row) { - $parentIds[] = $row['entity_id']; - } + $parentIds = $this->getConnection()->fetchCol($select); return $parentIds; } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php new file mode 100644 index 0000000000000..d29f163ee1129 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php @@ -0,0 +1,77 @@ +configurableTypeMock = $this->getMockBuilder(Configurable::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) + ->getMock(); + + $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock, $this->productRepositoryMock); + } + + public function testAfterGetIdentities() + { + $productId = 1; + $productIdentity = 'cache_tag_1'; + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $parentProductId = 2; + $parentProductIdentity = 'cache_tag_2'; + $parentProductMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $productMock->expects($this->once()) + ->method('getId') + ->willReturn($productId); + $this->configurableTypeMock->expects($this->once()) + ->method('getParentIdsByChild') + ->with($productId) + ->willReturn([$parentProductId]); + $this->productRepositoryMock->expects($this->once()) + ->method('getById') + ->with($parentProductId) + ->willReturn($parentProductMock); + $parentProductMock->expects($this->once()) + ->method('getIdentities') + ->willReturn([$parentProductIdentity]); + + $productIdentities = $this->plugin->afterGetIdentities($productMock, [$productIdentity]); + $this->assertEquals([$productIdentity, $parentProductIdentity], $productIdentities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Cache/Tag/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Cache/Tag/ConfigurableTest.php deleted file mode 100644 index a3f1435f84d2f..0000000000000 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Cache/Tag/ConfigurableTest.php +++ /dev/null @@ -1,66 +0,0 @@ -typeResource = $this->createMock( - \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable::class - ); - - $this->model = new Configurable($this->typeResource); - } - - public function testGetWithScalar() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Provided argument is not an object'); - $this->model->getTags('scalar'); - } - - public function testGetTagsWithObject() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Provided argument must be a product'); - $this->model->getTags(new \stdClass()); - } - - public function testGetTagsWithVariation() - { - $product = $this->createMock(\Magento\Catalog\Model\Product::class); - - $identities = ['id1', 'id2']; - - $product->expects($this->once()) - ->method('getIdentities') - ->willReturn($identities); - - $parentId = 4; - $this->typeResource->expects($this->once()) - ->method('getParentIdsByChild') - ->willReturn([$parentId]); - - $expected = array_merge($identities, [\Magento\Catalog\Model\Product::CACHE_TAG . '_' . $parentId]); - - $this->assertEquals($expected, $this->model->getTags($product)); - } -} diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 3f04081eaf645..15dbc53a5447a 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -168,13 +168,6 @@ - - - - \Magento\ConfigurableProduct\Model\Product\Cache\Tag\Configurable - - - @@ -209,4 +202,7 @@ + + + diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml index 7dca29263bae3..0ba3c7ed2d7d6 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml @@ -13,8 +13,6 @@ */ ?> -getCurrencySymbolsData();?> -
diff --git a/app/code/Magento/Customer/Api/AccountDelegationInterface.php b/app/code/Magento/Customer/Api/AccountDelegationInterface.php new file mode 100644 index 0000000000000..e3a738530c49d --- /dev/null +++ b/app/code/Magento/Customer/Api/AccountDelegationInterface.php @@ -0,0 +1,31 @@ +logger->critical($e); + } catch (\UnexpectedValueException $e) { + $this->logger->error($e); } } diff --git a/app/code/Magento/Customer/Model/Customer/NotificationStorage.php b/app/code/Magento/Customer/Model/Customer/NotificationStorage.php index 7054324851f34..11e0b9b916559 100644 --- a/app/code/Magento/Customer/Model/Customer/NotificationStorage.php +++ b/app/code/Magento/Customer/Model/Customer/NotificationStorage.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Model\Customer; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\Serialize\SerializerInterface; @@ -18,21 +19,21 @@ class NotificationStorage private $cache; /** - * @param FrontendInterface $cache - */ - - /** - * @param FrontendInterface $cache + * @var SerializerInterface */ private $serializer; /** * NotificationStorage constructor. * @param FrontendInterface $cache + * @param SerializerInterface $serializer */ - public function __construct(FrontendInterface $cache) - { + public function __construct( + FrontendInterface $cache, + SerializerInterface $serializer = null + ) { $this->cache = $cache; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); } /** @@ -45,7 +46,7 @@ public function __construct(FrontendInterface $cache) public function add($notificationType, $customerId) { $this->cache->save( - $this->getSerializer()->serialize([ + $this->serializer->serialize([ 'customer_id' => $customerId, 'notification_type' => $notificationType ]), @@ -88,19 +89,4 @@ private function getCacheKey($notificationType, $customerId) { return 'notification_' . $notificationType . '_' . $customerId; } - - /** - * Get serializer - * - * @return SerializerInterface - * @deprecated 100.2.0 - */ - private function getSerializer() - { - if ($this->serializer === null) { - $this->serializer = \Magento\Framework\App\ObjectManager::getInstance() - ->get(SerializerInterface::class); - } - return $this->serializer; - } } diff --git a/app/code/Magento/Customer/Model/Delegation/AccountDelegation.php b/app/code/Magento/Customer/Model/Delegation/AccountDelegation.php new file mode 100644 index 0000000000000..85c67213c4613 --- /dev/null +++ b/app/code/Magento/Customer/Model/Delegation/AccountDelegation.php @@ -0,0 +1,53 @@ +redirectFactory = $redirectFactory; + $this->storage = $storage; + } + + /** + * {@inheritdoc} + */ + public function createRedirectForNew( + CustomerInterface $customer, + array $mixedData = null + ): Redirect { + $this->storage->storeNewOperation($customer, $mixedData); + + return $this->redirectFactory->create()->setPath('customer/account/create'); + } +} diff --git a/app/code/Magento/Customer/Model/Delegation/Data/NewOperation.php b/app/code/Magento/Customer/Model/Delegation/Data/NewOperation.php new file mode 100644 index 0000000000000..5dcefc2326794 --- /dev/null +++ b/app/code/Magento/Customer/Model/Delegation/Data/NewOperation.php @@ -0,0 +1,54 @@ +customer = $customer; + $this->additionalData = $additionalData; + } + + /** + * @return CustomerInterface + */ + public function getCustomer(): CustomerInterface + { + return $this->customer; + } + + /** + * @return array + */ + public function getAdditionalData(): array + { + return $this->additionalData; + } +} diff --git a/app/code/Magento/Customer/Model/Delegation/Storage.php b/app/code/Magento/Customer/Model/Delegation/Storage.php new file mode 100644 index 0000000000000..71a61d59057cb --- /dev/null +++ b/app/code/Magento/Customer/Model/Delegation/Storage.php @@ -0,0 +1,151 @@ +newFactory = $newFactory; + $this->customerFactory = $customerFactory; + $this->addressFactory = $addressFactory; + $this->regionFactory = $regionFactory; + $this->logger = $logger; + $this->session = $session; + } + + /** + * Store data for new account operation. + * + * @param CustomerInterface $customer + * @param array $delegatedData + * + * @return void + */ + public function storeNewOperation(CustomerInterface $customer, array $delegatedData): void + { + /** @var Customer $customer */ + $customerData = $customer->__toArray(); + $addressesData = []; + if ($customer->getAddresses()) { + /** @var Address $address */ + foreach ($customer->getAddresses() as $address) { + $addressesData[] = $address->__toArray(); + } + } + $this->session->setCustomerFormData($customerData); + $this->session->setDelegatedNewCustomerData([ + 'customer' => $customerData, + 'addresses' => $addressesData, + 'delegated_data' => $delegatedData, + ]); + } + + /** + * Retrieve delegated new operation data and mark it as used. + * + * @return NewOperation|null + */ + public function consumeNewOperation() + { + try { + $serialized = $this->session->getDelegatedNewCustomerData(true); + } catch (\Throwable $exception) { + $this->logger->error($exception); + $serialized = null; + } + if ($serialized === null) { + return null; + } + + /** @var AddressInterface[] $addresses */ + $addresses = []; + foreach ($serialized['addresses'] as $addressData) { + if (isset($addressData['region'])) { + /** @var RegionInterface $region */ + $region = $this->regionFactory->create( + ['data' => $addressData['region']] + ); + $addressData['region'] = $region; + } + $addresses[] = $this->addressFactory->create( + ['data' => $addressData] + ); + } + $customerData = $serialized['customer']; + $customerData['addresses'] = $addresses; + + return $this->newFactory->create([ + 'customer' => $this->customerFactory->create( + ['data' => $customerData] + ), + 'additionalData' => $serialized['delegated_data'], + ]); + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 91a593c347806..29e35c721a3be 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -7,16 +7,20 @@ namespace Magento\Customer\Model\ResourceModel; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Delegation\Data\NewOperation; use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\ImageProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; use Magento\Framework\App\ObjectManager; /** * Customer repository. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface { @@ -95,6 +99,11 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte */ private $notificationStorage; + /** + * @var DelegatedStorage + */ + private $delegatedStorage; + /** * @param \Magento\Customer\Model\CustomerFactory $customerFactory * @param \Magento\Customer\Model\Data\CustomerSecureFactory $customerSecureFactory @@ -111,6 +120,7 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface $collectionProcessor * @param NotificationStorage $notificationStorage + * @param DelegatedStorage|null $delegatedStorage * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -128,7 +138,8 @@ public function __construct( ImageProcessorInterface $imageProcessor, \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor, - NotificationStorage $notificationStorage + NotificationStorage $notificationStorage, + DelegatedStorage $delegatedStorage = null ) { $this->customerFactory = $customerFactory; $this->customerSecureFactory = $customerSecureFactory; @@ -145,6 +156,8 @@ public function __construct( $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->collectionProcessor = $collectionProcessor; $this->notificationStorage = $notificationStorage; + $this->delegatedStorage = $delegatedStorage + ?? ObjectManager::getInstance()->get(DelegatedStorage::class); } /** @@ -152,8 +165,10 @@ public function __construct( * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null) + public function save(CustomerInterface $customer, $passwordHash = null) { + /** @var NewOperation|null $delegatedNewOperation */ + $delegatedNewOperation = !$customer->getId() ? $this->delegatedStorage->consumeNewOperation() : null; $prevCustomerData = null; $prevCustomerDataArr = null; if ($customer->getId()) { @@ -167,56 +182,49 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, $prevCustomerData ); - $origAddresses = $customer->getAddresses(); $customer->setAddresses([]); - $customerData = $this->extensibleDataObjectConverter->toNestedArray( - $customer, - [], - \Magento\Customer\Api\Data\CustomerInterface::class - ); - + $customerData = $this->extensibleDataObjectConverter->toNestedArray($customer, [], CustomerInterface::class); $customer->setAddresses($origAddresses); + /** @var Customer $customerModel */ $customerModel = $this->customerFactory->create(['data' => $customerData]); + //Model's actual ID field maybe different than "id" so "id" field from $customerData may be ignored. + $customerModel->setId($customer->getId()); $storeId = $customerModel->getStoreId(); if ($storeId === null) { $customerModel->setStoreId($this->storeManager->getStore()->getId()); } - $customerModel->setId($customer->getId()); - // Need to use attribute set or future updates can cause data loss if (!$customerModel->getAttributeSetId()) { - $customerModel->setAttributeSetId( - \Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER - ); + $customerModel->setAttributeSetId(CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER); } $this->populateCustomerWithSecureData($customerModel, $passwordHash); - // If customer email was changed, reset RpToken info - if ($prevCustomerData - && $prevCustomerData->getEmail() !== $customerModel->getEmail() - ) { + if ($prevCustomerData && $prevCustomerData->getEmail() !== $customerModel->getEmail()) { $customerModel->setRpToken(null); $customerModel->setRpTokenCreatedAt(null); } - if (!array_key_exists('default_billing', $customerArr) && - null !== $prevCustomerDataArr && - array_key_exists('default_billing', $prevCustomerDataArr) + if (!array_key_exists('default_billing', $customerArr) + && null !== $prevCustomerDataArr + && array_key_exists('default_billing', $prevCustomerDataArr) ) { $customerModel->setDefaultBilling($prevCustomerDataArr['default_billing']); } - - if (!array_key_exists('default_shipping', $customerArr) && - null !== $prevCustomerDataArr && - array_key_exists('default_shipping', $prevCustomerDataArr) + if (!array_key_exists('default_shipping', $customerArr) + && null !== $prevCustomerDataArr + && array_key_exists('default_shipping', $prevCustomerDataArr) ) { $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); } - $customerModel->save(); $this->customerRegistry->push($customerModel); $customerId = $customerModel->getId(); - + if (!$customer->getAddresses() + && $delegatedNewOperation + && $delegatedNewOperation->getCustomer()->getAddresses() + ) { + $customer->setAddresses($delegatedNewOperation->getCustomer()->getAddresses()); + } if ($customer->getAddresses() !== null) { if ($customer->getId()) { $existingAddresses = $this->getById($customer->getId())->getAddresses(); @@ -227,7 +235,6 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa } else { $existingAddressIds = []; } - $savedAddressIds = []; foreach ($customer->getAddresses() as $address) { $address->setCustomerId($customerId) @@ -237,7 +244,6 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $savedAddressIds[] = $address->getId(); } } - $addressIdsToDelete = array_diff($existingAddressIds, $savedAddressIds); foreach ($addressIdsToDelete as $addressId) { $this->addressRepository->deleteById($addressId); @@ -247,8 +253,13 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa $savedCustomer = $this->get($customer->getEmail(), $customer->getWebsiteId()); $this->eventManager->dispatch( 'customer_save_after_data_object', - ['customer_data_object' => $savedCustomer, 'orig_customer_data_object' => $prevCustomerData] + [ + 'customer_data_object' => $savedCustomer, + 'orig_customer_data_object' => $prevCustomerData, + 'delegate_data' => $delegatedNewOperation ? $delegatedNewOperation->getAdditionalData() : [], + ] ); + return $savedCustomer; } @@ -310,7 +321,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) $collection = $this->customerFactory->create()->getCollection(); $this->extensionAttributesJoinProcessor->process( $collection, - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); // This is needed to make sure all the attributes are properly loaded foreach ($this->customerMetadata->getAllAttributesMetadata() as $metadata) { @@ -342,7 +353,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) /** * {@inheritdoc} */ - public function delete(\Magento\Customer\Api\Data\CustomerInterface $customer) + public function delete(CustomerInterface $customer) { return $this->deleteById($customer->getId()); } diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php index 71b0297fdd114..680e68b5c4c0f 100644 --- a/app/code/Magento/Customer/Model/Session.php +++ b/app/code/Magento/Customer/Model/Session.php @@ -555,7 +555,7 @@ public function setAfterAuthUrl($url) } /** - * Reset core session hosts after reseting session ID + * Reset core session hosts after resetting session ID * * @return $this */ diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index cfd1729e4e06e..9e3a16a307923 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -1875,4 +1875,105 @@ private function prepareDateTimeFactory() return $dateTime; } + + /** + * @return void + */ + public function testCreateAccountUnexpectedValueException(): void + { + $websiteId = 1; + $storeId = null; + $defaultStoreId = 1; + $customerId = 1; + $customerEmail = 'email@email.com'; + $newLinkToken = '2jh43j5h2345jh23lh452h345hfuzasd96ofu'; + $exception = new \UnexpectedValueException('Template file was not found'); + + $datetime = $this->prepareDateTimeFactory(); + + $address = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); + $address->expects($this->once()) + ->method('setCustomerId') + ->with($customerId); + $store = $this->createMock(\Magento\Store\Model\Store::class); + $store->expects($this->once()) + ->method('getId') + ->willReturn($defaultStoreId); + $website = $this->createMock(\Magento\Store\Model\Website::class); + $website->expects($this->atLeastOnce()) + ->method('getStoreIds') + ->willReturn([1, 2, 3]); + $website->expects($this->once()) + ->method('getDefaultStore') + ->willReturn($store); + $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $customer->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($customerId); + $customer->expects($this->atLeastOnce()) + ->method('getEmail') + ->willReturn($customerEmail); + $customer->expects($this->atLeastOnce()) + ->method('getWebsiteId') + ->willReturn($websiteId); + $customer->expects($this->atLeastOnce()) + ->method('getStoreId') + ->willReturn($storeId); + $customer->expects($this->once()) + ->method('setStoreId') + ->with($defaultStoreId); + $customer->expects($this->once()) + ->method('getAddresses') + ->willReturn([$address]); + $customer->expects($this->once()) + ->method('setAddresses') + ->with(null); + $this->customerRepository->expects($this->once()) + ->method('get') + ->with($customerEmail) + ->willReturn($customer); + $this->share->expects($this->once()) + ->method('isWebsiteScope') + ->willReturn(true); + $this->storeManager->expects($this->atLeastOnce()) + ->method('getWebsite') + ->with($websiteId) + ->willReturn($website); + $this->customerRepository->expects($this->atLeastOnce()) + ->method('save') + ->willReturn($customer); + $this->addressRepository->expects($this->atLeastOnce()) + ->method('save') + ->with($address); + $this->customerRepository->expects($this->once()) + ->method('getById') + ->with($customerId) + ->willReturn($customer); + $this->random->expects($this->once()) + ->method('getUniqueHash') + ->willReturn($newLinkToken); + $customerSecure = $this->createPartialMock( + \Magento\Customer\Model\Data\CustomerSecure::class, + ['setRpToken', 'setRpTokenCreatedAt', 'getPasswordHash'] + ); + $customerSecure->expects($this->any()) + ->method('setRpToken') + ->with($newLinkToken); + $customerSecure->expects($this->any()) + ->method('setRpTokenCreatedAt') + ->with($datetime) + ->willReturnSelf(); + $customerSecure->expects($this->any()) + ->method('getPasswordHash') + ->willReturn(null); + $this->customerRegistry->expects($this->atLeastOnce()) + ->method('retrieveSecureData') + ->willReturn($customerSecure); + $this->emailNotificationMock->expects($this->once()) + ->method('newAccount') + ->willThrowException($exception); + $this->logger->expects($this->once())->method('error')->with($exception); + + $this->accountManagement->createAccount($customer); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php index 06133dd89d754..bd1dc774b5319 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php @@ -419,7 +419,11 @@ public function testSave() ->method('dispatch') ->with( 'customer_save_after_data_object', - ['customer_data_object' => $this->customer, 'orig_customer_data_object' => $origCustomer] + [ + 'customer_data_object' => $this->customer, + 'orig_customer_data_object' => $origCustomer, + 'delegate_data' => [], + ] ); $this->model->save($this->customer); @@ -646,7 +650,11 @@ public function testSaveWithPasswordHash() ->method('dispatch') ->with( 'customer_save_after_data_object', - ['customer_data_object' => $this->customer, 'orig_customer_data_object' => $origCustomer] + [ + 'customer_data_object' => $this->customer, + 'orig_customer_data_object' => $origCustomer, + 'delegate_data' => [], + ] ); $this->model->save($this->customer, $passwordHash); diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index 43c2b9cf7bb80..0d99c1145e81b 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -433,4 +433,7 @@ + diff --git a/app/code/Magento/Customer/view/frontend/templates/logout.phtml b/app/code/Magento/Customer/view/frontend/templates/logout.phtml index 43665045ce3e2..5a99b7d931b9b 100644 --- a/app/code/Magento/Customer/view/frontend/templates/logout.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/logout.phtml @@ -7,13 +7,12 @@ /** @var \Magento\Framework\View\Element\Template $block */ ?>

escapeHtml(__('You have signed out and will go to our homepage in 5 seconds.')) ?>

- diff --git a/app/code/Magento/Customer/view/frontend/web/js/logout-redirect.js b/app/code/Magento/Customer/view/frontend/web/js/logout-redirect.js new file mode 100644 index 0000000000000..6792626df6a08 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/web/js/logout-redirect.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage' +], function ($) { + 'use strict'; + + return function (data) { + $($.mage.redirect(data.url, 'assign', 5000)); + }; +}); diff --git a/app/code/Magento/Deploy/App/Mode/ConfigProvider.php b/app/code/Magento/Deploy/App/Mode/ConfigProvider.php index 900908a1f158f..142e3fe819438 100644 --- a/app/code/Magento/Deploy/App/Mode/ConfigProvider.php +++ b/app/code/Magento/Deploy/App/Mode/ConfigProvider.php @@ -16,7 +16,7 @@ class ConfigProvider * [ * 'developer' => [ * 'production' => [ - * {{setting_path}} => ['value' => {{setting_value}}, 'lock' => {{lock_value}}] + * {{setting_path}} => {{setting_value}} * ] * ] * ] @@ -41,7 +41,7 @@ public function __construct(array $config = []) * need to turn off 'dev/debug/debug_logging' setting in this case method * will return array * [ - * {{setting_path}} => ['value' => {{setting_value}}, 'lock' => {{lock_value}}] + * {{setting_path}} => {{setting_value}} * ] * * @param string $currentMode diff --git a/app/code/Magento/Deploy/Model/Mode.php b/app/code/Magento/Deploy/Model/Mode.php index d4ae72d63bce7..fe24fb297e978 100644 --- a/app/code/Magento/Deploy/Model/Mode.php +++ b/app/code/Magento/Deploy/Model/Mode.php @@ -234,17 +234,17 @@ protected function setStoreMode($mode) private function saveAppConfigs($mode) { $configs = $this->configProvider->getConfigs($this->getMode(), $mode); - foreach ($configs as $path => $item) { - $this->emulatedAreaProcessor->process(function () use ($path, $item) { + foreach ($configs as $path => $value) { + $this->emulatedAreaProcessor->process(function () use ($path, $value) { $this->processorFacadeFactory->create()->processWithLockTarget( $path, - $item['value'], + $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - $item['lock'] + true ); }); - $this->output->writeln('Config "' . $path . ' = ' . $item['value'] . '" has been saved.'); + $this->output->writeln('Config "' . $path . ' = ' . $value . '" has been saved.'); } } } diff --git a/app/code/Magento/Deploy/Test/Unit/Model/ModeTest.php b/app/code/Magento/Deploy/Test/Unit/Model/ModeTest.php index 50725a3382073..5cb8a8e47bc97 100644 --- a/app/code/Magento/Deploy/Test/Unit/Model/ModeTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Model/ModeTest.php @@ -228,7 +228,7 @@ public function testEnableProductionModeMinimal() ->method('getConfigs') ->with('developer', 'production') ->willReturn([ - 'dev/debug/debug_logging' => ['value' => 0, 'lock' => false] + 'dev/debug/debug_logging' => 0, ]); $this->emulatedAreaProcessor->expects($this->once()) ->method('process') @@ -247,7 +247,7 @@ public function testEnableProductionModeMinimal() 0, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - false + true ); $this->outputMock->expects($this->once()) ->method('writeln') diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index 9e936f70c9986..ce7c84c95538a 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -76,32 +76,7 @@ - - 0 - false - - - - - - - 1 - false - - - - - - - 0 - false - - - - - 1 - false - + 0 diff --git a/app/code/Magento/Developer/etc/adminhtml/system.xml b/app/code/Magento/Developer/etc/adminhtml/system.xml index 0166814d889c2..9663cff72bc9d 100644 --- a/app/code/Magento/Developer/etc/adminhtml/system.xml +++ b/app/code/Magento/Developer/etc/adminhtml/system.xml @@ -28,6 +28,7 @@ + Not available in production mode. Magento\Config\Model\Config\Source\Yesno diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php index dd83c28b43dff..855fac5041b21 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Indexer/Price.php @@ -62,6 +62,7 @@ protected function _applyDownloadableLink() { $connection = $this->getConnection(); $table = $this->_getDownloadableLinkPriceTable(); + $finalPriceTable = $this->_getDefaultFinalPriceTable(); $this->_prepareDownloadableLinkPriceTable(); @@ -71,7 +72,7 @@ protected function _applyDownloadableLink() $ifPrice = $connection->getIfNullSql('dlpw.price_id', 'dlpd.price'); $select = $connection->select()->from( - ['i' => $this->_getDefaultFinalPriceTable()], + ['i' => $finalPriceTable], ['entity_id', 'customer_group_id', 'website_id'] )->join( ['dl' => $dlType->getBackend()->getTable()], @@ -119,7 +120,7 @@ protected function _applyDownloadableLink() ] ); - $query = $select->crossUpdateFromSelect(['i' => $this->_getDefaultFinalPriceTable()]); + $query = $select->crossUpdateFromSelect(['i' => $finalPriceTable]); $connection->query($query); $connection->delete($table); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index a7170888f8a01..f5c7d88919f3c 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -141,6 +141,11 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens 'static', ]; + /** + * @var \Magento\Eav\Api\Data\AttributeExtensionFactory + */ + private $eavExtensionFactory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -157,6 +162,7 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Eav\Api\Data\AttributeExtensionFactory|null $eavExtensionFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @codeCoverageIgnore */ @@ -175,7 +181,8 @@ public function __construct( \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Eav\Api\Data\AttributeExtensionFactory $eavExtensionFactory = null ) { parent::__construct( $context, @@ -194,6 +201,8 @@ public function __construct( $this->optionDataFactory = $optionDataFactory; $this->dataObjectProcessor = $dataObjectProcessor; $this->dataObjectHelper = $dataObjectHelper; + $this->eavExtensionFactory = $eavExtensionFactory ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Eav\Api\Data\AttributeExtensionFactory::class); } /** @@ -1314,7 +1323,13 @@ public function setValidationRules(array $validationRules = null) */ public function getExtensionAttributes() { - return $this->_getExtensionAttributes(); + $extensionAttributes = $this->_getExtensionAttributes(); + if (!($extensionAttributes instanceof \Magento\Eav\Api\Data\AttributeExtensionInterface)) { + /** @var \Magento\Eav\Api\Data\AttributeExtensionInterface $extensionAttributes */ + $extensionAttributes = $this->eavExtensionFactory->create(); + $this->setExtensionAttributes($extensionAttributes); + } + return $extensionAttributes; } /** diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php index 67a0bbefe7510..3d4c9e89a035f 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php @@ -165,7 +165,7 @@ public function getValue(\Magento\Framework\DataObject $object) $options = $opt->getAllOptions(); if ($options) { foreach ($options as $option) { - if ($option['value'] == $value) { + if ($option['value'] === $value) { $valueOption = $option['label']; } } diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index 4830ecfbb74b3..a6ecdaf24ebbb 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -531,14 +531,13 @@ protected function cancelDesignConfig() * * @param string $templateId * @return $this - * @throws \Magento\Framework\Exception\MailException */ public function setForcedArea($templateId) { - if ($this->area) { - throw new \LogicException(__('The area is already set.')); + if ($this->area === null) { + $this->area = $this->emailConfig->getTemplateArea($templateId); } - $this->area = $this->emailConfig->getTemplateArea($templateId); + return $this; } diff --git a/app/code/Magento/Email/Model/Template/Config.php b/app/code/Magento/Email/Model/Template/Config.php index bdd9054e7969b..9b45f509d97e5 100644 --- a/app/code/Magento/Email/Model/Template/Config.php +++ b/app/code/Magento/Email/Model/Template/Config.php @@ -205,8 +205,9 @@ public function getTemplateFilename($templateId, $designParams = []) $designParams['module'] = $module; $file = $this->_getInfo($templateId, 'file'); + $filename = $this->getFilename($file, $designParams, $module); - return $this->viewFileSystem->getEmailTemplateFileName($file, $designParams, $module); + return $filename; } /** @@ -230,4 +231,26 @@ protected function _getInfo($templateId, $fieldName) } return $data[$templateId][$fieldName]; } + + /** + * Retrieve template file path. + * + * @param string $file + * @param array $designParams + * @param string $module + * + * @return string + * + * @throws \UnexpectedValueException + */ + private function getFilename(string $file, array $designParams, string $module): string + { + $filename = $this->viewFileSystem->getEmailTemplateFileName($file, $designParams, $module); + + if ($filename === false) { + throw new \UnexpectedValueException("Template file '{$file}' is not found."); + } + + return $filename; + } } diff --git a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php index 46f3fecfb8848..4f545360616c6 100644 --- a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php @@ -117,10 +117,11 @@ protected function setUp() /** * Return the model under test with additional methods mocked. * - * @param $mockedMethods array + * @param array $mockedMethods + * @param array $data * @return \Magento\Email\Model\Template|\PHPUnit_Framework_MockObject_MockObject */ - protected function getModelMock(array $mockedMethods = []) + protected function getModelMock(array $mockedMethods = [], array $data = []) { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); return $this->getMockForAbstractClass( @@ -136,7 +137,8 @@ protected function getModelMock(array $mockedMethods = []) 'scopeConfig' => $this->scopeConfig, 'emailConfig' => $this->emailConfig, 'filterFactory' => $this->filterFactory, - 'templateFactory' => $this->templateFactory + 'templateFactory' => $this->templateFactory, + 'data' => $data, ] ), '', @@ -431,4 +433,33 @@ public function testGetDesignConfig() $expectedConfig = ['area' => 'test_area', 'store' => 2]; $this->assertEquals($expectedConfig, $model->getDesignConfig()->getData()); } + + /** + * @return void + */ + public function testSetForcedAreaWhenAreaIsNotSet(): void + { + $templateId = 'test_template'; + $model = $this->getModelMock([], ['area' => null]); + + $this->emailConfig->expects($this->once()) + ->method('getTemplateArea') + ->with($templateId); + + $model->setForcedArea($templateId); + } + + /** + * @return void + */ + public function testSetForcedAreaWhenAreaIsSet(): void + { + $templateId = 'test_template'; + $model = $this->getModelMock([], ['area' => 'frontend']); + + $this->emailConfig->expects($this->never()) + ->method('getTemplateArea'); + + $model->setForcedArea($templateId); + } } diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php index 47c3ac1e7e450..b396f2ede8977 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php @@ -272,6 +272,20 @@ public function testGetTemplateFilenameWithNoParams() $this->assertEquals('_files/Fixture/ModuleOne/view/frontend/email/one.html', $actualResult); } + /** + * @expectedException \UnexpectedValueException + * @expectedExceptionMessage Template file 'one.html' is not found + * @return void + */ + public function testGetTemplateFilenameWrongFileName(): void + { + $this->viewFileSystem->expects($this->once())->method('getEmailTemplateFileName') + ->with('one.html', $this->designParams, 'Fixture_ModuleOne') + ->willReturn(false); + + $this->model->getTemplateFilename('template_one', $this->designParams); + } + /** * @param string $getterMethod * @param $argument diff --git a/app/code/Magento/GiftMessage/view/frontend/web/template/gift-message-form.html b/app/code/Magento/GiftMessage/view/frontend/web/template/gift-message-form.html index 20739f621ecff..15a36cc0e977a 100644 --- a/app/code/Magento/GiftMessage/view/frontend/web/template/gift-message-form.html +++ b/app/code/Magento/GiftMessage/view/frontend/web/template/gift-message-form.html @@ -12,26 +12,24 @@
-
-
@@ -46,7 +44,6 @@
-
diff --git a/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js b/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js index eb708ab8b6320..a8b8303d47cdd 100644 --- a/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js +++ b/app/code/Magento/GoogleAnalytics/view/frontend/web/js/google-analytics.js @@ -53,7 +53,7 @@ define([ } // Process orders data - if (config.ordersTrackingData.length) { + if (config.ordersTrackingData.hasOwnProperty('currency')) { ga('require', 'ec', 'ec.js'); ga('set', 'currencyCode', config.ordersTrackingData.currency); diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index ffdf5511b7492..37ca2d8d7b378 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -30,4 +30,4 @@ type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navig enum SortEnum @doc(description: "This enumeration indicates whether to return results in ascending or descending order") { ASC DESC -} +} \ No newline at end of file diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php index 214b93560669f..0748c5818c857 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php @@ -20,22 +20,22 @@ class InstantPurchaseOption { /** - * @var PaymentTokenInterface + * @var PaymentTokenInterface|null */ private $paymentToken; /** - * @var AddressIn + * @var Address|null */ private $shippingAddress; /** - * @var Address + * @var Address|null */ private $billingAddress; /** - * @var ShippingMethodInterface + * @var ShippingMethodInterface|null */ private $shippingMethod; diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionLoadingFactory.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionLoadingFactory.php index d1dc71b80d5d1..b203cfdad2221 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionLoadingFactory.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionLoadingFactory.php @@ -100,7 +100,7 @@ public function create( /** * Loads customer address model by identifier. * - * @param $addressId + * @param int $addressId * @return Address */ private function getAddress($addressId): Address diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php index 96c01cdbb6663..ca0e9351967ad 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php @@ -39,7 +39,7 @@ public function get($type) : DeferredShippingMethodChooserInterface { if (!isset($this->choosers[$type])) { throw new \InvalidArgumentException(sprintf( - 'Deferred shipping method chooser is not registered.', + 'Deferred shipping method %s is not registered.', $type )); } diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationsManager.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationsManager.php index 9c93febe0db36..3ad2e000e97d3 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationsManager.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationsManager.php @@ -146,7 +146,7 @@ private function findIntegrations(int $storeId): array * * * @param VaultPaymentInterface $paymentMethod - * @param $storeId + * @param int|string|null|\Magento\Store\Model\Store $storeId * @return bool */ private function isIntegrationAvailable(VaultPaymentInterface $paymentMethod, $storeId): bool diff --git a/app/code/Magento/Marketplace/view/adminhtml/templates/partners.phtml b/app/code/Magento/Marketplace/view/adminhtml/templates/partners.phtml index 309df6f883a49..b63bf9ebd50eb 100644 --- a/app/code/Magento/Marketplace/view/adminhtml/templates/partners.phtml +++ b/app/code/Magento/Marketplace/view/adminhtml/templates/partners.phtml @@ -11,7 +11,7 @@ $partners = $block->getPartners(); ?> - getPartners() as $partner) : ?> +
*/ diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 2197598489358..5963e62e948f9 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -319,9 +319,19 @@ public function getQuote() } /** + * @deprecated + * typo in method name, see getBillingAddressTotals() * @return mixed */ public function getBillinAddressTotals() + { + return $this->getBillingAddressTotals(); + } + + /** + * @return mixed + */ + public function getBillingAddressTotals() { $address = $this->getQuote()->getBillingAddress(); return $this->getShippingAddressTotals($address); diff --git a/app/code/Magento/Multishipping/i18n/en_US.csv b/app/code/Magento/Multishipping/i18n/en_US.csv index 1e3e1880758ee..43cc785c56eab 100644 --- a/app/code/Magento/Multishipping/i18n/en_US.csv +++ b/app/code/Magento/Multishipping/i18n/en_US.csv @@ -88,3 +88,5 @@ Options,Options "Review Order","Review Order" "Select Shipping Method","Select Shipping Method" "We received your order!","We received your order!" +"Ship to:","Ship to:" +"Error:","Error:" \ No newline at end of file diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml index d4d446a7567db..4590b7c584085 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml @@ -186,7 +186,7 @@ - renderTotals($block->getBillinAddressTotals()); ?> + renderTotals($block->getBillingAddressTotals()); ?>
diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/results.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/results.phtml index d6fdef6ae5f9a..dacf96f9c0baf 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/results.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/results.phtml @@ -32,7 +32,7 @@ $orderIds = $block->getOrderIds(); getOrderShippingAddress($orderId); ?>
- escapeHtml('Ship to:'); ?> + escapeHtml(__('Ship to:')); ?> escapeHtml($block->formatOrderShippingAddress($shippingAddress)); ?> @@ -65,7 +65,7 @@ $orderIds = $block->getOrderIds();
isShippingAddress($address)) : ?> - escapeHtml('Ship to:'); ?> + escapeHtml(__('Ship to:')); ?> escapeHtml($block->formatQuoteShippingAddress($address)); ?> @@ -76,7 +76,7 @@ $orderIds = $block->getOrderIds();
- escapeHtml('Error:'); ?> + escapeHtml(__('Error:')); ?> getAddressError($address); ?> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml index c8e7c375089cd..57c4afaee6541 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/success.phtml @@ -22,7 +22,7 @@ getCheckoutData()->getOrderShippingAddress($orderId); ?>
- escapeHtml('Ship to:'); ?> + escapeHtml(__('Ship to:')); ?> escapeHtml( $block->getCheckoutData()->formatOrderShippingAddress($shippingAddress) diff --git a/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php b/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php index 724a488570207..ce7e95950c937 100644 --- a/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php +++ b/app/code/Magento/NewRelicReporting/Model/Observer/ReportApplicationHandledExceptionToNewRelic.php @@ -37,6 +37,9 @@ public function __construct( $this->newRelicWrapper = $newRelicWrapper; } + /** + * @param Observer $observer + */ public function execute(Observer $observer) { if ($this->config->isNewRelicEnabled()) { diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php index f0fce97da512a..055af4162d5f3 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php @@ -21,6 +21,11 @@ class PlaceOrder extends \Magento\Paypal\Controller\Express\AbstractExpress */ protected $agreementsValidator; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Customer\Model\Session $customerSession @@ -31,6 +36,8 @@ class PlaceOrder extends \Magento\Paypal\Controller\Express\AbstractExpress * @param \Magento\Framework\Url\Helper\Data $urlHelper * @param \Magento\Customer\Model\Url $customerUrl * @param \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -41,9 +48,9 @@ public function __construct( \Magento\Framework\Session\Generic $paypalSession, \Magento\Framework\Url\Helper\Data $urlHelper, \Magento\Customer\Model\Url $customerUrl, - \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator + \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator, + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { - $this->agreementsValidator = $agreementValidator; parent::__construct( $context, $customerSession, @@ -54,6 +61,11 @@ public function __construct( $urlHelper, $customerUrl ); + + $this->agreementsValidator = $agreementValidator; + $this->paymentFailures = $paymentFailures ? : $this->_objectManager->get( + \Magento\Sales\Api\PaymentFailuresInterface::class + ); } /** @@ -148,6 +160,8 @@ private function processException(\Exception $exception, string $message): void */ protected function _processPaypalApiError($exception) { + $this->paymentFailures->handle((int)$this->_getCheckoutSession()->getQuoteId(), $exception->getMessage()); + switch ($exception->getCode()) { case ApiProcessableException::API_MAX_PAYMENT_ATTEMPTS_EXCEEDED: case ApiProcessableException::API_TRANSACTION_EXPIRED: diff --git a/app/code/Magento/Paypal/Controller/Payflow.php b/app/code/Magento/Paypal/Controller/Payflow.php index ab21986bde3ba..78c0536e393ac 100644 --- a/app/code/Magento/Paypal/Controller/Payflow.php +++ b/app/code/Magento/Paypal/Controller/Payflow.php @@ -41,6 +41,11 @@ abstract class Payflow extends \Magento\Framework\App\Action\Action */ protected $_redirectBlockName = 'payflow.link.iframe'; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Checkout\Model\Session $checkoutSession @@ -48,6 +53,7 @@ abstract class Payflow extends \Magento\Framework\App\Action\Action * @param \Magento\Paypal\Model\PayflowlinkFactory $payflowModelFactory * @param \Magento\Paypal\Helper\Checkout $checkoutHelper * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -55,14 +61,19 @@ public function __construct( \Magento\Sales\Model\OrderFactory $orderFactory, \Magento\Paypal\Model\PayflowlinkFactory $payflowModelFactory, \Magento\Paypal\Helper\Checkout $checkoutHelper, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { + parent::__construct($context); + $this->_checkoutSession = $checkoutSession; $this->_orderFactory = $orderFactory; $this->_logger = $logger; $this->_payflowModelFactory = $payflowModelFactory; $this->_checkoutHelper = $checkoutHelper; - parent::__construct($context); + $this->paymentFailures = $paymentFailures ?: $this->_objectManager->get( + \Magento\Sales\Api\PaymentFailuresInterface::class + ); } /** @@ -74,6 +85,10 @@ public function __construct( protected function _cancelPayment($errorMsg = '') { $errorMsg = trim(strip_tags($errorMsg)); + $order = $this->_checkoutSession->getLastRealOrder(); + if ($order->getId()) { + $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMsg); + } $gotoSection = false; $this->_checkoutHelper->cancelCurrentOrder($errorMsg); diff --git a/app/code/Magento/Paypal/Controller/Transparent/Response.php b/app/code/Magento/Paypal/Controller/Transparent/Response.php index 23ac20ca8c87b..c54dd529588b9 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/Response.php +++ b/app/code/Magento/Paypal/Controller/Transparent/Response.php @@ -14,6 +14,8 @@ use Magento\Paypal\Model\Payflow\Service\Response\Transaction; use Magento\Paypal\Model\Payflow\Service\Response\Validator\ResponseValidator; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Sales\Api\PaymentFailuresInterface; +use Magento\Framework\Session\Generic as Session; /** * Class Response @@ -47,6 +49,16 @@ class Response extends \Magento\Framework\App\Action\Action */ private $transparent; + /** + * @var PaymentFailuresInterface + */ + private $paymentFailures; + + /** + * @var Session + */ + private $sessionTransparent; + /** * Constructor * @@ -56,6 +68,8 @@ class Response extends \Magento\Framework\App\Action\Action * @param ResponseValidator $responseValidator * @param LayoutFactory $resultLayoutFactory * @param Transparent $transparent + * @param Session|null $sessionTransparent + * @param PaymentFailuresInterface|null $paymentFailures */ public function __construct( Context $context, @@ -63,7 +77,9 @@ public function __construct( Transaction $transaction, ResponseValidator $responseValidator, LayoutFactory $resultLayoutFactory, - Transparent $transparent + Transparent $transparent, + Session $sessionTransparent = null, + PaymentFailuresInterface $paymentFailures = null ) { parent::__construct($context); $this->coreRegistry = $coreRegistry; @@ -71,6 +87,8 @@ public function __construct( $this->responseValidator = $responseValidator; $this->resultLayoutFactory = $resultLayoutFactory; $this->transparent = $transparent; + $this->sessionTransparent = $sessionTransparent ?: $this->_objectManager->get(Session::class); + $this->paymentFailures = $paymentFailures ?: $this->_objectManager->get(PaymentFailuresInterface::class); } /** @@ -86,6 +104,7 @@ public function execute() } catch (LocalizedException $exception) { $parameters['error'] = true; $parameters['error_msg'] = $exception->getMessage(); + $this->paymentFailures->handle((int)$this->sessionTransparent->getQuoteId(), $parameters['error_msg']); } $this->coreRegistry->register(Iframe::REGISTRY_KEY, $parameters); diff --git a/app/code/Magento/Paypal/Model/Api/Nvp.php b/app/code/Magento/Paypal/Model/Api/Nvp.php index 6933c613ef748..624068395394d 100644 --- a/app/code/Magento/Paypal/Model/Api/Nvp.php +++ b/app/code/Magento/Paypal/Model/Api/Nvp.php @@ -844,7 +844,7 @@ public function callGetExpressCheckoutDetails() $request = $this->_exportToRequest($this->_getExpressCheckoutDetailsRequest); $response = $this->call(self::GET_EXPRESS_CHECKOUT_DETAILS, $request); $this->_importFromResponse($this->_paymentInformationResponse, $response); - $this->_exportAddressses($response); + $this->_exportAddresses($response); } /** @@ -1025,7 +1025,7 @@ public function callGetPalDetails() } /** - * Set Customer BillingA greement call + * Set Customer BillingAgreement call * * @return void * @link https://cms.paypal.com/us/cgi-bin/?&cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_SetCustomerBillingAgreement @@ -1425,7 +1425,7 @@ protected function _deformatNVP($nvpstr) $nvpstr = strpos($nvpstr, "\r\n\r\n") !== false ? substr($nvpstr, strpos($nvpstr, "\r\n\r\n") + 4) : $nvpstr; while (strlen($nvpstr)) { - //postion of Key + //position of Key $keypos = strpos($nvpstr, '='); //position of value $valuepos = strpos($nvpstr, '&') ? strpos($nvpstr, '&') : strlen($nvpstr); @@ -1433,7 +1433,7 @@ protected function _deformatNVP($nvpstr) /*getting the Key and Value values and storing in a Associative Array*/ $keyval = substr($nvpstr, $intial, $keypos); $valval = substr($nvpstr, $keypos + 1, $valuepos - $keypos - 1); - //decoding the respose + //decoding the response $nvpArray[urldecode($keyval)] = urldecode($valval); $nvpstr = substr($nvpstr, $valuepos + 1, strlen($nvpstr)); } @@ -1461,8 +1461,21 @@ protected function _exportLineItems(array &$request, $i = 0) * * @param array $data * @return void + * @deprecated 100.2.2 typo in method name + * @see _exportAddresses */ protected function _exportAddressses($data) + { + $this->_exportAddresses($data); + } + + /** + * Create billing and shipping addresses basing on response data + * + * @param array $data + * @return void + */ + protected function _exportAddresses($data) { $address = new \Magento\Framework\DataObject(); \Magento\Framework\DataObject\Mapper::accumulateByMap($data, $address, $this->_billingAddressMap); diff --git a/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php b/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php index 661d1f3814a0b..1ec7f4832bcb2 100644 --- a/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php +++ b/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php @@ -24,7 +24,7 @@ class AvsEmsCodeMapper implements PaymentVerificationInterface * * @var string */ - private static $unavailableCode = 'U'; + private static $unavailableCode = ''; /** * List of mapping AVS codes diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php index 9d215ca6cbe17..da5599984b701 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php @@ -11,7 +11,6 @@ use Magento\Paypal\Model\Payflow\Transparent; use Magento\Paypal\Model\Payflowpro; use Magento\Quote\Model\Quote; -use Magento\Sales\Model\Order\Payment; /** * Class SecureToken @@ -59,6 +58,7 @@ public function __construct( */ public function requestToken(Quote $quote) { + $this->transparent->setStore($quote->getStoreId()); $request = $this->transparent->buildBasicRequest(); $request->setTrxtype(Payflowpro::TRXTYPE_AUTH_ONLY); diff --git a/app/code/Magento/Paypal/Model/Payflowlink.php b/app/code/Magento/Paypal/Model/Payflowlink.php index 792309bd76cf9..1955ef3c67661 100644 --- a/app/code/Magento/Paypal/Model/Payflowlink.php +++ b/app/code/Magento/Paypal/Model/Payflowlink.php @@ -10,6 +10,7 @@ use Magento\Payment\Model\Method\ConfigInterfaceFactory; use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\OrderSender; /** @@ -239,11 +240,13 @@ public function initialize($paymentAction, $stateObject) case \Magento\Paypal\Model\Config::PAYMENT_ACTION_AUTH: case \Magento\Paypal\Model\Config::PAYMENT_ACTION_SALE: $payment = $this->getInfoInstance(); + /** @var Order $order */ $order = $payment->getOrder(); $order->setCanSendNewEmailFlag(false); $payment->setAmountAuthorized($order->getTotalDue()); $payment->setBaseAmountAuthorized($order->getBaseTotalDue()); $this->_generateSecureSilentPostHash($payment); + $this->setStore($order->getStoreId()); $request = $this->_buildTokenRequest($payment); $response = $this->postRequest($request, $this->getConfig()); $this->_processTokenErrors($response, $payment); diff --git a/app/code/Magento/Paypal/Model/Payflowpro.php b/app/code/Magento/Paypal/Model/Payflowpro.php index 125aa0f6e65a7..b5fdaf4ae9fd4 100644 --- a/app/code/Magento/Paypal/Model/Payflowpro.php +++ b/app/code/Magento/Paypal/Model/Payflowpro.php @@ -647,7 +647,7 @@ public function buildBasicRequest() * * @param DataObject $response * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Payment\Gateway\Command\CommandException * @throws \Magento\Framework\Exception\State\InvalidTransitionException */ public function processErrors(DataObject $response) @@ -659,9 +659,9 @@ public function processErrors(DataObject $response) } elseif ($response->getResultCode() != self::RESPONSE_CODE_APPROVED && $response->getResultCode() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER ) { - throw new \Magento\Framework\Exception\LocalizedException(__($response->getRespmsg())); + throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); } elseif ($response->getOrigresult() == self::RESPONSE_CODE_DECLINED_BY_FILTER) { - throw new \Magento\Framework\Exception\LocalizedException(__($response->getRespmsg())); + throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); } } diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php index e25864bbc2f3c..bd4da25cb84d0 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Paypal\Test\Unit\Controller\Payflow; +use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Checkout\Block\Onepage\Success; use Magento\Checkout\Model\Session; use Magento\Framework\App\Action\Context; @@ -90,6 +91,11 @@ class ReturnUrlTest extends \PHPUnit\Framework\TestCase */ private $objectManager; + /** + * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentFailures; + /** * @inheritdoc */ @@ -138,6 +144,17 @@ protected function setUp() ->setMethods(['getLastRealOrderId', 'getLastRealOrder', 'restoreQuote']) ->getMock(); + $this->quote = $this->getMockBuilder(CartInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->context->expects($this->any())->method('getView')->willReturn($this->view); + $this->context->expects($this->any())->method('getRequest')->willReturn($this->request); + + $this->paymentFailures = $this->getMockBuilder(PaymentFailuresInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->method('getView') ->willReturn($this->view); $this->context->method('getRequest') @@ -148,6 +165,7 @@ protected function setUp() 'checkoutSession' => $this->checkoutSession, 'orderFactory' => $this->orderFactory, 'checkoutHelper' => $this->checkoutHelper, + 'paymentFailures' => $this->paymentFailures, ]); } @@ -321,6 +339,7 @@ public function testCheckAdvancedAcceptingByPaymentMethod() 'checkoutSession' => $this->checkoutSession, 'orderFactory' => $this->orderFactory, 'checkoutHelper' => $this->checkoutHelper, + 'paymentFailures' => $this->paymentFailures, ]); $returnUrl->execute(); diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php index a10d103860c65..acefebb779200 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php @@ -8,16 +8,16 @@ use Magento\Framework\App\Action\Context; use Magento\Framework\App\RequestInterface; use Magento\Framework\Registry; +use Magento\Framework\Session\Generic as Session; use Magento\Framework\View\Result\Layout; use Magento\Framework\View\Result\LayoutFactory; use Magento\Paypal\Controller\Transparent\Response; use Magento\Paypal\Model\Payflow\Service\Response\Transaction; use Magento\Paypal\Model\Payflow\Service\Response\Validator\ResponseValidator; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Sales\Api\PaymentFailuresInterface; /** - * Class ResponseTest - * * Test for class \Magento\Paypal\Controller\Transparent\Response * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -53,6 +53,19 @@ class ResponseTest extends \PHPUnit\Framework\TestCase */ private $payflowFacade; + /** + * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentFailures; + + /** + * @var Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionTransparent; + + /** + * @inheritdoc + */ protected function setUp() { $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) @@ -97,6 +110,14 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods([]) ->getMock(); + $this->paymentFailures = $this->getMockBuilder(PaymentFailuresInterface::class) + ->disableOriginalConstructor() + ->setMethods(['handle']) + ->getMock(); + $this->sessionTransparent = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getQuoteId']) + ->getMock(); $this->object = new Response( $this->contextMock, @@ -104,7 +125,9 @@ protected function setUp() $this->transactionMock, $this->responseValidatorMock, $this->resultLayoutFactoryMock, - $this->payflowFacade + $this->payflowFacade, + $this->sessionTransparent, + $this->paymentFailures ); } @@ -131,6 +154,8 @@ public function testExecute() $this->resultLayoutMock->expects($this->once()) ->method('getLayout') ->willReturn($this->getLayoutMock()); + $this->paymentFailures->expects($this->never()) + ->method('handle'); $this->assertInstanceOf(\Magento\Framework\Controller\ResultInterface::class, $this->object->execute()); } @@ -156,6 +181,12 @@ public function testExecuteWithException() $this->resultLayoutMock->expects($this->once()) ->method('getLayout') ->willReturn($this->getLayoutMock()); + $this->sessionTransparent->method('getQuoteId') + ->willReturn(1); + $this->paymentFailures->expects($this->once()) + ->method('handle') + ->with(1) + ->willReturnSelf(); $this->assertInstanceOf(\Magento\Framework\Controller\ResultInterface::class, $this->object->execute()); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php index eb259043a2d4f..ea86a04206f7b 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php @@ -85,17 +85,17 @@ public function testGetCodeWithException() public function getCodeDataProvider() { return [ - ['avsZip' => null, 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => null, 'avsStreet' => 'Y', 'expected' => 'U'], - ['avsZip' => 'Y', 'avsStreet' => null, 'expected' => 'U'], + ['avsZip' => null, 'avsStreet' => null, 'expected' => ''], + ['avsZip' => null, 'avsStreet' => 'Y', 'expected' => ''], + ['avsZip' => 'Y', 'avsStreet' => null, 'expected' => ''], ['avsZip' => 'Y', 'avsStreet' => 'Y', 'expected' => 'Y'], ['avsZip' => 'N', 'avsStreet' => 'Y', 'expected' => 'A'], ['avsZip' => 'Y', 'avsStreet' => 'N', 'expected' => 'Z'], ['avsZip' => 'N', 'avsStreet' => 'N', 'expected' => 'N'], - ['avsZip' => 'X', 'avsStreet' => 'Y', 'expected' => 'U'], - ['avsZip' => 'N', 'avsStreet' => 'X', 'expected' => 'U'], - ['avsZip' => '', 'avsStreet' => 'Y', 'expected' => 'U'], - ['avsZip' => 'N', 'avsStreet' => '', 'expected' => 'U'] + ['avsZip' => 'X', 'avsStreet' => 'Y', 'expected' => ''], + ['avsZip' => 'N', 'avsStreet' => 'X', 'expected' => ''], + ['avsZip' => '', 'avsStreet' => 'Y', 'expected' => ''], + ['avsZip' => 'N', 'avsStreet' => '', 'expected' => ''] ]; } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php index d4a7db25cae89..d8e54ad28fcc8 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php @@ -10,6 +10,9 @@ use Magento\Framework\UrlInterface; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Paypal\Model\PayflowConfig; +use Magento\Quote\Model\Quote; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Test class for \Magento\Paypal\Model\Payflow\Service\Request\SecureToken @@ -19,23 +22,26 @@ class SecureTokenTest extends \PHPUnit\Framework\TestCase /** * @var SecureToken */ - protected $model; + private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject|Transparent + * @var Transparent|MockObject */ - protected $transparent; + private $transparent; /** - * @var \PHPUnit_Framework_MockObject_MockObject|Random + * @var Random|MockObject */ - protected $mathRandom; + private $mathRandom; /** - * @var \PHPUnit_Framework_MockObject_MockObject|UrlInterface + * @var UrlInterface|MockObject */ - protected $url; + private $url; + /** + * @inheritdoc + */ protected function setUp() { $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); @@ -52,11 +58,29 @@ protected function setUp() public function testRequestToken() { $request = new DataObject(); + $storeId = 1; $secureTokenID = 'Sdj46hDokds09c8k2klaGJdKLl032ekR'; + $response = new DataObject([ + 'result' => '0', + 'respmsg' => 'Approved', + 'securetoken' => '80IgSbabyj0CtBDWHZZeQN3', + 'securetokenid' => $secureTokenID, + 'result_code' => '0', + ]); + + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $quote->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); $this->transparent->expects($this->once()) ->method('buildBasicRequest') ->willReturn($request); + $this->transparent->expects($this->once()) + ->method('setStore') + ->with($storeId); $this->transparent->expects($this->once()) ->method('fillCustomerContacts'); $this->transparent->expects($this->once()) @@ -64,7 +88,7 @@ public function testRequestToken() ->willReturn($this->createMock(\Magento\Paypal\Model\PayflowConfig::class)); $this->transparent->expects($this->once()) ->method('postRequest') - ->willReturn(new DataObject()); + ->willReturn($response); $this->mathRandom->expects($this->once()) ->method('getUniqueHash') @@ -73,8 +97,6 @@ public function testRequestToken() $this->url->expects($this->exactly(3)) ->method('getUrl'); - $quote = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->model->requestToken($quote); $this->assertEquals($secureTokenID, $request->getSecuretokenid()); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php index 362615e965d1b..80c8194e07654 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php @@ -101,16 +101,20 @@ protected function setUp() public function testInitialize() { + $storeId = 1; $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->exactly(2)) + ->method('getStoreId') + ->willReturn($storeId); $this->infoInstance->expects($this->any()) ->method('getOrder') - ->will($this->returnValue($order)); + ->willReturn($order); $this->infoInstance->expects($this->any()) ->method('setAdditionalInformation') - ->will($this->returnSelf()); + ->willReturnSelf(); $this->paypalConfig->expects($this->once()) ->method('getBuildNotationCode') - ->will($this->returnValue('build notation code')); + ->willReturn('build notation code'); $response = new \Magento\Framework\DataObject( [ @@ -148,6 +152,7 @@ public function testInitialize() $stateObject = new \Magento\Framework\DataObject(); $this->model->initialize(\Magento\Paypal\Model\Config::PAYMENT_ACTION_AUTH, $stateObject); + self::assertEquals($storeId, $this->model->getStore(), '{Store} should be set'); } /** diff --git a/app/code/Magento/Paypal/etc/adminhtml/system.xml b/app/code/Magento/Paypal/etc/adminhtml/system.xml index 1c8da8127f8fe..c1ff4c9b1c6ca 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system.xml @@ -32,6 +32,7 @@ + 1 paypal-top-section payments-other-header \Magento\Config\Block\System\Config\Form\Fieldset diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml index cdd4779a2fd87..532fa88c4986a 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml @@ -135,7 +135,7 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); name="payment[is_active_payment_token_enabler]" class="admin__control-checkbox"/>
diff --git a/app/code/Magento/Quote/Model/PaymentMethodManagement.php b/app/code/Magento/Quote/Model/PaymentMethodManagement.php index 91d8fe4dbcffd..b6e4bcf5ccc8f 100644 --- a/app/code/Magento/Quote/Model/PaymentMethodManagement.php +++ b/app/code/Magento/Quote/Model/PaymentMethodManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Model; @@ -52,38 +53,37 @@ public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method) { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->get($cartId); - + $quote->setTotalsCollectedFlag(false); $method->setChecks([ \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_CHECKOUT, \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_COUNTRY, \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_CURRENCY, \Magento\Payment\Model\Method\AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX, ]); - $payment = $quote->getPayment(); - - $data = $method->getData(); - $payment->importData($data); if ($quote->isVirtual()) { - $quote->getBillingAddress()->setPaymentMethod($payment->getMethod()); + $address = $quote->getBillingAddress(); } else { + $address = $quote->getShippingAddress(); // check if shipping address is set - if ($quote->getShippingAddress()->getCountryId() === null) { + if ($address->getCountryId() === null) { throw new InvalidTransitionException( __('The shipping address is missing. Set the address and try again.') ); } - $quote->getShippingAddress()->setPaymentMethod($payment->getMethod()); - } - if (!$quote->isVirtual() && $quote->getShippingAddress()) { - $quote->getShippingAddress()->setCollectShippingRates(true); + $address->setCollectShippingRates(true); } + $paymentData = $method->getData(); + $payment = $quote->getPayment(); + $payment->importData($paymentData); + $address->setPaymentMethod($payment->getMethod()); + if (!$this->zeroTotalValidator->isApplicable($payment->getMethodInstance(), $quote)) { throw new InvalidTransitionException(__('The requested Payment Method is not available.')); } - $quote->setTotalsCollectedFlag(false)->collectTotals()->save(); + $quote->save(); return $quote->getPayment()->getId(); } diff --git a/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php index 68b077fcdb965..f18d1fa1b06e5 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Test\Unit\Model; @@ -152,8 +153,8 @@ public function testSetVirtualProduct() ->with($paymentMethod) ->willReturnSelf(); - $quoteMock->expects($this->exactly(2))->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(true); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->expects($this->once())->method('isVirtual')->willReturn(true); $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); @@ -165,7 +166,6 @@ public function testSetVirtualProduct() ->willReturn(true); $quoteMock->expects($this->once())->method('setTotalsCollectedFlag')->with(false)->willReturnSelf(); - $quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf(); $quoteMock->expects($this->once())->method('save')->willReturnSelf(); $paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); @@ -218,9 +218,9 @@ public function testSetVirtualProductThrowsExceptionIfPaymentMethodNotAvailable( ->with($paymentMethod) ->willReturnSelf(); - $quoteMock->expects($this->once())->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(true); - $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->method('isVirtual')->willReturn(true); + $quoteMock->method('getBillingAddress')->willReturn($billingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); $paymentMock->expects($this->once())->method('getMethodInstance')->willReturn($methodInstance); @@ -268,17 +268,20 @@ public function testSetSimpleProduct() $shippingAddressMock = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, - ['getCountryId', 'setPaymentMethod'] + ['getCountryId', 'setPaymentMethod', 'setCollectShippingRates'] ); $shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(100); $shippingAddressMock->expects($this->once()) ->method('setPaymentMethod') ->with($paymentMethod) ->willReturnSelf(); + $shippingAddressMock->expects($this->once()) + ->method('setCollectShippingRates') + ->with(true); - $quoteMock->expects($this->exactly(2))->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(false); - $quoteMock->expects($this->exactly(4))->method('getShippingAddress')->willReturn($shippingAddressMock); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->method('isVirtual')->willReturn(false); + $quoteMock->method('getShippingAddress')->willReturn($shippingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); $paymentMock->expects($this->once())->method('getMethodInstance')->willReturn($methodInstance); @@ -289,7 +292,6 @@ public function testSetSimpleProduct() ->willReturn(true); $quoteMock->expects($this->once())->method('setTotalsCollectedFlag')->with(false)->willReturnSelf(); - $quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf(); $quoteMock->expects($this->once())->method('save')->willReturnSelf(); $paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); @@ -303,7 +305,6 @@ public function testSetSimpleProduct() public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() { $cartId = 100; - $methodData = ['method' => 'data']; $quoteMock = $this->createPartialMock( \Magento\Quote\Model\Quote::class, @@ -311,6 +312,7 @@ public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() ); $this->quoteRepositoryMock->expects($this->once())->method('get')->with($cartId)->willReturn($quoteMock); + /** @var \Magento\Quote\Model\Quote\Payment|\PHPUnit_Framework_MockObject_MockObject $methodMock */ $methodMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['setChecks', 'getData']); $methodMock->expects($this->once()) ->method('setChecks') @@ -321,17 +323,13 @@ public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() \Magento\Payment\Model\Method\AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX, ]) ->willReturnSelf(); - $methodMock->expects($this->once())->method('getData')->willReturn($methodData); - - $paymentMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['importData']); - $paymentMock->expects($this->once())->method('importData')->with($methodData)->willReturnSelf(); + $methodMock->expects($this->never())->method('getData'); $shippingAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getCountryId']); $shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(null); - $quoteMock->expects($this->once())->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); - $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($shippingAddressMock); + $quoteMock->method('isVirtual')->willReturn(false); + $quoteMock->method('getShippingAddress')->willReturn($shippingAddressMock); $this->model->set($cartId, $methodMock); } diff --git a/app/code/Magento/ReleaseNotification/Ui/Renderer/NotificationRenderer.php b/app/code/Magento/ReleaseNotification/Ui/Renderer/NotificationRenderer.php index a43b33b5a8cdf..c4760bd9d28c3 100644 --- a/app/code/Magento/ReleaseNotification/Ui/Renderer/NotificationRenderer.php +++ b/app/code/Magento/ReleaseNotification/Ui/Renderer/NotificationRenderer.php @@ -174,7 +174,7 @@ private function buildFooter(array $footer) * correct HTML format. * * @param string $content - * @returns string + * @return string */ private function formatContentWithLinks($content) { diff --git a/app/code/Magento/Review/view/frontend/templates/redirect.phtml b/app/code/Magento/Review/view/frontend/templates/redirect.phtml index fc74cadacb319..2fdb5e90a9c18 100644 --- a/app/code/Magento/Review/view/frontend/templates/redirect.phtml +++ b/app/code/Magento/Review/view/frontend/templates/redirect.phtml @@ -8,9 +8,6 @@ ?> getProduct()->getProductUrl()}#info-product_reviews"); exit; ?> diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index 41a55f4c25166..e1c9bf99f2675 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -6,9 +6,14 @@ namespace Magento\Rule\Model\Condition\Sql; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Rule\Model\Condition\AbstractCondition; use Magento\Rule\Model\Condition\Combine; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Entity\Collection\AbstractCollection; /** * Class SQL Builder @@ -41,12 +46,22 @@ class Builder */ protected $_expressionFactory; + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + /** * @param ExpressionFactory $expressionFactory + * @param AttributeRepositoryInterface|null $attributeRepository */ - public function __construct(ExpressionFactory $expressionFactory) - { + public function __construct( + ExpressionFactory $expressionFactory, + AttributeRepositoryInterface $attributeRepository = null + ) { $this->_expressionFactory = $expressionFactory; + $this->attributeRepository = $attributeRepository ?: + ObjectManager::getInstance()->get(AttributeRepositoryInterface::class); } /** @@ -88,14 +103,14 @@ protected function _getChildCombineTablesToJoin(Combine $combine, $tables = []) /** * Join tables from conditions combination to collection * - * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @param AbstractCollection $collection * @param Combine $combine * @return $this */ protected function _joinTablesToCollection( - \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection, + AbstractCollection $collection, Combine $combine - ) { + ): Builder { foreach ($this->_getCombineTablesToJoin($combine) as $alias => $joinTable) { /** @var $condition AbstractCondition */ $collection->getSelect()->joinLeft( @@ -104,6 +119,7 @@ protected function _joinTablesToCollection( isset($joinTable['columns']) ? $joinTable['columns'] : '*' ); } + return $this; } @@ -112,11 +128,15 @@ protected function _joinTablesToCollection( * * @param AbstractCondition $condition * @param string $value + * @param bool $isDefaultStoreUsed * @return string * @throws \Magento\Framework\Exception\LocalizedException */ - protected function _getMappedSqlCondition(AbstractCondition $condition, $value = '') - { + protected function _getMappedSqlCondition( + AbstractCondition $condition, + string $value = '', + bool $isDefaultStoreUsed = true + ): string { $argument = $condition->getMappedSqlField(); // If rule hasn't valid argument - create negative expression to prevent incorrect rule behavior. @@ -130,9 +150,16 @@ protected function _getMappedSqlCondition(AbstractCondition $condition, $value = throw new \Magento\Framework\Exception\LocalizedException(__('Unknown condition operator')); } + $defaultValue = 0; + // Check if attribute has a table with default value and add it to the query + if ($this->canAttributeHaveDefaultValue($condition->getAttribute(), $isDefaultStoreUsed)) { + $defaultField = 'at_' . $condition->getAttribute() . '_default.value'; + $defaultValue = $this->_connection->quoteIdentifier($defaultField); + } + $sql = str_replace( ':field', - $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), 0), + $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), $defaultValue), $this->_conditionOperatorMap[$conditionOperator] ); @@ -144,11 +171,15 @@ protected function _getMappedSqlCondition(AbstractCondition $condition, $value = /** * @param Combine $combine * @param string $value + * @param bool $isDefaultStoreUsed * @return string * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function _getMappedSqlCombination(Combine $combine, $value = '') - { + protected function _getMappedSqlCombination( + Combine $combine, + string $value = '', + bool $isDefaultStoreUsed = true + ): string { $out = (!empty($value) ? $value : ''); $value = ($combine->getValue() ? '' : ' NOT '); $getAggregator = $combine->getAggregator(); @@ -158,33 +189,68 @@ protected function _getMappedSqlCombination(Combine $combine, $value = '') $con = ($getAggregator == 'any' ? Select::SQL_OR : Select::SQL_AND); $con = (isset($conditions[$key+1]) ? $con : ''); if ($condition instanceof Combine) { - $out .= $this->_getMappedSqlCombination($condition, $value); + $out .= $this->_getMappedSqlCombination($condition, $value, $isDefaultStoreUsed); } else { - $out .= $this->_getMappedSqlCondition($condition, $value); + $out .= $this->_getMappedSqlCondition($condition, $value, $isDefaultStoreUsed); } $out .= $out ? (' ' . $con) : ''; } + return $this->_expressionFactory->create(['expression' => $out]); } /** * Attach conditions filter to collection * - * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @param AbstractCollection $collection * @param Combine $combine - * * @return void */ public function attachConditionToCollection( - \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection, + AbstractCollection $collection, Combine $combine - ) { + ): void { $this->_connection = $collection->getResource()->getConnection(); $this->_joinTablesToCollection($collection, $combine); - $whereExpression = (string)$this->_getMappedSqlCombination($combine); + $isDefaultStoreUsed = $this->checkIsDefaultStoreUsed($collection); + $whereExpression = (string)$this->_getMappedSqlCombination($combine, '', $isDefaultStoreUsed); if (!empty($whereExpression)) { // Select ::where method adds braces even on empty expression $collection->getSelect()->where($whereExpression); } } + + /** + * Check is default store used. + * + * @param AbstractCollection $collection + * @return bool + */ + private function checkIsDefaultStoreUsed(AbstractCollection $collection): bool + { + return (int)$collection->getStoreId() === (int)$collection->getDefaultStoreId(); + } + + /** + * Check if attribute can have default value. + * + * @param string $attributeCode + * @param bool $isDefaultStoreUsed + * @return bool + */ + private function canAttributeHaveDefaultValue(string $attributeCode, bool $isDefaultStoreUsed): bool + { + if ($isDefaultStoreUsed) { + return false; + } + + try { + $attribute = $this->attributeRepository->get(Product::ENTITY, $attributeCode); + } catch (NoSuchEntityException $e) { + // It's not exceptional case as we want to check if we have such attribute or not + return false; + } + + return !$attribute->isScopeGlobal(); + } } diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php index f53098c4bb97e..daf7b1462c722 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php @@ -35,7 +35,12 @@ public function testAttachConditionToCollection() { $collection = $this->createPartialMock( \Magento\Eav\Model\Entity\Collection\AbstractCollection::class, - ['getResource', 'getSelect'] + [ + 'getResource', + 'getSelect', + 'getStoreId', + 'getDefaultStoreId', + ] ); $combine = $this->createPartialMock(\Magento\Rule\Model\Condition\Combine::class, ['getConditions']); $resource = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['getConnection']); @@ -53,10 +58,15 @@ public function testAttachConditionToCollection() $collection->expects($this->once()) ->method('getResource') ->will($this->returnValue($resource)); - $collection->expects($this->any()) ->method('getSelect') ->will($this->returnValue($select)); + $collection->expects($this->once()) + ->method('getStoreId') + ->willReturn(1); + $collection->expects($this->once()) + ->method('getDefaultStoreId') + ->willReturn(1); $resource->expects($this->once()) ->method('getConnection') diff --git a/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.js b/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.js new file mode 100644 index 0000000000000..c9c36c4fa585a --- /dev/null +++ b/app/code/Magento/Rule/view/adminhtml/web/conditions-data-normalizer.js @@ -0,0 +1,140 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore' +], function ($, _) { + 'use strict'; + + /** + * @constructor + */ + var ConditionsDataNormalizer = function () { + this.patterns = { + validate: /^[a-z0-9_-][a-z0-9_-]*(?:\[(?:\d*|[a-z0-9_-]+)\])*$/i, + key: /[a-z0-9_-]+|(?=\[\])/gi, + push: /^$/, + fixed: /^\d+$/, + named: /^[a-z0-9_-]+$/i + }; + }; + + ConditionsDataNormalizer.prototype = { + /** + * Will convert an object: + * { + * "foo[bar][1][baz]": 123, + * "foo[bar][1][blah]": 321 + * "foo[bar][1--1][ah]": 456 + * } + * + * to + * { + * "foo": { + * "bar": { + * "1": { + * "baz": 123, + * "blah": 321 + * }, + * "1--1": { + * "ah": 456 + * } + * } + * } + * } + */ + normalize: function normalize(value) { + var el, _this = this; + + this.pushes = {}; + this.data = {}; + + _.each(value, function (e, i) { + el = {}; + el[i] = e; + + _this._addPair({ + name: i, + value: e + }); + }); + + return this.data; + }, + + /** + * @param {Object} base + * @param {String} key + * @param {String} value + * @return {Object} + * @private + */ + _build: function build(base, key, value) { + base[key] = value; + + return base; + }, + + /** + * @param {Object} root + * @param {String} value + * @return {*} + * @private + */ + _makeObject: function makeObject(root, value) { + var keys = root.match(this.patterns.key), + k, idx; // nest, nest, ..., nest + + while ((k = keys.pop()) !== undefined) { + // foo[] + if (this.patterns.push.test(k)) { + idx = this._incrementPush(root.replace(/\[\]$/, '')); + value = this._build([], idx, value); + } // foo[n] + else if (this.patterns.fixed.test(k)) { + value = this._build({}, k, value); + } // foo; foo[bar] + else if (this.patterns.named.test(k)) { + value = this._build({}, k, value); + } + } + + return value; + }, + + /** + * @param {String} key + * @return {Number} + * @private + */ + _incrementPush: function incrementPush(key) { + if (this.pushes[key] === undefined) { + this.pushes[key] = 0; + } + + return this.pushes[key]++; + }, + + /** + * @param {Object} pair + * @return {Object} + * @private + */ + _addPair: function addPair(pair) { + var obj = this._makeObject(pair.name, pair.value); + + if (!this.patterns.validate.test(pair.name)) { + return this; + } + + this.data = $.extend(true, this.data, obj); + + return this; + } + }; + + return ConditionsDataNormalizer; +}); diff --git a/app/code/Magento/Sales/Api/OrderCustomerDelegateInterface.php b/app/code/Magento/Sales/Api/OrderCustomerDelegateInterface.php new file mode 100644 index 0000000000000..2902903b0b7d0 --- /dev/null +++ b/app/code/Magento/Sales/Api/OrderCustomerDelegateInterface.php @@ -0,0 +1,25 @@ +objectCopyService = $objectCopyService; $this->accountManagement = $accountManagement; @@ -74,9 +87,10 @@ public function __construct( $this->customerFactory = $customerFactory; $this->addressFactory = $addressFactory; $this->regionFactory = $regionFactory; - $this->quoteAddressFactory = $quoteAddressFactory ?: ObjectManager::getInstance()->get( - \Magento\Quote\Model\Quote\AddressFactory::class - ); + $this->quoteAddressFactory = $quoteAddressFactory + ?: ObjectManager::getInstance()->get(QuoteAddressFactory::class); + $this->customerExtractor = $orderCustomerExtractor + ?? ObjectManager::getInstance()->get(OrderCustomerExtractor::class); } /** @@ -86,50 +100,23 @@ public function create($orderId) { $order = $this->orderRepository->get($orderId); if ($order->getCustomerId()) { - throw new AlreadyExistsException(__("This order already has associated customer account")); - } - $customerData = $this->objectCopyService->copyFieldsetToTarget( - 'order_address', - 'to_customer', - $order->getBillingAddress(), - [] - ); - $addresses = $order->getAddresses(); - foreach ($addresses as $address) { - if (!$this->isNeededToSaveAddress($address->getData('quote_address_id'))) { - continue; - } - $addressData = $this->objectCopyService->copyFieldsetToTarget( - 'order_address', - 'to_customer_address', - $address, - [] + throw new AlreadyExistsException( + __('This order already has associated customer account') ); - /** @var \Magento\Customer\Api\Data\AddressInterface $customerAddress */ - $customerAddress = $this->addressFactory->create(['data' => $addressData]); - switch ($address->getAddressType()) { - case QuoteAddress::ADDRESS_TYPE_BILLING: - $customerAddress->setIsDefaultBilling(true); - break; - case QuoteAddress::ADDRESS_TYPE_SHIPPING: - $customerAddress->setIsDefaultShipping(true); - break; - } + } - if (is_string($address->getRegion())) { - /** @var \Magento\Customer\Api\Data\RegionInterface $region */ - $region = $this->regionFactory->create(); - $region->setRegion($address->getRegion()); - $region->setRegionCode($address->getRegionCode()); - $region->setRegionId($address->getRegionId()); - $customerAddress->setRegion($region); + $customer = $this->customerExtractor->extract($orderId); + /** @var AddressInterface[] $filteredAddresses */ + $filteredAddresses = []; + foreach ($customer->getAddresses() as $address) { + if ($this->needToSaveAddress($order, $address)) { + $filteredAddresses[] = $address; } - $customerData['addresses'][] = $customerAddress; } + $customer->setAddresses($filteredAddresses); - /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ - $customer = $this->customerFactory->create(['data' => $customerData]); $account = $this->accountManagement->createAccount($customer); + $order = $this->orderRepository->get($orderId); $order->setCustomerId($account->getId()); $order->setCustomerIsGuest(0); $this->orderRepository->save($order); @@ -138,21 +125,36 @@ public function create($orderId) } /** - * Check if we need to save address in address book. - * - * @param int $quoteAddressId + * @param OrderInterface $order + * @param AddressInterface $address * * @return bool */ - private function isNeededToSaveAddress($quoteAddressId) - { - $saveInAddressBook = true; + private function needToSaveAddress( + OrderInterface $order, + AddressInterface $address + ): bool { + /** @var OrderAddressInterface|null $orderAddress */ + $orderAddress = null; + if ($address->isDefaultBilling()) { + $orderAddress = $order->getBillingAddress(); + } elseif ($address->isDefaultShipping()) { + $orderAddress = $order->getShippingAddress(); + } + if ($orderAddress) { + $quoteAddressId = $orderAddress->getData('quote_address_id'); + if ($quoteAddressId) { + /** @var QuoteAddress $quote */ + $quote = $this->quoteAddressFactory->create() + ->load($quoteAddressId); + if ($quote && $quote->getId()) { + return (bool)(int)$quote->getData('save_in_address_book'); + } + } - $quoteAddress = $this->quoteAddressFactory->create()->load($quoteAddressId); - if ($quoteAddress && $quoteAddress->getId()) { - $saveInAddressBook = (int)$quoteAddress->getData('save_in_address_book'); + return true; } - return $saveInAddressBook; + return false; } } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php index 2944b8ccef647..92d00d0436634 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php @@ -131,14 +131,17 @@ protected function prepareTemplate(Order $order) 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), ]; - $transport = new DataObject($transport); + $transportObject = new DataObject($transport); + /** + * Event argument `transport` is @deprecated. Use `transportObject` instead. + */ $this->eventManager->dispatch( 'email_order_set_template_vars_before', - ['sender' => $this, 'transport' => $transport] + ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] ); - $this->templateContainer->setTemplateVars($transport->getData()); + $this->templateContainer->setTemplateVars($transportObject->getData()); parent::prepareTemplate($order); } diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index f1bbb7d39469b..7916eb9db2b80 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -117,6 +117,7 @@ public function get($id) } $this->addProductOption($orderItem); + $this->addParentItem($orderItem); $this->registry[$id] = $orderItem; } return $this->registry[$id]; @@ -216,6 +217,20 @@ protected function addProductOption(OrderItemInterface $orderItem) return $this; } + /** + * Set parent item. + * + * @param OrderItemInterface $orderItem + * @throws InputException + * @throws NoSuchEntityException + */ + private function addParentItem(OrderItemInterface $orderItem) + { + if ($parentId = $orderItem->getParentItemId()) { + $orderItem->setParentItem($this->get($parentId)); + } + } + /** * Set product options data * diff --git a/app/code/Magento/Sales/Model/Order/OrderCustomerDelegate.php b/app/code/Magento/Sales/Model/Order/OrderCustomerDelegate.php new file mode 100644 index 0000000000000..5d0cd4f37df5a --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/OrderCustomerDelegate.php @@ -0,0 +1,54 @@ +customerExtractor = $customerExtractor; + $this->delegateService = $delegateService; + } + + /** + * {@inheritdoc} + */ + public function delegateNew(int $orderId): Redirect + { + return $this->delegateService->createRedirectForNew( + $this->customerExtractor->extract($orderId), + ['__sales_assign_order_id' => $orderId] + ); + } +} diff --git a/app/code/Magento/Sales/Model/Order/OrderCustomerExtractor.php b/app/code/Magento/Sales/Model/Order/OrderCustomerExtractor.php new file mode 100644 index 0000000000000..2a93f389e569f --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/OrderCustomerExtractor.php @@ -0,0 +1,142 @@ +orderRepository = $orderRepository; + $this->customerRepository = $customerRepository; + $this->objectCopyService = $objectCopyService; + $this->addressFactory = $addressFactory; + $this->regionFactory = $regionFactory; + $this->customerFactory = $customerFactory; + $this->quoteAddressFactory = $quoteAddressFactory; + } + + /** + * @param int $orderId + * + * @return CustomerInterface + */ + public function extract(int $orderId): CustomerInterface + { + $order = $this->orderRepository->get($orderId); + + //Simply return customer from DB. + if ($order->getCustomerId()) { + return $this->customerRepository->getById($order->getCustomerId()); + } + + //Prepare customer data from order data if customer doesn't exist yet. + $customerData = $this->objectCopyService->copyFieldsetToTarget( + 'order_address', + 'to_customer', + $order->getBillingAddress(), + [] + ); + $addresses = $order->getAddresses(); + foreach ($addresses as $address) { + $addressData = $this->objectCopyService->copyFieldsetToTarget( + 'order_address', + 'to_customer_address', + $address, + [] + ); + /** @var AddressInterface $customerAddress */ + $customerAddress = $this->addressFactory->create(['data' => $addressData]); + switch ($address->getAddressType()) { + case QuoteAddress::ADDRESS_TYPE_BILLING: + $customerAddress->setIsDefaultBilling(true); + break; + case QuoteAddress::ADDRESS_TYPE_SHIPPING: + $customerAddress->setIsDefaultShipping(true); + break; + } + + if (is_string($address->getRegion())) { + /** @var RegionInterface $region */ + $region = $this->regionFactory->create(); + $region->setRegion($address->getRegion()); + $region->setRegionCode($address->getRegionCode()); + $region->setRegionId($address->getRegionId()); + $customerAddress->setRegion($region); + } + $customerData['addresses'][] = $customerAddress; + } + + return $this->customerFactory->create(['data' => $customerData]); + } +} diff --git a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php index 1b781890e0f7f..80612277e68d5 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php +++ b/app/code/Magento/Sales/Model/ResourceModel/EntityAbstract.php @@ -123,10 +123,15 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) { /** @var \Magento\Sales\Model\AbstractModel $object */ if ($object instanceof EntityInterface && $object->getIncrementId() == null) { + $store = $object->getStore(); + $storeId = $store->getId(); + if ($storeId === null) { + $storeId = $store->getGroup()->getDefaultStoreId(); + } $object->setIncrementId( $this->sequenceManager->getSequence( $object->getEntityType(), - $object->getStore()->getGroup()->getDefaultStoreId() + $storeId )->getNextValue() ); } diff --git a/app/code/Magento/Sales/Model/Service/OrderService.php b/app/code/Magento/Sales/Model/Service/OrderService.php index 1eb3fad11278f..e4a71f028cc82 100644 --- a/app/code/Magento/Sales/Model/Service/OrderService.php +++ b/app/code/Magento/Sales/Model/Service/OrderService.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Model\Service; use Magento\Sales\Api\OrderManagementInterface; +use Magento\Payment\Gateway\Command\CommandException; /** * Class OrderService @@ -49,6 +50,11 @@ class OrderService implements OrderManagementInterface */ protected $orderCommentSender; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + /** * Constructor * @@ -59,6 +65,7 @@ class OrderService implements OrderManagementInterface * @param \Magento\Sales\Model\OrderNotifier $notifier * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures */ public function __construct( \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, @@ -67,7 +74,8 @@ public function __construct( \Magento\Framework\Api\FilterBuilder $filterBuilder, \Magento\Sales\Model\OrderNotifier $notifier, \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender + \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender, + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { $this->orderRepository = $orderRepository; $this->historyRepository = $historyRepository; @@ -76,6 +84,8 @@ public function __construct( $this->notifier = $notifier; $this->eventManager = $eventManager; $this->orderCommentSender = $orderCommentSender; + $this->paymentFailures = $paymentFailures ? : \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Api\PaymentFailuresInterface::class); } /** @@ -192,6 +202,9 @@ public function place(\Magento\Sales\Api\Data\OrderInterface $order) return $this->orderRepository->save($order); //commit } catch (\Exception $e) { + if ($e instanceof CommandException) { + $this->paymentFailures->handle((int)$order->getQuoteId(), __($e->getMessage())); + } throw $e; //rollback; } diff --git a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php new file mode 100644 index 0000000000000..3a49bbce256ef --- /dev/null +++ b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php @@ -0,0 +1,294 @@ + Configuration > Sales > Checkout > Payment Failed Emails configuration. + */ +class PaymentFailuresService implements PaymentFailuresInterface +{ + /** + * Store config + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StateInterface + */ + private $inlineTranslation; + + /** + * @var TransportBuilder + */ + private $transportBuilder; + + /** + * @var TimezoneInterface + */ + private $localeDate; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StateInterface $inlineTranslation + * @param TransportBuilder $transportBuilder + * @param TimezoneInterface $localeDate + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StateInterface $inlineTranslation, + TransportBuilder $transportBuilder, + TimezoneInterface $localeDate, + CartRepositoryInterface $cartRepository + ) { + $this->scopeConfig = $scopeConfig; + $this->inlineTranslation = $inlineTranslation; + $this->transportBuilder = $transportBuilder; + $this->localeDate = $localeDate; + $this->cartRepository = $cartRepository; + } + + /** + * Sends an email about failed transaction. + * + * @param int $cartId + * @param string $message + * @param string $checkoutType + * @return PaymentFailuresInterface + */ + public function handle( + int $cartId, + string $message, + string $checkoutType = 'onepage' + ): PaymentFailuresInterface { + $this->inlineTranslation->suspend(); + $quote = $this->cartRepository->get($cartId); + + $template = $this->getConfigValue('checkout/payment_failed/template', $quote); + $receiver = $this->getConfigValue('checkout/payment_failed/receiver', $quote); + $sendTo = [ + [ + 'email' => $this->getConfigValue('trans_email/ident_' . $receiver . '/email', $quote), + 'name' => $this->getConfigValue('trans_email/ident_' . $receiver . '/name', $quote), + ], + ]; + + $copyMethod = $this->getConfigValue('checkout/payment_failed/copy_method', $quote); + $copyTo = $this->getConfigEmails($quote); + + $bcc = []; + if (!empty($copyTo)) { + switch ($copyMethod) { + case 'bcc': + $bcc = $copyTo; + break; + case 'copy': + foreach ($copyTo as $email) { + $sendTo[] = ['email' => $email, 'name' => null]; + } + break; + } + } + + foreach ($sendTo as $recipient) { + $transport = $this->transportBuilder + ->setTemplateIdentifier($template) + ->setTemplateOptions([ + 'area' => FrontNameResolver::AREA_CODE, + 'store' => Store::DEFAULT_STORE_ID, + ]) + ->setTemplateVars($this->getTemplateVars($quote, $message, $checkoutType)) + ->setFrom($this->getSendFrom($quote)) + ->addTo($recipient['email'], $recipient['name']) + ->addBcc($bcc) + ->getTransport(); + + $transport->sendMessage(); + } + + $this->inlineTranslation->resume(); + + return $this; + } + + /** + * Returns mail template variables. + * + * @param Quote $quote + * @param string $message + * @param string $checkoutType + * @return array + */ + private function getTemplateVars(Quote $quote, string $message, string $checkoutType): array + { + return [ + 'reason' => $message, + 'checkoutType' => $checkoutType, + 'dateAndTime' => $this->getLocaleDate(), + 'customer' => $this->getCustomerName($quote), + 'customerEmail' => $quote->getBillingAddress()->getEmail(), + 'billingAddress' => $quote->getBillingAddress(), + 'shippingAddress' => $quote->getShippingAddress(), + 'shippingMethod' => $this->getConfigValue( + 'carriers/' . $this->getShippingMethod($quote) . '/title', + $quote + ), + 'paymentMethod' => $this->getConfigValue( + 'payment/' . $this->getPaymentMethod($quote) . '/title', + $quote + ), + 'items' => implode('
', $this->getQuoteItems($quote)), + 'total' => $quote->getCurrency()->getStoreCurrencyCode() . ' ' . $quote->getGrandTotal(), + ]; + } + + /** + * Returns scope config value by config path. + * + * @param string $configPath + * @param Quote $quote + * @return mixed + */ + private function getConfigValue(string $configPath, Quote $quote) + { + return $this->scopeConfig->getValue( + $configPath, + ScopeInterface::SCOPE_STORE, + $quote->getStoreId() + ); + } + + /** + * Returns shipping method from quote. + * + * @param Quote $quote + * @return string + */ + private function getShippingMethod(Quote $quote): string + { + $shippingMethod = ''; + $shippingInfo = $quote->getShippingAddress()->getShippingMethod(); + + if ($shippingInfo) { + $data = explode('_', $shippingInfo); + $shippingMethod = $data[0]; + } + + return $shippingMethod; + } + + /** + * Returns payment method title from quote. + * + * @param Quote $quote + * @return string + */ + private function getPaymentMethod(Quote $quote): string + { + $paymentMethod = $quote->getPayment()->getMethod() ?? ''; + + return $paymentMethod; + } + + /** + * Returns quote visible items. + * + * @param Quote $quote + * @return array + */ + private function getQuoteItems(Quote $quote): array + { + $items = []; + foreach ($quote->getAllVisibleItems() as $item) { + $itemData = $item->getProduct()->getName() . ' x ' . $item->getQty() . ' ' . + $quote->getCurrency()->getStoreCurrencyCode() . ' ' . + $item->getProduct()->getFinalPrice($item->getQty()); + $items[] = $itemData; + } + + return $items; + } + + /** + * Gets email values by configuration path. + * + * @param Quote $quote + * @return array|false + */ + private function getConfigEmails(Quote $quote) + { + $configData = $this->getConfigValue('checkout/payment_failed/copy_to', $quote); + if (!empty($configData)) { + return explode(',', $configData); + } + + return false; + } + + /** + * Returns sender identity. + * + * @param Quote $quote + * @return string + */ + private function getSendFrom(Quote $quote): string + { + return $this->getConfigValue('checkout/payment_failed/identity', $quote); + } + + /** + * Returns current locale date and time + * + * @return string + */ + private function getLocaleDate(): string + { + return $this->localeDate->formatDateTime( + new \DateTime(), + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::MEDIUM + ); + } + + /** + * Returns customer name. + * + * @param Quote $quote + * @return string + */ + private function getCustomerName(Quote $quote): string + { + $customer = __('Guest')->render(); + if (!$quote->getCustomerIsGuest()) { + $customer = $quote->getCustomer()->getFirstname() . ' ' . + $quote->getCustomer()->getLastname(); + } + + return $customer; + } +} diff --git a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php new file mode 100644 index 0000000000000..cade86d18e935 --- /dev/null +++ b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php @@ -0,0 +1,54 @@ +orderRepository = $orderRepository; + } + + /** + * {@inheritdoc} + */ + public function execute(Observer $observer) + { + $event = $observer->getEvent(); + /** @var CustomerInterface $customer */ + $customer = $event->getData('customer_data_object'); + /** @var array $delegateData */ + $delegateData = $event->getData('delegate_data'); + if (array_key_exists('__sales_assign_order_id', $delegateData)) { + $orderId = $delegateData['__sales_assign_order_id']; + $order = $this->orderRepository->get($orderId); + if (!$order->getCustomerId()) { + //if customer ID wasn't already assigned then assigning. + $order->setCustomerId($customer->getId()); + $order->setCustomerIsGuest(0); + $this->orderRepository->save($order); + } + } + } +} diff --git a/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php b/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php index 775a7dab95cfe..cd8c705750d6c 100644 --- a/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php +++ b/app/code/Magento/Sales/Observer/Backend/SubtractQtyFromQuotesObserver.php @@ -31,6 +31,6 @@ public function __construct(\Magento\Quote\Model\ResourceModel\Quote $quote) public function execute(\Magento\Framework\Event\Observer $observer) { $product = $observer->getEvent()->getProduct(); - $this->_quote->substractProductFromQuotes($product); + $this->_quote->subtractProductFromQuotes($product); } } diff --git a/app/code/Magento/Sales/Setup/SalesSetup.php b/app/code/Magento/Sales/Setup/SalesSetup.php index bfc05c549ddb3..4be2b38b074e7 100644 --- a/app/code/Magento/Sales/Setup/SalesSetup.php +++ b/app/code/Magento/Sales/Setup/SalesSetup.php @@ -303,6 +303,9 @@ public function getEncryptor() return $this->encryptor; } + /** + * @return \Magento\Framework\DB\Adapter\AdapterInterface + */ public function getConnection() { return $this->getSetup()->getConnection(self::$connectionName); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php deleted file mode 100644 index 2794860793ed6..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CustomerManagementTest.php +++ /dev/null @@ -1,185 +0,0 @@ -objectCopyService = $this->createMock(\Magento\Framework\DataObject\Copy::class); - $this->accountManagement = $this->createMock(\Magento\Customer\Api\AccountManagementInterface::class); - $this->customerFactory = $this->createPartialMock( - \Magento\Customer\Api\Data\CustomerInterfaceFactory::class, - ['create'] - ); - $this->addressFactory = $this->createPartialMock( - \Magento\Customer\Api\Data\AddressInterfaceFactory::class, - ['create'] - ); - $this->regionFactory = $this->createPartialMock( - \Magento\Customer\Api\Data\RegionInterfaceFactory::class, - ['create'] - ); - $this->orderRepository = $this->createMock(\Magento\Sales\Api\OrderRepositoryInterface::class); - $this->quoteAddressFactory = $this->createMock(\Magento\Quote\Model\Quote\AddressFactory::class); - - $this->service = new \Magento\Sales\Model\Order\CustomerManagement( - $this->objectCopyService, - $this->accountManagement, - $this->customerFactory, - $this->addressFactory, - $this->regionFactory, - $this->orderRepository, - $this->quoteAddressFactory - ); - } - - /** - * @expectedException \Magento\Framework\Exception\AlreadyExistsException - */ - public function testCreateThrowsExceptionIfCustomerAlreadyExists() - { - $orderMock = $this->createMock(\Magento\Sales\Api\Data\OrderInterface::class); - $orderMock->expects($this->once())->method('getCustomerId')->will($this->returnValue('customer_id')); - $this->orderRepository->expects($this->once())->method('get')->with(1)->will($this->returnValue($orderMock)); - $this->service->create(1); - } - - public function testCreateCreatesCustomerBasedonGuestOrder() - { - $orderMock = $this->createMock(\Magento\Sales\Model\Order::class); - $orderMock->expects($this->once())->method('getCustomerId')->will($this->returnValue(null)); - $orderMock->expects($this->any())->method('getBillingAddress')->will($this->returnValue('billing_address')); - - $orderBillingAddress = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['getData']); - $orderBillingAddress->expects($this->once()) - ->method('getAddressType') - ->willReturn(Address::ADDRESS_TYPE_BILLING); - - $orderShippingAddress = $this->createPartialMockForAbstractClass(OrderAddressInterface::class, ['getData']); - $orderShippingAddress->expects($this->once()) - ->method('getAddressType') - ->willReturn(Address::ADDRESS_TYPE_SHIPPING); - - $orderMock->expects($this->any()) - ->method('getAddresses') - ->will($this->returnValue([$orderBillingAddress, $orderShippingAddress])); - - $billingQuoteAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $billingQuoteAddress->expects($this->once())->method('load')->willReturn($billingQuoteAddress); - $billingQuoteAddress->expects($this->once())->method('getId')->willReturn(4); - $billingQuoteAddress->expects($this->once())->method('getData')->with('save_in_address_book')->willReturn(1); - - $shippingQuoteAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $shippingQuoteAddress->expects($this->once())->method('load')->willReturn($shippingQuoteAddress); - $shippingQuoteAddress->expects($this->once())->method('getId')->willReturn(5); - $shippingQuoteAddress->expects($this->once())->method('getData')->with('save_in_address_book')->willReturn(1); - $this->quoteAddressFactory->expects($this->exactly(2))->method('create') - ->willReturnOnConsecutiveCalls($billingQuoteAddress, $shippingQuoteAddress); - $this->orderRepository->expects($this->once())->method('get')->with(1)->will($this->returnValue($orderMock)); - $this->objectCopyService->expects($this->any())->method('copyFieldsetToTarget')->will($this->returnValueMap( - [ - ['order_address', 'to_customer', 'billing_address', [], 'global', ['customer_data' => []]], - ['order_address', 'to_customer_address', $orderBillingAddress, [], 'global', 'address_data'], - ['order_address', 'to_customer_address', $orderShippingAddress, [], 'global', 'address_data'], - ] - )); - - $addressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - $addressMock->expects($this->any()) - ->method('setIsDefaultBilling') - ->with(true) - ->willReturnSelf(); - $addressMock->expects($this->any()) - ->method('setIsDefaultShipping') - ->with(true) - ->willReturnSelf(); - - $this->addressFactory->expects($this->any())->method('create')->with(['data' => 'address_data'])->will( - $this->returnValue($addressMock) - ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $customerMock->expects($this->any())->method('getId')->will($this->returnValue('customer_id')); - $this->customerFactory->expects($this->once())->method('create')->with( - ['data' => ['customer_data' => [], 'addresses' => [$addressMock, $addressMock]]] - )->will($this->returnValue($customerMock)); - $this->accountManagement->expects($this->once())->method('createAccount')->with($customerMock)->will( - $this->returnValue($customerMock) - ); - $orderMock->expects($this->once())->method('setCustomerId')->with('customer_id'); - $this->orderRepository->expects($this->once())->method('save')->with($orderMock); - $this->assertEquals($customerMock, $this->service->create(1)); - } - - /** - * Get mock for abstract class with methods. - * - * @param string $className - * @param array $methods - * - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function createPartialMockForAbstractClass($className, $methods = []) - { - return $this->getMockForAbstractClass( - $className, - [], - '', - true, - true, - true, - $methods - ); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php index a6a828c888fc0..949121eadee44 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/Backend/SubtractQtyFromQuotesObserverTest.php @@ -48,7 +48,7 @@ public function testSubtractQtyFromQuotes() ['getId', 'getStatus', '__wakeup'] ); $this->_eventMock->expects($this->once())->method('getProduct')->will($this->returnValue($productMock)); - $this->_quoteMock->expects($this->once())->method('substractProductFromQuotes')->with($productMock); + $this->_quoteMock->expects($this->once())->method('subtractProductFromQuotes')->with($productMock); $this->_model->execute($this->_observerMock); } } diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index cbc06856f5283..ac25cdab22f56 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -65,6 +65,7 @@ + @@ -991,4 +992,7 @@ + diff --git a/app/code/Magento/Sales/etc/events.xml b/app/code/Magento/Sales/etc/events.xml index 9ec983acab5bd..b3a7a4ab99577 100644 --- a/app/code/Magento/Sales/etc/events.xml +++ b/app/code/Magento/Sales/etc/events.xml @@ -51,4 +51,9 @@ + + + diff --git a/app/code/Magento/Search/view/frontend/web/form-mini.js b/app/code/Magento/Search/view/frontend/web/form-mini.js index de16305bbbe8d..27a15017cb3fc 100644 --- a/app/code/Magento/Search/view/frontend/web/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/form-mini.js @@ -55,7 +55,7 @@ define([ this.autoComplete = $(this.options.destinationSelector); this.searchForm = $(this.options.formSelector); this.submitBtn = this.searchForm.find(this.options.submitBtn)[0]; - this.searchLabel = $(this.options.searchLabel); + this.searchLabel = this.searchForm.find(this.options.searchLabel); this.isExpandable = this.options.isExpandable; _.bindAll(this, '_onKeyDown', '_onPropertyChange', '_onSubmit'); @@ -226,6 +226,7 @@ define([ case $.ui.keyCode.ENTER: this.searchForm.trigger('submit'); + e.preventDefault(); break; case $.ui.keyCode.DOWN: diff --git a/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php index a26beda520944..5be5ccbc5e55a 100644 --- a/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php +++ b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php @@ -60,7 +60,7 @@ public function __construct( * * @param string $paymentCode * @return PaymentVerificationInterface - * @throws \Exception + * @throws ConfigurationMismatchException */ public function createPaymentCvv($paymentCode) { @@ -73,7 +73,7 @@ public function createPaymentCvv($paymentCode) * * @param string $paymentCode * @return PaymentVerificationInterface - * @throws \Exception + * @throws ConfigurationMismatchException */ public function createPaymentAvs($paymentCode) { diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php index 02031e6f5b9b5..19408e99ae02e 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php @@ -30,7 +30,7 @@ class DebuggerFactory /** * DebuggerFactory constructor. * - * @param bjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @param Config $config */ public function __construct( diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php index 858ce0f0f3287..5e544e4b4048e 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php @@ -7,12 +7,13 @@ use Magento\Framework\App\Area; use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Exception\ConfigurationMismatchException; use Magento\Framework\Intl\DateTimeFactory; use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Model\Order; +use Magento\Signifyd\Model\PaymentMethodMapper\PaymentMethodMapper; use Magento\Signifyd\Model\PaymentVerificationFactory; use Magento\Signifyd\Model\SignifydOrderSessionId; -use Magento\Signifyd\Model\PaymentMethodMapper\PaymentMethodMapper; /** * Prepare data related to purchase event represented in case creation request. @@ -72,6 +73,7 @@ public function __construct( * * @param Order $order * @return array + * @throws ConfigurationMismatchException */ public function build(Order $order) { @@ -202,6 +204,7 @@ private function getOrderChannel() * * @param OrderPaymentInterface $orderPayment * @return string + * @throws ConfigurationMismatchException */ private function getAvsCode(OrderPaymentInterface $orderPayment) { @@ -214,6 +217,7 @@ private function getAvsCode(OrderPaymentInterface $orderPayment) * * @param OrderPaymentInterface $orderPayment * @return string + * @throws ConfigurationMismatchException */ private function getCvvCode(OrderPaymentInterface $orderPayment) { diff --git a/app/code/Magento/Signifyd/etc/adminhtml/system.xml b/app/code/Magento/Signifyd/etc/adminhtml/system.xml index d9ba2f7ffdff2..71f5916ca5325 100644 --- a/app/code/Magento/Signifyd/etc/adminhtml/system.xml +++ b/app/code/Magento/Signifyd/etc/adminhtml/system.xml @@ -13,7 +13,7 @@ Magento_Sales::fraud_protection signifyd-logo-header - + Magento\Signifyd\Block\Adminhtml\System\Config\Fieldset\Info signifyd-about-header @@ -26,12 +26,12 @@ https://www.signifyd.com/magento-guaranteed-fraud-protection - + signifyd-about-header View our setup guide for step-by-step instructions on how to integrate Signifyd with Magento.
For support contact support@signifyd.com.]]>
- + Magento\Config\Model\Config\Source\Yesno fraud_protection/signifyd/active diff --git a/app/code/Magento/Signifyd/etc/di.xml b/app/code/Magento/Signifyd/etc/di.xml index 92ad8a0bfd87a..fd78fff27f619 100644 --- a/app/code/Magento/Signifyd/etc/di.xml +++ b/app/code/Magento/Signifyd/etc/di.xml @@ -15,11 +15,7 @@ - - - U - - + diff --git a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml index 345f063a7aaa3..f14df1c70a790 100644 --- a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml +++ b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml @@ -10,32 +10,18 @@ Swagger UI - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + @@ -43,6 +29,7 @@ + diff --git a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml index 27b3767f274bc..b20da68734579 100644 --- a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml +++ b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml @@ -12,11 +12,48 @@ * Modified by Magento, Modifications Copyright © Magento, Inc. All rights reserved. */ -/** @var \Magento\Swagger\Block\Index $block */ +/** @var \Magento\Swagger\Block\Index $block + * + * @codingStandardsIgnoreFile + */ $schemaUrl = $block->getSchemaUrl(); ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +