diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php index 0f10fd633cb5b..5476fd05a0fed 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost.php +++ b/app/code/Magento/Authorizenet/Model/Directpost.php @@ -5,10 +5,9 @@ */ namespace Magento\Authorizenet\Model; -use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\App\ObjectManager; use Magento\Payment\Model\Method\ConfigInterface; use Magento\Payment\Model\Method\TransparentInterface; -use Magento\Sales\Model\Order\Email\Sender\OrderSender; /** * Authorize.net DirectPost payment method model. @@ -102,7 +101,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra protected $response; /** - * @var OrderSender + * @var \Magento\Sales\Model\Order\Email\Sender\OrderSender */ protected $orderSender; @@ -123,6 +122,16 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra */ private $psrLogger; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + + /** + * @var \Magento\Sales\Model\Order + */ + private $order; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -134,18 +143,19 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra * @param \Magento\Framework\Module\ModuleListInterface $moduleList * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Authorizenet\Helper\Data $dataHelper - * @param Directpost\Request\Factory $requestFactory - * @param Directpost\Response\Factory $responseFactory + * @param \Magento\Authorizenet\Model\Directpost\Request\Factory $requestFactory + * @param \Magento\Authorizenet\Model\Directpost\Response\Factory $responseFactory * @param \Magento\Authorizenet\Model\TransactionService $transactionService * @param \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory * @param \Magento\Sales\Model\OrderFactory $orderFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository - * @param OrderSender $orderSender + * @param \Magento\Sales\Model\Order\Email\Sender\OrderSender $orderSender * @param \Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -161,8 +171,8 @@ public function __construct( \Magento\Authorizenet\Helper\Data $dataHelper, \Magento\Authorizenet\Model\Directpost\Request\Factory $requestFactory, \Magento\Authorizenet\Model\Directpost\Response\Factory $responseFactory, - TransactionService $transactionService, - ZendClientFactory $httpClientFactory, + \Magento\Authorizenet\Model\TransactionService $transactionService, + \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory, \Magento\Sales\Model\OrderFactory $orderFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Quote\Api\CartRepositoryInterface $quoteRepository, @@ -170,7 +180,8 @@ public function __construct( \Magento\Sales\Api\TransactionRepositoryInterface $transactionRepository, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { $this->orderFactory = $orderFactory; $this->storeManager = $storeManager; @@ -179,6 +190,8 @@ public function __construct( $this->orderSender = $orderSender; $this->transactionRepository = $transactionRepository; $this->_code = static::METHOD_CODE; + $this->paymentFailures = $paymentFailures ? : ObjectManager::getInstance() + ->get(\Magento\Sales\Api\PaymentFailuresInterface::class); parent::__construct( $context, @@ -561,13 +574,10 @@ public function process(array $responseData) $this->validateResponse(); $response = $this->getResponse(); - //operate with order - $orderIncrementId = $response->getXInvoiceNum(); $responseText = $this->dataHelper->wrapGatewayError($response->getXResponseReasonText()); $isError = false; - if ($orderIncrementId) { - /* @var $order \Magento\Sales\Model\Order */ - $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + if ($this->getOrderIncrementId()) { + $order = $this->getOrderFromResponse(); //check payment method $payment = $order->getPayment(); if (!$payment || $payment->getMethod() != $this->getCode()) { @@ -632,9 +642,10 @@ public function checkResponseCode() return true; case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: - throw new \Magento\Framework\Exception\LocalizedException( - $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()) - ); + $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()); + $order = $this->getOrderFromResponse(); + $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMessage); + throw new \Magento\Framework\Exception\LocalizedException($errorMessage); default: throw new \Magento\Framework\Exception\LocalizedException( __('There was a payment authorization error.') @@ -988,12 +999,40 @@ protected function getTransactionResponse($transactionId) private function getPsrLogger() { if (null === $this->psrLogger) { - $this->psrLogger = \Magento\Framework\App\ObjectManager::getInstance() + $this->psrLogger = ObjectManager::getInstance() ->get(\Psr\Log\LoggerInterface::class); } return $this->psrLogger; } + /** + * Fetch order by increment id from response. + * + * @return \Magento\Sales\Model\Order + */ + private function getOrderFromResponse(): \Magento\Sales\Model\Order + { + if (!$this->order) { + $this->order = $this->orderFactory->create(); + + if ($incrementId = $this->getOrderIncrementId()) { + $this->order = $this->order->loadByIncrementId($incrementId); + } + } + + return $this->order; + } + + /** + * Fetch order increment id from response. + * + * @return string + */ + private function getOrderIncrementId(): string + { + return $this->getResponse()->getXInvoiceNum(); + } + /** * Checks if filter action is Report Only. Transactions that trigger this filter are processed as normal, * but are also reported in the Merchant Interface as triggering this filter. diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php index dbb6ac8333c14..95c67f67852da 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/DirectpostTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Authorizenet\Test\Unit\Model; +use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Framework\Simplexml\Element; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Authorizenet\Model\Directpost; @@ -74,6 +75,14 @@ class DirectpostTest extends \PHPUnit\Framework\TestCase */ protected $requestFactory; + /** + * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentFailures; + + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) @@ -104,6 +113,12 @@ protected function setUp() ->setMethods(['getTransactionDetails']) ->getMock(); + $this->paymentFailures = $this->getMockBuilder( + PaymentFailuresInterface::class + ) + ->disableOriginalConstructor() + ->getMock(); + $this->requestFactory = $this->getRequestFactoryMock(); $httpClientFactoryMock = $this->getHttpClientFactoryMock(); @@ -117,7 +132,8 @@ protected function setUp() 'responseFactory' => $this->responseFactoryMock, 'transactionRepository' => $this->transactionRepositoryMock, 'transactionService' => $this->transactionServiceMock, - 'httpClientFactory' => $httpClientFactoryMock + 'httpClientFactory' => $httpClientFactoryMock, + 'paymentFailures' => $this->paymentFailures, ] ); } @@ -313,12 +329,16 @@ public function checkResponseCodeSuccessDataProvider() } /** - * @param bool $responseCode + * Checks response failures behaviour. + * + * @param int $responseCode + * @param int $failuresHandlerCalls + * @return void * * @expectedException \Magento\Framework\Exception\LocalizedException * @dataProvider checkResponseCodeFailureDataProvider */ - public function testCheckResponseCodeFailure($responseCode) + public function testCheckResponseCodeFailure(int $responseCode, int $failuresHandlerCalls): void { $reasonText = 'reason text'; @@ -333,18 +353,35 @@ public function testCheckResponseCodeFailure($responseCode) ->with($reasonText) ->willReturn(__('Gateway error: %1', $reasonText)); + $orderMock = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + + $orderMock->expects($this->exactly($failuresHandlerCalls)) + ->method('getQuoteId') + ->willReturn(1); + + $this->paymentFailures->expects($this->exactly($failuresHandlerCalls)) + ->method('handle') + ->with(1); + + $reflection = new \ReflectionClass($this->directpost); + $order = $reflection->getProperty('order'); + $order->setAccessible(true); + $order->setValue($this->directpost, $orderMock); + $this->directpost->checkResponseCode(); } /** * @return array */ - public function checkResponseCodeFailureDataProvider() + public function checkResponseCodeFailureDataProvider(): array { return [ - ['responseCode' => Directpost::RESPONSE_CODE_DECLINED], - ['responseCode' => Directpost::RESPONSE_CODE_ERROR], - ['responseCode' => 999999] + ['responseCode' => Directpost::RESPONSE_CODE_DECLINED, 1], + ['responseCode' => Directpost::RESPONSE_CODE_ERROR, 1], + ['responseCode' => 999999, 0], ]; } diff --git a/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php b/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php index 1d5057d83d6cf..f9fae8a469b1d 100644 --- a/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php +++ b/app/code/Magento/Braintree/Model/AvsEmsCodeMapper.php @@ -24,7 +24,7 @@ class AvsEmsCodeMapper implements PaymentVerificationInterface * * @var string */ - private static $unavailableCode = 'U'; + private static $unavailableCode = ''; /** * List of mapping AVS codes diff --git a/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php b/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php index 9b80a2237a8fb..c82634d36db31 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/AvsEmsCodeMapperTest.php @@ -84,11 +84,11 @@ public function testGetCodeWithException() public function getCodeDataProvider() { return [ - ['avsZip' => null, 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => null, 'avsStreet' => 'M', 'expected' => 'U'], - ['avsZip' => 'M', 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => 'M', 'avsStreet' => 'Unknown', 'expected' => 'U'], - ['avsZip' => 'I', 'avsStreet' => 'A', 'expected' => 'U'], + ['avsZip' => null, 'avsStreet' => null, 'expected' => ''], + ['avsZip' => null, 'avsStreet' => 'M', 'expected' => ''], + ['avsZip' => 'M', 'avsStreet' => null, 'expected' => ''], + ['avsZip' => 'M', 'avsStreet' => 'Unknown', 'expected' => ''], + ['avsZip' => 'I', 'avsStreet' => 'A', 'expected' => ''], ['avsZip' => 'M', 'avsStreet' => 'M', 'expected' => 'Y'], ['avsZip' => 'N', 'avsStreet' => 'M', 'expected' => 'A'], ['avsZip' => 'M', 'avsStreet' => 'N', 'expected' => 'Z'], diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index beb6f2b13bcfe..95339870b4d61 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeDefaultValueFilter; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks; use Magento\Catalog\Model\Product\Link\Resolver as LinkResolver; +use Magento\Catalog\Model\Product\LinkTypeProvider; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; @@ -81,7 +85,7 @@ class Helper private $dateTimeFilter; /** - * @var \Magento\Catalog\Model\Product\LinkTypeProvider + * @var LinkTypeProvider */ private $linkTypeProvider; @@ -99,10 +103,10 @@ class Helper * @param ProductLinks $productLinks * @param \Magento\Backend\Helper\Js $jsHelper * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter - * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory|null $customOptionFactory - * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory - * @param \Magento\Catalog\Api\ProductRepositoryInterface|null $productRepository - * @param \Magento\Catalog\Model\Product\LinkTypeProvider|null $linkTypeProvider + * @param CustomOptionFactory|null $customOptionFactory + * @param ProductLinkFactory |null $productLinkFactory + * @param ProductRepositoryInterface|null $productRepository + * @param LinkTypeProvider|null $linkTypeProvider * @param AttributeFilter|null $attributeFilter * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -113,10 +117,10 @@ public function __construct( \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $productLinks, \Magento\Backend\Helper\Js $jsHelper, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory = null, - \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository = null, - \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider = null, + CustomOptionFactory $customOptionFactory = null, + ProductLinkFactory $productLinkFactory = null, + ProductRepositoryInterface $productRepository = null, + LinkTypeProvider $linkTypeProvider = null, AttributeFilter $attributeFilter = null ) { $this->request = $request; @@ -125,16 +129,13 @@ public function __construct( $this->productLinks = $productLinks; $this->jsHelper = $jsHelper; $this->dateFilter = $dateFilter; - $this->customOptionFactory = $customOptionFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); - $this->productLinkFactory = $productLinkFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); - $this->productRepository = $productRepository ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->linkTypeProvider = $linkTypeProvider ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Product\LinkTypeProvider::class); - $this->attributeFilter = $attributeFilter ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(AttributeFilter::class); + + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $this->customOptionFactory = $customOptionFactory ?: $objectManager->get(CustomOptionFactory::class); + $this->productLinkFactory = $productLinkFactory ?: $objectManager->get(ProductLinkFactory::class); + $this->productRepository = $productRepository ?: $objectManager->get(ProductRepositoryInterface::class); + $this->linkTypeProvider = $linkTypeProvider ?: $objectManager->get(LinkTypeProvider::class); + $this->attributeFilter = $attributeFilter ?: $objectManager->get(AttributeFilter::class); } /** @@ -150,8 +151,7 @@ public function __construct( */ public function initializeFromData(\Magento\Catalog\Model\Product $product, array $productData) { - unset($productData['custom_attributes']); - unset($productData['extension_attributes']); + unset($productData['custom_attributes'], $productData['extension_attributes']); if ($productData) { $stockData = isset($productData['stock_data']) ? $productData['stock_data'] : []; @@ -199,28 +199,13 @@ public function initializeFromData(\Magento\Catalog\Model\Product $product, arra $productData['tier_price'] = isset($productData['tier_price']) ? $productData['tier_price'] : []; $useDefaults = (array)$this->request->getPost('use_default', []); - $productData = $this->attributeFilter->prepareProductAttributes($product, $productData, $useDefaults); - $product->addData($productData); if ($wasLockedMedia) { $product->lockAttribute('media'); } - /** - * Check "Use Default Value" checkboxes values - */ - foreach ($useDefaults as $attributeCode => $useDefaultState) { - if ($useDefaultState) { - $product->setData($attributeCode, null); - // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false - if ($product->hasData('use_config_' . $attributeCode)) { - $product->setData('use_config_' . $attributeCode, false); - } - } - } - $product = $this->setProductLinks($product); $product = $this->fillProductOptions($product, $productOptions); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php index ee3a6d491e92f..188b0b22f33bf 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php @@ -28,11 +28,17 @@ class AttributeFilter * @param array $useDefaults * @return array */ - public function prepareProductAttributes(Product $product, array $productData, array $useDefaults) + public function prepareProductAttributes(Product $product, array $productData, array $useDefaults): array { - foreach ($productData as $attribute => $value) { - if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attribute, $value)) { - unset($productData[$attribute]); + $attributeList = $product->getAttributes(); + foreach ($productData as $attributeCode => $attributeValue) { + if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attributeCode, $attributeValue)) { + unset($productData[$attributeCode]); + } + + if (isset($useDefaults[$attributeCode]) && $useDefaults[$attributeCode] === '1') { + $productData = $this->prepareDefaultData($attributeList, $attributeCode, $productData); + $productData = $this->prepareConfigData($product, $attributeCode, $productData); } } @@ -41,14 +47,54 @@ public function prepareProductAttributes(Product $product, array $productData, a /** * @param Product $product - * @param $useDefaults - * @param $attribute - * @param $value + * @param string $attributeCode + * @param array $productData + * @return array + */ + private function prepareConfigData(Product $product, string $attributeCode, array $productData): array + { + // UI component sends value even if field is disabled, so 'Use Config Settings' must be reset to false + if ($product->hasData('use_config_' . $attributeCode)) { + $productData['use_config_' . $attributeCode] = false; + } + + return $productData; + } + + /** + * @param array $attributeList + * @param string $attributeCode + * @param array $productData + * @return array + */ + private function prepareDefaultData(array $attributeList, string $attributeCode, array $productData): array + { + if (isset($attributeList[$attributeCode])) { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + $attribute = $attributeList[$attributeCode]; + $attributeType = $attribute->getBackendType(); + // For non-numberic types set the attributeValue to 'false' to trigger their removal from the db + if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { + $attribute->setIsRequired(false); + $productData[$attributeCode] = false; + } else { + $productData[$attributeCode] = null; + } + } + + return $productData; + } + + /** + * @param Product $product + * @param array $useDefaults + * @param string $attribute + * @param mixed $value * @return bool */ - private function isAttributeShouldNotBeUpdated(Product $product, $useDefaults, $attribute, $value) : bool + private function isAttributeShouldNotBeUpdated(Product $product, array $useDefaults, $attribute, $value): bool { - $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === "1"; + $considerUseDefaultsAttribute = !isset($useDefaults[$attribute]) || $useDefaults[$attribute] === '1'; return ($value === '' && $considerUseDefaultsAttribute && !$product->getData($attribute)); } diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 6a82658342824..03ddab3d44547 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -233,7 +233,8 @@ public function __construct( public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey([$editMode, $storeId]); - if (!isset($this->instances[$sku][$cacheKey]) || $forceReload) { + $cachedProduct = $this->getProductFromLocalCache($sku, $cacheKey); + if ($cachedProduct === null || $forceReload) { $product = $this->productFactory->create(); $productId = $this->resourceModel->getIdBySku($sku); @@ -250,11 +251,10 @@ public function get($sku, $editMode = false, $storeId = null, $forceReload = fal } $product->load($productId); $this->cacheProduct($cacheKey, $product); + $cachedProduct = $product; } - if (!isset($this->instances[$sku])) { - $sku = trim($sku); - } - return $this->instances[$sku][$cacheKey]; + + return $cachedProduct; } /** @@ -312,7 +312,7 @@ protected function getCacheKey($data) private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterface $product) { $this->instancesById[$product->getId()][$cacheKey] = $product; - $this->instances[$product->getSku()][$cacheKey] = $product; + $this->saveProductInLocalCache($product, $cacheKey); if ($this->cacheLimit && count($this->instances) > $this->cacheLimit) { $offset = round($this->cacheLimit / -2); @@ -338,7 +338,7 @@ protected function initializeProductData(array $productData, $createNew) $product->setWebsiteIds([$this->storeManager->getStore(true)->getWebsiteId()]); } } else { - unset($this->instances[$productData['sku']]); + $this->removeProductFromLocalCache($productData['sku']); $product = $this->get($productData['sku']); } @@ -613,7 +613,7 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO if ($tierPrices !== null) { $product->setData('tier_price', $tierPrices); } - unset($this->instances[$product->getSku()]); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); $this->resourceModel->save($product); } catch (ConnectionException $exception) { @@ -650,8 +650,9 @@ public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveO $e ); } - unset($this->instances[$product->getSku()]); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); + return $this->get($product->getSku(), false, $product->getStoreId()); } @@ -663,7 +664,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductInterface $product) $sku = $product->getSku(); $productId = $product->getId(); try { - unset($this->instances[$product->getSku()]); + $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); $this->resourceModel->delete($product); } catch (ValidatorException $e) { @@ -673,8 +674,9 @@ public function delete(\Magento\Catalog\Api\Data\ProductInterface $product) __('The "%1" product couldn\'t be removed.', $sku) ); } - unset($this->instances[$sku]); + $this->removeProductFromLocalCache($sku); unset($this->instancesById[$productId]); + return true; } @@ -796,4 +798,54 @@ private function getCollectionProcessor() } return $this->collectionProcessor; } + + /** + * Gets product from the local cache by SKU. + * + * @param string $sku + * @param string $cacheKey + * @return Product|null + */ + private function getProductFromLocalCache(string $sku, string $cacheKey) + { + $preparedSku = $this->prepareSku($sku); + + return $this->instances[$preparedSku][$cacheKey] ?? null; + } + + /** + * Removes product in the local cache. + * + * @param string $sku + * @return void + */ + private function removeProductFromLocalCache(string $sku) :void + { + $preparedSku = $this->prepareSku($sku); + unset($this->instances[$preparedSku]); + } + + /** + * Saves product in the local cache. + * + * @param Product $product + * @param string $cacheKey + * @return void + */ + private function saveProductInLocalCache(Product $product, string $cacheKey) : void + { + $preparedSku = $this->prepareSku($product->getSku()); + $this->instances[$preparedSku][$cacheKey] = $product; + } + + /** + * Converts SKU to lower case and trims. + * + * @param string $sku + * @return string + */ + private function prepareSku(string $sku): string + { + return mb_strtolower(trim($sku)); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php index 28617addc6d27..424427b871456 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class AttributeFilterTest extends \PHPUnit\Framework\TestCase { @@ -16,12 +19,12 @@ class AttributeFilterTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $objectManagerMock; /** - * @var Product|\PHPUnit_Framework_MockObject_MockObject + * @var Product|MockObject */ protected $productMock; @@ -44,15 +47,25 @@ public function testPrepareProductAttributes( $expectedProductData, $initialProductData ) { + /** @var MockObject | Product $productMockMap */ $productMockMap = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getData']) + ->setMethods(['getData', 'getAttributes']) ->getMock(); if (!empty($initialProductData)) { $productMockMap->expects($this->any())->method('getData')->willReturnMap($initialProductData); } + if ($useDefaults) { + $productMockMap + ->expects($this->once()) + ->method('getAttributes') + ->willReturn( + $this->getProductAttributesMock($useDefaults) + ); + } + $actualProductData = $this->model->prepareProductAttributes($productMockMap, $requestProductData, $useDefaults); $this->assertEquals($expectedProductData, $actualProductData); } @@ -69,15 +82,15 @@ public function setupInputDataProvider() 'name' => 'testName', 'sku' => 'testSku', 'price' => '100', - 'description' => '' + 'description' => '', ], 'useDefaults' => [], 'expectedProductData' => [ 'name' => 'testName', 'sku' => 'testSku', - 'price' => '100' + 'price' => '100', ], - 'initialProductData' => [] + 'initialProductData' => [], ], 'update_product_without_use_defaults' => [ 'productData' => [ @@ -85,21 +98,21 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => '', - 'special_price' => null + 'special_price' => null, ], 'useDefaults' => [], 'expectedProductData' => [ 'name' => 'testName2', 'sku' => 'testSku2', 'price' => '101', - 'special_price' => null + 'special_price' => null, ], 'initialProductData' => [ ['name', 'testName2'], ['sku', 'testSku2'], ['price', '101'], - ['special_price', null] - ] + ['special_price', null], + ], ], 'update_product_without_use_defaults_2' => [ 'productData' => [ @@ -107,7 +120,7 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => 'updated description', - 'special_price' => null + 'special_price' => null, ], 'useDefaults' => [], 'expectedProductData' => [ @@ -115,14 +128,14 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => 'updated description', - 'special_price' => null + 'special_price' => null, ], 'initialProductData' => [ ['name', 'testName2'], ['sku', 'testSku2'], ['price', '101'], - ['special_price', null] - ] + ['special_price', null], + ], ], 'update_product_with_use_defaults' => [ 'productData' => [ @@ -130,25 +143,25 @@ public function setupInputDataProvider() 'sku' => 'testSku2', 'price' => '101', 'description' => '', - 'special_price' => null + 'special_price' => null, ], 'useDefaults' => [ - 'description' => '0' + 'description' => '0', ], 'expectedProductData' => [ 'name' => 'testName2', 'sku' => 'testSku2', 'price' => '101', 'special_price' => null, - 'description' => '' + 'description' => '', ], 'initialProductData' => [ ['name', 'testName2'], ['sku', 'testSku2'], ['price', '101'], ['special_price', null], - ['description', 'descr text'] - ] + ['description', 'descr text'], + ], ], 'update_product_with_use_defaults_2' => [ 'requestProductData' => [ @@ -156,48 +169,73 @@ public function setupInputDataProvider() 'sku' => 'testSku3', 'price' => '103', 'description' => 'descr modified', - 'special_price' => '100' + 'special_price' => '100', ], 'useDefaults' => [ - 'description' => '0' + 'description' => '0', ], 'expectedProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', 'price' => '103', 'special_price' => '100', - 'description' => 'descr modified' + 'description' => 'descr modified', ], 'initialProductData' => [ - ['name', null,'testName2'], + ['name', null, 'testName2'], ['sku', null, 'testSku2'], ['price', null, '101'], - ['description', null, 'descr text'] - ] + ['description', null, 'descr text'], + ], ], 'update_product_with_use_defaults_3' => [ 'requestProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', 'price' => '103', - 'special_price' => '100' + 'special_price' => '100', + 'description' => 'descr modified', ], 'useDefaults' => [ - 'description' => '1' + 'description' => '1', ], 'expectedProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', 'price' => '103', 'special_price' => '100', + 'description' => false, ], 'initialProductData' => [ - ['name', null,'testName2'], + ['name', null, 'testName2'], ['sku', null, 'testSku2'], ['price', null, '101'], - ['description', null, 'descr text'] - ] + ['description', null, 'descr text'], + ], ], ]; } + + /** + * @param array $useDefaults + * @return array + */ + private function getProductAttributesMock(array $useDefaults): array + { + $returnArray = []; + foreach ($useDefaults as $attributecode => $isDefault) { + if ($isDefault === '1') { + /** @var Attribute | MockObject $attribute */ + $attribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + $attribute->expects($this->any()) + ->method('getBackendType') + ->willReturn('varchar'); + + $returnArray[$attributecode] = $attribute; + } + } + return $returnArray; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index a370cbea13c2b..bf5c3d8276295 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -381,11 +381,11 @@ public function testGetByIdAbsentProduct() public function testGetByIdProductInEditMode() { $productId = 123; - $this->productFactoryMock->expects($this->once())->method('create') - ->will($this->returnValue($this->productMock)); - $this->productMock->expects($this->once())->method('setData')->with('_edit_mode', true); - $this->productMock->expects($this->once())->method('load')->with($productId); + $this->productFactoryMock->method('create')->willReturn($this->productMock); + $this->productMock->method('setData')->with('_edit_mode', true); + $this->productMock->method('load')->with($productId); $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($productId, true)); } @@ -411,6 +411,7 @@ public function testGetByIdForCacheKeyGenerate($identifier, $editMode, $storeId) } $this->productMock->expects($this->once())->method('load')->with($identifier); $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($identifier); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); //Second invocation should just return from cache $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); @@ -433,6 +434,7 @@ public function testGetByIdForcedReload() $this->serializerMock->expects($this->exactly(3))->method('serialize'); $this->productMock->expects($this->exactly(4))->method('getId')->willReturn($identifier); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); //second invocation should just return from cache $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId)); @@ -532,6 +534,7 @@ public function testGetByIdWithSetStoreId() $this->productMock->expects($this->once())->method('setData')->with('store_id', $storeId); $this->productMock->expects($this->once())->method('load')->with($productId); $this->productMock->expects($this->atLeastOnce())->method('getId')->willReturn($productId); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->getById($productId, false, $storeId)); } @@ -585,7 +588,8 @@ public function testSaveNew() ->expects($this->once()) ->method('toNestedArray') ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->save($this->productMock)); } @@ -597,7 +601,8 @@ public function testSaveNew() public function testSaveUnableToSaveException() { $this->storeManagerMock->expects($this->any())->method('getWebsites')->willReturn([1 => 'default']); - $this->resourceModelMock->expects($this->exactly(1))->method('getIdBySku')->will($this->returnValue(null)); + $this->resourceModelMock->expects($this->exactly(1)) + ->method('getIdBySku')->willReturn(null); $this->productFactoryMock->expects($this->exactly(2)) ->method('create') ->will($this->returnValue($this->productMock)); @@ -610,7 +615,8 @@ public function testSaveUnableToSaveException() ->expects($this->once()) ->method('toNestedArray') ->will($this->returnValue($this->productData)); - $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -637,6 +643,7 @@ public function testSaveException() ->method('toNestedArray') ->will($this->returnValue($this->productData)); $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -661,6 +668,7 @@ public function testSaveInvalidProductException() ->method('toNestedArray') ->will($this->returnValue($this->productData)); $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -692,6 +700,7 @@ public function testSaveThrowsTemporaryStateExceptionIfDatabaseConnectionErrorOc $this->productMock->expects($this->once()) ->method('getWebsiteIds') ->willReturn([]); + $this->productMock->method('getSku')->willReturn('simple'); $this->model->save($this->productMock); } @@ -734,9 +743,8 @@ public function testGetList() { $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class); $collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); - $this->collectionFactoryMock->expects($this->once())->method('create')->willReturn($collectionMock); - + $this->productMock->method('getSku')->willReturn('simple'); $collectionMock->expects($this->once())->method('addAttributeToSelect')->with('*'); $collectionMock->expects($this->exactly(2))->method('joinAttribute')->withConsecutive( ['status', 'catalog_product/status', 'entity_id', null, 'inner'], @@ -1299,6 +1307,7 @@ public function testSaveWithDifferentWebsites() ]); $this->productMock->expects($this->once())->method('getWebsiteIds')->willReturn([1,2,3]); $this->productMock->expects($this->once())->method('setWebsiteIds')->willReturn([2,3]); + $this->productMock->method('getSku')->willReturn('simple'); $this->assertEquals($this->productMock, $this->model->save($this->productMock)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php index a29379647b9e1..0426e389d9aeb 100755 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -5,11 +5,10 @@ */ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav; use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface; use Magento\Framework\App\RequestInterface; -use Magento\Framework\EntityManager\EventManager; use Magento\Framework\Phrase; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Api\Data\StoreInterface; @@ -257,7 +256,15 @@ protected function setUp() $this->searchResultsMock = $this->getMockBuilder(SearchResultsInterface::class) ->getMockForAbstractClass(); $this->eavAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods(['load', 'getAttributeGroupCode', 'getApplyTo', 'getFrontendInput', 'getAttributeCode']) + ->setMethods([ + 'load', + 'getAttributeGroupCode', + 'getApplyTo', + 'getFrontendInput', + 'getAttributeCode', + 'usesSource', + 'getSource', + ]) ->disableOriginalConstructor() ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) @@ -451,64 +458,61 @@ public function testModifyData() } /** - * @param int $productId + * @param int|null $productId * @param bool $productRequired - * @param string $attrValue - * @param string $note + * @param string|null $attrValue * @param array $expected + * @return void * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::isProductExists * @covers \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav::setupAttributeMeta * @dataProvider setupAttributeMetaDataProvider */ - public function testSetupAttributeMetaDefaultAttribute($productId, $productRequired, $attrValue, $note, $expected) - { - $configPath = 'arguments/data/config'; + public function testSetupAttributeMetaDefaultAttribute( + $productId, + bool $productRequired, + $attrValue, + array $expected + ) : void { + $configPath = 'arguments/data/config'; $groupCode = 'product-details'; $sortOrder = '0'; + $attributeOptions = [ + ['value' => 1, 'label' => 'Int label'], + ['value' => 1.5, 'label' => 'Float label'], + ['value' => true, 'label' => 'Boolean label'], + ['value' => 'string', 'label' => 'String label'], + ['value' => ['test1', 'test2'], 'label' => 'Array label'], + ]; + $attributeOptionsExpected = [ + ['value' => '1', 'label' => 'Int label'], + ['value' => '1.5', 'label' => 'Float label'], + ['value' => '1', 'label' => 'Boolean label'], + ['value' => 'string', 'label' => 'String label'], + ['value' => ['test1', 'test2'], 'label' => 'Array label'], + ]; - $this->productMock->expects($this->any()) - ->method('getId') - ->willReturn($productId); - - $this->productAttributeMock->expects($this->any()) - ->method('getIsRequired') - ->willReturn($productRequired); - - $this->productAttributeMock->expects($this->any()) - ->method('getDefaultValue') - ->willReturn('required_value'); - - $this->productAttributeMock->expects($this->any()) - ->method('getAttributeCode') - ->willReturn('code'); - - $this->productAttributeMock->expects($this->any()) - ->method('getValue') - ->willReturn('value'); - - $this->productAttributeMock->expects($this->any()) - ->method('getNote') - ->willReturn($note); - - $this->productAttributeMock->expects($this->any()) - ->method('getDefaultFrontendLabel') - ->willReturn(new Phrase('mylabel')); + $this->productMock->method('getId')->willReturn($productId); + $this->productAttributeMock->method('getIsRequired')->willReturn($productRequired); + $this->productAttributeMock->method('getDefaultValue')->willReturn('required_value'); + $this->productAttributeMock->method('getAttributeCode')->willReturn('code'); + $this->productAttributeMock->method('getValue')->willReturn('value'); $attributeMock = $this->getMockBuilder(AttributeInterface::class) ->setMethods(['getValue']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $attributeMock->expects($this->any()) - ->method('getValue') - ->willReturn($attrValue); + $attributeMock->method('getValue')->willReturn($attrValue); - $this->productMock->expects($this->any()) - ->method('getCustomAttribute') - ->willReturn($attributeMock); + $this->productMock->method('getCustomAttribute')->willReturn($attributeMock); + $this->eavAttributeMock->method('usesSource')->willReturn(true); + + $attributeSource = $this->getMockBuilder(SourceInterface::class)->getMockForAbstractClass(); + $attributeSource->method('getAllOptions')->willReturn($attributeOptions); - $this->arrayManagerMock->expects($this->any()) - ->method('set') + $this->eavAttributeMock->method('getSource')->willReturn($attributeSource); + + $this->arrayManagerMock->method('set') ->with( $configPath, [], @@ -516,16 +520,21 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi ) ->willReturn($expected); - $this->arrayManagerMock->expects($this->any()) + $this->arrayManagerMock->expects($this->once()) ->method('merge') + ->with( + $this->anything(), + $this->anything(), + $this->callback( + function ($value) use ($attributeOptionsExpected) { + return $value['options'] === $attributeOptionsExpected; + } + ) + ) ->willReturn($expected); - $this->arrayManagerMock->expects($this->any()) - ->method('get') - ->willReturn([]); - - $this->arrayManagerMock->expects($this->any()) - ->method('exists'); + $this->arrayManagerMock->method('get')->willReturn([]); + $this->arrayManagerMock->method('exists')->willReturn(true); $this->assertEquals( $expected, @@ -539,147 +548,82 @@ public function testSetupAttributeMetaDefaultAttribute($productId, $productRequi public function setupAttributeMetaDataProvider() { return [ - 'default_null_prod_not_new_and_required' => $this->defaultNullProdNotNewAndRequired(), - 'default_null_prod_not_new_and_not_required' => $this->defaultNullProdNotNewAndNotRequired(), - 'default_null_prod_new_and_not_required' => $this->defaultNullProdNewAndNotRequired(), - 'default_null_prod_new_and_required' => $this->defaultNullProdNewAndRequired(), - 'default_null_prod_new_and_required_and_filled_notice' => - $this->defaultNullProdNewAndRequiredAndFilledNotice() - ]; - } - - /** - * @return array - */ - private function defaultNullProdNotNewAndRequired() - { - return [ - 'productId' => 1, - 'productRequired' => true, - 'attrValue' => 'val', - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => true, - 'notice' => null, - 'default' => null, - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 - ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNotNewAndNotRequired() - { - return [ - 'productId' => 1, - 'productRequired' => false, - 'attrValue' => 'val', - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => null, - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'default_null_prod_not_new_and_required' => [ + 'productId' => 1, + 'productRequired' => true, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => true, + 'notice' => null, + 'default' => null, + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNewAndNotRequired() - { - return [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => 'required_value', - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'default_null_prod_not_new_and_not_required' => [ + 'productId' => 1, + 'productRequired' => false, + 'attrValue' => 'val', + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => null, + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNewAndRequired() - { - return [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'note' => null, - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => null, - 'default' => 'required_value', - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 - ], - ]; - } - - /** - * @return array - */ - private function defaultNullProdNewAndRequiredAndFilledNotice() - { - return [ - 'productId' => null, - 'productRequired' => false, - 'attrValue' => null, - 'note' => 'example notice', - 'expected' => [ - 'dataType' => null, - 'formElement' => null, - 'visible' => null, - 'required' => false, - 'notice' => __('example notice'), - 'default' => 'required_value', - 'label' => new Phrase('mylabel'), - 'code' => 'code', - 'source' => 'product-details', - 'scopeLabel' => '', - 'globalScope' => false, - 'sortOrder' => 0 + 'default_null_prod_new_and_not_required' => [ + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], ], + 'default_null_prod_new_and_required' => [ + 'productId' => null, + 'productRequired' => false, + 'attrValue' => null, + 'expected' => [ + 'dataType' => null, + 'formElement' => null, + 'visible' => null, + 'required' => false, + 'notice' => null, + 'default' => 'required_value', + 'label' => new Phrase(null), + 'code' => 'code', + 'source' => 'product-details', + 'scopeLabel' => '', + 'globalScope' => false, + 'sortOrder' => 0, + ], + ] ]; } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index b216ee8c9c547..0e6f17d761bc3 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -611,8 +611,9 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { + $options = $attributeModel->getSource()->getAllOptions(); $meta = $this->arrayManager->merge($configPath, $meta, [ - 'options' => $attributeModel->getSource()->getAllOptions(), + 'options' => $this->convertOptionsValueToString($options), ]); } @@ -683,6 +684,23 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute) return $attribute->getDefaultValue(); } + /** + * Convert options value to string. + * + * @param array $options + * @return array + */ + private function convertOptionsValueToString(array $options) : array + { + array_walk($options, function (&$value) { + if (isset($value['value']) && is_scalar($value['value'])) { + $value['value'] = (string)$value['value']; + } + }); + + return $options; + } + /** * @param ProductAttributeInterface $attribute * @param array $meta diff --git a/app/code/Magento/Checkout/Helper/Data.php b/app/code/Magento/Checkout/Helper/Data.php index b3c2e17e5d678..636d4aaca21f0 100644 --- a/app/code/Magento/Checkout/Helper/Data.php +++ b/app/code/Magento/Checkout/Helper/Data.php @@ -9,6 +9,7 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\Store\Model\Store; use Magento\Store\Model\ScopeInterface; +use Magento\Sales\Api\PaymentFailuresInterface; /** * Checkout default helper @@ -52,6 +53,11 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper */ protected $priceCurrency; + /** + * @var PaymentFailuresInterface + */ + private $paymentFailures; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -60,6 +66,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param PriceCurrencyInterface $priceCurrency + * @param PaymentFailuresInterface|null $paymentFailures * @codeCoverageIgnore */ public function __construct( @@ -69,7 +76,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder, \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, - PriceCurrencyInterface $priceCurrency + PriceCurrencyInterface $priceCurrency, + PaymentFailuresInterface $paymentFailures = null ) { $this->_storeManager = $storeManager; $this->_checkoutSession = $checkoutSession; @@ -77,6 +85,8 @@ public function __construct( $this->_transportBuilder = $transportBuilder; $this->inlineTranslation = $inlineTranslation; $this->priceCurrency = $priceCurrency; + $this->paymentFailures = $paymentFailures ? : \Magento\Framework\App\ObjectManager::getInstance() + ->get(PaymentFailuresInterface::class); parent::__construct($context); } @@ -202,126 +212,13 @@ public function getBaseSubtotalInclTax($item) * @param string $message * @param string $checkoutType * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function sendPaymentFailedEmail($checkout, $message, $checkoutType = 'onepage') - { - $this->inlineTranslation->suspend(); - - $template = $this->scopeConfig->getValue( - 'checkout/payment_failed/template', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - - $copyTo = $this->_getEmails('checkout/payment_failed/copy_to', $checkout->getStoreId()); - $copyMethod = $this->scopeConfig->getValue( - 'checkout/payment_failed/copy_method', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - $bcc = []; - if ($copyTo && $copyMethod == 'bcc') { - $bcc = $copyTo; - } - - $_receiver = $this->scopeConfig->getValue( - 'checkout/payment_failed/receiver', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ); - $sendTo = [ - [ - 'email' => $this->scopeConfig->getValue( - 'trans_email/ident_' . $_receiver . '/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ), - 'name' => $this->scopeConfig->getValue( - 'trans_email/ident_' . $_receiver . '/name', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ), - ], - ]; - - if ($copyTo && $copyMethod == 'copy') { - foreach ($copyTo as $email) { - $sendTo[] = ['email' => $email, 'name' => null]; - } - } - $shippingMethod = ''; - if ($shippingInfo = $checkout->getShippingAddress()->getShippingMethod()) { - $data = explode('_', $shippingInfo); - $shippingMethod = $data[0]; - } - - $paymentMethod = ''; - if ($paymentInfo = $checkout->getPayment()) { - $paymentMethod = $paymentInfo->getMethod(); - } - - $items = ''; - foreach ($checkout->getAllVisibleItems() as $_item) { - /* @var $_item \Magento\Quote\Model\Quote\Item */ - $items .= - $_item->getProduct()->getName() . ' x ' . $_item->getQty() . ' ' . $checkout->getStoreCurrencyCode() - . ' ' . $_item->getProduct()->getFinalPrice( - $_item->getQty() - ) . "\n"; - } - $total = $checkout->getStoreCurrencyCode() . ' ' . $checkout->getGrandTotal(); - - foreach ($sendTo as $recipient) { - $transport = $this->_transportBuilder->setTemplateIdentifier( - $template - )->setTemplateOptions( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => Store::DEFAULT_STORE_ID - ] - )->setTemplateVars( - [ - 'reason' => $message, - 'checkoutType' => $checkoutType, - 'dateAndTime' => $this->_localeDate->formatDateTime( - new \DateTime(), - \IntlDateFormatter::MEDIUM, - \IntlDateFormatter::MEDIUM - ), - 'customer' => $checkout->getCustomerFirstname() . ' ' . $checkout->getCustomerLastname(), - 'customerEmail' => $checkout->getCustomerEmail(), - 'billingAddress' => $checkout->getBillingAddress(), - 'shippingAddress' => $checkout->getShippingAddress(), - 'shippingMethod' => $this->scopeConfig->getValue( - 'carriers/' . $shippingMethod . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ), - 'paymentMethod' => $this->scopeConfig->getValue( - 'payment/' . $paymentMethod . '/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ), - 'items' => nl2br($items), - 'total' => $total, - ] - )->setFrom( - $this->scopeConfig->getValue( - 'checkout/payment_failed/identity', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $checkout->getStoreId() - ) - )->addTo( - $recipient['email'], - $recipient['name'] - )->addBcc( - $bcc - )->getTransport(); - - $transport->sendMessage(); - } - - $this->inlineTranslation->resume(); + public function sendPaymentFailedEmail( + \Magento\Quote\Model\Quote $checkout, + string $message, + string $checkoutType = 'onepage' + ): \Magento\Checkout\Helper\Data { + $this->paymentFailures->handle((int)$checkout->getId(), $message, $checkoutType); return $this; } diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index a17cf41585649..333226b7d216f 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Model; @@ -10,6 +11,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Model\Quote; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -135,13 +137,19 @@ public function savePaymentInformation( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + /** @var Quote $quote */ + $quote = $this->cartRepository->getActive($quoteIdMask->getQuoteId()); + if ($billingAddress) { $billingAddress->setEmail($email); - $this->billingAddressManagement->assign($cartId, $billingAddress); + $quote->removeAddress($quote->getBillingAddress()->getId()); + $quote->setBillingAddress($billingAddress); + $quote->setDataChanges(true); } else { - $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); - $this->cartRepository->getActive($quoteIdMask->getQuoteId())->getBillingAddress()->setEmail($email); + $quote->getBillingAddress()->setEmail($email); } + $this->limitShippingCarrier($quote); $this->paymentMethodManagement->set($cartId, $paymentMethod); return true; @@ -169,4 +177,22 @@ private function getLogger() } return $this->logger; } + + /** + * Limits shipping rates request by carrier from shipping address. + * + * @param Quote $quote + * + * @return void + * @see \Magento\Shipping\Model\Shipping::collectRates + */ + private function limitShippingCarrier(Quote $quote) : void + { + $shippingAddress = $quote->getShippingAddress(); + if ($shippingAddress && $shippingAddress->getShippingMethod()) { + $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); + $shippingCarrier = array_shift($shippingDataArray); + $shippingAddress->setLimitCarrier($shippingCarrier); + } + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php index c403156dc13e9..53132ffaa748b 100644 --- a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php @@ -6,8 +6,7 @@ namespace Magento\Checkout\Test\Unit\Helper; -use \Magento\Checkout\Helper\Data; - +use Magento\Checkout\Helper\Data; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\ScopeInterface; @@ -24,38 +23,36 @@ class DataTest extends \PHPUnit\Framework\TestCase /** * @var Data */ - private $_helper; + private $helper; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $_transportBuilder; + private $transportBuilder; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $_translator; + private $translator; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_checkoutSession; + private $checkoutSession; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_scopeConfig; + private $scopeConfig; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_collectionFactory; + private $eventManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @inheritdoc */ - protected $_eventManager; - protected function setUp() { $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -63,191 +60,88 @@ protected function setUp() $arguments = $objectManagerHelper->getConstructArguments($className); /** @var \Magento\Framework\App\Helper\Context $context */ $context = $arguments['context']; - $this->_translator = $arguments['inlineTranslation']; - $this->_eventManager = $context->getEventManager(); - $this->_scopeConfig = $context->getScopeConfig(); - $this->_scopeConfig->expects($this->any()) + $this->translator = $arguments['inlineTranslation']; + $this->eventManager = $context->getEventManager(); + $this->scopeConfig = $context->getScopeConfig(); + $this->scopeConfig->expects($this->any()) ->method('getValue') - ->will( - $this->returnValueMap( + ->willReturnMap( + [ + [ + 'checkout/payment_failed/template', + ScopeInterface::SCOPE_STORE, + 8, + 'fixture_email_template_payment_failed', + ], + [ + 'checkout/payment_failed/receiver', + ScopeInterface::SCOPE_STORE, + 8, + 'sysadmin', + ], + [ + 'trans_email/ident_sysadmin/email', + ScopeInterface::SCOPE_STORE, + 8, + 'sysadmin@example.com', + ], + [ + 'trans_email/ident_sysadmin/name', + ScopeInterface::SCOPE_STORE, + 8, + 'System Administrator', + ], + [ + 'checkout/payment_failed/identity', + ScopeInterface::SCOPE_STORE, + 8, + 'noreply@example.com', + ], + [ + 'carriers/ground/title', + ScopeInterface::SCOPE_STORE, + null, + 'Ground Shipping', + ], [ - [ - 'checkout/payment_failed/template', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'fixture_email_template_payment_failed' - ], - [ - 'checkout/payment_failed/receiver', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'sysadmin' - ], - [ - 'trans_email/ident_sysadmin/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'sysadmin@example.com' - ], - [ - 'trans_email/ident_sysadmin/name', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'System Administrator' - ], - [ - 'checkout/payment_failed/identity', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - 8, - 'noreply@example.com' - ], - [ - 'carriers/ground/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'Ground Shipping' - ], - [ - 'payment/fixture-payment-method/title', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'Check Money Order' - ], - [ - 'checkout/options/onepage_checkout_enabled', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - null, - 'One Page Checkout' - ] - ] - ) + 'payment/fixture-payment-method/title', + ScopeInterface::SCOPE_STORE, + null, + 'Check Money Order', + ], + [ + 'checkout/options/onepage_checkout_enabled', + ScopeInterface::SCOPE_STORE, + null, + 'One Page Checkout', + ], + ] ); - $this->_checkoutSession = $arguments['checkoutSession']; + $this->checkoutSession = $arguments['checkoutSession']; $arguments['localeDate']->expects($this->any()) ->method('formatDateTime') ->willReturn('Oct 02, 2013'); - $this->_transportBuilder = $arguments['transportBuilder']; + $this->transportBuilder = $arguments['transportBuilder']; $this->priceCurrency = $arguments['priceCurrency']; - $this->_helper = $objectManagerHelper->getObject($className, $arguments); + $this->helper = $objectManagerHelper->getObject($className, $arguments); } /** * @return void - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSendPaymentFailedEmail() { - $shippingAddress = new \Magento\Framework\DataObject(['shipping_method' => 'ground_transportation']); - $billingAddress = new \Magento\Framework\DataObject(['street' => 'Fixture St']); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateOptions' - )->with( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - ] - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateIdentifier' - )->with( - 'fixture_email_template_payment_failed' - )->will( - $this->returnSelf() - ); + $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $quoteMock->expects($this->any())->method('getId')->willReturn(1); - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setFrom' - )->with( - 'noreply@example.com' - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'addTo' - )->with( - 'sysadmin@example.com', - 'System Administrator' - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects( - $this->once() - )->method( - 'setTemplateVars' - )->with( - [ - 'reason' => 'test message', - 'checkoutType' => 'onepage', - 'dateAndTime' => 'Oct 02, 2013', - 'customer' => 'John Doe', - 'customerEmail' => 'john.doe@example.com', - 'billingAddress' => $billingAddress, - 'shippingAddress' => $shippingAddress, - 'shippingMethod' => 'Ground Shipping', - 'paymentMethod' => 'Check Money Order', - 'items' => "Product One x 2 USD 10
\nProduct Two x 3 USD 60
\n", - 'total' => 'USD 70' - ] - )->will( - $this->returnSelf() - ); - - $this->_transportBuilder->expects($this->once())->method('addBcc')->will($this->returnSelf()); - $this->_transportBuilder->expects( - $this->once() - )->method( - 'getTransport' - )->will( - $this->returnValue($this->createMock(\Magento\Framework\Mail\TransportInterface::class)) - ); - - $this->_translator->expects($this->at(1))->method('suspend'); - $this->_translator->expects($this->at(1))->method('resume'); - - $productOne = $this->createMock(\Magento\Catalog\Model\Product::class); - $productOne->expects($this->once())->method('getName')->will($this->returnValue('Product One')); - $productOne->expects($this->once())->method('getFinalPrice')->with(2)->will($this->returnValue(10)); - - $productTwo = $this->createMock(\Magento\Catalog\Model\Product::class); - $productTwo->expects($this->once())->method('getName')->will($this->returnValue('Product Two')); - $productTwo->expects($this->once())->method('getFinalPrice')->with(3)->will($this->returnValue(60)); - - $quote = new \Magento\Framework\DataObject( - [ - 'store_id' => 8, - 'store_currency_code' => 'USD', - 'grand_total' => 70, - 'customer_firstname' => 'John', - 'customer_lastname' => 'Doe', - 'customer_email' => 'john.doe@example.com', - 'billing_address' => $billingAddress, - 'shipping_address' => $shippingAddress, - 'payment' => new \Magento\Framework\DataObject(['method' => 'fixture-payment-method']), - 'all_visible_items' => [ - new \Magento\Framework\DataObject(['product' => $productOne, 'qty' => 2]), - new \Magento\Framework\DataObject(['product' => $productTwo, 'qty' => 3]) - ] - ] - ); - $this->assertSame($this->_helper, $this->_helper->sendPaymentFailedEmail($quote, 'test message')); + $this->assertSame($this->helper, $this->helper->sendPaymentFailedEmail($quoteMock, 'test message')); } /** @@ -255,14 +149,14 @@ public function testSendPaymentFailedEmail() */ public function testGetCheckout() { - $this->assertEquals($this->_checkoutSession, $this->_helper->getCheckout()); + $this->assertEquals($this->checkoutSession, $this->helper->getCheckout()); } public function testGetQuote() { $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->_checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); - $this->assertEquals($quoteMock, $this->_helper->getQuote()); + $this->checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); + $this->assertEquals($quoteMock, $this->helper->getQuote()); } public function testFormatPrice() @@ -270,26 +164,26 @@ public function testFormatPrice() $price = 5.5; $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $storeMock = $this->createPartialMock(\Magento\Store\Model\Store::class, ['formatPrice', '__wakeup']); - $this->_checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); + $this->checkoutSession->expects($this->once())->method('getQuote')->will($this->returnValue($quoteMock)); $quoteMock->expects($this->once())->method('getStore')->will($this->returnValue($storeMock)); $this->priceCurrency->expects($this->once())->method('format')->will($this->returnValue('5.5')); - $this->assertEquals('5.5', $this->_helper->formatPrice($price)); + $this->assertEquals('5.5', $this->helper->formatPrice($price)); } public function testConvertPrice() { $price = 5.5; $this->priceCurrency->expects($this->once())->method('convertAndFormat')->willReturn($price); - $this->assertEquals(5.5, $this->_helper->convertPrice($price)); + $this->assertEquals(5.5, $this->helper->convertPrice($price)); } public function testCanOnepageCheckout() { - $this->_scopeConfig->expects($this->once())->method('getValue')->with( + $this->scopeConfig->expects($this->once())->method('getValue')->with( 'checkout/options/onepage_checkout_enabled', 'store' )->will($this->returnValue(true)); - $this->assertTrue($this->_helper->canOnepageCheckout()); + $this->assertTrue($this->helper->canOnepageCheckout()); } public function testIsContextCheckout() @@ -310,18 +204,18 @@ public function testIsContextCheckout() public function testIsCustomerMustBeLogged() { - $this->_scopeConfig->expects($this->once())->method('isSetFlag')->with( + $this->scopeConfig->expects($this->once())->method('isSetFlag')->with( 'checkout/options/customer_must_be_logged', \Magento\Store\Model\ScopeInterface::SCOPE_STORE )->will($this->returnValue(true)); - $this->assertTrue($this->_helper->isCustomerMustBeLogged()); + $this->assertTrue($this->helper->isCustomerMustBeLogged()); } public function testGetPriceInclTax() { $itemMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getPriceInclTax']); $itemMock->expects($this->exactly(2))->method('getPriceInclTax')->will($this->returnValue(5.5)); - $this->assertEquals(5.5, $this->_helper->getPriceInclTax($itemMock)); + $this->assertEquals(5.5, $this->helper->getPriceInclTax($itemMock)); } public function testGetPriceInclTaxWithoutTax() @@ -362,7 +256,7 @@ public function testGetSubtotalInclTax() $expected = 5.5; $itemMock = $this->createPartialMock(\Magento\Framework\DataObject::class, ['getRowTotalInclTax']); $itemMock->expects($this->exactly(2))->method('getRowTotalInclTax')->will($this->returnValue($rowTotalInclTax)); - $this->assertEquals($expected, $this->_helper->getSubtotalInclTax($itemMock)); + $this->assertEquals($expected, $this->helper->getSubtotalInclTax($itemMock)); } public function testGetSubtotalInclTaxNegative() @@ -380,7 +274,7 @@ public function testGetSubtotalInclTaxNegative() $itemMock->expects($this->once()) ->method('getDiscountTaxCompensation')->will($this->returnValue($discountTaxCompensation)); $itemMock->expects($this->once())->method('getRowTotal')->will($this->returnValue($rowTotal)); - $this->assertEquals($expected, $this->_helper->getSubtotalInclTax($itemMock)); + $this->assertEquals($expected, $this->helper->getSubtotalInclTax($itemMock)); } public function testGetBasePriceInclTaxWithoutQty() @@ -427,7 +321,7 @@ public function testGetBaseSubtotalInclTax() $itemMock->expects($this->once())->method('getBaseTaxAmount'); $itemMock->expects($this->once())->method('getBaseDiscountTaxCompensation'); $itemMock->expects($this->once())->method('getBaseRowTotal'); - $this->_helper->getBaseSubtotalInclTax($itemMock); + $this->helper->getBaseSubtotalInclTax($itemMock); } public function testIsAllowedGuestCheckoutWithoutStore() @@ -435,9 +329,9 @@ public function testIsAllowedGuestCheckoutWithoutStore() $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $store = null; $quoteMock->expects($this->once())->method('getStoreId')->will($this->returnValue(1)); - $this->_scopeConfig->expects($this->once()) + $this->scopeConfig->expects($this->once()) ->method('isSetFlag') ->will($this->returnValue(true)); - $this->assertTrue($this->_helper->isAllowedGuestCheckout($quoteMock, $store)); + $this->assertTrue($this->helper->isAllowedGuestCheckout($quoteMock, $store)); } } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index da0de5d4f0a3d..853ae0157e64a 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Test\Unit\Model; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\QuoteIdMask; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -96,6 +100,7 @@ public function testSavePaymentInformationAndPlaceOrder() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); @@ -119,10 +124,6 @@ public function testSavePaymentInformationAndPlaceOrder() ->willReturn($adapterMockForCheckout); $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); $adapterMockForCheckout->expects($this->once())->method('commit'); - - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $this->cartManagementMock->expects($this->once())->method('placeOrder')->with($cartId)->willReturn($orderId); @@ -142,6 +143,7 @@ public function testSavePaymentInformationAndPlaceOrderException() $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) @@ -164,12 +166,9 @@ public function testSavePaymentInformationAndPlaceOrderException() ->willReturn($adapterMockForCheckout); $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); $adapterMockForCheckout->expects($this->once())->method('rollback'); - - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $exception = new \Exception(__('DB exception')); + $exception = new \Magento\Framework\Exception\CouldNotSaveException(__('DB exception')); $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); @@ -185,11 +184,9 @@ public function testSavePaymentInformation() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $this->assertTrue($this->model->savePaymentInformation($cartId, $email, $paymentMock, $billingAddressMock)); @@ -201,13 +198,13 @@ public function testSavePaymentInformationWithoutBillingAddress() $email = 'email@magento.com'; $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); $this->billingAddressManagementMock->expects($this->never())->method('assign'); $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $quoteIdMaskMock = $this->createPartialMock(\Magento\Quote\Model\QuoteIdMask::class, ['getQuoteId', 'load']); + $quoteIdMaskMock = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); $this->quoteIdMaskFactoryMock->expects($this->once())->method('create')->willReturn($quoteIdMaskMock); $quoteIdMaskMock->expects($this->once())->method('load')->with($cartId, 'masked_id')->willReturnSelf(); $quoteIdMaskMock->expects($this->once())->method('getQuoteId')->willReturn($cartId); @@ -228,6 +225,15 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() $paymentMock = $this->createMock(\Magento\Quote\Api\Data\PaymentInterface::class); $billingAddressMock = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); + $quoteMock = $this->createMock(Quote::class); + $quoteMock->method('getBillingAddress')->willReturn($billingAddressMock); + $this->cartRepositoryMock->method('getActive')->with($cartId)->willReturn($quoteMock); + + $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); + $this->quoteIdMaskFactoryMock->method('create')->willReturn($quoteIdMask); + $quoteIdMask->method('load')->with($cartId, 'masked_id')->willReturnSelf(); + $quoteIdMask->method('getQuoteId')->willReturn($cartId); + $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); $adapterMockForSales = $this->getMockBuilder(AdapterInterface::class) @@ -250,10 +256,7 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() ->willReturn($adapterMockForCheckout); $adapterMockForCheckout->expects($this->once())->method('beginTransaction'); $adapterMockForCheckout->expects($this->once())->method('rollback'); - - $this->billingAddressManagementMock->expects($this->once()) - ->method('assign') - ->with($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); $phrase = new \Magento\Framework\Phrase(__('DB exception')); $exception = new \Magento\Framework\Exception\LocalizedException($phrase); @@ -262,4 +265,57 @@ public function testSavePaymentInformationAndPlaceOrderWithLocalizedException() $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); } + + /** + * @param int $cartId + * @param \PHPUnit_Framework_MockObject_MockObject $billingAddressMock + * @return void + */ + private function getMockForAssignBillingAddress( + int $cartId, + \PHPUnit_Framework_MockObject_MockObject $billingAddressMock + ) : void { + $quoteIdMask = $this->createPartialMock(QuoteIdMask::class, ['getQuoteId', 'load']); + $this->quoteIdMaskFactoryMock->method('create') + ->willReturn($quoteIdMask); + $quoteIdMask->method('load') + ->with($cartId, 'masked_id') + ->willReturnSelf(); + $quoteIdMask->method('getQuoteId') + ->willReturn($cartId); + + $billingAddressId = 1; + $quote = $this->createMock(Quote::class); + $quoteBillingAddress = $this->createMock(Address::class); + $quoteShippingAddress = $this->createPartialMock( + Address::class, + ['setLimitCarrier', 'getShippingMethod'] + ); + $this->cartRepositoryMock->method('getActive') + ->with($cartId) + ->willReturn($quote); + $quote->expects($this->once()) + ->method('getBillingAddress') + ->willReturn($quoteBillingAddress); + $quote->expects($this->once()) + ->method('getShippingAddress') + ->willReturn($quoteShippingAddress); + $quoteBillingAddress->expects($this->once()) + ->method('getId') + ->willReturn($billingAddressId); + $quote->expects($this->once()) + ->method('removeAddress') + ->with($billingAddressId); + $quote->expects($this->once()) + ->method('setBillingAddress') + ->with($billingAddressMock); + $quote->expects($this->once()) + ->method('setDataChanges') + ->willReturnSelf(); + $quoteShippingAddress->method('getShippingMethod') + ->willReturn('flatrate_flatrate'); + $quoteShippingAddress->expects($this->once()) + ->method('setLimitCarrier') + ->with('flatrate'); + } } diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 5f695adc9f4b4..540565345bd9b 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -7,7 +7,6 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-catalog-inventory": "*", "magento/module-config": "*", diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js index c707792111c82..0f2b0f4e26869 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/step-navigator.js @@ -182,6 +182,15 @@ define([ }); }, + /** + * Sets window location hash. + * + * @param {String} hash + */ + setHash: function (hash) { + window.location.hash = hash; + }, + /** * Next step. */ @@ -199,7 +208,7 @@ define([ if (steps().length > activeIndex + 1) { code = steps()[activeIndex + 1].code; steps()[activeIndex + 1].isVisible(true); - window.location = window.checkoutConfig.checkoutUrl + '#' + code; + this.setHash(code); document.body.scrollTop = document.documentElement.scrollTop = 0; } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js index 72cf4e3d479c3..683a18d0e4ead 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js @@ -25,6 +25,11 @@ define([ initialize: function () { this._super(); window.addEventListener('hashchange', _.bind(stepNavigator.handleHash, stepNavigator)); + + if (!window.location.hash) { + stepNavigator.setHash(stepNavigator.steps().sort(stepNavigator.sortItems)[0].code); + } + stepNavigator.handleHash(); }, diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 7d0b271b9b137..4fc74fa695829 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -873,6 +873,8 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); + } catch (\UnexpectedValueException $e) { + $this->logger->error($e); } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index cfd1729e4e06e..9e3a16a307923 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -1875,4 +1875,105 @@ private function prepareDateTimeFactory() return $dateTime; } + + /** + * @return void + */ + public function testCreateAccountUnexpectedValueException(): void + { + $websiteId = 1; + $storeId = null; + $defaultStoreId = 1; + $customerId = 1; + $customerEmail = 'email@email.com'; + $newLinkToken = '2jh43j5h2345jh23lh452h345hfuzasd96ofu'; + $exception = new \UnexpectedValueException('Template file was not found'); + + $datetime = $this->prepareDateTimeFactory(); + + $address = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); + $address->expects($this->once()) + ->method('setCustomerId') + ->with($customerId); + $store = $this->createMock(\Magento\Store\Model\Store::class); + $store->expects($this->once()) + ->method('getId') + ->willReturn($defaultStoreId); + $website = $this->createMock(\Magento\Store\Model\Website::class); + $website->expects($this->atLeastOnce()) + ->method('getStoreIds') + ->willReturn([1, 2, 3]); + $website->expects($this->once()) + ->method('getDefaultStore') + ->willReturn($store); + $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $customer->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($customerId); + $customer->expects($this->atLeastOnce()) + ->method('getEmail') + ->willReturn($customerEmail); + $customer->expects($this->atLeastOnce()) + ->method('getWebsiteId') + ->willReturn($websiteId); + $customer->expects($this->atLeastOnce()) + ->method('getStoreId') + ->willReturn($storeId); + $customer->expects($this->once()) + ->method('setStoreId') + ->with($defaultStoreId); + $customer->expects($this->once()) + ->method('getAddresses') + ->willReturn([$address]); + $customer->expects($this->once()) + ->method('setAddresses') + ->with(null); + $this->customerRepository->expects($this->once()) + ->method('get') + ->with($customerEmail) + ->willReturn($customer); + $this->share->expects($this->once()) + ->method('isWebsiteScope') + ->willReturn(true); + $this->storeManager->expects($this->atLeastOnce()) + ->method('getWebsite') + ->with($websiteId) + ->willReturn($website); + $this->customerRepository->expects($this->atLeastOnce()) + ->method('save') + ->willReturn($customer); + $this->addressRepository->expects($this->atLeastOnce()) + ->method('save') + ->with($address); + $this->customerRepository->expects($this->once()) + ->method('getById') + ->with($customerId) + ->willReturn($customer); + $this->random->expects($this->once()) + ->method('getUniqueHash') + ->willReturn($newLinkToken); + $customerSecure = $this->createPartialMock( + \Magento\Customer\Model\Data\CustomerSecure::class, + ['setRpToken', 'setRpTokenCreatedAt', 'getPasswordHash'] + ); + $customerSecure->expects($this->any()) + ->method('setRpToken') + ->with($newLinkToken); + $customerSecure->expects($this->any()) + ->method('setRpTokenCreatedAt') + ->with($datetime) + ->willReturnSelf(); + $customerSecure->expects($this->any()) + ->method('getPasswordHash') + ->willReturn(null); + $this->customerRegistry->expects($this->atLeastOnce()) + ->method('retrieveSecureData') + ->willReturn($customerSecure); + $this->emailNotificationMock->expects($this->once()) + ->method('newAccount') + ->willThrowException($exception); + $this->logger->expects($this->once())->method('error')->with($exception); + + $this->accountManagement->createAccount($customer); + } } diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index 4830ecfbb74b3..a6ecdaf24ebbb 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -531,14 +531,13 @@ protected function cancelDesignConfig() * * @param string $templateId * @return $this - * @throws \Magento\Framework\Exception\MailException */ public function setForcedArea($templateId) { - if ($this->area) { - throw new \LogicException(__('The area is already set.')); + if ($this->area === null) { + $this->area = $this->emailConfig->getTemplateArea($templateId); } - $this->area = $this->emailConfig->getTemplateArea($templateId); + return $this; } diff --git a/app/code/Magento/Email/Model/Template/Config.php b/app/code/Magento/Email/Model/Template/Config.php index bdd9054e7969b..9b45f509d97e5 100644 --- a/app/code/Magento/Email/Model/Template/Config.php +++ b/app/code/Magento/Email/Model/Template/Config.php @@ -205,8 +205,9 @@ public function getTemplateFilename($templateId, $designParams = []) $designParams['module'] = $module; $file = $this->_getInfo($templateId, 'file'); + $filename = $this->getFilename($file, $designParams, $module); - return $this->viewFileSystem->getEmailTemplateFileName($file, $designParams, $module); + return $filename; } /** @@ -230,4 +231,26 @@ protected function _getInfo($templateId, $fieldName) } return $data[$templateId][$fieldName]; } + + /** + * Retrieve template file path. + * + * @param string $file + * @param array $designParams + * @param string $module + * + * @return string + * + * @throws \UnexpectedValueException + */ + private function getFilename(string $file, array $designParams, string $module): string + { + $filename = $this->viewFileSystem->getEmailTemplateFileName($file, $designParams, $module); + + if ($filename === false) { + throw new \UnexpectedValueException("Template file '{$file}' is not found."); + } + + return $filename; + } } diff --git a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php index 46f3fecfb8848..4f545360616c6 100644 --- a/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/AbstractTemplateTest.php @@ -117,10 +117,11 @@ protected function setUp() /** * Return the model under test with additional methods mocked. * - * @param $mockedMethods array + * @param array $mockedMethods + * @param array $data * @return \Magento\Email\Model\Template|\PHPUnit_Framework_MockObject_MockObject */ - protected function getModelMock(array $mockedMethods = []) + protected function getModelMock(array $mockedMethods = [], array $data = []) { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); return $this->getMockForAbstractClass( @@ -136,7 +137,8 @@ protected function getModelMock(array $mockedMethods = []) 'scopeConfig' => $this->scopeConfig, 'emailConfig' => $this->emailConfig, 'filterFactory' => $this->filterFactory, - 'templateFactory' => $this->templateFactory + 'templateFactory' => $this->templateFactory, + 'data' => $data, ] ), '', @@ -431,4 +433,33 @@ public function testGetDesignConfig() $expectedConfig = ['area' => 'test_area', 'store' => 2]; $this->assertEquals($expectedConfig, $model->getDesignConfig()->getData()); } + + /** + * @return void + */ + public function testSetForcedAreaWhenAreaIsNotSet(): void + { + $templateId = 'test_template'; + $model = $this->getModelMock([], ['area' => null]); + + $this->emailConfig->expects($this->once()) + ->method('getTemplateArea') + ->with($templateId); + + $model->setForcedArea($templateId); + } + + /** + * @return void + */ + public function testSetForcedAreaWhenAreaIsSet(): void + { + $templateId = 'test_template'; + $model = $this->getModelMock([], ['area' => 'frontend']); + + $this->emailConfig->expects($this->never()) + ->method('getTemplateArea'); + + $model->setForcedArea($templateId); + } } diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php index 47c3ac1e7e450..b396f2ede8977 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/ConfigTest.php @@ -272,6 +272,20 @@ public function testGetTemplateFilenameWithNoParams() $this->assertEquals('_files/Fixture/ModuleOne/view/frontend/email/one.html', $actualResult); } + /** + * @expectedException \UnexpectedValueException + * @expectedExceptionMessage Template file 'one.html' is not found + * @return void + */ + public function testGetTemplateFilenameWrongFileName(): void + { + $this->viewFileSystem->expects($this->once())->method('getEmailTemplateFileName') + ->with('one.html', $this->designParams, 'Fixture_ModuleOne') + ->willReturn(false); + + $this->model->getTemplateFilename('template_one', $this->designParams); + } + /** * @param string $getterMethod * @param $argument diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php index f0fce97da512a..055af4162d5f3 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php @@ -21,6 +21,11 @@ class PlaceOrder extends \Magento\Paypal\Controller\Express\AbstractExpress */ protected $agreementsValidator; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Customer\Model\Session $customerSession @@ -31,6 +36,8 @@ class PlaceOrder extends \Magento\Paypal\Controller\Express\AbstractExpress * @param \Magento\Framework\Url\Helper\Data $urlHelper * @param \Magento\Customer\Model\Url $customerUrl * @param \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -41,9 +48,9 @@ public function __construct( \Magento\Framework\Session\Generic $paypalSession, \Magento\Framework\Url\Helper\Data $urlHelper, \Magento\Customer\Model\Url $customerUrl, - \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator + \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator, + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { - $this->agreementsValidator = $agreementValidator; parent::__construct( $context, $customerSession, @@ -54,6 +61,11 @@ public function __construct( $urlHelper, $customerUrl ); + + $this->agreementsValidator = $agreementValidator; + $this->paymentFailures = $paymentFailures ? : $this->_objectManager->get( + \Magento\Sales\Api\PaymentFailuresInterface::class + ); } /** @@ -148,6 +160,8 @@ private function processException(\Exception $exception, string $message): void */ protected function _processPaypalApiError($exception) { + $this->paymentFailures->handle((int)$this->_getCheckoutSession()->getQuoteId(), $exception->getMessage()); + switch ($exception->getCode()) { case ApiProcessableException::API_MAX_PAYMENT_ATTEMPTS_EXCEEDED: case ApiProcessableException::API_TRANSACTION_EXPIRED: diff --git a/app/code/Magento/Paypal/Controller/Payflow.php b/app/code/Magento/Paypal/Controller/Payflow.php index ab21986bde3ba..78c0536e393ac 100644 --- a/app/code/Magento/Paypal/Controller/Payflow.php +++ b/app/code/Magento/Paypal/Controller/Payflow.php @@ -41,6 +41,11 @@ abstract class Payflow extends \Magento\Framework\App\Action\Action */ protected $_redirectBlockName = 'payflow.link.iframe'; + /** + * @var \Magento\Sales\Api\PaymentFailuresInterface + */ + private $paymentFailures; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Checkout\Model\Session $checkoutSession @@ -48,6 +53,7 @@ abstract class Payflow extends \Magento\Framework\App\Action\Action * @param \Magento\Paypal\Model\PayflowlinkFactory $payflowModelFactory * @param \Magento\Paypal\Helper\Checkout $checkoutHelper * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Sales\Api\PaymentFailuresInterface|null $paymentFailures */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -55,14 +61,19 @@ public function __construct( \Magento\Sales\Model\OrderFactory $orderFactory, \Magento\Paypal\Model\PayflowlinkFactory $payflowModelFactory, \Magento\Paypal\Helper\Checkout $checkoutHelper, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures = null ) { + parent::__construct($context); + $this->_checkoutSession = $checkoutSession; $this->_orderFactory = $orderFactory; $this->_logger = $logger; $this->_payflowModelFactory = $payflowModelFactory; $this->_checkoutHelper = $checkoutHelper; - parent::__construct($context); + $this->paymentFailures = $paymentFailures ?: $this->_objectManager->get( + \Magento\Sales\Api\PaymentFailuresInterface::class + ); } /** @@ -74,6 +85,10 @@ public function __construct( protected function _cancelPayment($errorMsg = '') { $errorMsg = trim(strip_tags($errorMsg)); + $order = $this->_checkoutSession->getLastRealOrder(); + if ($order->getId()) { + $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMsg); + } $gotoSection = false; $this->_checkoutHelper->cancelCurrentOrder($errorMsg); diff --git a/app/code/Magento/Paypal/Controller/Transparent/Response.php b/app/code/Magento/Paypal/Controller/Transparent/Response.php index 23ac20ca8c87b..c54dd529588b9 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/Response.php +++ b/app/code/Magento/Paypal/Controller/Transparent/Response.php @@ -14,6 +14,8 @@ use Magento\Paypal\Model\Payflow\Service\Response\Transaction; use Magento\Paypal\Model\Payflow\Service\Response\Validator\ResponseValidator; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Sales\Api\PaymentFailuresInterface; +use Magento\Framework\Session\Generic as Session; /** * Class Response @@ -47,6 +49,16 @@ class Response extends \Magento\Framework\App\Action\Action */ private $transparent; + /** + * @var PaymentFailuresInterface + */ + private $paymentFailures; + + /** + * @var Session + */ + private $sessionTransparent; + /** * Constructor * @@ -56,6 +68,8 @@ class Response extends \Magento\Framework\App\Action\Action * @param ResponseValidator $responseValidator * @param LayoutFactory $resultLayoutFactory * @param Transparent $transparent + * @param Session|null $sessionTransparent + * @param PaymentFailuresInterface|null $paymentFailures */ public function __construct( Context $context, @@ -63,7 +77,9 @@ public function __construct( Transaction $transaction, ResponseValidator $responseValidator, LayoutFactory $resultLayoutFactory, - Transparent $transparent + Transparent $transparent, + Session $sessionTransparent = null, + PaymentFailuresInterface $paymentFailures = null ) { parent::__construct($context); $this->coreRegistry = $coreRegistry; @@ -71,6 +87,8 @@ public function __construct( $this->responseValidator = $responseValidator; $this->resultLayoutFactory = $resultLayoutFactory; $this->transparent = $transparent; + $this->sessionTransparent = $sessionTransparent ?: $this->_objectManager->get(Session::class); + $this->paymentFailures = $paymentFailures ?: $this->_objectManager->get(PaymentFailuresInterface::class); } /** @@ -86,6 +104,7 @@ public function execute() } catch (LocalizedException $exception) { $parameters['error'] = true; $parameters['error_msg'] = $exception->getMessage(); + $this->paymentFailures->handle((int)$this->sessionTransparent->getQuoteId(), $parameters['error_msg']); } $this->coreRegistry->register(Iframe::REGISTRY_KEY, $parameters); diff --git a/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php b/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php index 661d1f3814a0b..1ec7f4832bcb2 100644 --- a/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php +++ b/app/code/Magento/Paypal/Model/Payflow/AvsEmsCodeMapper.php @@ -24,7 +24,7 @@ class AvsEmsCodeMapper implements PaymentVerificationInterface * * @var string */ - private static $unavailableCode = 'U'; + private static $unavailableCode = ''; /** * List of mapping AVS codes diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php index 9d215ca6cbe17..da5599984b701 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php @@ -11,7 +11,6 @@ use Magento\Paypal\Model\Payflow\Transparent; use Magento\Paypal\Model\Payflowpro; use Magento\Quote\Model\Quote; -use Magento\Sales\Model\Order\Payment; /** * Class SecureToken @@ -59,6 +58,7 @@ public function __construct( */ public function requestToken(Quote $quote) { + $this->transparent->setStore($quote->getStoreId()); $request = $this->transparent->buildBasicRequest(); $request->setTrxtype(Payflowpro::TRXTYPE_AUTH_ONLY); diff --git a/app/code/Magento/Paypal/Model/Payflowlink.php b/app/code/Magento/Paypal/Model/Payflowlink.php index 792309bd76cf9..1955ef3c67661 100644 --- a/app/code/Magento/Paypal/Model/Payflowlink.php +++ b/app/code/Magento/Paypal/Model/Payflowlink.php @@ -10,6 +10,7 @@ use Magento\Payment\Model\Method\ConfigInterfaceFactory; use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\OrderSender; /** @@ -239,11 +240,13 @@ public function initialize($paymentAction, $stateObject) case \Magento\Paypal\Model\Config::PAYMENT_ACTION_AUTH: case \Magento\Paypal\Model\Config::PAYMENT_ACTION_SALE: $payment = $this->getInfoInstance(); + /** @var Order $order */ $order = $payment->getOrder(); $order->setCanSendNewEmailFlag(false); $payment->setAmountAuthorized($order->getTotalDue()); $payment->setBaseAmountAuthorized($order->getBaseTotalDue()); $this->_generateSecureSilentPostHash($payment); + $this->setStore($order->getStoreId()); $request = $this->_buildTokenRequest($payment); $response = $this->postRequest($request, $this->getConfig()); $this->_processTokenErrors($response, $payment); diff --git a/app/code/Magento/Paypal/Model/Payflowpro.php b/app/code/Magento/Paypal/Model/Payflowpro.php index 125aa0f6e65a7..b5fdaf4ae9fd4 100644 --- a/app/code/Magento/Paypal/Model/Payflowpro.php +++ b/app/code/Magento/Paypal/Model/Payflowpro.php @@ -647,7 +647,7 @@ public function buildBasicRequest() * * @param DataObject $response * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Payment\Gateway\Command\CommandException * @throws \Magento\Framework\Exception\State\InvalidTransitionException */ public function processErrors(DataObject $response) @@ -659,9 +659,9 @@ public function processErrors(DataObject $response) } elseif ($response->getResultCode() != self::RESPONSE_CODE_APPROVED && $response->getResultCode() != self::RESPONSE_CODE_FRAUDSERVICE_FILTER ) { - throw new \Magento\Framework\Exception\LocalizedException(__($response->getRespmsg())); + throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); } elseif ($response->getOrigresult() == self::RESPONSE_CODE_DECLINED_BY_FILTER) { - throw new \Magento\Framework\Exception\LocalizedException(__($response->getRespmsg())); + throw new \Magento\Payment\Gateway\Command\CommandException(__($response->getRespmsg())); } } diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php index e25864bbc2f3c..bd4da25cb84d0 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Paypal\Test\Unit\Controller\Payflow; +use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Checkout\Block\Onepage\Success; use Magento\Checkout\Model\Session; use Magento\Framework\App\Action\Context; @@ -90,6 +91,11 @@ class ReturnUrlTest extends \PHPUnit\Framework\TestCase */ private $objectManager; + /** + * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentFailures; + /** * @inheritdoc */ @@ -138,6 +144,17 @@ protected function setUp() ->setMethods(['getLastRealOrderId', 'getLastRealOrder', 'restoreQuote']) ->getMock(); + $this->quote = $this->getMockBuilder(CartInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->context->expects($this->any())->method('getView')->willReturn($this->view); + $this->context->expects($this->any())->method('getRequest')->willReturn($this->request); + + $this->paymentFailures = $this->getMockBuilder(PaymentFailuresInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->method('getView') ->willReturn($this->view); $this->context->method('getRequest') @@ -148,6 +165,7 @@ protected function setUp() 'checkoutSession' => $this->checkoutSession, 'orderFactory' => $this->orderFactory, 'checkoutHelper' => $this->checkoutHelper, + 'paymentFailures' => $this->paymentFailures, ]); } @@ -321,6 +339,7 @@ public function testCheckAdvancedAcceptingByPaymentMethod() 'checkoutSession' => $this->checkoutSession, 'orderFactory' => $this->orderFactory, 'checkoutHelper' => $this->checkoutHelper, + 'paymentFailures' => $this->paymentFailures, ]); $returnUrl->execute(); diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php index a10d103860c65..acefebb779200 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/ResponseTest.php @@ -8,16 +8,16 @@ use Magento\Framework\App\Action\Context; use Magento\Framework\App\RequestInterface; use Magento\Framework\Registry; +use Magento\Framework\Session\Generic as Session; use Magento\Framework\View\Result\Layout; use Magento\Framework\View\Result\LayoutFactory; use Magento\Paypal\Controller\Transparent\Response; use Magento\Paypal\Model\Payflow\Service\Response\Transaction; use Magento\Paypal\Model\Payflow\Service\Response\Validator\ResponseValidator; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Sales\Api\PaymentFailuresInterface; /** - * Class ResponseTest - * * Test for class \Magento\Paypal\Controller\Transparent\Response * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -53,6 +53,19 @@ class ResponseTest extends \PHPUnit\Framework\TestCase */ private $payflowFacade; + /** + * @var PaymentFailuresInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentFailures; + + /** + * @var Session|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionTransparent; + + /** + * @inheritdoc + */ protected function setUp() { $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) @@ -97,6 +110,14 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods([]) ->getMock(); + $this->paymentFailures = $this->getMockBuilder(PaymentFailuresInterface::class) + ->disableOriginalConstructor() + ->setMethods(['handle']) + ->getMock(); + $this->sessionTransparent = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getQuoteId']) + ->getMock(); $this->object = new Response( $this->contextMock, @@ -104,7 +125,9 @@ protected function setUp() $this->transactionMock, $this->responseValidatorMock, $this->resultLayoutFactoryMock, - $this->payflowFacade + $this->payflowFacade, + $this->sessionTransparent, + $this->paymentFailures ); } @@ -131,6 +154,8 @@ public function testExecute() $this->resultLayoutMock->expects($this->once()) ->method('getLayout') ->willReturn($this->getLayoutMock()); + $this->paymentFailures->expects($this->never()) + ->method('handle'); $this->assertInstanceOf(\Magento\Framework\Controller\ResultInterface::class, $this->object->execute()); } @@ -156,6 +181,12 @@ public function testExecuteWithException() $this->resultLayoutMock->expects($this->once()) ->method('getLayout') ->willReturn($this->getLayoutMock()); + $this->sessionTransparent->method('getQuoteId') + ->willReturn(1); + $this->paymentFailures->expects($this->once()) + ->method('handle') + ->with(1) + ->willReturnSelf(); $this->assertInstanceOf(\Magento\Framework\Controller\ResultInterface::class, $this->object->execute()); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php index eb259043a2d4f..ea86a04206f7b 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/AvsEmsCodeMapperTest.php @@ -85,17 +85,17 @@ public function testGetCodeWithException() public function getCodeDataProvider() { return [ - ['avsZip' => null, 'avsStreet' => null, 'expected' => 'U'], - ['avsZip' => null, 'avsStreet' => 'Y', 'expected' => 'U'], - ['avsZip' => 'Y', 'avsStreet' => null, 'expected' => 'U'], + ['avsZip' => null, 'avsStreet' => null, 'expected' => ''], + ['avsZip' => null, 'avsStreet' => 'Y', 'expected' => ''], + ['avsZip' => 'Y', 'avsStreet' => null, 'expected' => ''], ['avsZip' => 'Y', 'avsStreet' => 'Y', 'expected' => 'Y'], ['avsZip' => 'N', 'avsStreet' => 'Y', 'expected' => 'A'], ['avsZip' => 'Y', 'avsStreet' => 'N', 'expected' => 'Z'], ['avsZip' => 'N', 'avsStreet' => 'N', 'expected' => 'N'], - ['avsZip' => 'X', 'avsStreet' => 'Y', 'expected' => 'U'], - ['avsZip' => 'N', 'avsStreet' => 'X', 'expected' => 'U'], - ['avsZip' => '', 'avsStreet' => 'Y', 'expected' => 'U'], - ['avsZip' => 'N', 'avsStreet' => '', 'expected' => 'U'] + ['avsZip' => 'X', 'avsStreet' => 'Y', 'expected' => ''], + ['avsZip' => 'N', 'avsStreet' => 'X', 'expected' => ''], + ['avsZip' => '', 'avsStreet' => 'Y', 'expected' => ''], + ['avsZip' => 'N', 'avsStreet' => '', 'expected' => ''] ]; } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php index d4a7db25cae89..d8e54ad28fcc8 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Request/SecureTokenTest.php @@ -10,6 +10,9 @@ use Magento\Framework\UrlInterface; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; +use Magento\Paypal\Model\PayflowConfig; +use Magento\Quote\Model\Quote; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Test class for \Magento\Paypal\Model\Payflow\Service\Request\SecureToken @@ -19,23 +22,26 @@ class SecureTokenTest extends \PHPUnit\Framework\TestCase /** * @var SecureToken */ - protected $model; + private $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject|Transparent + * @var Transparent|MockObject */ - protected $transparent; + private $transparent; /** - * @var \PHPUnit_Framework_MockObject_MockObject|Random + * @var Random|MockObject */ - protected $mathRandom; + private $mathRandom; /** - * @var \PHPUnit_Framework_MockObject_MockObject|UrlInterface + * @var UrlInterface|MockObject */ - protected $url; + private $url; + /** + * @inheritdoc + */ protected function setUp() { $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); @@ -52,11 +58,29 @@ protected function setUp() public function testRequestToken() { $request = new DataObject(); + $storeId = 1; $secureTokenID = 'Sdj46hDokds09c8k2klaGJdKLl032ekR'; + $response = new DataObject([ + 'result' => '0', + 'respmsg' => 'Approved', + 'securetoken' => '80IgSbabyj0CtBDWHZZeQN3', + 'securetokenid' => $secureTokenID, + 'result_code' => '0', + ]); + + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $quote->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); $this->transparent->expects($this->once()) ->method('buildBasicRequest') ->willReturn($request); + $this->transparent->expects($this->once()) + ->method('setStore') + ->with($storeId); $this->transparent->expects($this->once()) ->method('fillCustomerContacts'); $this->transparent->expects($this->once()) @@ -64,7 +88,7 @@ public function testRequestToken() ->willReturn($this->createMock(\Magento\Paypal\Model\PayflowConfig::class)); $this->transparent->expects($this->once()) ->method('postRequest') - ->willReturn(new DataObject()); + ->willReturn($response); $this->mathRandom->expects($this->once()) ->method('getUniqueHash') @@ -73,8 +97,6 @@ public function testRequestToken() $this->url->expects($this->exactly(3)) ->method('getUrl'); - $quote = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->model->requestToken($quote); $this->assertEquals($secureTokenID, $request->getSecuretokenid()); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php index 362615e965d1b..80c8194e07654 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php @@ -101,16 +101,20 @@ protected function setUp() public function testInitialize() { + $storeId = 1; $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->exactly(2)) + ->method('getStoreId') + ->willReturn($storeId); $this->infoInstance->expects($this->any()) ->method('getOrder') - ->will($this->returnValue($order)); + ->willReturn($order); $this->infoInstance->expects($this->any()) ->method('setAdditionalInformation') - ->will($this->returnSelf()); + ->willReturnSelf(); $this->paypalConfig->expects($this->once()) ->method('getBuildNotationCode') - ->will($this->returnValue('build notation code')); + ->willReturn('build notation code'); $response = new \Magento\Framework\DataObject( [ @@ -148,6 +152,7 @@ public function testInitialize() $stateObject = new \Magento\Framework\DataObject(); $this->model->initialize(\Magento\Paypal\Model\Config::PAYMENT_ACTION_AUTH, $stateObject); + self::assertEquals($storeId, $this->model->getStore(), '{Store} should be set'); } /** diff --git a/app/code/Magento/Quote/Model/PaymentMethodManagement.php b/app/code/Magento/Quote/Model/PaymentMethodManagement.php index 91d8fe4dbcffd..b6e4bcf5ccc8f 100644 --- a/app/code/Magento/Quote/Model/PaymentMethodManagement.php +++ b/app/code/Magento/Quote/Model/PaymentMethodManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Model; @@ -52,38 +53,37 @@ public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method) { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->get($cartId); - + $quote->setTotalsCollectedFlag(false); $method->setChecks([ \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_CHECKOUT, \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_COUNTRY, \Magento\Payment\Model\Method\AbstractMethod::CHECK_USE_FOR_CURRENCY, \Magento\Payment\Model\Method\AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX, ]); - $payment = $quote->getPayment(); - - $data = $method->getData(); - $payment->importData($data); if ($quote->isVirtual()) { - $quote->getBillingAddress()->setPaymentMethod($payment->getMethod()); + $address = $quote->getBillingAddress(); } else { + $address = $quote->getShippingAddress(); // check if shipping address is set - if ($quote->getShippingAddress()->getCountryId() === null) { + if ($address->getCountryId() === null) { throw new InvalidTransitionException( __('The shipping address is missing. Set the address and try again.') ); } - $quote->getShippingAddress()->setPaymentMethod($payment->getMethod()); - } - if (!$quote->isVirtual() && $quote->getShippingAddress()) { - $quote->getShippingAddress()->setCollectShippingRates(true); + $address->setCollectShippingRates(true); } + $paymentData = $method->getData(); + $payment = $quote->getPayment(); + $payment->importData($paymentData); + $address->setPaymentMethod($payment->getMethod()); + if (!$this->zeroTotalValidator->isApplicable($payment->getMethodInstance(), $quote)) { throw new InvalidTransitionException(__('The requested Payment Method is not available.')); } - $quote->setTotalsCollectedFlag(false)->collectTotals()->save(); + $quote->save(); return $quote->getPayment()->getId(); } diff --git a/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php index 68b077fcdb965..f18d1fa1b06e5 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/PaymentMethodManagementTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Test\Unit\Model; @@ -152,8 +153,8 @@ public function testSetVirtualProduct() ->with($paymentMethod) ->willReturnSelf(); - $quoteMock->expects($this->exactly(2))->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(true); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->expects($this->once())->method('isVirtual')->willReturn(true); $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); @@ -165,7 +166,6 @@ public function testSetVirtualProduct() ->willReturn(true); $quoteMock->expects($this->once())->method('setTotalsCollectedFlag')->with(false)->willReturnSelf(); - $quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf(); $quoteMock->expects($this->once())->method('save')->willReturnSelf(); $paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); @@ -218,9 +218,9 @@ public function testSetVirtualProductThrowsExceptionIfPaymentMethodNotAvailable( ->with($paymentMethod) ->willReturnSelf(); - $quoteMock->expects($this->once())->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(true); - $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->method('isVirtual')->willReturn(true); + $quoteMock->method('getBillingAddress')->willReturn($billingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); $paymentMock->expects($this->once())->method('getMethodInstance')->willReturn($methodInstance); @@ -268,17 +268,20 @@ public function testSetSimpleProduct() $shippingAddressMock = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, - ['getCountryId', 'setPaymentMethod'] + ['getCountryId', 'setPaymentMethod', 'setCollectShippingRates'] ); $shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(100); $shippingAddressMock->expects($this->once()) ->method('setPaymentMethod') ->with($paymentMethod) ->willReturnSelf(); + $shippingAddressMock->expects($this->once()) + ->method('setCollectShippingRates') + ->with(true); - $quoteMock->expects($this->exactly(2))->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->exactly(2))->method('isVirtual')->willReturn(false); - $quoteMock->expects($this->exactly(4))->method('getShippingAddress')->willReturn($shippingAddressMock); + $quoteMock->method('getPayment')->willReturn($paymentMock); + $quoteMock->method('isVirtual')->willReturn(false); + $quoteMock->method('getShippingAddress')->willReturn($shippingAddressMock); $methodInstance = $this->getMockForAbstractClass(\Magento\Payment\Model\MethodInterface::class); $paymentMock->expects($this->once())->method('getMethodInstance')->willReturn($methodInstance); @@ -289,7 +292,6 @@ public function testSetSimpleProduct() ->willReturn(true); $quoteMock->expects($this->once())->method('setTotalsCollectedFlag')->with(false)->willReturnSelf(); - $quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf(); $quoteMock->expects($this->once())->method('save')->willReturnSelf(); $paymentMock->expects($this->once())->method('getId')->willReturn($paymentId); @@ -303,7 +305,6 @@ public function testSetSimpleProduct() public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() { $cartId = 100; - $methodData = ['method' => 'data']; $quoteMock = $this->createPartialMock( \Magento\Quote\Model\Quote::class, @@ -311,6 +312,7 @@ public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() ); $this->quoteRepositoryMock->expects($this->once())->method('get')->with($cartId)->willReturn($quoteMock); + /** @var \Magento\Quote\Model\Quote\Payment|\PHPUnit_Framework_MockObject_MockObject $methodMock */ $methodMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['setChecks', 'getData']); $methodMock->expects($this->once()) ->method('setChecks') @@ -321,17 +323,13 @@ public function testSetSimpleProductTrowsExceptionIfShippingAddressNotSet() \Magento\Payment\Model\Method\AbstractMethod::CHECK_ORDER_TOTAL_MIN_MAX, ]) ->willReturnSelf(); - $methodMock->expects($this->once())->method('getData')->willReturn($methodData); - - $paymentMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Payment::class, ['importData']); - $paymentMock->expects($this->once())->method('importData')->with($methodData)->willReturnSelf(); + $methodMock->expects($this->never())->method('getData'); $shippingAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getCountryId']); $shippingAddressMock->expects($this->once())->method('getCountryId')->willReturn(null); - $quoteMock->expects($this->once())->method('getPayment')->willReturn($paymentMock); - $quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); - $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($shippingAddressMock); + $quoteMock->method('isVirtual')->willReturn(false); + $quoteMock->method('getShippingAddress')->willReturn($shippingAddressMock); $this->model->set($cartId, $methodMock); } diff --git a/app/code/Magento/Sales/Api/PaymentFailuresInterface.php b/app/code/Magento/Sales/Api/PaymentFailuresInterface.php new file mode 100644 index 0000000000000..485ff1ffbe248 --- /dev/null +++ b/app/code/Magento/Sales/Api/PaymentFailuresInterface.php @@ -0,0 +1,28 @@ +orderRepository = $orderRepository; $this->historyRepository = $historyRepository; @@ -76,6 +84,8 @@ public function __construct( $this->notifier = $notifier; $this->eventManager = $eventManager; $this->orderCommentSender = $orderCommentSender; + $this->paymentFailures = $paymentFailures ? : \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Api\PaymentFailuresInterface::class); } /** @@ -192,6 +202,9 @@ public function place(\Magento\Sales\Api\Data\OrderInterface $order) return $this->orderRepository->save($order); //commit } catch (\Exception $e) { + if ($e instanceof CommandException) { + $this->paymentFailures->handle((int)$order->getQuoteId(), __($e->getMessage())); + } throw $e; //rollback; } diff --git a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php new file mode 100644 index 0000000000000..3a49bbce256ef --- /dev/null +++ b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php @@ -0,0 +1,294 @@ + Configuration > Sales > Checkout > Payment Failed Emails configuration. + */ +class PaymentFailuresService implements PaymentFailuresInterface +{ + /** + * Store config + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StateInterface + */ + private $inlineTranslation; + + /** + * @var TransportBuilder + */ + private $transportBuilder; + + /** + * @var TimezoneInterface + */ + private $localeDate; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StateInterface $inlineTranslation + * @param TransportBuilder $transportBuilder + * @param TimezoneInterface $localeDate + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StateInterface $inlineTranslation, + TransportBuilder $transportBuilder, + TimezoneInterface $localeDate, + CartRepositoryInterface $cartRepository + ) { + $this->scopeConfig = $scopeConfig; + $this->inlineTranslation = $inlineTranslation; + $this->transportBuilder = $transportBuilder; + $this->localeDate = $localeDate; + $this->cartRepository = $cartRepository; + } + + /** + * Sends an email about failed transaction. + * + * @param int $cartId + * @param string $message + * @param string $checkoutType + * @return PaymentFailuresInterface + */ + public function handle( + int $cartId, + string $message, + string $checkoutType = 'onepage' + ): PaymentFailuresInterface { + $this->inlineTranslation->suspend(); + $quote = $this->cartRepository->get($cartId); + + $template = $this->getConfigValue('checkout/payment_failed/template', $quote); + $receiver = $this->getConfigValue('checkout/payment_failed/receiver', $quote); + $sendTo = [ + [ + 'email' => $this->getConfigValue('trans_email/ident_' . $receiver . '/email', $quote), + 'name' => $this->getConfigValue('trans_email/ident_' . $receiver . '/name', $quote), + ], + ]; + + $copyMethod = $this->getConfigValue('checkout/payment_failed/copy_method', $quote); + $copyTo = $this->getConfigEmails($quote); + + $bcc = []; + if (!empty($copyTo)) { + switch ($copyMethod) { + case 'bcc': + $bcc = $copyTo; + break; + case 'copy': + foreach ($copyTo as $email) { + $sendTo[] = ['email' => $email, 'name' => null]; + } + break; + } + } + + foreach ($sendTo as $recipient) { + $transport = $this->transportBuilder + ->setTemplateIdentifier($template) + ->setTemplateOptions([ + 'area' => FrontNameResolver::AREA_CODE, + 'store' => Store::DEFAULT_STORE_ID, + ]) + ->setTemplateVars($this->getTemplateVars($quote, $message, $checkoutType)) + ->setFrom($this->getSendFrom($quote)) + ->addTo($recipient['email'], $recipient['name']) + ->addBcc($bcc) + ->getTransport(); + + $transport->sendMessage(); + } + + $this->inlineTranslation->resume(); + + return $this; + } + + /** + * Returns mail template variables. + * + * @param Quote $quote + * @param string $message + * @param string $checkoutType + * @return array + */ + private function getTemplateVars(Quote $quote, string $message, string $checkoutType): array + { + return [ + 'reason' => $message, + 'checkoutType' => $checkoutType, + 'dateAndTime' => $this->getLocaleDate(), + 'customer' => $this->getCustomerName($quote), + 'customerEmail' => $quote->getBillingAddress()->getEmail(), + 'billingAddress' => $quote->getBillingAddress(), + 'shippingAddress' => $quote->getShippingAddress(), + 'shippingMethod' => $this->getConfigValue( + 'carriers/' . $this->getShippingMethod($quote) . '/title', + $quote + ), + 'paymentMethod' => $this->getConfigValue( + 'payment/' . $this->getPaymentMethod($quote) . '/title', + $quote + ), + 'items' => implode('
', $this->getQuoteItems($quote)), + 'total' => $quote->getCurrency()->getStoreCurrencyCode() . ' ' . $quote->getGrandTotal(), + ]; + } + + /** + * Returns scope config value by config path. + * + * @param string $configPath + * @param Quote $quote + * @return mixed + */ + private function getConfigValue(string $configPath, Quote $quote) + { + return $this->scopeConfig->getValue( + $configPath, + ScopeInterface::SCOPE_STORE, + $quote->getStoreId() + ); + } + + /** + * Returns shipping method from quote. + * + * @param Quote $quote + * @return string + */ + private function getShippingMethod(Quote $quote): string + { + $shippingMethod = ''; + $shippingInfo = $quote->getShippingAddress()->getShippingMethod(); + + if ($shippingInfo) { + $data = explode('_', $shippingInfo); + $shippingMethod = $data[0]; + } + + return $shippingMethod; + } + + /** + * Returns payment method title from quote. + * + * @param Quote $quote + * @return string + */ + private function getPaymentMethod(Quote $quote): string + { + $paymentMethod = $quote->getPayment()->getMethod() ?? ''; + + return $paymentMethod; + } + + /** + * Returns quote visible items. + * + * @param Quote $quote + * @return array + */ + private function getQuoteItems(Quote $quote): array + { + $items = []; + foreach ($quote->getAllVisibleItems() as $item) { + $itemData = $item->getProduct()->getName() . ' x ' . $item->getQty() . ' ' . + $quote->getCurrency()->getStoreCurrencyCode() . ' ' . + $item->getProduct()->getFinalPrice($item->getQty()); + $items[] = $itemData; + } + + return $items; + } + + /** + * Gets email values by configuration path. + * + * @param Quote $quote + * @return array|false + */ + private function getConfigEmails(Quote $quote) + { + $configData = $this->getConfigValue('checkout/payment_failed/copy_to', $quote); + if (!empty($configData)) { + return explode(',', $configData); + } + + return false; + } + + /** + * Returns sender identity. + * + * @param Quote $quote + * @return string + */ + private function getSendFrom(Quote $quote): string + { + return $this->getConfigValue('checkout/payment_failed/identity', $quote); + } + + /** + * Returns current locale date and time + * + * @return string + */ + private function getLocaleDate(): string + { + return $this->localeDate->formatDateTime( + new \DateTime(), + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::MEDIUM + ); + } + + /** + * Returns customer name. + * + * @param Quote $quote + * @return string + */ + private function getCustomerName(Quote $quote): string + { + $customer = __('Guest')->render(); + if (!$quote->getCustomerIsGuest()) { + $customer = $quote->getCustomer()->getFirstname() . ' ' . + $quote->getCustomer()->getLastname(); + } + + return $customer; + } +} diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index ce2948983edbe..ac25cdab22f56 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -65,6 +65,7 @@ + diff --git a/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php index a26beda520944..5be5ccbc5e55a 100644 --- a/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php +++ b/app/code/Magento/Signifyd/Model/PaymentVerificationFactory.php @@ -60,7 +60,7 @@ public function __construct( * * @param string $paymentCode * @return PaymentVerificationInterface - * @throws \Exception + * @throws ConfigurationMismatchException */ public function createPaymentCvv($paymentCode) { @@ -73,7 +73,7 @@ public function createPaymentCvv($paymentCode) * * @param string $paymentCode * @return PaymentVerificationInterface - * @throws \Exception + * @throws ConfigurationMismatchException */ public function createPaymentAvs($paymentCode) { diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php index 02031e6f5b9b5..19408e99ae02e 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Debugger/DebuggerFactory.php @@ -30,7 +30,7 @@ class DebuggerFactory /** * DebuggerFactory constructor. * - * @param bjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @param Config $config */ public function __construct( diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php index 858ce0f0f3287..5e544e4b4048e 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/PurchaseBuilder.php @@ -7,12 +7,13 @@ use Magento\Framework\App\Area; use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Exception\ConfigurationMismatchException; use Magento\Framework\Intl\DateTimeFactory; use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Model\Order; +use Magento\Signifyd\Model\PaymentMethodMapper\PaymentMethodMapper; use Magento\Signifyd\Model\PaymentVerificationFactory; use Magento\Signifyd\Model\SignifydOrderSessionId; -use Magento\Signifyd\Model\PaymentMethodMapper\PaymentMethodMapper; /** * Prepare data related to purchase event represented in case creation request. @@ -72,6 +73,7 @@ public function __construct( * * @param Order $order * @return array + * @throws ConfigurationMismatchException */ public function build(Order $order) { @@ -202,6 +204,7 @@ private function getOrderChannel() * * @param OrderPaymentInterface $orderPayment * @return string + * @throws ConfigurationMismatchException */ private function getAvsCode(OrderPaymentInterface $orderPayment) { @@ -214,6 +217,7 @@ private function getAvsCode(OrderPaymentInterface $orderPayment) * * @param OrderPaymentInterface $orderPayment * @return string + * @throws ConfigurationMismatchException */ private function getCvvCode(OrderPaymentInterface $orderPayment) { diff --git a/app/code/Magento/Signifyd/etc/di.xml b/app/code/Magento/Signifyd/etc/di.xml index 92ad8a0bfd87a..fd78fff27f619 100644 --- a/app/code/Magento/Signifyd/etc/di.xml +++ b/app/code/Magento/Signifyd/etc/di.xml @@ -15,11 +15,7 @@ - - - U - - + diff --git a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml index 345f063a7aaa3..f14df1c70a790 100644 --- a/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml +++ b/app/code/Magento/Swagger/view/frontend/layout/swagger_index_index.xml @@ -10,32 +10,18 @@ Swagger UI - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + @@ -43,6 +29,7 @@ + diff --git a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml index 27b3767f274bc..b20da68734579 100644 --- a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml +++ b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml @@ -12,11 +12,48 @@ * Modified by Magento, Modifications Copyright © Magento, Inc. All rights reserved. */ -/** @var \Magento\Swagger\Block\Index $block */ +/** @var \Magento\Swagger\Block\Index $block + * + * @codingStandardsIgnoreFile + */ $schemaUrl = $block->getSchemaUrl(); ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +