From 21fcce9bb669791b3977d78d05694499eb985062 Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Fri, 1 Jun 2018 10:43:06 +0300 Subject: [PATCH 01/20] MAGETWO-93788: Google Analytics - addToCart event triggers before updating the cart --- app/code/Magento/Braintree/i18n/de_DE.csv | 193 +++++++++++++++++ .../Controller/Cart/UpdateItemQty.php | 161 ++++++++++++++ .../Checkout/Controller/Cart/UpdatePost.php | 49 ++++- .../Model/Cart/RequestQuantityProcessor.php | 48 +++++ .../Cart/RequestQuantityProcessorTest.php | 88 ++++++++ .../view/frontend/templates/cart/form.phtml | 4 +- .../web/js/action/update-shopping-cart.js | 127 ++++++++++++ .../Controller/Checkout/CheckItems.php | 196 ++++++++++++++++++ app/code/Magento/Multishipping/i18n/en_US.csv | 3 +- .../view/frontend/requirejs-config.js | 3 +- .../templates/checkout/addresses.phtml | 99 +++++++-- .../Controller/Cart/UpdateItemQtyTest.php | 120 +++++++++++ .../Controller/Checkout/CheckItemsTest.php | 176 ++++++++++++++++ 13 files changed, 1234 insertions(+), 33 deletions(-) create mode 100644 app/code/Magento/Braintree/i18n/de_DE.csv create mode 100644 app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php create mode 100644 app/code/Magento/Checkout/Model/Cart/RequestQuantityProcessor.php create mode 100644 app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js create mode 100644 app/code/Magento/Multishipping/Controller/Checkout/CheckItems.php create mode 100644 dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Multishipping/Controller/Checkout/CheckItemsTest.php diff --git a/app/code/Magento/Braintree/i18n/de_DE.csv b/app/code/Magento/Braintree/i18n/de_DE.csv new file mode 100644 index 0000000000000..d55233159689d --- /dev/null +++ b/app/code/Magento/Braintree/i18n/de_DE.csv @@ -0,0 +1,193 @@ +cc_type,"Credit Card Type" +cc_number,"Credit Card Number" +avsPostalCodeResponseCode,"AVS Postal Code Response Code" +avsStreetAddressResponseCode,"AVS Street Address Response Code" +cvvResponseCode,"CVV Response Code" +processorAuthorizationCode,"Processor Authorization Code" +processorResponseCode,"Processor Response Code" +processorResponseText,"Processor Response Text" +braintree,Braintree +liabilityShifted,"Liability Shifted" +liabilityShiftPossible,"Liability Shift Possible" +riskDataId,"Risk ID" +riskDataDecision,"Risk Decision" +paymentId,"Payment Id" +payerEmail,"Payer Email" +sale,Sale +credit,Credit +authorization_expired,"Authorization expired" +authorizing,Authorizing +authorized,Authorized +gateway_rejected,"Gateway rejected" +failed,Failed +processor_declined,"Processor declined" +settled,Settled +settling,Settling +submitted_for_settlement,"Submitted for settlement" +voided,Voided +unrecognized,Unrecognized +settlement_declined,"Settlement declined" +settlement_pending,"Settlement pending" +settlement_confirmed,"Settlement confirmed" +paypal_account,"Paypal account" +coinbase_account,"Coinbase account" +europe_bank_accout,"Europe bank account" +credit_card,"Credit card" +apple_pay_card,"Apple pay card" +android_pay_card,"Android pay card" +Country,Country +"Allowed Credit Card Types","Allowed Credit Card Types" +"Add Rule","Add Rule" +"Braintree Settlement Report","Braintree Settlement Report" +"Sorry, but something went wrong","Sorry, but something went wrong" +"We can't initialize checkout.","We can't initialize checkout." +"No authorization transaction to proceed capture.","No authorization transaction to proceed capture." +"Braintree error response.","Braintree error response." +"Payment method nonce can't be retrieved.","Payment method nonce can't be retrieved." +"Wrong transaction status","Wrong transaction status" +Authorize,Authorize +"Authorize and Capture","Authorize and Capture" +"--Please Select--","--Please Select--" +"Please agree to all the terms and conditions before placing the order.","Please agree to all the terms and conditions before placing the order." +"Credit Card Type","Credit Card Type" +"Credit Card Number","Credit Card Number" +"Please, enter valid Credit Card Number","Please, enter valid Credit Card Number" +"Expiration Date","Expiration Date" +"Please, enter valid Expiration Date","Please, enter valid Expiration Date" +"Card Verification Number","Card Verification Number" +"Please, enter valid Card Verification Number","Please, enter valid Card Verification Number" +ending,ending +expires,expires +"Are you sure you want to delete this PayPal account","Are you sure you want to delete this PayPal account" +"PayPal Account","PayPal Account" +"PayPal Logo","PayPal Logo" +Actions,Actions +Delete,Delete +"Credit Card Information","Credit Card Information" +"What is this?","What is this?" +"Save for later use.","Save for later use." +"Place Order","Place Order" +"This payment is not available","This payment is not available" +MM,MM +YY,YY +"Please try again with another form of payment.","---Bitte versuchen Sie es erneut mit einer anderen Zahlungsmethode@" +"Sorry, but something went wrong.","Sorry, but something went wrong." +"Payment ","Payment " +Braintree,Braintree +"Accept credit/debit cards and PayPal in your Magento store.
No setup or monthly fees and your customers never leave your store to complete the purchase.","Accept credit/debit cards and PayPal in your Magento store.
No setup or monthly fees and your customers never leave your store to complete the purchase." +"Enable this Solution","Enable this Solution" +"Enable PayPal through Braintree","Enable PayPal through Braintree" +"Vault Enabled","Vault Enabled" +"Basic Braintree Settings","Basic Braintree Settings" +Title,Title +Environment,Environment +"Payment Action","Payment Action" +"Merchant ID","Merchant ID" +"Public Key","Public Key" +"Private Key","Private Key" +"Advanced Braintree Settings","Advanced Braintree Settings" +"Vault Title","Vault Title" +"Merchant Account ID","Merchant Account ID" +"Advanced Fraud Protection","Advanced Fraud Protection" +"Kount Merchant ID","Kount Merchant ID" +Debug,Debug +"CVV Verification","CVV Verification" +"Credit Card Types","Credit Card Types" +"Sort Order","Sort Order" +"Country Specific Settings","Country Specific Settings" +"Payment from Applicable Countries","Payment from Applicable Countries" +"Payment from Specific Countries","Payment from Specific Countries" +"Country Specific Credit Card Types","Country Specific Credit Card Types" +"PayPal through Braintree","PayPal through Braintree" +"Override Merchant Name","Override Merchant Name" +"Require Customer's Billing Address","Require Customer's Billing Address" +"Allow to Edit Shipping Address Entered During Checkout on PayPal Side","Allow to Edit Shipping Address Entered During Checkout on PayPal Side" +"Display on Shopping Cart","Display on Shopping Cart" +"Skip Order Review","Skip Order Review" +"3D Secure Verification Settings","3D Secure Verification Settings" +"3D Secure Verification","3D Secure Verification" +"Threshold Amount","Threshold Amount" +"Verify for Applicable Countries","Verify for Applicable Countries" +"Verify for Specific Countries","Verify for Specific Countries" +"Dynamic Descriptors","Dynamic Descriptors" +Name,Name +Phone,Phone +URL,URL +"Apply filters in order to get results. Only first 100 records will be displayed in the grid, you will be able to download full version of the report in .csv format.","Apply filters in order to get results. Only first 100 records will be displayed in the grid, you will be able to download full version of the report in .csv format." +Select...,Select... +Status,Status +"Transaction Type","Transaction Type" +"Payment Type","Payment Type" +"PayPal Payment ID","PayPal Payment ID" +"Created At","Created At" +"Transaction ID","Transaction ID" +"Order ID","Order ID" +Type,Type +Amount,Amount +"Settlement Code","Settlement Code" +"Settlement Response Text","Settlement Response Text" +"Refund Ids","Refund Ids" +"Settlement Batch ID","Settlement Batch ID" +Currency,Currency +"Addresses must have at least one field filled in.","Addresses must have at least one field filled in." +"Company is too long.","Company is too long." +"Extended address is too long.","Extended address is too long." +"First name is too long.","First name is too long." +"Last name is too long.","Last name is too long." +"Locality is too long.","Locality is too long." +"Postal code can only contain letters, numbers, spaces, and hyphens.","Postal code can only contain letters, numbers, spaces, and hyphens." +"Postal code is required.","Postal code is required." +"Postal code may contain no more than 9 letter or number characters.","Postal code may contain no more than 9 letter or number characters." +"Region is too long.","Region is too long." +"Street address is required.","Street address is required." +"Street address is too long.","Street address is too long." +"US state codes must be two characters to meet PayPal Seller Protection requirements.","US state codes must be two characters to meet PayPal Seller Protection requirements." +"Country name is not an accepted country.","Country name is not an accepted country." +"Provided country information is inconsistent.","Provided country information is inconsistent." +"Country code is not accepted. Please contact the store administrator.","Country code is not accepted. Please contact the store administrator." +"Customer has already reached the maximum of 50 addresses.","Customer has already reached the maximum of 50 addresses." +"Address is invalid. Please contact the store administrator.","Address is invalid. Please contact the store administrator." +"Address is invalid.","Address is invalid." +"Billing address format is invalid.","Billing address format is invalid." +"Cardholder name is too long.","Cardholder name is too long." +"Credit card type is not accepted by this merchant account.","Credit card type is not accepted by this merchant account." +"CVV is required.","CVV is required." +"CVV must be 4 digits for American Express and 3 digits for other card types.","CVV must be 4 digits for American Express and 3 digits for other card types." +"Expiration date is required.","Expiration date is required." +"Expiration date is invalid.","Expiration date is invalid." +"Expiration year is invalid. It must be between 1975 and 2201.","Expiration year is invalid. It must be between 1975 and 2201." +"Expiration month is invalid.","Expiration month is invalid." +"Expiration year is invalid.","Expiration year is invalid." +"Credit card number is required.","Credit card number is required." +"Credit card number is invalid.","Credit card number is invalid." +"Credit card number must be 12-19 digits.","Credit card number must be 12-19 digits." +"CVV verification failed.","CVV verification failed." +"Postal code verification failed.","Postal code verification failed." +"Credit card number is prohibited.","Credit card number is prohibited." +"Incomplete PayPal account information.","Incomplete PayPal account information." +"Invalid PayPal account information.","Invalid PayPal account information." +"PayPal Accounts are not accepted by this merchant account.","PayPal Accounts are not accepted by this merchant account." +"Error communicating with PayPal.","Error communicating with PayPal." +"PayPal authentication expired.","PayPal authentication expired." +"Error executing PayPal order.","Error executing PayPal order." +"Error executing PayPal billing agreement.","Error executing PayPal billing agreement." +"Cannot provide a billing address unless also providing a credit card.","Cannot provide a billing address unless also providing a credit card." +"Transaction can only be voided if status is authorized, submitted_for_settlement, or - for PayPal - settlement_pending.","Transaction can only be voided if status is authorized, submitted_for_settlement, or - for PayPal - settlement_pending." +"Credit transactions cannot be refunded.","Credit transactions cannot be refunded." +"Cannot refund a transaction unless it is settled.","Cannot refund a transaction unless it is settled." +"Cannot submit for settlement unless status is authorized.","Cannot submit for settlement unless status is authorized." +"Customer does not have any credit cards.","Customer does not have any credit cards." +"Transaction has already been completely refunded.","Transaction has already been completely refunded." +"Payment instrument type is not accepted by this merchant account.","Payment instrument type is not accepted by this merchant account." +"Processor authorization code cannot be set unless for a voice authorization.","Processor authorization code cannot be set unless for a voice authorization." +"Refund amount is too large.","Refund amount is too large." +"Settlement amount is too large.","Settlement amount is too large." +"Cannot refund a transaction with a suspended merchant account.","Cannot refund a transaction with a suspended merchant account." +"Merchant account does not support refunds.","Merchant account does not support refunds." +"PayPal is not enabled for your merchant account.","PayPal is not enabled for your merchant account." +"Cannot refund a transaction transaction in settling status on this merchant account. Try again after the transaction has settled.","Cannot refund a transaction transaction in settling status on this merchant account. Try again after the transaction has settled." +"Cannot submit for partial settlement.","Cannot submit for partial settlement." +"Partial settlements are not supported by this processor.","Partial settlements are not supported by this processor." +"Transaction can not be voided if status of a PayPal partial settlement child transaction is settlement_pending.","Transaction can not be voided if status of a PayPal partial settlement child transaction is settlement_pending." +"Too many concurrent attempts to refund this transaction. Try again later.","Too many concurrent attempts to refund this transaction. Try again later." +"Too many concurrent attempts to void this transaction. Try again later.","Too many concurrent attempts to void this transaction. Try again later." \ No newline at end of file diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php new file mode 100644 index 0000000000000..ac4a93e6066a4 --- /dev/null +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php @@ -0,0 +1,161 @@ +quantityProcessor = $quantityProcessor; + $this->formKeyValidator = $formKeyValidator; + $this->checkoutSession = $checkoutSession; + $this->json = $json; + $this->logger = $logger; + parent::__construct($context); + } + + /** + * @return void + */ + public function execute() + { + try { + if (!$this->formKeyValidator->validate($this->getRequest())) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + + $cartData = $this->getRequest()->getParam('cart'); + if (!is_array($cartData)) { + throw new LocalizedException( + __('Something went wrong while saving the page. Please refresh the page and try again.') + ); + } + + $cartData = $this->quantityProcessor->process($cartData); + $quote = $this->checkoutSession->getQuote(); + + foreach ($cartData as $itemId => $itemInfo) { + $item = $quote->getItemById($itemId); + $qty = isset($itemInfo['qty']) ? (double)$itemInfo['qty'] : 0; + if ($item) { + $this->updateItemQuantity($item, $qty); + } + } + + $this->jsonResponse(); + } catch (LocalizedException $e) { + $this->jsonResponse($e->getMessage()); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $this->jsonResponse('Something went wrong while saving the page. Please refresh the page and try again.'); + } + } + + /** + * Updates quote item quantity. + * + * @param Item $item + * @param float $qty + * @throws LocalizedException + */ + private function updateItemQuantity(Item $item, float $qty) + { + if ($qty > 0) { + $item->setQty($qty); + + if ($item->getHasError()) { + throw new LocalizedException(__($item->getMessage())); + } + } + } + + /** + * JSON response builder. + * + * @param string $error + * @return void + */ + private function jsonResponse(string $error = '') + { + $this->getResponse()->representJson( + $this->json->serialize($this->getResponseData($error)) + ); + } + + /** + * Returns response data. + * + * @param string $error + * @return array + */ + private function getResponseData(string $error = ''): array + { + $response = [ + 'success' => true, + ]; + + if (!empty($error)) { + $response = [ + 'success' => false, + 'error_message' => $error, + ]; + } + + return $response; + } +} diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php index 174cb38b0e9a9..05ce48c9b46ce 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php @@ -6,8 +6,45 @@ */ namespace Magento\Checkout\Controller\Cart; +use Magento\Checkout\Model\Cart\RequestQuantityProcessor; + class UpdatePost extends \Magento\Checkout\Controller\Cart { + /** + * @var RequestQuantityProcessor + */ + private $quantityProcessor; + + /** + * @param \Magento\Framework\App\Action\Context $context + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator + * @param \Magento\Checkout\Model\Cart $cart + * @param RequestQuantityProcessor $quantityProcessor + */ + public function __construct( + \Magento\Framework\App\Action\Context $context, + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Checkout\Model\Session $checkoutSession, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator, + \Magento\Checkout\Model\Cart $cart, + RequestQuantityProcessor $quantityProcessor = null + ) { + parent::__construct( + $context, + $scopeConfig, + $checkoutSession, + $storeManager, + $formKeyValidator, + $cart + ); + + $this->quantityProcessor = $quantityProcessor ?: $this->_objectManager->get(RequestQuantityProcessor::class); + } + /** * Empty customer's shopping cart * @@ -34,20 +71,10 @@ protected function _updateShoppingCart() try { $cartData = $this->getRequest()->getParam('cart'); if (is_array($cartData)) { - $filter = new \Zend_Filter_LocalizedToNormalized( - ['locale' => $this->_objectManager->get( - \Magento\Framework\Locale\ResolverInterface::class - )->getLocale()] - ); - foreach ($cartData as $index => $data) { - if (isset($data['qty'])) { - $cartData[$index]['qty'] = $filter->filter(trim($data['qty'])); - } - } if (!$this->cart->getCustomerSession()->getCustomerId() && $this->cart->getQuote()->getCustomerId()) { $this->cart->getQuote()->setCustomerId(null); } - + $cartData = $this->quantityProcessor->process($cartData); $cartData = $this->cart->suggestItemsQty($cartData); $this->cart->updateItems($cartData)->save(); } diff --git a/app/code/Magento/Checkout/Model/Cart/RequestQuantityProcessor.php b/app/code/Magento/Checkout/Model/Cart/RequestQuantityProcessor.php new file mode 100644 index 0000000000000..971b35c8f3e3d --- /dev/null +++ b/app/code/Magento/Checkout/Model/Cart/RequestQuantityProcessor.php @@ -0,0 +1,48 @@ +localeResolver = $localeResolver; + } + + /** + * Process cart request data + * + * @param array $cartData + * @return array + */ + public function process(array $cartData): array + { + $filter = new \Zend\I18n\Filter\NumberParse($this->localeResolver->getLocale()); + + foreach ($cartData as $index => $data) { + if (isset($data['qty'])) { + $data['qty'] = is_string($data['qty']) ? trim($data['qty']) : $data['qty']; + $cartData[$index]['qty'] = $filter->filter($data['qty']); + } + } + + return $cartData; + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php new file mode 100644 index 0000000000000..daabb080b1c9a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php @@ -0,0 +1,88 @@ +localeResolver = $this->getMockBuilder(ResolverInterface::class) + ->getMockForAbstractClass(); + + $this->localeResolver->method('getLocale') + ->willReturn('en_US'); + + $this->requestProcessor = new RequestQuantityProcessor( + $this->localeResolver + ); + } + + /** + * Test of cart data processing. + * + * @param array $cartData + * @param array $expected + * @dataProvider cartDataProvider + */ + public function testProcess($cartData, $expected) + { + $this->assertEquals($this->requestProcessor->process($cartData), $expected); + } + + public function cartDataProvider() + { + return [ + 'empty_array' => [ + 'cartData' => [], + 'expected' => [], + ], + 'strings_array' => [ + 'cartData' => [ + ['qty' => ' 10 '], + ['qty' => ' 0.5 '] + ], + 'expected' => [ + ['qty' => 10], + ['qty' => 0.5] + ], + ], + 'integer_array' => [ + 'cartData' => [ + ['qty' => 1], + ['qty' => 0.002] + ], + 'expected' => [ + ['qty' => 1], + ['qty' => 0.002] + ], + ], + 'array_of arrays' => [ + 'cartData' => [ + ['qty' => [1, 2 ,3]], + ], + 'expected' => [ + ['qty' => [1, 2, 3]], + ], + ], + ]; + } +} diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 4940cd7f26747..71b1392d5391f 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -13,7 +13,9 @@
getBlockHtml('formkey') ?>
diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js new file mode 100644 index 0000000000000..ce1527b3d72d6 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -0,0 +1,127 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/modal/alert', + 'jquery', + 'jquery/ui', + 'mage/validation' +], function (alert, $) { + 'use strict'; + + $.widget('mage.updateShoppingCart', { + options: { + validationURL: '', + eventName: 'updateCartItemQty' + }, + + /** @inheritdoc */ + _create: function () { + this._on(this.element, { + 'submit': this.onSubmit + }); + }, + + /** + * Prevents default submit action and calls form validator. + * + * @param {Event} event + * @return {Boolean} + */ + onSubmit: function (event) { + if (!this.options.validationURL) { + return true; + } + + if (this.isValid()) { + event.preventDefault(); + this.validateItems(this.options.validationURL, this.element.serialize()); + } + + return false; + }, + + /** + * Validates requested form. + * + * @return {Boolean} + */ + isValid: function () { + return this.element.validation() && this.element.validation('isValid'); + }, + + /** + * Validates updated shopping cart data. + * + * @param {String} url - request url + * @param {Object} data - post data for ajax call + */ + validateItems: function (url, data) { + $.extend(data, { + 'form_key': $.mage.cookies.get('form_key') + }); + + $.ajax({ + url: url, + data: data, + type: 'post', + dataType: 'json', + context: this, + + /** @inheritdoc */ + beforeSend: function () { + $(document.body).trigger('processStart'); + }, + + /** @inheritdoc */ + complete: function () { + $(document.body).trigger('processStop'); + } + }) + .done(function (response) { + if (response.success) { + this.onSuccess(); + } else { + this.onError(response); + } + }) + .fail(function () { + this.submitForm(); + }); + }, + + /** + * Form validation succeed. + */ + onSuccess: function () { + $(document).trigger('ajax:' + this.options.eventName); + this.submitForm(); + }, + + /** + * Form validation failed. + */ + onError: function (response) { + if (response['error_message']) { + alert({ + content: response['error_message'] + }); + } else { + this.submitForm(); + } + }, + + /** + * Real submit of validated form. + */ + submitForm: function () { + this.element + .off('submit', this.onSubmit) + .submit(); + } + }); + + return $.mage.updateShoppingCart; +}); diff --git a/app/code/Magento/Multishipping/Controller/Checkout/CheckItems.php b/app/code/Magento/Multishipping/Controller/Checkout/CheckItems.php new file mode 100644 index 0000000000000..e9a46f1cf00f7 --- /dev/null +++ b/app/code/Magento/Multishipping/Controller/Checkout/CheckItems.php @@ -0,0 +1,196 @@ +checkoutSession = $checkoutSession; + $this->helper = $helper; + $this->json = $json; + $this->logger = $logger; + + parent::__construct( + $context, + $customerSession, + $customerRepository, + $accountManagement + ); + } + + /** + * @return void + */ + public function execute() + { + try { + $shippingInfo = $this->getRequest()->getPost('ship'); + if (!\is_array($shippingInfo)) { + throw new LocalizedException( + __('We are unable to process your request. Please, try again later.') + ); + } + + $itemsInfo = $this->collectItemsInfo($shippingInfo); + $totalQuantity = array_sum($itemsInfo); + + $maxQuantity = $this->helper->getMaximumQty(); + if ($totalQuantity > $maxQuantity) { + throw new LocalizedException( + __('Maximum qty allowed for Shipping to multiple addresses is %1', $maxQuantity) + ); + } + + $quote = $this->checkoutSession->getQuote(); + foreach ($quote->getAllItems() as $item) { + if (isset($itemsInfo[$item->getId()])) { + $this->updateItemQuantity($item, $itemsInfo[$item->getId()]); + } + } + + if ($quote->getHasError()) { + throw new LocalizedException(__($quote->getMessage())); + } + + $this->jsonResponse(); + } catch (LocalizedException $e) { + $this->jsonResponse($e->getMessage()); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $this->jsonResponse('We are unable to process your request. Please, try again later.'); + } + } + + /** + * Updates quote item quantity. + * + * @param Item $item + * @param float $quantity + * @throws LocalizedException + */ + private function updateItemQuantity(Item $item, float $quantity) + { + if ($quantity > 0) { + $item->setQty($quantity); + if ($item->getHasError()) { + throw new LocalizedException(__($item->getMessage())); + } + } + } + + /** + * Group posted items. + * + * @param array $shippingInfo + * @return array + */ + private function collectItemsInfo(array $shippingInfo): array + { + $itemsInfo = []; + foreach ($shippingInfo as $itemData) { + if (!\is_array($itemData)) { + continue; + } + foreach ($itemData as $quoteItemId => $data) { + if (!isset($itemsInfo[$quoteItemId])) { + $itemsInfo[$quoteItemId] = 0; + } + $itemsInfo[$quoteItemId] += (double)$data['qty']; + } + } + + return $itemsInfo; + } + + /** + * JSON response builder. + * + * @param string $error + * @return void + */ + private function jsonResponse(string $error = '') + { + $this->getResponse()->representJson( + $this->json->serialize($this->getResponseData($error)) + ); + } + + /** + * Returns response data. + * + * @param string $error + * @return array + */ + private function getResponseData(string $error = ''): array + { + $response = [ + 'success' => true, + ]; + + if (!empty($error)) { + $response = [ + 'success' => false, + 'error_message' => $error, + ]; + } + + return $response; + } +} diff --git a/app/code/Magento/Multishipping/i18n/en_US.csv b/app/code/Magento/Multishipping/i18n/en_US.csv index 43cc785c56eab..43615e697b931 100644 --- a/app/code/Magento/Multishipping/i18n/en_US.csv +++ b/app/code/Magento/Multishipping/i18n/en_US.csv @@ -89,4 +89,5 @@ Options,Options "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 +"Error:","Error:" +"We are unable to process your request. Please, try again later.","We are unable to process your request. Please, try again later." diff --git a/app/code/Magento/Multishipping/view/frontend/requirejs-config.js b/app/code/Magento/Multishipping/view/frontend/requirejs-config.js index f14159ba0a85a..0235d8b848154 100644 --- a/app/code/Magento/Multishipping/view/frontend/requirejs-config.js +++ b/app/code/Magento/Multishipping/view/frontend/requirejs-config.js @@ -9,7 +9,8 @@ var config = { multiShipping: 'Magento_Multishipping/js/multi-shipping', orderOverview: 'Magento_Multishipping/js/overview', payment: 'Magento_Multishipping/js/payment', - billingLoader: 'Magento_Checkout/js/checkout-loader' + billingLoader: 'Magento_Checkout/js/checkout-loader', + cartUpdate: 'Magento_Checkout/js/action/update-shopping-cart' } } }; diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml index a55b84c0a2d8a..0f96f7cdb708a 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml @@ -14,20 +14,42 @@ * @var $block \Magento\Multishipping\Block\Checkout\Addresses */ ?> - + +
- + escapeHtml(__('Please select a shipping address for applicable items.')) ?>
- + - - - + + + @@ -35,24 +57,37 @@ getItems() as $_index => $_item): ?> getQuoteItem()) : ?> - + @@ -73,12 +111,35 @@
- +
- - - + + + + escapeHtml(__('Back to Shopping Cart')) ?> +
diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php new file mode 100644 index 0000000000000..44bbd90cc6a2d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/UpdateItemQtyTest.php @@ -0,0 +1,120 @@ +json = $this->_objectManager->create(Json::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + $this->session = $this->_objectManager->create(Session::class); + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + } + + /** + * Tests of cart validation. + * + * @param array $requestQuantity + * @param array $expectedResponse + * + * @magentoDbIsolation enabled + * @magentoAppArea frontend + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product.php + * @dataProvider requestDataProvider + */ + public function testExecute($requestQuantity, $expectedResponse) + { + try { + /** @var $product Product */ + $product = $this->productRepository->get('simple'); + } catch (\Exception $e) { + $this->fail('No such product entity'); + } + + $quoteItem = $this->session + ->getQuote() + ->getItemByProduct($product); + + $this->assertNotNull($quoteItem, 'Cannot get quote item for simple product'); + + $request = []; + if (!empty($requestQuantity) && is_array($requestQuantity)) { + $request= [ + 'form_key' => $this->formKey->getFormKey(), + 'cart' => [ + $quoteItem->getId() => $requestQuantity, + ] + ]; + } + + $this->getRequest()->setPostValue($request); + $this->dispatch('checkout/cart/updateItemQty'); + $response = $this->getResponse()->getBody(); + + $this->assertEquals($this->json->unserialize($response), $expectedResponse); + } + + /** + * Variations of request data. + * @returns array + */ + public function requestDataProvider(): array + { + return [ + [ + 'request' => [], + 'response' => [ + 'success' => false, + 'error_message' => 'Something went wrong while saving the page.'. + ' Please refresh the page and try again.' + ] + ], + [ + 'request' => ['qty' => 2], + 'response' => [ + 'success' => true, + ] + ], + [ + 'request' => ['qty' => 230], + 'response' => [ + 'success' => false, + 'error_message' => 'We don\'t have as many "Simple Product" as you requested.'] + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Controller/Checkout/CheckItemsTest.php b/dev/tests/integration/testsuite/Magento/Multishipping/Controller/Checkout/CheckItemsTest.php new file mode 100644 index 0000000000000..636239ee7a52e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Controller/Checkout/CheckItemsTest.php @@ -0,0 +1,176 @@ +checkoutSession = $this->_objectManager->get(Session::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->json = $this->_objectManager->get(Json::class); + $this->quote = $this->getQuote('test01'); + $this->checkoutSession->setQuoteId($this->quote->getId()); + $this->checkoutSession->setCartWasUpdated(false); + } + + /** + * Validator of quote items. + * + * @param array $requestQuantity + * @param array $expectedResponse + * + * @magentoConfigFixture current_store multishipping/options/checkout_multiple 1 + * @magentoConfigFixture current_store multishipping/options/checkout_multiple_maximum_qty 200 + * @dataProvider requestDataProvider + */ + public function testExecute($requestQuantity, $expectedResponse) + { + $this->loginCustomer(); + + try { + /** @var $product Product */ + $product = $this->productRepository->get('simple'); + } catch (\Exception $e) { + $this->fail('No such product entity'); + } + + $quoteItem = $this->quote->getItemByProduct($product); + $this->assertNotFalse($quoteItem, 'Cannot get quote item for simple product'); + + $request = []; + if (!empty($requestQuantity) && is_array($requestQuantity)) { + $request= [ + 'ship' => [ + [$quoteItem->getId() => $requestQuantity], + ] + ]; + } + + $this->getRequest()->setPostValue($request); + $this->dispatch('multishipping/checkout/checkItems'); + $response = $this->getResponse()->getBody(); + + $this->assertEquals($expectedResponse, $this->json->unserialize($response)); + } + + /** + * Authenticates customer and creates customer session. + */ + private function loginCustomer() + { + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + /** @var AccountManagementInterface $service */ + $service = $this->_objectManager->create(AccountManagementInterface::class); + try { + $customer = $service->authenticate('customer@example.com', 'password'); + } catch (LocalizedException $e) { + $this->fail($e->getMessage()); + } + /** @var CustomerSession $customerSession */ + $customerSession = $this->_objectManager->create(CustomerSession::class, [$logger]); + $customerSession->setCustomerDataAsLoggedIn($customer); + } + + /** + * Gets quote by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote($reservedOrderId) + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + /** @var QuoteRepository $quoteRepository */ + $quoteRepository = $this->_objectManager->get(QuoteRepository::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + return array_pop($items); + } + + /** + * Variations of request data. + * @returns array + */ + public function requestDataProvider(): array + { + return [ + [ + 'request' => [], + 'response' => [ + 'success' => false, + 'error_message' => 'We are unable to process your request. Please, try again later.' + ] + ], + [ + 'request' => ['qty' => 2], + 'response' => [ + 'success' => true, + ] + ], + [ + 'request' => ['qty' => 101], + 'response' => [ + 'success' => false, + 'error_message' => 'We don\'t have as many "Simple Product" as you requested.'] + ], + [ + 'request' => ['qty' => 230], + 'response' => [ + 'success' => false, + 'error_message' => 'Maximum qty allowed for Shipping to multiple addresses is 200'] + ], + ]; + } +} From 8d7e38a9a30ed92c719b7ab1403cf480707e5f2d Mon Sep 17 00:00:00 2001 From: Ievgen Sentiabov Date: Tue, 31 Jul 2018 15:23:24 +0300 Subject: [PATCH 02/20] MAGETWO-93712: [2.3] Free Shipping Cart Price Rule not working when UPS shipping method is enabled and Free Shipping is set to "For matching items only" - Fixed Cart Rules applying for Cart Items --- .../Model/Quote/Address/FreeShipping.php | 60 +++-- .../Model/Quote/Address/FreeShippingTest.php | 225 +++++++++++++----- .../Model/Carrier/AbstractCarrier.php | 3 + .../Magento/Ups/Model/CarrierTest.php | 24 ++ 4 files changed, 233 insertions(+), 79 deletions(-) diff --git a/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php b/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php index e1397dc6e097b..c6234a1247a53 100644 --- a/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php +++ b/app/code/Magento/OfflineShipping/Model/Quote/Address/FreeShipping.php @@ -3,27 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\OfflineShipping\Model\Quote\Address; -class FreeShipping implements \Magento\Quote\Model\Quote\Address\FreeShippingInterface +use Magento\OfflineShipping\Model\SalesRule\Calculator; +use Magento\OfflineShipping\Model\SalesRule\ExtendedCalculator; +use Magento\Quote\Model\Quote\Address\FreeShippingInterface; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Store\Model\StoreManagerInterface; + +class FreeShipping implements FreeShippingInterface { /** - * @var \Magento\OfflineShipping\Model\SalesRule\Calculator + * @var ExtendedCalculator */ protected $calculator; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\OfflineShipping\Model\SalesRule\Calculator $calculator + * @param StoreManagerInterface $storeManager + * @param Calculator $calculator */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\OfflineShipping\Model\SalesRule\Calculator $calculator + StoreManagerInterface $storeManager, + Calculator $calculator ) { $this->storeManager = $storeManager; $this->calculator = $calculator; @@ -39,6 +47,7 @@ public function isFreeShipping(\Magento\Quote\Model\Quote $quote, $items) return false; } + $result = false; $addressFreeShipping = true; $store = $this->storeManager->getStore($quote->getStoreId()); $this->calculator->init( @@ -62,21 +71,24 @@ public function isFreeShipping(\Magento\Quote\Model\Quote $quote, $items) } $this->calculator->processFreeShipping($item); - $itemFreeShipping = (bool)$item->getFreeShipping(); - $addressFreeShipping = $addressFreeShipping && $itemFreeShipping; - - if ($addressFreeShipping && !$item->getAddress()->getFreeShipping()) { - $item->getAddress()->setFreeShipping(true); + // at least one item matches to the rule and the rule mode is not a strict + if ((bool)$item->getAddress()->getFreeShipping()) { + $result = true; + break; } - /** Parent free shipping we apply to all children*/ - $this->applyToChildren($item, $itemFreeShipping); + $itemFreeShipping = (bool)$item->getFreeShipping(); + $addressFreeShipping = $addressFreeShipping && $itemFreeShipping; + $result = $addressFreeShipping; } - return (bool)$shippingAddress->getFreeShipping(); + + $shippingAddress->setFreeShipping((int)$result); + $this->applyToItems($items, $result); + return $result; } /** - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param AbstractItem $item * @param bool $isFreeShipping * @return void */ @@ -91,4 +103,20 @@ protected function applyToChildren(\Magento\Quote\Model\Quote\Item\AbstractItem } } } + + /** + * Sets free shipping availability to the quote items. + * + * @param array $items + * @param bool $freeShipping + */ + private function applyToItems(array $items, bool $freeShipping) + { + /** @var AbstractItem $item */ + foreach ($items as $item) { + $item->getAddress() + ->setFreeShipping((int)$freeShipping); + $this->applyToChildren($item, $freeShipping); + } + } } diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php index 5a8b1afa786bb..8266a39bbbb6d 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/Quote/Address/FreeShippingTest.php @@ -3,94 +3,193 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\OfflineShipping\Test\Unit\Model\Quote\Address; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\OfflineShipping\Model\Quote\Address\FreeShipping; +use Magento\OfflineShipping\Model\SalesRule\Calculator; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Item; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class FreeShippingTest extends \PHPUnit\Framework\TestCase { + private static $websiteId = 1; + + private static $customerGroupId = 2; + + private static $couponCode = 3; + + private static $storeId = 1; + /** - * @var \Magento\OfflineShipping\Model\Quote\Address\FreeShipping + * @var FreeShipping */ private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface + * @var MockObject|StoreManagerInterface */ - private $storeManagerMock; + private $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\OfflineShipping\Model\SalesRule\Calculator + * @var MockObject|Calculator */ - private $calculatorMock; + private $calculator; protected function setUp() { - $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->calculatorMock = $this->createMock(\Magento\OfflineShipping\Model\SalesRule\Calculator::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->calculator = $this->createMock(Calculator::class); - $this->model = new \Magento\OfflineShipping\Model\Quote\Address\FreeShipping( - $this->storeManagerMock, - $this->calculatorMock + $this->model = new FreeShipping( + $this->storeManager, + $this->calculator ); } - public function testIsFreeShippingIfNoItems() + /** + * Checks free shipping availability based on quote items and cart rule calculations. + * + * @param int $addressFree + * @param int $fItemFree + * @param int $sItemFree + * @param bool $expected + * @dataProvider itemsDataProvider + */ + public function testIsFreeShipping(int $addressFree, int $fItemFree, int $sItemFree, bool $expected) { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->assertFalse($this->model->isFreeShipping($quoteMock, [])); + $address = $this->getShippingAddress(); + $this->withStore(); + $quote = $this->getQuote($address); + $fItem = $this->getItem($quote); + $sItem = $this->getItem($quote); + $items = [$fItem, $sItem]; + + $this->calculator->method('init') + ->with(self::$websiteId, self::$customerGroupId, self::$couponCode); + $this->calculator->method('processFreeShipping') + ->withConsecutive( + [$fItem], + [$sItem] + ) + ->willReturnCallback(function () use ($fItem, $sItem, $addressFree, $fItemFree, $sItemFree) { + // emulate behavior of cart rule calculator + $fItem->getAddress()->setFreeShipping($addressFree); + $fItem->setFreeShipping($fItemFree); + $sItem->setFreeShipping($sItemFree); + }); + + $actual = $this->model->isFreeShipping($quote, $items); + self::assertEquals($expected, $actual); + self::assertEquals($expected, $address->getFreeShipping()); } - public function testIsFreeShipping() + /** + * Gets list of variations with free shipping availability. + * + * @return array + */ + public function itemsDataProvider(): array { - $storeId = 100; - $websiteId = 200; - $customerGroupId = 300; - $objectManagerMock = new ObjectManagerHelper($this); - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getShippingAddress', 'getStoreId', 'getCustomerGroupId', 'getCouponCode'] - ); - $itemMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, [ - 'getNoDiscount', - 'getParentItemId', - 'getFreeShipping', - 'getAddress', - 'isChildrenCalculated', - 'getHasChildren', - 'getChildren' - ]); - - $quoteMock->expects($this->once())->method('getStoreId')->willReturn($storeId); - $storeMock = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); - $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $this->storeManagerMock->expects($this->once())->method('getStore')->with($storeId)->willReturn($storeMock); - - $quoteMock->expects($this->once())->method('getCustomerGroupId')->willReturn($customerGroupId); - $quoteMock->expects($this->once())->method('getCouponCode')->willReturn(null); - - $this->calculatorMock->expects($this->once()) - ->method('init') - ->with($websiteId, $customerGroupId, null) - ->willReturnSelf(); - - $itemMock->expects($this->once())->method('getNoDiscount')->willReturn(false); - $itemMock->expects($this->once())->method('getParentItemId')->willReturn(false); - $this->calculatorMock->expects($this->exactly(2))->method('processFreeShipping')->willReturnSelf(); - $itemMock->expects($this->once())->method('getFreeShipping')->willReturn(true); - - $addressMock = $objectManagerMock->getObject(\Magento\Quote\Model\Quote\Address::class); - $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($addressMock); - $itemMock->expects($this->exactly(2))->method('getAddress')->willReturn($addressMock); - - $itemMock->expects($this->once())->method('getHasChildren')->willReturn(true); - $itemMock->expects($this->once())->method('isChildrenCalculated')->willReturn(true); - - $childMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['setFreeShipping']); - $childMock->expects($this->once())->method('setFreeShipping')->with(true)->willReturnSelf(); - $itemMock->expects($this->once())->method('getChildren')->willReturn([$childMock]); - - $this->assertTrue($this->model->isFreeShipping($quoteMock, [$itemMock])); + return [ + ['addressFree' => 1, 'fItemFree' => 0, 'sItemFree' => 0, 'expected' => true], + ['addressFree' => 0, 'fItemFree' => 1, 'sItemFree' => 0, 'expected' => false], + ['addressFree' => 0, 'fItemFree' => 0, 'sItemFree' => 1, 'expected' => false], + ['addressFree' => 0, 'fItemFree' => 1, 'sItemFree' => 1, 'expected' => true], + ]; + } + + /** + * Creates mock object for store entity. + */ + private function withStore() + { + $store = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->method('getStore') + ->with(self::$storeId) + ->willReturn($store); + + $store->method('getWebsiteId') + ->willReturn(self::$websiteId); + } + + /** + * Get mock object for quote entity. + * + * @param Address $address + * @return Quote + */ + private function getQuote(Address $address): Quote + { + /** @var Quote|MockObject $quote */ + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getCouponCode', 'getCustomerGroupId', 'getShippingAddress', 'getStoreId', 'getItemsQty', + 'getVirtualItemsQty' + ] + ) + ->getMock(); + + $quote->method('getStoreId') + ->willReturn(self::$storeId); + $quote->method('getCustomerGroupId') + ->willReturn(self::$customerGroupId); + $quote->method('getCouponCode') + ->willReturn(self::$couponCode); + $quote->method('getShippingAddress') + ->willReturn($address); + $quote->method('getItemsQty') + ->willReturn(2); + $quote->method('getVirtualItemsQty') + ->willReturn(0); + + return $quote; + } + + /** + * Gets stub object for shipping address. + * + * @return Address|MockObject + */ + private function getShippingAddress(): Address + { + /** @var Address|MockObject $address */ + $address = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->setMethods(['beforeSave']) + ->getMock(); + + return $address; + } + + /** + * Gets stub object for quote item. + * + * @param Quote $quote + * @return Item + */ + private function getItem(Quote $quote): Item + { + /** @var Item|MockObject $item */ + $item = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->setMethods(['getHasChildren']) + ->getMock(); + $item->setQuote($quote); + $item->setNoDiscount(0); + $item->setParentItemId(0); + $item->method('getHasChildren') + ->willReturn(0); + + return $item; } } diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index 6763046373ce7..9d7bfff5e559c 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -407,6 +407,9 @@ public function getSortOrder() */ protected function _updateFreeMethodQuote($request) { + if (!$request->getFreeShipping()) { + return; + } if ($request->getFreeMethodWeight() == $request->getPackageWeight() || !$request->hasFreeMethodWeight()) { return; } diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index a9f6752c9de82..334cb336862a9 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -51,4 +51,28 @@ public function testGetShipConfirmUrlLive() $this->carrier->getShipConfirmUrl() ); } + + /** + * @magentoConfigFixture current_store carriers/ups/active 1 + * @magentoConfigFixture current_store carriers/ups/allowed_methods 1DA,GND + * @magentoConfigFixture current_store carriers/ups/free_method GND + */ + public function testCollectFreeRates() + { + $rateRequest = Bootstrap::getObjectManager()->get(RateRequestFactory::class)->create(); + $rateRequest->setDestCountryId('US'); + $rateRequest->setDestRegionId('CA'); + $rateRequest->setDestPostcode('90001'); + $rateRequest->setPackageQty(1); + $rateRequest->setPackageWeight(1); + $rateRequest->setFreeMethodWeight(0); + $rateRequest->setLimitCarrier($this->carrier::CODE); + $rateRequest->setFreeShipping(true); + + $rateResult = $this->carrier->collectRates($rateRequest); + $result = $rateResult->asArray(); + $methods = $result[$this->carrier::CODE]['methods']; + $this->assertEquals(0, $methods['GND']['price']); + $this->assertNotEquals(0, $methods['1DA']['price']); + } } From 599031d7c797b5188b7a80d2bb75c860933170a1 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Wed, 6 Jun 2018 10:47:20 +0300 Subject: [PATCH 03/20] MAGETWO-93707: GiftCard "Open Amount From" could't be re-saved - Validation rule for numbers was extended to accept formatted floats like 2,000,000.50 --- .../view/base/web/js/lib/validation/rules.js | 2 +- .../Ui/base/js/lib/validation/rules.test.js | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index c63b4588d2d16..e52791deaf2ad 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -636,7 +636,7 @@ define([ 'validate-number': [ function (value) { return utils.isEmptyNoTrim(value) || - !isNaN(utils.parseNumber(value)) && /^\s*-?\d*(\.\d*)?\s*$/.test(value); + !isNaN(utils.parseNumber(value)) && /^\s*-?\d*(,\d*)*(\.\d*)?\s*$/.test(value); }, $.mage.__('Please enter a valid number in this field.') ], diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js index 1dfdfca8b2f50..0703374aaa9d6 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js @@ -39,6 +39,37 @@ define([ expect(rules['range-words'].handler(value, params)).toBe(true); }); }); + describe('"validate-number" method', function () { + it('Check on empty value', function () { + var value = ''; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + + it('Check on integer', function () { + var value = '125'; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + + it('Check on float', function () { + var value = '1000.50'; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + + it('Check on formatted float', function () { + var value = '1,000,000.50'; + + expect(rules['validate-number'].handler(value)).toBe(true); + }); + + it('Check on not a number', function () { + var value = 'string'; + + expect(rules['validate-number'].handler(value)).toBe(false); + }); + }); }); describe('validate-color', function () { From d621d43e66c6ec7274cd7fd9ef62f9f57b4d2044 Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Tue, 31 Jul 2018 15:55:42 +0300 Subject: [PATCH 04/20] MAGETWO-93788: Google Analytics - addToCart event triggers before updating the cart --- app/code/Magento/Braintree/i18n/de_DE.csv | 193 ---------------------- 1 file changed, 193 deletions(-) delete mode 100644 app/code/Magento/Braintree/i18n/de_DE.csv diff --git a/app/code/Magento/Braintree/i18n/de_DE.csv b/app/code/Magento/Braintree/i18n/de_DE.csv deleted file mode 100644 index d55233159689d..0000000000000 --- a/app/code/Magento/Braintree/i18n/de_DE.csv +++ /dev/null @@ -1,193 +0,0 @@ -cc_type,"Credit Card Type" -cc_number,"Credit Card Number" -avsPostalCodeResponseCode,"AVS Postal Code Response Code" -avsStreetAddressResponseCode,"AVS Street Address Response Code" -cvvResponseCode,"CVV Response Code" -processorAuthorizationCode,"Processor Authorization Code" -processorResponseCode,"Processor Response Code" -processorResponseText,"Processor Response Text" -braintree,Braintree -liabilityShifted,"Liability Shifted" -liabilityShiftPossible,"Liability Shift Possible" -riskDataId,"Risk ID" -riskDataDecision,"Risk Decision" -paymentId,"Payment Id" -payerEmail,"Payer Email" -sale,Sale -credit,Credit -authorization_expired,"Authorization expired" -authorizing,Authorizing -authorized,Authorized -gateway_rejected,"Gateway rejected" -failed,Failed -processor_declined,"Processor declined" -settled,Settled -settling,Settling -submitted_for_settlement,"Submitted for settlement" -voided,Voided -unrecognized,Unrecognized -settlement_declined,"Settlement declined" -settlement_pending,"Settlement pending" -settlement_confirmed,"Settlement confirmed" -paypal_account,"Paypal account" -coinbase_account,"Coinbase account" -europe_bank_accout,"Europe bank account" -credit_card,"Credit card" -apple_pay_card,"Apple pay card" -android_pay_card,"Android pay card" -Country,Country -"Allowed Credit Card Types","Allowed Credit Card Types" -"Add Rule","Add Rule" -"Braintree Settlement Report","Braintree Settlement Report" -"Sorry, but something went wrong","Sorry, but something went wrong" -"We can't initialize checkout.","We can't initialize checkout." -"No authorization transaction to proceed capture.","No authorization transaction to proceed capture." -"Braintree error response.","Braintree error response." -"Payment method nonce can't be retrieved.","Payment method nonce can't be retrieved." -"Wrong transaction status","Wrong transaction status" -Authorize,Authorize -"Authorize and Capture","Authorize and Capture" -"--Please Select--","--Please Select--" -"Please agree to all the terms and conditions before placing the order.","Please agree to all the terms and conditions before placing the order." -"Credit Card Type","Credit Card Type" -"Credit Card Number","Credit Card Number" -"Please, enter valid Credit Card Number","Please, enter valid Credit Card Number" -"Expiration Date","Expiration Date" -"Please, enter valid Expiration Date","Please, enter valid Expiration Date" -"Card Verification Number","Card Verification Number" -"Please, enter valid Card Verification Number","Please, enter valid Card Verification Number" -ending,ending -expires,expires -"Are you sure you want to delete this PayPal account","Are you sure you want to delete this PayPal account" -"PayPal Account","PayPal Account" -"PayPal Logo","PayPal Logo" -Actions,Actions -Delete,Delete -"Credit Card Information","Credit Card Information" -"What is this?","What is this?" -"Save for later use.","Save for later use." -"Place Order","Place Order" -"This payment is not available","This payment is not available" -MM,MM -YY,YY -"Please try again with another form of payment.","---Bitte versuchen Sie es erneut mit einer anderen Zahlungsmethode@" -"Sorry, but something went wrong.","Sorry, but something went wrong." -"Payment ","Payment " -Braintree,Braintree -"Accept credit/debit cards and PayPal in your Magento store.
No setup or monthly fees and your customers never leave your store to complete the purchase.","Accept credit/debit cards and PayPal in your Magento store.
No setup or monthly fees and your customers never leave your store to complete the purchase." -"Enable this Solution","Enable this Solution" -"Enable PayPal through Braintree","Enable PayPal through Braintree" -"Vault Enabled","Vault Enabled" -"Basic Braintree Settings","Basic Braintree Settings" -Title,Title -Environment,Environment -"Payment Action","Payment Action" -"Merchant ID","Merchant ID" -"Public Key","Public Key" -"Private Key","Private Key" -"Advanced Braintree Settings","Advanced Braintree Settings" -"Vault Title","Vault Title" -"Merchant Account ID","Merchant Account ID" -"Advanced Fraud Protection","Advanced Fraud Protection" -"Kount Merchant ID","Kount Merchant ID" -Debug,Debug -"CVV Verification","CVV Verification" -"Credit Card Types","Credit Card Types" -"Sort Order","Sort Order" -"Country Specific Settings","Country Specific Settings" -"Payment from Applicable Countries","Payment from Applicable Countries" -"Payment from Specific Countries","Payment from Specific Countries" -"Country Specific Credit Card Types","Country Specific Credit Card Types" -"PayPal through Braintree","PayPal through Braintree" -"Override Merchant Name","Override Merchant Name" -"Require Customer's Billing Address","Require Customer's Billing Address" -"Allow to Edit Shipping Address Entered During Checkout on PayPal Side","Allow to Edit Shipping Address Entered During Checkout on PayPal Side" -"Display on Shopping Cart","Display on Shopping Cart" -"Skip Order Review","Skip Order Review" -"3D Secure Verification Settings","3D Secure Verification Settings" -"3D Secure Verification","3D Secure Verification" -"Threshold Amount","Threshold Amount" -"Verify for Applicable Countries","Verify for Applicable Countries" -"Verify for Specific Countries","Verify for Specific Countries" -"Dynamic Descriptors","Dynamic Descriptors" -Name,Name -Phone,Phone -URL,URL -"Apply filters in order to get results. Only first 100 records will be displayed in the grid, you will be able to download full version of the report in .csv format.","Apply filters in order to get results. Only first 100 records will be displayed in the grid, you will be able to download full version of the report in .csv format." -Select...,Select... -Status,Status -"Transaction Type","Transaction Type" -"Payment Type","Payment Type" -"PayPal Payment ID","PayPal Payment ID" -"Created At","Created At" -"Transaction ID","Transaction ID" -"Order ID","Order ID" -Type,Type -Amount,Amount -"Settlement Code","Settlement Code" -"Settlement Response Text","Settlement Response Text" -"Refund Ids","Refund Ids" -"Settlement Batch ID","Settlement Batch ID" -Currency,Currency -"Addresses must have at least one field filled in.","Addresses must have at least one field filled in." -"Company is too long.","Company is too long." -"Extended address is too long.","Extended address is too long." -"First name is too long.","First name is too long." -"Last name is too long.","Last name is too long." -"Locality is too long.","Locality is too long." -"Postal code can only contain letters, numbers, spaces, and hyphens.","Postal code can only contain letters, numbers, spaces, and hyphens." -"Postal code is required.","Postal code is required." -"Postal code may contain no more than 9 letter or number characters.","Postal code may contain no more than 9 letter or number characters." -"Region is too long.","Region is too long." -"Street address is required.","Street address is required." -"Street address is too long.","Street address is too long." -"US state codes must be two characters to meet PayPal Seller Protection requirements.","US state codes must be two characters to meet PayPal Seller Protection requirements." -"Country name is not an accepted country.","Country name is not an accepted country." -"Provided country information is inconsistent.","Provided country information is inconsistent." -"Country code is not accepted. Please contact the store administrator.","Country code is not accepted. Please contact the store administrator." -"Customer has already reached the maximum of 50 addresses.","Customer has already reached the maximum of 50 addresses." -"Address is invalid. Please contact the store administrator.","Address is invalid. Please contact the store administrator." -"Address is invalid.","Address is invalid." -"Billing address format is invalid.","Billing address format is invalid." -"Cardholder name is too long.","Cardholder name is too long." -"Credit card type is not accepted by this merchant account.","Credit card type is not accepted by this merchant account." -"CVV is required.","CVV is required." -"CVV must be 4 digits for American Express and 3 digits for other card types.","CVV must be 4 digits for American Express and 3 digits for other card types." -"Expiration date is required.","Expiration date is required." -"Expiration date is invalid.","Expiration date is invalid." -"Expiration year is invalid. It must be between 1975 and 2201.","Expiration year is invalid. It must be between 1975 and 2201." -"Expiration month is invalid.","Expiration month is invalid." -"Expiration year is invalid.","Expiration year is invalid." -"Credit card number is required.","Credit card number is required." -"Credit card number is invalid.","Credit card number is invalid." -"Credit card number must be 12-19 digits.","Credit card number must be 12-19 digits." -"CVV verification failed.","CVV verification failed." -"Postal code verification failed.","Postal code verification failed." -"Credit card number is prohibited.","Credit card number is prohibited." -"Incomplete PayPal account information.","Incomplete PayPal account information." -"Invalid PayPal account information.","Invalid PayPal account information." -"PayPal Accounts are not accepted by this merchant account.","PayPal Accounts are not accepted by this merchant account." -"Error communicating with PayPal.","Error communicating with PayPal." -"PayPal authentication expired.","PayPal authentication expired." -"Error executing PayPal order.","Error executing PayPal order." -"Error executing PayPal billing agreement.","Error executing PayPal billing agreement." -"Cannot provide a billing address unless also providing a credit card.","Cannot provide a billing address unless also providing a credit card." -"Transaction can only be voided if status is authorized, submitted_for_settlement, or - for PayPal - settlement_pending.","Transaction can only be voided if status is authorized, submitted_for_settlement, or - for PayPal - settlement_pending." -"Credit transactions cannot be refunded.","Credit transactions cannot be refunded." -"Cannot refund a transaction unless it is settled.","Cannot refund a transaction unless it is settled." -"Cannot submit for settlement unless status is authorized.","Cannot submit for settlement unless status is authorized." -"Customer does not have any credit cards.","Customer does not have any credit cards." -"Transaction has already been completely refunded.","Transaction has already been completely refunded." -"Payment instrument type is not accepted by this merchant account.","Payment instrument type is not accepted by this merchant account." -"Processor authorization code cannot be set unless for a voice authorization.","Processor authorization code cannot be set unless for a voice authorization." -"Refund amount is too large.","Refund amount is too large." -"Settlement amount is too large.","Settlement amount is too large." -"Cannot refund a transaction with a suspended merchant account.","Cannot refund a transaction with a suspended merchant account." -"Merchant account does not support refunds.","Merchant account does not support refunds." -"PayPal is not enabled for your merchant account.","PayPal is not enabled for your merchant account." -"Cannot refund a transaction transaction in settling status on this merchant account. Try again after the transaction has settled.","Cannot refund a transaction transaction in settling status on this merchant account. Try again after the transaction has settled." -"Cannot submit for partial settlement.","Cannot submit for partial settlement." -"Partial settlements are not supported by this processor.","Partial settlements are not supported by this processor." -"Transaction can not be voided if status of a PayPal partial settlement child transaction is settlement_pending.","Transaction can not be voided if status of a PayPal partial settlement child transaction is settlement_pending." -"Too many concurrent attempts to refund this transaction. Try again later.","Too many concurrent attempts to refund this transaction. Try again later." -"Too many concurrent attempts to void this transaction. Try again later.","Too many concurrent attempts to void this transaction. Try again later." \ No newline at end of file From 95dd29f5b93434faa07adbcfaf0ee11814db99d1 Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Thu, 17 May 2018 11:58:21 +0300 Subject: [PATCH 05/20] MAGETWO-91329: 'Use Default Value' checkboxes in Design section of a category are not checked by default --- .../view/adminhtml/requirejs-config.js | 15 +- .../adminhtml/ui_component/category_form.xml | 16 +- .../components/use-parent-settings/select.js | 15 ++ .../use-parent-settings/single-checkbox.js | 15 ++ .../use-parent-settings/textarea.js | 15 ++ .../toggle-disabled-mixin.js | 62 ++++++++ .../view/base/web/js/form/element/abstract.js | 3 +- .../form/element/helper/service.html | 3 +- .../toggle-disabled-mixin.test.js | 141 ++++++++++++++++++ .../Ui/base/js/form/element/abstract.test.js | 6 + 10 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js create mode 100644 app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js create mode 100644 app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js create mode 100644 app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/use-parent-settings/toggle-disabled-mixin.test.js diff --git a/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js b/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js index 9053c700d1a3b..0677b0a5811c2 100644 --- a/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js +++ b/app/code/Magento/Catalog/view/adminhtml/requirejs-config.js @@ -17,5 +17,18 @@ var config = { }, deps: [ 'Magento_Catalog/catalog/product' - ] + ], + config: { + mixins: { + 'Magento_Catalog/js/components/use-parent-settings/select': { + 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true + }, + 'Magento_Catalog/js/components/use-parent-settings/textarea': { + 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true + }, + 'Magento_Catalog/js/components/use-parent-settings/single-checkbox': { + 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin': true + } + } + } }; diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index 2976f1be14fa5..537932a2d4daa 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -452,34 +452,34 @@ - + string - ${ $.parentName }.custom_use_parent_settings:checked + ${ $.parentName }.custom_use_parent_settings:checked - + string - ${ $.parentName }.custom_use_parent_settings:checked + ${ $.parentName }.custom_use_parent_settings:checked - + string - ns = ${ $.ns }, index = custom_use_parent_settings :checked + ${ $.parentName }.custom_use_parent_settings:checked - + 0 @@ -492,7 +492,7 @@ boolean - ns = ${ $.ns }, index = custom_use_parent_settings:checked + ${ $.parentName }.custom_use_parent_settings:checked diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js new file mode 100644 index 0000000000000..1ddb24f3eefbb --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/select.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Ui/js/form/element/select' +], function (Component) { + 'use strict'; + + return Component; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js new file mode 100644 index 0000000000000..0f166d3b45582 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/single-checkbox.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Ui/js/form/element/single-checkbox' +], function (Component) { + 'use strict'; + + return Component; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js new file mode 100644 index 0000000000000..3ef2bb21241a7 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/textarea.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Ui/js/form/element/textarea' +], function (Component) { + 'use strict'; + + return Component; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js new file mode 100644 index 0000000000000..d140cc0fad74e --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/use-parent-settings/toggle-disabled-mixin.js @@ -0,0 +1,62 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore' +], function (_) { + 'use strict'; + + var mixin = { + defaults: { + imports: { + toggleDisabled: '${ $.parentName }.custom_use_parent_settings:checked' + }, + useParent: false, + useDefaults: false + }, + + /** + * Disable form input if settings for parent section is used + * or default value is applied. + * + * @param {Boolean} isUseParent + */ + toggleDisabled: function (isUseParent) { + var disabled = this.useParent = isUseParent; + + if (!disabled && !_.isUndefined(this.service)) { + disabled = !!this.isUseDefault(); + } + + this.saveUseDefaults(); + this.disabled(disabled); + }, + + /** + * Stores original state of the field. + */ + saveUseDefaults: function () { + this.useDefaults = this.disabled(); + }, + + /** @inheritdoc */ + setInitialValue: function () { + this._super(); + this.isUseDefault(this.useDefaults); + + return this; + }, + + /** @inheritdoc */ + toggleUseDefault: function (state) { + this._super(); + this.disabled(state || this.useParent); + } + }; + + return function (target) { + return target.extend(mixin); + }; +}); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js index 5177b4a378d69..854af5494b385 100755 --- a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js @@ -40,6 +40,7 @@ define([ showFallbackReset: false, additionalClasses: {}, isUseDefault: '', + serviceDisabled: false, valueUpdate: false, // ko binding valueUpdate switcherConfig: { @@ -97,7 +98,7 @@ define([ this._super(); this.observe('error disabled focused preview visible value warn notice isDifferedFromDefault') - .observe('isUseDefault') + .observe('isUseDefault serviceDisabled') .observe({ 'required': !!rules['required-entry'] }); diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/helper/service.html b/app/code/Magento/Ui/view/base/web/templates/form/element/helper/service.html index e3c3ab5b5df5c..46f0a2df52441 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/element/helper/service.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/element/helper/service.html @@ -12,6 +12,7 @@ name: 'use_default[' + $data.index + ']', 'data-form-part': $data.ns " - ko-checked="isUseDefault"> + ko-checked="isUseDefault" + ko-disabled="$data.serviceDisabled"> diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/use-parent-settings/toggle-disabled-mixin.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/use-parent-settings/toggle-disabled-mixin.test.js new file mode 100644 index 0000000000000..f3e53673a4e59 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/adminhtml/js/use-parent-settings/toggle-disabled-mixin.test.js @@ -0,0 +1,141 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*eslint max-nested-callbacks: 0*/ +define([ + 'underscore', + 'Magento_Catalog/js/components/use-parent-settings/select', + 'Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin' +], function (_, select, mixin) { + 'use strict'; + + var CustomInput = mixin(select), + defaultProperties = { + name: 'uiSelect', + dataScope: '', + provider: 'provider', + service: true + }, + obj; + + describe('toggle-disabled-mixin structure tests', function () { + var defaultContext = require.s.contexts._; + + obj = new CustomInput(defaultProperties); + + it('mixin is present in RequireJs config', function () { + var requireJsConfig = defaultContext.config + .config.mixins['Magento_Catalog/js/components/use-parent-settings/select']; + + expect( + requireJsConfig['Magento_Catalog/js/components/use-parent-settings/toggle-disabled-mixin'] + ).toBe(true); + }); + + it('Check for useParent property', function () { + expect(obj.hasOwnProperty('useParent')).toBeTruthy(); + expect(typeof obj.useParent).toEqual('boolean'); + expect(obj.useParent).toEqual(false); + }); + + it('Check for useDefaults property', function () { + expect(obj.hasOwnProperty('useDefaults')).toBeTruthy(); + expect(typeof obj.useDefaults).toEqual('boolean'); + expect(obj.useDefaults).toEqual(false); + }); + + it('Check for toggleDisabled method', function () { + expect(obj.toggleDisabled).toBeDefined(); + expect(typeof obj.toggleDisabled).toEqual('function'); + }); + + it('Check for saveUseDefaults method', function () { + expect(obj.saveUseDefaults).toBeDefined(); + expect(typeof obj.saveUseDefaults).toEqual('function'); + }); + + it('Check for setInitialValue method', function () { + expect(obj.setInitialValue).toBeDefined(); + expect(typeof obj.setInitialValue).toEqual('function'); + }); + + it('Check for toggleUseDefault method', function () { + expect(obj.toggleUseDefault).toBeDefined(); + expect(typeof obj.toggleUseDefault).toEqual('function'); + }); + }); + + describe('toggle-disabled-mixin functionality', function () { + var dataProvider = [ + { + defaults: { + useParent: false, + useDefaults: false + }, + expected: { + disabled: false + } + }, + { + defaults: { + useParent: true, + useDefaults: false + }, + expected: { + disabled: true + } + }, + { + defaults: { + useParent: false, + useDefaults: true + }, + expected: { + disabled: true + } + }, + { + defaults: { + useParent: true, + useDefaults: true + }, + expected: { + disabled: true + } + } + ]; + + dataProvider.forEach(function (state) { + describe(JSON.stringify(state.defaults), function () { + + beforeEach(function () { + obj = new CustomInput( + _.extend(defaultProperties, state.defaults) + ); + }); + + it('Check disabled state', function () { + expect(obj.disabled()).toEqual(state.expected.disabled); + }); + + it('Check checked state', function () { + expect(obj.isUseDefault()).toEqual(state.defaults.useDefaults); + }); + + it('Check of using parent settings', function () { + obj.toggleDisabled(true); + expect(obj.isUseDefault()).toEqual(state.defaults.useDefaults); + expect(obj.disabled()).toEqual(true); + }); + + it('Check of using self settings', function () { + obj.toggleDisabled(false); + expect(obj.isUseDefault()).toEqual(state.defaults.useDefaults); + expect(obj.disabled()).toEqual(obj.isUseDefault()); + }); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/abstract.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/abstract.test.js index c5b153f7cc100..390c5aa89fcc7 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/abstract.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/abstract.test.js @@ -303,5 +303,11 @@ define([ expect(model.validate).toHaveBeenCalled(); }); }); + describe('serviceDisabled property', function () { + it('check property state', function () { + expect(typeof model.serviceDisabled).toEqual('function'); + expect(model.serviceDisabled()).toBeFalsy(); + }); + }); }); }); From 052471d724e3b8a182e57d755444573722040584 Mon Sep 17 00:00:00 2001 From: Ievgen Sentiabov Date: Thu, 2 Aug 2018 15:39:59 +0300 Subject: [PATCH 06/20] MAGETWO-93712: [2.3] Free Shipping Cart Price Rule not working when UPS shipping method is enabled and Free Shipping is set to "For matching items only" - Fixed integration test --- app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php | 2 ++ .../integration/testsuite/Magento/Ups/Model/CarrierTest.php | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index 9d7bfff5e559c..fac733a3fc5fe 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -451,6 +451,8 @@ protected function _updateFreeMethodQuote($request) } } } + } else { + $price = 0; } /** diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index 334cb336862a9..7cf58a1ff2daa 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Ups\Model; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Quote\Model\Quote\Address\RateRequestFactory; + class CarrierTest extends \PHPUnit\Framework\TestCase { /** @@ -14,7 +17,7 @@ class CarrierTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->carrier = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->carrier = Bootstrap::getObjectManager()->create( \Magento\Ups\Model\Carrier::class ); } From ab1e70d56e9dbd6e525e6598b370b099a0a4e113 Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Thu, 2 Aug 2018 15:58:17 +0300 Subject: [PATCH 07/20] MAGETWO-91329: 'Use Default Value' checkboxes in Design section of a category are not checked by default --- .../app/code/Magento/Ui/base/js/lib/ko/bind/i18n.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/i18n.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/i18n.test.js index 0f50cc2b6b9b1..201959a2598fd 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/i18n.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/i18n.test.js @@ -38,14 +38,18 @@ define([ /** Stub */ turnOffInlineTranslation = function () { manageInlineTranslation(false); - }; + }, + + storedConfig; beforeEach(function () { + storedConfig = context.config.config; $(document.body).append(elWithStaticText); $(document.body).append(elWithVariable); }); afterEach(function () { + context.config.config = storedConfig; elWithStaticText.remove(); elWithVariable.remove(); }); From de295f709a44a56ce5c2a9c5c80826b6ee6730cf Mon Sep 17 00:00:00 2001 From: Ievgen Sentiabov Date: Thu, 2 Aug 2018 18:21:08 +0300 Subject: [PATCH 08/20] MAGETWO-93706: [2.3] Can't reorder sales order with configurable product - Added correct link to a parent product entity --- .../OrderedProductAvailabilityChecker.php | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php b/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php index 42d7d91fb90e8..8ef1e24125981 100644 --- a/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php +++ b/app/code/Magento/ConfigurableProductSales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php @@ -5,11 +5,12 @@ */ namespace Magento\ConfigurableProductSales\Model\Order\Reorder; -use Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityCheckerInterface; -use Magento\Sales\Model\Order\Item; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Sales\Model\Order\Item; +use Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityCheckerInterface; use Magento\Store\Model\Store; /** @@ -27,16 +28,24 @@ class OrderedProductAvailabilityChecker implements OrderedProductAvailabilityChe */ private $metadataPool; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @param ResourceConnection $resourceConnection * @param MetadataPool $metadataPool + * @param ProductRepositoryInterface $productRepository */ public function __construct( ResourceConnection $resourceConnection, - MetadataPool $metadataPool + MetadataPool $metadataPool, + ProductRepositoryInterface $productRepository ) { $this->resourceConnection = $resourceConnection; $this->metadataPool = $metadataPool; + $this->productRepository = $productRepository; } /** @@ -48,7 +57,9 @@ public function isAvailable(Item $item) $superAttribute = $buyRequest->getData()['super_attribute'] ?? []; $connection = $this->getConnection(); $select = $connection->select(); - $orderItemParentId = $item->getParentItem()->getProductId(); + $linkField = $this->getMetadata()->getLinkField(); + $parentItem = $this->productRepository->getById($item->getParentItem()->getProductId()); + $orderItemParentId = $parentItem->getData($linkField); $select->from( ['cpe' => $this->resourceConnection->getTableName('catalog_product_entity')], ['cpe.entity_id'] @@ -67,7 +78,7 @@ public function isAvailable(Item $item) ['cpid' . $attributeId => $this->resourceConnection->getTableName('catalog_product_entity_int')], sprintf( 'cpe.%1$s = cpid%2$d.%1$s AND cpid%2$d.attribute_id = %2$d AND cpid%2$d.store_id = %3$d', - $this->getMetadata()->getLinkField(), + $linkField, $attributeId, Store::DEFAULT_STORE_ID ), @@ -77,7 +88,7 @@ public function isAvailable(Item $item) ['cpis' . $attributeId => $this->resourceConnection->getTableName('catalog_product_entity_int')], sprintf( 'cpe.%1$s = cpis%2$d.%1$s AND cpis%2$d.attribute_id = %2$d AND cpis%2$d.store_id = %3$d', - $this->getMetadata()->getLinkField(), + $linkField, $attributeId, $item->getStoreId() ), From 3d535787f482a380fedf94049a855c2e078885b9 Mon Sep 17 00:00:00 2001 From: Ievgen Sentiabov Date: Thu, 2 Aug 2018 18:28:24 +0300 Subject: [PATCH 09/20] MAGETWO-91601: [2.3] Shipping Progress Dates are wrong for Tracking Popup - Specified full datetime for date formatting --- .../Shipping/view/frontend/templates/tracking/progress.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml index 84126d8eeb636..237eba09ff802 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/progress.phtml @@ -23,7 +23,7 @@ $track = $block->getData('track');
getProgressdetail() as $detail): ?> - formatDeliveryDate($detail['deliverydate']) : ''); ?> + formatDeliveryDate($detail['deliverydate'] . ' ' . $detail['deliverytime']) : ''); ?> formatDeliveryTime($detail['deliverytime'], $detail['deliverydate']) : ''); ?>
+ escapeHtml(__('Please select a shipping address for applicable items.')) ?> +
escapeHtml(__('Product')) ?>escapeHtml(__('Qty')) ?>escapeHtml(__('Send To')) ?>  
getItemHtml($_item->getQuoteItem()) ?> + getItemHtml($_item->getQuoteItem()) ?> +
-
getProduct()->getIsVirtual()): ?> -
+
+ escapeHtml(__('A shipping selection is not applicable.')) ?> +
-
- - + + escapeHtml(__('Remove item')) ?>
From 63ac4bfe052fe885a7d98a98304932ee962befc7 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Fri, 15 Jun 2018 16:28:18 +0300 Subject: [PATCH 10/20] MAGETWO-91673: Not Visible Custom Address Attributes Showing on Checkout --- .../Checkout/Model/DefaultConfigProvider.php | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php index b5727bf8f365e..16f13511001e9 100644 --- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php +++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php @@ -8,12 +8,14 @@ use Magento\Catalog\Helper\Product\ConfigurationPool; use Magento\Checkout\Helper\Data as CheckoutHelper; use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; use Magento\Customer\Model\Context as CustomerContext; use Magento\Customer\Model\Session as CustomerSession; use Magento\Customer\Model\Url as CustomerUrlManager; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Locale\FormatInterface as LocaleFormat; use Magento\Framework\UrlInterface; @@ -159,6 +161,11 @@ class DefaultConfigProvider implements ConfigProviderInterface */ protected $urlBuilder; + /** + * @var AddressMetadataInterface + */ + private $addressMetadata; + /** * @param CheckoutHelper $checkoutHelper * @param Session $checkoutSession @@ -186,6 +193,7 @@ class DefaultConfigProvider implements ConfigProviderInterface * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement * @param UrlInterface $urlBuilder + * @param AddressMetadataInterface $addressMetadata * @codeCoverageIgnore * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -215,7 +223,8 @@ public function __construct( \Magento\Shipping\Model\Config $shippingMethodConfig, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement, - UrlInterface $urlBuilder + UrlInterface $urlBuilder, + AddressMetadataInterface $addressMetadata = null ) { $this->checkoutHelper = $checkoutHelper; $this->checkoutSession = $checkoutSession; @@ -243,6 +252,7 @@ public function __construct( $this->storeManager = $storeManager; $this->paymentMethodManagement = $paymentMethodManagement; $this->urlBuilder = $urlBuilder; + $this->addressMetadata = $addressMetadata ?: ObjectManager::getInstance()->get(AddressMetadataInterface::class); } /** @@ -324,11 +334,34 @@ private function getCustomerData() $customerData = $customer->__toArray(); foreach ($customer->getAddresses() as $key => $address) { $customerData['addresses'][$key]['inline'] = $this->getCustomerAddressInline($address); + if ($address->getCustomAttributes()) { + $customerData['addresses'][$key]['custom_attributes'] = $this->filterNotVisibleAttributes( + $customerData['addresses'][$key]['custom_attributes'] + ); + } } } return $customerData; } + /** + * Filter not visible on storefront custom attributes. + * + * @param array $attributes + * @return array + */ + private function filterNotVisibleAttributes(array $attributes) + { + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + unset($attributes[$attributeMetadata->getAttributeCode()]); + } + } + + return $attributes; + } + /** * Set additional customer address data * From 80fc4aa81a948ebf8031fc30a50612ca61f04fd5 Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Tue, 12 Jun 2018 11:26:32 +0300 Subject: [PATCH 11/20] MAGETWO-93715: [2.3] Delete action in grid could be sent multiple times --- .../Adminhtml/Product/MassDelete.php | 2 +- .../Ui/Component/MassAction/Filter.php | 14 +- .../Adminhtml/Index/MassAssignGroupTest.php | 85 +++++++---- .../Adminhtml/Index/MassDeleteTest.php | 137 +++++++++++++++--- .../Adminhtml/Index/MassSubscribeTest.php | 74 +++++++--- .../_files/five_repository_customers.php | 49 +++++++ .../five_repository_customers_rollback.php | 29 ++++ 7 files changed, 315 insertions(+), 75 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers_rollback.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php index f32c6edd57394..366356597fdfb 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php @@ -66,7 +66,7 @@ public function execute() } if ($productDeleted) { - $this->messageManager->addSuccessMessage( + $this->messageManager->addSuccess( __('A total of %1 record(s) have been deleted.', $productDeleted) ); } diff --git a/app/code/Magento/Ui/Component/MassAction/Filter.php b/app/code/Magento/Ui/Component/MassAction/Filter.php index 6877303f5c491..a8ed5d901d860 100644 --- a/app/code/Magento/Ui/Component/MassAction/Filter.php +++ b/app/code/Magento/Ui/Component/MassAction/Filter.php @@ -99,14 +99,12 @@ public function getCollection(AbstractDb $collection) throw new LocalizedException(__('An item needs to be selected. Select and try again.')); } } - /** @var \Magento\Customer\Model\ResourceModel\Customer\Collection $collection */ - $idsArray = $this->getFilterIds(); - if (!empty($idsArray)) { - $collection->addFieldToFilter( - $collection->getIdFieldName(), - ['in' => $idsArray] - ); - } + + $collection->addFieldToFilter( + $collection->getIdFieldName(), + ['in' => $this->getFilterIds()] + ); + return $collection; } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php index ef5b4cae5ff16..8ee12fa1aa8d5 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -3,16 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Controller\Adminhtml\Index; -use Magento\TestFramework\Helper\Bootstrap; +use Magento\Backend\Model\Session; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; /** * @magentoAppArea adminhtml */ -class MassAssignGroupTest extends \Magento\TestFramework\TestCase\AbstractBackendController +class MassAssignGroupTest extends AbstractBackendController { /** * Base controller URL @@ -29,9 +34,7 @@ class MassAssignGroupTest extends \Magento\TestFramework\TestCase\AbstractBacken protected function setUp() { parent::setUp(); - $this->customerRepository = Bootstrap::getObjectManager()->get( - \Magento\Customer\Api\CustomerRepositoryInterface::class - ); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); } protected function tearDown() @@ -39,75 +42,97 @@ protected function tearDown() /** * Unset customer data */ - Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->setCustomerData(null); + Bootstrap::getObjectManager()->get(Session::class)->setCustomerData(null); /** * Unset messages */ - Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->getMessages(true); + Bootstrap::getObjectManager()->get(Session::class)->getMessages(true); } /** - * @magentoDataFixture Magento/Customer/_files/customer.php + * Tests os update a single customer record. + * + * @magentoDataFixture Magento/Customer/_files/five_repository_customers.php + * @magentoDbIsolation disabled */ public function testMassAssignGroupAction() { - $customer = $this->customerRepository->getById(1); + $customerEmail = 'customer1@example.com'; + /** @var CustomerInterface $customer */ + $customer = $this->customerRepository->get($customerEmail); $this->assertEquals(1, $customer->getGroupId()); - $this->getRequest() - ->setParam('group', 0) - ->setPostValue('namespace', 'customer_listing') - ->setPostValue('selected', [1]); + $params = [ + 'group' => 0, + 'namespace' => 'customer_listing', + 'selected' => [$customer->getId()] + ]; + + $this->getRequest()->setParams($params); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( - $this->equalTo(['A total of 1 record(s) were updated.']), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + self::equalTo(['A total of 1 record(s) were updated.']), + MessageInterface::TYPE_SUCCESS ); $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl)); - $customer = $this->customerRepository->getById(1); + $customer = $this->customerRepository->get($customerEmail); $this->assertEquals(0, $customer->getGroupId()); } /** - * @magentoDataFixture Magento/Customer/_files/twenty_one_customers.php + * Tests os update a multiple customer records. + * + * @magentoDataFixture Magento/Customer/_files/five_repository_customers.php + * @magentoDbIsolation disabled */ public function testLargeGroupMassAssignGroupAction() { - - for ($i = 1; $i < 22; $i++) { - $customer = $this->customerRepository->getById($i); + $ids = []; + for ($i = 1; $i <= 5; $i++) { + /** @var CustomerInterface $customer */ + $customer = $this->customerRepository->get('customer' . $i . '@example.com'); $this->assertEquals(1, $customer->getGroupId()); + $ids[] = $customer->getId(); } - $this->getRequest() - ->setParam('group', 0) - ->setPostValue('namespace', 'customer_listing') - ->setPostValue('selected', [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); + $params = [ + 'group' => 0, + 'namespace' => 'customer_listing', + 'selected' => $ids, + ]; + + $this->getRequest()->setParams($params); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( - $this->equalTo(['A total of 21 record(s) were updated.']), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + self::equalTo(['A total of 5 record(s) were updated.']), + MessageInterface::TYPE_SUCCESS ); $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl)); - for ($i = 1; $i < 22; $i++) { - $customer = $this->customerRepository->getById($i); + for ($i = 1; $i < 5; $i++) { + /** @var CustomerInterface $customer */ + $customer = $this->customerRepository->get('customer' . $i . '@example.com'); $this->assertEquals(0, $customer->getGroupId()); } } /** * Valid group Id but no customer Ids specified + * * @magentoDbIsolation enabled */ public function testMassAssignGroupActionNoCustomerIds() { - $this->getRequest()->setParam('group', 0)->setPostValue('namespace', 'customer_listing'); + $params = [ + 'group' => 0, + 'namespace' => 'customer_listing', + ]; + $this->getRequest()->setParams($params); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( $this->equalTo(['An item needs to be selected. Select and try again.']), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR + MessageInterface::TYPE_ERROR ); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php index b7aefe7c31707..2a916c1c00c66 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php @@ -3,61 +3,162 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Backend\Model\Session; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use PHPUnit\Framework\Constraint\Constraint; +use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; /** * @magentoAppArea adminhtml */ -class MassDeleteTest extends \Magento\TestFramework\TestCase\AbstractBackendController +class MassDeleteTest extends AbstractBackendController { + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + /** * Base controller URL * * @var string */ - protected $baseControllerUrl = 'http://localhost/index.php/backend/customer/index/index'; + private $baseControllerUrl = 'http://localhost/index.php/backend/customer/index/index'; + + protected function setUp() + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + } protected function tearDown() { /** * Unset customer data */ - Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->setCustomerData(null); + Bootstrap::getObjectManager()->get(Session::class)->setCustomerData(null); /** * Unset messages */ - Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->getMessages(true); + Bootstrap::getObjectManager()->get(Session::class)->getMessages(true); } /** - * @magentoDataFixture Magento/Customer/_files/customer.php + * Validates failure attempts to delete customers from grid. + * + * @param array|null $ids + * @param Constraint $constraint + * @param string|null $messageType + * @magentoDataFixture Magento/Customer/_files/five_repository_customers.php + * @magentoDbIsolation disabled + * @dataProvider failedRequestDataProvider */ - public function testMassDeleteAction() + public function testFailedMassDeleteAction($ids, Constraint $constraint, $messageType) { - $this->getRequest()->setPostValue('selected', [1])->setPostValue('namespace', 'customer_listing'); - $this->dispatch('backend/customer/index/massDelete'); - $this->assertSessionMessages( - $this->equalTo(['A total of 1 record(s) were deleted.']), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + $this->massDeleteAssertions($ids, $constraint, $messageType); + } + + /** + * Validates success attempt to delete customer from grid. + * + * @param array $emails + * @param Constraint $constraint + * @param string $messageType + * @magentoDataFixture Magento/Customer/_files/five_repository_customers.php + * @magentoDbIsolation disabled + * @dataProvider successRequestDataProvider + */ + public function testSuccessMassDeleteAction(array $emails, Constraint $constraint, string $messageType) + { + $ids = []; + foreach ($emails as $email) { + /** @var CustomerInterface $customer */ + $customer = $this->customerRepository->get($email); + $ids[] = $customer->getId(); + } + + $this->massDeleteAssertions( + $ids, + $constraint, + $messageType ); - $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl)); } /** - * Valid group Id but no customer Ids specified - * @magentoDbIsolation enabled + * Performs required request and assertions. + * + * @param array|null $ids + * @param Constraint $constraint + * @param string|null $messageType */ - public function testMassDeleteActionNoCustomerIds() + private function massDeleteAssertions($ids, Constraint $constraint, $messageType) { - $this->getRequest()->setPostValue('namespace', 'customer_listing'); + $requestData = [ + 'selected' => $ids, + 'namespace' => 'customer_listing', + ]; + + $this->getRequest()->setParams($requestData); $this->dispatch('backend/customer/index/massDelete'); $this->assertSessionMessages( - $this->equalTo(['An item needs to be selected. Select and try again.']), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR + $constraint, + $messageType ); + $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl)); + } + + /** + * Provides sets of data for unsuccessful attempts. + * + * @return array + */ + public function failedRequestDataProvider(): array + { + return [ + [ + 'ids' => [], + 'constraint' => self::equalTo(['Please select item(s).']), + 'messageType' => MessageInterface::TYPE_ERROR, + ], + [ + 'ids' => [111], + 'constraint' => self::isEmpty(), + 'messageType' => null, + ], + [ + 'ids' => null, + 'constraint' => self::equalTo(['Please select item(s).']), + 'messageType' => MessageInterface::TYPE_ERROR, + ] + ]; + } + + /** + * Provides sets of data for successful attempts. + * + * @return array + */ + public function successRequestDataProvider(): array + { + return [ + [ + 'customerEmails' => ['customer1@example.com'], + 'constraint' => self::equalTo(['A total of 1 record(s) were deleted.']), + 'messageType' => MessageInterface::TYPE_SUCCESS, + ], + [ + 'customerEmails' => ['customer2@example.com', 'customer3@example.com'], + 'constraint' => self::equalTo(['A total of 2 record(s) were deleted.']), + 'messageType' => MessageInterface::TYPE_SUCCESS, + ], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php index d9880b2dc741a..c2fc7b1b58756 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassSubscribeTest.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Backend\Model\Session; +use Magento\Framework\Message\MessageInterface; use Magento\Newsletter\Model\Subscriber; +use Magento\Newsletter\Model\SubscriberFactory; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; /** * @magentoAppArea adminhtml @@ -26,58 +32,90 @@ protected function tearDown() /** * Unset customer data */ - Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->setCustomerData(null); + Bootstrap::getObjectManager()->get(Session::class)->setCustomerData(null); /** * Unset messages */ - Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->getMessages(true); + Bootstrap::getObjectManager()->get(Session::class)->getMessages(true); } /** - * @magentoDataFixture Magento/Customer/_files/two_customers.php + * Tests subscriber status of customers. + * + * @magentoDataFixture Magento/Customer/_files/five_repository_customers.php + * @magentoDbIsolation disabled */ public function testMassSubscriberAction() { - // Pre-condition - /** @var \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory */ - $subscriberFactory = Bootstrap::getObjectManager()->get(\Magento\Newsletter\Model\SubscriberFactory::class); - $this->assertNull($subscriberFactory->create()->loadByCustomerId(1)->getSubscriberStatus()); - $this->assertNull($subscriberFactory->create()->loadByCustomerId(2)->getSubscriberStatus()); - // Setup - $this->getRequest()->setPostValue('selected', [1, 2])->setPostValue('namespace', 'customer_listing'); + /** @var SubscriberFactory $subscriberFactory */ + $subscriberFactory = Bootstrap::getObjectManager()->get(SubscriberFactory::class); + $customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + + $this->assertNull( + $subscriberFactory->create() + ->loadByEmail('customer1@example.com') + ->getSubscriberStatus() + ); + $this->assertNull( + $subscriberFactory->create() + ->loadByEmail('customer2@example.com') + ->getSubscriberStatus() + ); + + /** @var CustomerInterface $customer1 */ + $customer1 = $customerRepository->get('customer1@example.com'); + /** @var CustomerInterface $customer2 */ + $customer2 = $customerRepository->get('customer2@example.com'); + + $params = [ + 'selected' => [ + $customer1->getId(), + $customer2->getId(), + ], + 'namespace' => 'customer_listing', + ]; + $this->getRequest()->setParams($params); - // Test $this->dispatch('backend/customer/index/massSubscribe'); // Assertions $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl)); $this->assertSessionMessages( - $this->equalTo(['A total of 2 record(s) were updated.']), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + self::equalTo(['A total of 2 record(s) were updated.']), + MessageInterface::TYPE_SUCCESS ); $this->assertEquals( Subscriber::STATUS_SUBSCRIBED, - $subscriberFactory->create()->loadByCustomerId(1)->getSubscriberStatus() + $subscriberFactory->create() + ->loadByEmail('customer1@example.com') + ->getSubscriberStatus() ); $this->assertEquals( Subscriber::STATUS_SUBSCRIBED, - $subscriberFactory->create()->loadByCustomerId(2)->getSubscriberStatus() + $subscriberFactory->create() + ->loadByEmail('customer2@example.com') + ->getSubscriberStatus() ); } /** + * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ public function testMassSubscriberActionNoSelection() { - $this->getRequest()->setPostValue('namespace', 'customer_listing'); + $params = [ + 'namespace' => 'customer_listing' + ]; + + $this->getRequest()->setParams($params); $this->dispatch('backend/customer/index/massSubscribe'); $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl)); $this->assertSessionMessages( - $this->equalTo(['An item needs to be selected. Select and try again.']), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR + self::equalTo(['An item needs to be selected. Select and try again.']), + MessageInterface::TYPE_ERROR ); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers.php b/dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers.php new file mode 100644 index 0000000000000..1722e471a5bbc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers.php @@ -0,0 +1,49 @@ +create(CustomerRepositoryInterface::class); +/** @var CustomerInterfaceFactory $customerFactory */ +$customerFactory = $objectManager->get(CustomerInterfaceFactory::class); + +for ($i = 1; $i <= 5; $i++) { + /** @var CustomerInterface $customer */ + $customer = $customerFactory->create(); + $customer->setFirstname('John') + ->setGroupId(1) + ->setLastname('Smith') + ->setWebsiteId(1) + ->setEmail('customer'.$i.'@example.com'); + try { + $customerRepository->save($customer, 'password'); + } catch (\Exception $e) { + } +} + +/** @var EavModelConfig $eavConfig */ +$eavConfig = $objectManager->get(EavModelConfig::class); +$eavConfig->clear(); + +/** @var IndexerRegistry $indexerRegistry */ +$indexerRegistry = $objectManager->create(IndexerRegistry::class); +/** @var IndexerInterface $indexer */ +$indexer = $indexerRegistry->get(Customer::CUSTOMER_GRID_INDEXER_ID); +try { + $indexer->reindexAll(); +} catch (\Exception $e) { +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers_rollback.php new file mode 100644 index 0000000000000..5272d9cdbf06d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/five_repository_customers_rollback.php @@ -0,0 +1,29 @@ +create(CustomerRepositoryInterface::class); + +for ($i = 1; $i <= 5; $i++) { + try { + /** @var CustomerInterface $customer */ + $customer = $customerRepository->get('customer'.$i.'@example.com'); + $customerRepository->delete($customer); + } catch (\Exception $e) { + } +} + +/** @var EavModelConfig $eavConfig */ +$eavConfig = $objectManager->get(EavModelConfig::class); +$eavConfig->clear(); From c840ade149c58049ffbd01ebee623aef1bb9d743 Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Thu, 2 Aug 2018 19:00:38 +0300 Subject: [PATCH 12/20] MAGETWO-93715: [2.3] Delete action in grid could be sent multiple times --- .../Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php index 366356597fdfb..f32c6edd57394 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php @@ -66,7 +66,7 @@ public function execute() } if ($productDeleted) { - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( __('A total of %1 record(s) have been deleted.', $productDeleted) ); } From f9be0622781af767396836abe71714c756724a1c Mon Sep 17 00:00:00 2001 From: Ievgen Sentiabov Date: Fri, 3 Aug 2018 10:09:35 +0300 Subject: [PATCH 13/20] MAGETWO-91337: Admin global search preview works inconsistently. - Added fulltext search by title in the CMS pages grid --- .../Cms/Ui/Component/AddFilterInterface.php | 26 ++++++++ .../Magento/Cms/Ui/Component/DataProvider.php | 24 ++++++- .../Cms/Ui/Component/Page/FulltextFilter.php | 44 +++++++++++++ app/code/Magento/Cms/etc/di.xml | 8 +++ .../Adminhtml/FulltextGridSearchTest.php | 66 +++++++++++++++++++ .../Magento/Cms/Fixtures/page_list.php | 34 ++++++++++ .../Cms/Fixtures/page_list_rollback.php | 25 +++++++ 7 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Cms/Ui/Component/AddFilterInterface.php create mode 100644 app/code/Magento/Cms/Ui/Component/Page/FulltextFilter.php create mode 100644 dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php create mode 100644 dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php diff --git a/app/code/Magento/Cms/Ui/Component/AddFilterInterface.php b/app/code/Magento/Cms/Ui/Component/AddFilterInterface.php new file mode 100644 index 0000000000000..406b40fbc1647 --- /dev/null +++ b/app/code/Magento/Cms/Ui/Component/AddFilterInterface.php @@ -0,0 +1,26 @@ +meta = array_replace_recursive($meta, $this->prepareMetadata()); + $this->additionalFilterPool = $additionalFilterPool; } /** @@ -95,4 +105,16 @@ public function prepareMetadata() return $metadata; } + + /** + * @inheritdoc + */ + public function addFilter(Filter $filter) + { + if (!empty($this->additionalFilterPool[$filter->getField()])) { + $this->additionalFilterPool[$filter->getField()]->addFilter($this->searchCriteriaBuilder, $filter); + } else { + parent::addFilter($filter); + } + } } diff --git a/app/code/Magento/Cms/Ui/Component/Page/FulltextFilter.php b/app/code/Magento/Cms/Ui/Component/Page/FulltextFilter.php new file mode 100644 index 0000000000000..9b0c69a4f10c4 --- /dev/null +++ b/app/code/Magento/Cms/Ui/Component/Page/FulltextFilter.php @@ -0,0 +1,44 @@ +filterBuilder = $filterBuilder; + } + + /** + * @inheritdoc + */ + public function addFilter(SearchCriteriaBuilder $searchCriteriaBuilder, Filter $filter) + { + $titleFilter = $this->filterBuilder->setField('title') + ->setValue(sprintf('%%%s%%', $filter->getValue())) + ->setConditionType('like') + ->create(); + $searchCriteriaBuilder->addFilter($titleFilter); + } +} diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 978d75c9b1e56..b6e13c63302cd 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -225,5 +225,13 @@ Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor + + + + + Magento\Cms\Ui\Component\Page\FulltextFilter + + + diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php new file mode 100644 index 0000000000000..c740609773b90 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/FulltextGridSearchTest.php @@ -0,0 +1,66 @@ +getRequest() + ->getHeaders() + ->addHeaderLine('Accept', 'application/json'); + $this->dispatch($url); + $response = $this->getResponse(); + $data = json_decode($response->getBody(), true); + self::assertEquals($expectedRows, $data['totalRecords']); + + $titleList = array_column($data['items'], 'title'); + self::assertEquals($expectedTitles, $titleList); + } + + /** + * Gets list of variations with different search queries. + * + * @return array + */ + public function queryDataProvider(): array + { + return [ + [ + 'query' => 'simple', + 'expectedRows' => 3, + 'expectedTitles' => ['simplePage', 'simplePage01', '01simplePage'] + ], + [ + 'query' => 'page01', + 'expectedRows' => 1, + 'expectedTitles' => ['simplePage01'] + ], + [ + 'query' => '01simple', + 'expectedRows' => 1, + 'expectedTitles' => ['01simplePage'] + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php new file mode 100644 index 0000000000000..ae431f5c4cf1a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php @@ -0,0 +1,34 @@ + 'simplePage', + 'is_active' => 1 + ], + [ + 'title' => 'simplePage01', + 'is_active' => 1 + ], + [ + 'title' => '01simplePage', + 'is_active' => 1 + ], +]; + +/** @var PageRepositoryInterface $pageRepository */ +$pageRepository = $objectManager->get(PageRepositoryInterface::class); +foreach ($data as $item) { + $page = $objectManager->create(PageInterface::class, ['data' => $item]); + $pageRepository->save($page); +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php new file mode 100644 index 0000000000000..261cdba589653 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php @@ -0,0 +1,25 @@ +get(PageRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('title', ['simplePage', 'simplePage01', '01simplePage'], 'in') + ->create(); +$result = $pageRepository->getList($searchCriteria); + +foreach ($result->getItems() as $item) { + $pageRepository->delete($item); +} From 92622905ba9de5b8aedc727904ead4c1177e81e1 Mon Sep 17 00:00:00 2001 From: Ievgen Sentiabov Date: Fri, 3 Aug 2018 10:12:38 +0300 Subject: [PATCH 14/20] MAGETWO-92951: [2.3][B2B] Unable to add to cart grouped product by SKU with enabled Shared Catalog - Added quote ID to request params --- .../Block/Adminhtml/Order/Create/Form.php | 1 + .../Block/Adminhtml/Order/Create/FormTest.php | 197 ++++++++++++ .../adminhtml/web/order/create/scripts.js | 1 + .../Block/Adminhtml/Order/Create/FormTest.php | 282 +++++++++++------- 4 files changed, 365 insertions(+), 116 deletions(-) create mode 100644 app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php index 37e8d8e784744..12a8270acfff0 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php @@ -189,6 +189,7 @@ public function getOrderDataJson() $data['shipping_method_reseted'] = !(bool)$this->getQuote()->getShippingAddress()->getShippingMethod(); $data['payment_method'] = $this->getQuote()->getPayment()->getMethod(); } + $data['quote_id'] = $this->_sessionQuote->getQuoteId(); return $this->_jsonEncoder->encode($data); } diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php new file mode 100644 index 0000000000000..4cbd30b1392fa --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php @@ -0,0 +1,197 @@ +getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteSession = $this->getMockBuilder(QuoteSession::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId', 'getQuoteId', 'getStoreId', 'getStore', 'getQuote']) + ->getMock(); + /** @var Create|MockObject $create */ + $create = $this->getMockBuilder(Create::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var PriceCurrencyInterface|MockObject $priceCurrency */ + $priceCurrency = $this->getMockForAbstractClass(PriceCurrencyInterface::class); + /** @var EncoderInterface|MockObject $encoder */ + $encoder = $this->getMockForAbstractClass(EncoderInterface::class); + $encoder->method('encode') + ->willReturnCallback(function ($param) { + return json_encode($param); + }); + /** @var FormFactory|MockObject $formFactory */ + $formFactory = $this->getMockBuilder(FormFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); + + $this->localeCurrency = $this->getMockForAbstractClass(CurrencyInterface::class); + /** @var Mapper|MockObject $addressMapper */ + $addressMapper = $this->getMockBuilder(Mapper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->block = new Form( + $context, + $this->quoteSession, + $create, + $priceCurrency, + $encoder, + $formFactory, + $this->customerRepository, + $this->localeCurrency, + $addressMapper + ); + } + + /** + * Checks if order contains all needed data. + */ + public function testGetOrderDataJson() + { + $customerId = 1; + $storeId = 1; + $quoteId = 2; + $expected = [ + 'customer_id' => $customerId, + 'addresses' => [], + 'store_id' => $storeId, + 'currency_symbol' => '$', + 'shipping_method_reseted' => false, + 'payment_method' => 'free', + 'quote_id' => $quoteId + ]; + + $this->quoteSession->method('getCustomerId') + ->willReturn($customerId); + $this->quoteSession->method('getStoreId') + ->willReturn($storeId); + $this->quoteSession->method('getQuoteId') + ->willReturn($quoteId); + + $customer = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $customer->method('getAddresses') + ->willReturn([]); + $this->customerRepository->method('getById') + ->with($customerId) + ->willReturn($customer); + + $this->withCurrencySymbol('$'); + + $this->withQuote(); + + self::assertEquals($expected, json_decode($this->block->getOrderDataJson(), true)); + } + + /** + * Configures mock object for currency. + * + * @param string $symbol + */ + private function withCurrencySymbol(string $symbol) + { + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->method('getCurrentCurrencyCode') + ->willReturn('USD'); + $this->quoteSession->method('getStore') + ->willReturn($store); + + $currency = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->getMock(); + $currency->method('getSymbol') + ->willReturn($symbol); + $this->localeCurrency->method('getCurrency') + ->with('USD') + ->willReturn($currency); + } + + /** + * Configures shipping and payment mock objects. + */ + private function withQuote() + { + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteSession->method('getQuote') + ->willReturn($quote); + + $address = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->getMock(); + $address->method('getShippingMethod') + ->willReturn('free'); + $quote->method('getShippingAddress') + ->willReturn($address); + + $payment = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->getMock(); + $payment->method('getMethod') + ->willReturn('free'); + $quote->method('getPayment') + ->willReturn($payment); + } +} diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index 62bd9e27c832d..fbf5f5c1023e3 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -21,6 +21,7 @@ define([ this.loadBaseUrl = false; this.customerId = data.customer_id ? data.customer_id : false; this.storeId = data.store_id ? data.store_id : false; + this.quoteId = data['quote_id'] ? data['quote_id'] : false; this.currencyId = false; this.currencySymbol = data.currency_symbol ? data.currency_symbol : ''; this.addresses = data.addresses ? data.addresses : $H({}); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php index b6df7eb05c03d..abdbab2c24d16 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/FormTest.php @@ -1,151 +1,201 @@ _objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $sessionMock = $this->getMockBuilder( - \Magento\Backend\Model\Session\Quote::class - )->disableOriginalConstructor()->setMethods( - ['getCustomerId', 'getQuote', 'getStoreId', 'getStore'] - )->getMock(); - $sessionMock->expects($this->any())->method('getCustomerId')->will($this->returnValue(1)); - - $quote = $this->_objectManager->create(\Magento\Quote\Model\Quote::class)->load(1); - $sessionMock->expects($this->any())->method('getQuote')->will($this->returnValue($quote)); - - $sessionMock->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); - - $storeMock = $this->getMockBuilder( - \Magento\Store\Model\Store::class - )->disableOriginalConstructor()->setMethods( - ['getCurrentCurrencyCode'] - )->getMock(); - $storeMock->expects($this->any())->method('getCurrentCurrencyCode')->will($this->returnValue('USD')); - $sessionMock->expects($this->any())->method('getStore')->will($this->returnValue($storeMock)); - - /** @var \Magento\Framework\View\LayoutInterface $layout */ - $layout = $this->_objectManager->get(\Magento\Framework\View\LayoutInterface::class); - $this->_orderCreateBlock = $layout->createBlock( - \Magento\Sales\Block\Adminhtml\Order\Create\Form::class, - 'order_create_block' . rand(), - ['sessionQuote' => $sessionMock] + $this->objectManager = Bootstrap::getObjectManager(); + + $this->session = $this->getMockBuilder(QuoteSession::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId', 'getQuote', 'getStoreId', 'getStore', 'getQuoteId']) + ->getMock(); + $this->session->method('getCustomerId') + ->willReturn(1); + + $this->session->method('getStoreId') + ->willReturn(1); + + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->setMethods(['getCurrentCurrencyCode']) + ->getMock(); + $store->method('getCurrentCurrencyCode') + ->willReturn('USD'); + $this->session->method('getStore') + ->willReturn($store); + + /** @var LayoutInterface $layout */ + $layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $layout->createBlock( + Form::class, + 'order_create_block' . mt_rand(), + ['sessionQuote' => $this->session] ); parent::setUp(); } /** + * Checks if all needed order's data is correctly returned to the form. + * * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Sales/_files/quote.php */ public function testOrderDataJson() { - /** @var array $addressIds */ - $addressIds = $this->setUpMockAddress(); - $orderDataJson = $this->_orderCreateBlock->getOrderDataJson(); - $expectedOrderDataJson = <<assertEquals(json_decode($expectedOrderDataJson), json_decode($orderDataJson)); + $customerId = 1; + $quote = $this->getQuote('test01'); + $this->session->method('getQuote') + ->willReturn($quote); + $this->session->method('getQuoteId') + ->willReturn($quote->getId()); + $addressData = $this->getAddressData(); + $addressIds = $this->setUpMockAddress($customerId, $addressData); + $expected = [ + 'customer_id' => $customerId, + 'addresses' => [ + $addressIds[0] => $addressData[0], + $addressIds[1] => $addressData[1] + ], + 'store_id' => 1, + 'currency_symbol' => '$', + 'shipping_method_reseted' => true, + 'payment_method' => 'checkmo', + 'quote_id' => $quote->getId() + ]; + + self::assertEquals($expected, json_decode($this->block->getOrderDataJson(), true)); } - private function setUpMockAddress() + /** + * Saves customer's addresses. + * + * @param int $customerId + * @param array $addressData + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function setUpMockAddress(int $customerId, array $addressData) { - /** @var \Magento\Customer\Api\Data\RegionInterfaceFactory $regionFactory */ - $regionFactory = $this->_objectManager->create(\Magento\Customer\Api\Data\RegionInterfaceFactory::class); - - /** @var \Magento\Customer\Api\Data\AddressInterfaceFactory $addressFactory */ - $addressFactory = $this->_objectManager->create(\Magento\Customer\Api\Data\AddressInterfaceFactory::class); - /** @var \Magento\Customer\Api\AddressRepositoryInterface $addressRepository */ - $addressRepository = $this->_objectManager->create(\Magento\Customer\Api\AddressRepositoryInterface::class); - - $addressData1 = $addressFactory->create()->setCountryId( - 'US' - )->setCustomerId( - 1 - )->setIsDefaultBilling( - true - )->setIsDefaultShipping( - true - )->setPostcode( - '75477' - )->setRegion( - $regionFactory->create()->setRegionCode('AL')->setRegion('Alabama')->setRegionId(1) - )->setStreet( - ['Green str, 67'] - )->setTelephone( - '3468676' - )->setCity( - 'CityM' - )->setFirstname( - 'John' - )->setLastname( - 'Smith' - ); - - $addressData2 = $addressFactory->create()->setCountryId( - 'US' - )->setCustomerId( - 1 - )->setIsDefaultBilling( - false - )->setIsDefaultShipping( - false - )->setPostcode( - '47676' - )->setRegion( - $regionFactory->create()->setRegionCode('AL')->setRegion('Alabama')->setRegionId(1) - )->setStreet( - ['Black str, 48'] - )->setCity( - 'CityX' - )->setTelephone( - '3234676' - )->setFirstname( - 'John' - )->setLastname( - 'Smith' - ); + /** @var RegionInterfaceFactory $regionFactory */ + $regionFactory = $this->objectManager->create(RegionInterfaceFactory::class); + /** @var AddressInterfaceFactory $addressFactory */ + $addressFactory = $this->objectManager->create(AddressInterfaceFactory::class); + /** @var AddressRepositoryInterface $addressRepository */ + $addressRepository = $this->objectManager->create(AddressRepositoryInterface::class); + $region = $regionFactory->create() + ->setRegionCode('AL') + ->setRegion('Alabama') + ->setRegionId(1); + + $ids = []; + foreach ($addressData as $data) { + $address = $addressFactory->create(['data' => $data]); + $address->setRegion($region) + ->setCustomerId($customerId) + ->setIsDefaultBilling(true) + ->setIsDefaultShipping(true); + $address = $addressRepository->save($address); + $ids[] = $address->getId(); + } + + return $ids; + } - $savedAddress1 = $addressRepository->save($addressData1); - $savedAddress2 = $addressRepository->save($addressData2); + /** + * Gets test address data. + * + * @return array + */ + private function getAddressData(): array + { + return [ + [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => false, + 'street' => 'Green str, 67', + 'city' => 'CityM', + 'country_id' => 'US', + 'region' => 'Alabama', + 'region_id' => 1, + 'postcode' => '75477', + 'telephone' => '3468676', + 'vat_id' => false + ], + [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => false, + 'street' => 'Black str, 48', + 'city' => 'CityX', + 'country_id' => 'US', + 'region' => 'Alabama', + 'region_id' => 1, + 'postcode' => '47676', + 'telephone' => '3234676', + 'vat_id' => false, + ] + ]; + } - return [$savedAddress1->getId(), $savedAddress2->getId()]; + /** + * Gets quote by ID. + * + * @param string $reservedOrderId + * @return Quote + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getQuote(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + /** @var CartRepositoryInterface $repository */ + $repository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); } } From 5a7a5e1b469fcaa2dd809dd8ba0e1f33d1be0c24 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Mon, 18 Jun 2018 12:24:57 +0300 Subject: [PATCH 15/20] MAGETWO-92953: Once Payflow Pro payment is Declined, customer cannot continue checkout --- .../view/frontend/templates/transparent/iframe.phtml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml index f1988f1ca86bb..c127371cb5f47 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml @@ -67,9 +67,10 @@ $params = $block->getParams(); 'jquery', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/action/place-order', - 'Magento_Checkout/js/action/redirect-on-success' + 'Magento_Checkout/js/action/redirect-on-success', + 'Magento_Checkout/js/model/full-screen-loader' ], - function($, quote, placeOrderAction, redirectOnSuccessAction) { + function($, quote, placeOrderAction, redirectOnSuccessAction, fullScreenLoader) { var parent = window.top; $(parent).trigger('clearTimeout'); @@ -79,6 +80,12 @@ $params = $block->getParams(); function () { redirectOnSuccessAction.execute(); } + ).fail( + function () { + var parent = window.top; + $(parent).trigger('clearTimeout'); + fullScreenLoader.stopLoader(); + } ); } ); From 5672712e97d1144a4582886096580d67d2b2617c Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Wed, 20 Jun 2018 15:41:00 +0300 Subject: [PATCH 16/20] MAGETWO-92950: [2.3] Extra XHR POST request made on every page --- .../web/js/product/storage/storage-service.js | 2 +- .../product/storage/storage-service.test.js | 77 +++++++++++++++---- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js index 014002bdc4af9..e571baa23e497 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js @@ -47,7 +47,7 @@ define([ * @param {*} data */ add: function (data) { - if (!utils.compare(data, this.data()).equal) { + if (!_.isEmpty(data) && !utils.compare(data, this.data()).equal) { this.data(_.extend(utils.copy(this.data()), data)); } }, diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/storage/storage-service.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/storage/storage-service.test.js index 4ef1aa2a98976..1a3a5726080bc 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/storage/storage-service.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/storage/storage-service.test.js @@ -12,22 +12,11 @@ define([ var injector = new Squire(), mocks = { - 'Magento_Catalog/js/product/storage/ids-storage': { - name: 'IdsStorage', - initialize: jasmine.createSpy().and.returnValue({}) - }, 'Magento_Catalog/js/product/storage/data-storage': {}, 'Magento_Catalog/js/product/storage/ids-storage-compare': {} }, - obj; - - beforeEach(function (done) { - injector.mock(mocks); - injector.require(['Magento_Catalog/js/product/storage/storage-service'], function (insance) { - obj = insance; - done(); - }); - }); + obj, + utils; afterEach(function () { try { @@ -43,6 +32,19 @@ define([ }, storage; + beforeEach(function (done) { + injector.mock(mocks); + injector.require([ + 'Magento_Catalog/js/product/storage/ids-storage', + 'Magento_Catalog/js/product/storage/storage-service', + 'mageUtils' + ], function (IdsStorage, instance, mageUtils) { + obj = instance; + utils = mageUtils; + done(); + }); + }); + describe('"createStorage" method', function () { it('create new storage', function () { obj.processSubscribers = jasmine.createSpy(); @@ -73,5 +75,54 @@ define([ expect(typeof obj.getStorage(config.namespace)).toBe('object'); }); }); + describe('"add" method', function () { + var storageValue; + + beforeEach(function () { + storage = new obj.createStorage(config); + storageValue = { + 'property1': 1 + }; + + storage.set(storageValue); + }); + + it('method exists', function () { + expect(storage.add).toBeDefined(); + expect(typeof storage.add).toEqual('function'); + }); + + it('update value', function () { + spyOn(utils, 'copy').and.callThrough(); + expect(storage.get()).toEqual(storageValue); + + storageValue = { + 'property2': 2 + }; + + storage.add(storageValue); + + expect(utils.copy).toHaveBeenCalled(); + expect(storage.get()).toEqual( + { + 'property1': 1, + 'property2': 2 + } + ); + }); + + it('add empty value', function () { + spyOn(utils, 'copy').and.callThrough(); + + storage.add({}); + + expect(utils.copy).not.toHaveBeenCalled(); + expect(storage.get()).toEqual( + { + 'property1': 1 + } + ); + }); + }); }); }); From 62520aff20f28fbc3a21844ea897dd37e051e048 Mon Sep 17 00:00:00 2001 From: Iurii Ivashchenko Date: Fri, 3 Aug 2018 13:57:49 +0300 Subject: [PATCH 17/20] MAGETWO-93715: [2.3] Delete action in grid could be sent multiple times --- .../Customer/Controller/Adminhtml/Index/MassDeleteTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php index 2a916c1c00c66..940f7c84325f6 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php @@ -125,7 +125,7 @@ public function failedRequestDataProvider(): array return [ [ 'ids' => [], - 'constraint' => self::equalTo(['Please select item(s).']), + 'constraint' => self::equalTo(['An item needs to be selected. Select and try again.']), 'messageType' => MessageInterface::TYPE_ERROR, ], [ @@ -135,7 +135,7 @@ public function failedRequestDataProvider(): array ], [ 'ids' => null, - 'constraint' => self::equalTo(['Please select item(s).']), + 'constraint' => self::equalTo(['An item needs to be selected. Select and try again.']), 'messageType' => MessageInterface::TYPE_ERROR, ] ]; From d9a3cf5606d7cf1666bdd0c8fbaec2b549ec653f Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Fri, 3 Aug 2018 19:16:04 +0300 Subject: [PATCH 18/20] MAGETWO-92953: [2.3] Once Payflow Pro payment is Declined, customer cannot continue checkout - allow POST request without form key on response controller --- .../Controller/Transparent/Response.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Paypal/Controller/Transparent/Response.php b/app/code/Magento/Paypal/Controller/Transparent/Response.php index c54dd529588b9..2764121232f5a 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/Response.php +++ b/app/code/Magento/Paypal/Controller/Transparent/Response.php @@ -5,6 +5,7 @@ */ namespace Magento\Paypal\Controller\Transparent; +use Magento\Framework\App\CsrfAwareActionInterface; use Magento\Framework\Registry; use Magento\Framework\App\Action\Context; use Magento\Framework\View\Result\LayoutFactory; @@ -16,11 +17,13 @@ use Magento\Paypal\Model\Payflow\Transparent; use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Framework\Session\Generic as Session; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Request\InvalidRequestException; /** * Class Response */ -class Response extends \Magento\Framework\App\Action\Action +class Response extends \Magento\Framework\App\Action\Action implements CsrfAwareActionInterface { /** * Core registry @@ -91,6 +94,23 @@ public function __construct( $this->paymentFailures = $paymentFailures ?: $this->_objectManager->get(PaymentFailuresInterface::class); } + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } + /** * @return ResultInterface */ From 0d7182794c80b807e1a13ca728f9ddb73b858011 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Fri, 3 Aug 2018 21:06:33 +0300 Subject: [PATCH 19/20] MAGETWO-92953: [2.3] Once Payflow Pro payment is Declined, customer cannot continue checkout --- app/code/Magento/Paypal/Controller/Transparent/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Paypal/Controller/Transparent/Response.php b/app/code/Magento/Paypal/Controller/Transparent/Response.php index 2764121232f5a..e16d2289df845 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/Response.php +++ b/app/code/Magento/Paypal/Controller/Transparent/Response.php @@ -21,7 +21,7 @@ use Magento\Framework\App\Request\InvalidRequestException; /** - * Class Response + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Response extends \Magento\Framework\App\Action\Action implements CsrfAwareActionInterface { From 574e1e7c43ccf74e9dfd23cace75abb1bfc0def8 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi Date: Tue, 7 Aug 2018 12:26:39 +0300 Subject: [PATCH 20/20] MAGETWO-92953: [2.3] Once Payflow Pro payment is Declined, customer cannot continue checkout - fix static --- app/code/Magento/Paypal/Controller/Transparent/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Paypal/Controller/Transparent/Response.php b/app/code/Magento/Paypal/Controller/Transparent/Response.php index e16d2289df845..67ec9afff2c44 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/Response.php +++ b/app/code/Magento/Paypal/Controller/Transparent/Response.php @@ -21,7 +21,7 @@ use Magento\Framework\App\Request\InvalidRequestException; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Response extends \Magento\Framework\App\Action\Action implements CsrfAwareActionInterface {