diff --git a/app/code/Magento/Backend/Block/Store/Switcher.php b/app/code/Magento/Backend/Block/Store/Switcher.php index 9c35cfb5df81..2b9f70844df8 100644 --- a/app/code/Magento/Backend/Block/Store/Switcher.php +++ b/app/code/Magento/Backend/Block/Store/Switcher.php @@ -592,7 +592,7 @@ public function getHintHtml() 'What is this?' ) . '"' . ' class="admin__field-tooltip-action action-help">' . __( 'What is this?' - ) . '' . ' '; + ) . '' . ' '; } return $html; } diff --git a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml index 532fcdd0f598..046475baba67 100644 --- a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml +++ b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml @@ -19,26 +19,41 @@ Magento\CardinalCommerce\Model\Adminhtml\Source\Environment three_d_secure/cardinal/environment + + 1 + three_d_secure/cardinal/org_unit_id Magento\Config\Model\Config\Backend\Encrypted + + 1 + three_d_secure/cardinal/api_key Magento\Config\Model\Config\Backend\Encrypted + + 1 + three_d_secure/cardinal/api_identifier Magento\Config\Model\Config\Backend\Encrypted + + 1 + Magento\Config\Model\Config\Source\Yesno three_d_secure/cardinal/debug + + 1 + diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 4f730834412e..3a0920fb1c53 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -11,6 +11,11 @@ use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option\Type\Date; +use Magento\Catalog\Model\Product\Option\Type\DefaultType; +use Magento\Catalog\Model\Product\Option\Type\File; +use Magento\Catalog\Model\Product\Option\Type\Select; +use Magento\Catalog\Model\Product\Option\Type\Text; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Framework\EntityManager\MetadataPool; @@ -98,6 +103,16 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ protected $validatorPool; + /** + * @var string[] + */ + private $optionGroups; + + /** + * @var string[] + */ + private $optionTypesToGroups; + /** * @var MetadataPool */ @@ -121,6 +136,8 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory + * @param array $optionGroups + * @param array $optionTypesToGroups * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -135,14 +152,34 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null + ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null, + array $optionGroups = [], + array $optionTypesToGroups = [] ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; - $this->validatorPool = $validatorPool; $this->string = $string; + $this->validatorPool = $validatorPool; $this->customOptionValuesFactory = $customOptionValuesFactory ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + $this->optionGroups = $optionGroups ?: [ + self::OPTION_GROUP_DATE => Date::class, + self::OPTION_GROUP_FILE => File::class, + self::OPTION_GROUP_SELECT => Select::class, + self::OPTION_GROUP_TEXT => Text::class, + ]; + $this->optionTypesToGroups = $optionTypesToGroups ?: [ + self::OPTION_TYPE_FIELD => self::OPTION_GROUP_TEXT, + self::OPTION_TYPE_AREA => self::OPTION_GROUP_TEXT, + self::OPTION_TYPE_FILE => self::OPTION_GROUP_FILE, + self::OPTION_TYPE_DROP_DOWN => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_RADIO => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_CHECKBOX => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_MULTIPLE => self::OPTION_GROUP_SELECT, + self::OPTION_TYPE_DATE => self::OPTION_GROUP_DATE, + self::OPTION_TYPE_DATE_TIME => self::OPTION_GROUP_DATE, + self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE, + ]; parent::__construct( $context, @@ -314,36 +351,22 @@ public function getGroupByType($type = null) if ($type === null) { $type = $this->getType(); } - $optionGroupsToTypes = [ - self::OPTION_TYPE_FIELD => self::OPTION_GROUP_TEXT, - self::OPTION_TYPE_AREA => self::OPTION_GROUP_TEXT, - self::OPTION_TYPE_FILE => self::OPTION_GROUP_FILE, - self::OPTION_TYPE_DROP_DOWN => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_RADIO => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_CHECKBOX => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_MULTIPLE => self::OPTION_GROUP_SELECT, - self::OPTION_TYPE_DATE => self::OPTION_GROUP_DATE, - self::OPTION_TYPE_DATE_TIME => self::OPTION_GROUP_DATE, - self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE, - ]; - return $optionGroupsToTypes[$type] ?? ''; + return $this->optionTypesToGroups[$type] ?? ''; } /** * Group model factory * * @param string $type Option type - * @return \Magento\Catalog\Model\Product\Option\Type\DefaultType + * @return DefaultType * @throws LocalizedException */ public function groupFactory($type) { $group = $this->getGroupByType($type); - if (!empty($group)) { - return $this->optionTypeFactory->create( - 'Magento\Catalog\Model\Product\Option\Type\\' . $this->string->upperCaseWords($group) - ); + if (!empty($group) && isset($this->optionGroups[$group])) { + return $this->optionTypeFactory->create($this->optionGroups[$group]); } throw new LocalizedException(__('The option type to get group instance is incorrect.')); } diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index ff2fab73e037..e48e9f3b2b71 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -411,6 +411,28 @@ + + + + Magento\Catalog\Model\Product\Option\Type\Date + Magento\Catalog\Model\Product\Option\Type\File + Magento\Catalog\Model\Product\Option\Type\Select + Magento\Catalog\Model\Product\Option\Type\Text + + + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_TEXT + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_TEXT + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_FILE + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_SELECT + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_DATE + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_DATE + Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_GROUP_DATE + + + diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php index 1217270d780e..ff77db60a64e 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php @@ -102,7 +102,8 @@ protected function getAgreementsConfig() : nl2br($this->escaper->escapeHtml($agreement->getContent())), 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), 'mode' => $agreement->getMode(), - 'agreementId' => $agreement->getAgreementId() + 'agreementId' => $agreement->getAgreementId(), + 'contentHeight' => $agreement->getContentHeight() ]; } diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php index c8309bacb0a8..6b8477e0b491 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php +++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php @@ -77,6 +77,7 @@ public function testGetConfigIfContentIsHtml() $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; + $contentHeight = '100px'; $expectedResult = [ 'checkoutAgreements' => [ 'isEnabled' => 1, @@ -86,6 +87,7 @@ public function testGetConfigIfContentIsHtml() 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, 'agreementId' => $agreementId, + 'contentHeight' => $contentHeight ], ], ], @@ -116,6 +118,7 @@ public function testGetConfigIfContentIsHtml() $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); $agreement->expects($this->once())->method('getMode')->willReturn($mode); $agreement->expects($this->once())->method('getAgreementId')->willReturn($agreementId); + $agreement->expects($this->once())->method('getContentHeight')->willReturn($contentHeight); $this->assertEquals($expectedResult, $this->model->getConfig()); } @@ -133,6 +136,7 @@ public function testGetConfigIfContentIsNotHtml() $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; + $contentHeight = '100px'; $expectedResult = [ 'checkoutAgreements' => [ 'isEnabled' => 1, @@ -142,6 +146,7 @@ public function testGetConfigIfContentIsNotHtml() 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, 'agreementId' => $agreementId, + 'contentHeight' => $contentHeight ], ], ], @@ -172,6 +177,7 @@ public function testGetConfigIfContentIsNotHtml() $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); $agreement->expects($this->once())->method('getMode')->willReturn($mode); $agreement->expects($this->once())->method('getAgreementId')->willReturn($agreementId); + $agreement->expects($this->once())->method('getContentHeight')->willReturn($contentHeight); $this->assertEquals($expectedResult, $this->model->getConfig()); } diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js index 434676fc0411..a189c4291809 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js @@ -23,6 +23,7 @@ define([ agreements: agreementsConfig.agreements, modalTitle: ko.observable(null), modalContent: ko.observable(null), + contentHeight: ko.observable(null), modalWindow: null, /** @@ -42,6 +43,7 @@ define([ showContent: function (element) { this.modalTitle(element.checkboxText); this.modalContent(element.content); + this.contentHeight(element.contentHeight ? element.contentHeight : 'auto'); agreementsModal.showModal(); }, diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index 4b1a68624e54..f1c807fab3d2 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -35,7 +35,7 @@ diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index c60953e33e9e..5b50cc0ebd5e 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -9,12 +9,14 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Api\SearchCriteriaBuilder; /** * Configurable product type implementation @@ -194,9 +196,18 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $salableProcessor; + /** + * @var ProductAttributeRepositoryInterface|null + */ + private $productAttributeRepository; + + /** + * @var SearchCriteriaBuilder|null + */ + private $searchCriteriaBuilder; + /** * @codingStandardsIgnoreStart/End - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -214,9 +225,13 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable $catalogProductTypeConfigurable * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor + * @param \Magento\Framework\Cache\FrontendInterface|null $cache + * @param \Magento\Customer\Model\Session|null $customerSession * @param \Magento\Framework\Serialize\Serializer\Json $serializer * @param ProductInterfaceFactory $productFactory * @param SalableProcessor $salableProcessor + * @param ProductAttributeRepositoryInterface|null $productAttributeRepository + * @param SearchCriteriaBuilder|null $searchCriteriaBuilder * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -241,7 +256,9 @@ public function __construct( \Magento\Customer\Model\Session $customerSession = null, \Magento\Framework\Serialize\Serializer\Json $serializer = null, ProductInterfaceFactory $productFactory = null, - SalableProcessor $salableProcessor = null + SalableProcessor $salableProcessor = null, + ProductAttributeRepositoryInterface $productAttributeRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null ) { $this->typeConfigurableFactory = $typeConfigurableFactory; $this->_eavAttributeFactory = $eavAttributeFactory; @@ -256,6 +273,10 @@ public function __construct( $this->productFactory = $productFactory ?: ObjectManager::getInstance() ->get(ProductInterfaceFactory::class); $this->salableProcessor = $salableProcessor ?: ObjectManager::getInstance()->get(SalableProcessor::class); + $this->productAttributeRepository = $productAttributeRepository ?: + ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: + ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); parent::__construct( $catalogProductOption, $eavConfig, @@ -1231,19 +1252,16 @@ public function isPossibleBuyFromList($product) /** * Returns array of sub-products for specified configurable product - * - * $requiredAttributeIds - one dimensional array, if provided * Result array contains all children for specified configurable product * * @param \Magento\Catalog\Model\Product $product - * @param array $requiredAttributeIds + * @param array $requiredAttributeIds Attributes to include in the select; one-dimensional array * @return ProductInterface[] - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getUsedProducts($product, $requiredAttributeIds = null) { if (!$product->hasData($this->_usedProducts)) { - $collection = $this->getConfiguredUsedProductCollection($product, false); + $collection = $this->getConfiguredUsedProductCollection($product, false, $requiredAttributeIds); $usedProducts = array_values($collection->getItems()); $product->setData($this->_usedProducts, $usedProducts); } @@ -1390,16 +1408,18 @@ private function getUsedProductsCacheKey($keyParts) /** * Prepare collection for retrieving sub-products of specified configurable product - * * Retrieve related products collection with additional configuration * * @param \Magento\Catalog\Model\Product $product * @param bool $skipStockFilter + * @param array $requiredAttributeIds Attributes to include in the select * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection + * @throws \Magento\Framework\Exception\LocalizedException */ private function getConfiguredUsedProductCollection( \Magento\Catalog\Model\Product $product, - $skipStockFilter = true + $skipStockFilter = true, + $requiredAttributeIds = null ) { $collection = $this->getUsedProductCollection($product); @@ -1407,8 +1427,19 @@ private function getConfiguredUsedProductCollection( $collection->setFlag('has_stock_status_filter', true); } + $attributesForSelect = $this->getAttributesForCollection($product); + if ($requiredAttributeIds) { + $this->searchCriteriaBuilder->addFilter('attribute_id', $requiredAttributeIds, 'in'); + $requiredAttributes = $this->productAttributeRepository + ->getList($this->searchCriteriaBuilder->create())->getItems(); + $requiredAttributeCodes = []; + foreach ($requiredAttributes as $requiredAttribute) { + $requiredAttributeCodes[] = $requiredAttribute->getAttributeCode(); + } + $attributesForSelect = array_unique(array_merge($attributesForSelect, $requiredAttributeCodes)); + } $collection - ->addAttributeToSelect($this->getAttributesForCollection($product)) + ->addAttributeToSelect($attributesForSelect) ->addFilterByRequiredOptions() ->setStoreId($product->getStoreId()); diff --git a/app/code/Magento/Paypal/view/adminhtml/web/styles.css b/app/code/Magento/Paypal/view/adminhtml/web/styles.css index 9d63dbff5f3f..ee0bb1d0c420 100644 --- a/app/code/Magento/Paypal/view/adminhtml/web/styles.css +++ b/app/code/Magento/Paypal/view/adminhtml/web/styles.css @@ -28,9 +28,9 @@ .paypal-recommended-header > .admin__collapsible-block > a::before {content: "" !important;} .paypal-other-header > .admin__collapsible-block > a::before {content: '' !important; width: 0; height: 0; border-color: transparent; border-top-color: #000; border-style: solid; border-width: .8rem .5rem 0 .5rem; margin-top:1px; transition: all .2s linear;} .paypal-other-header > .admin__collapsible-block > a.open::before {border-color: transparent; border-bottom-color: #000; border-width: 0 .5rem .8rem .5rem;} -.paypal-other-header > .admin__collapsible-block > a {color: #007bdb !important; text-align: right;} .payments-other-header > .admin__collapsible-block > a, -.paypal-recommended-header > .admin__collapsible-block > a {display: inline-block;} +.paypal-recommended-header > .admin__collapsible-block > a, +.paypal-other-header > .admin__collapsible-block > a {display: inline-block;} .payments-other-header > .admin__collapsible-block > a::before, .paypal-recommended-header > .admin__collapsible-block > a::before {content: '' !important; width: 0; height: 0; border-color: transparent; border-top-color: #000; border-style: solid; border-width: .8rem .5rem 0 .5rem; margin-top:1px; transition: all .2s linear;} .payments-other-header > .admin__collapsible-block > a.open::before, diff --git a/app/code/Magento/Quote/Api/CartManagementInterface.php b/app/code/Magento/Quote/Api/CartManagementInterface.php index 7aa4bc4c7603..dc8ab7fedc87 100644 --- a/app/code/Magento/Quote/Api/CartManagementInterface.php +++ b/app/code/Magento/Quote/Api/CartManagementInterface.php @@ -52,6 +52,9 @@ public function getCartForCustomer($customerId); * @param int $customerId The customer ID. * @param int $storeId * @return boolean + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function assignCustomer($cartId, $customerId, $storeId); diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index 84ef699b6209..3a81341e2b02 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -298,22 +298,28 @@ public function assignCustomer($cartId, $customerId, $storeId) ); } try { - $this->quoteRepository->getForCustomer($customerId); - throw new StateException( - __("The customer can't be assigned to the cart because the customer already has an active cart.") - ); + $customerActiveQuote = $this->quoteRepository->getForCustomer($customerId); + + $quote->merge($customerActiveQuote); + $customerActiveQuote->setIsActive(0); + $this->quoteRepository->save($customerActiveQuote); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } $quote->setCustomer($customer); $quote->setCustomerIsGuest(0); + $quote->setIsActive(1); + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'quote_id'); if ($quoteIdMask->getId()) { $quoteIdMask->delete(); } + $this->quoteRepository->save($quote); + return true; } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index cd2afc39733f..2c61c192ead6 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -7,11 +7,13 @@ namespace Magento\Quote\Test\Unit\Model; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; use Magento\Framework\App\RequestInterface; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\Quote\Model\CustomerManagement; +use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Sales\Api\Data\OrderAddressInterface; @@ -199,7 +201,7 @@ protected function setUp() ); $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, [ 'assignCustomer', 'collectTotals', @@ -275,7 +277,7 @@ public function testCreateEmptyCartAnonymous() $storeId = 345; $quoteId = 2311; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $quoteAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, ['setCollectShippingRates'] @@ -306,14 +308,14 @@ public function testCreateEmptyCartForCustomer() $quoteId = 2311; $userId = 567; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $this->quoteRepositoryMock ->expects($this->once()) ->method('getActiveForCustomer') ->with($userId) - ->willThrowException(new NoSuchEntityException()); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + $customer = $this->getMockBuilder(CustomerInterface::class) ->setMethods(['getDefaultBilling'])->disableOriginalConstructor()->getMockForAbstractClass(); $quoteAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, @@ -342,14 +344,14 @@ public function testCreateEmptyCartForCustomerReturnExistsQuote() $storeId = 345; $userId = 567; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $quoteMock = $this->createMock(Quote::class); $this->quoteRepositoryMock ->expects($this->once()) ->method('getActiveForCustomer') ->with($userId)->willReturn($quoteMock); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + $customer = $this->getMockBuilder(CustomerInterface::class) ->setMethods(['getDefaultBilling'])->disableOriginalConstructor()->getMockForAbstractClass(); $quoteAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, @@ -379,8 +381,8 @@ public function testAssignCustomerFromAnotherStore() $customerId = 455; $storeId = 5; - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $quoteMock = $this->createMock(Quote::class); + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) @@ -395,7 +397,7 @@ public function testAssignCustomerFromAnotherStore() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); @@ -424,10 +426,10 @@ public function testAssignCustomerToNonanonymousCart() $storeId = 5; $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, ['getCustomerId', 'setCustomer', 'setCustomerIsGuest'] ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) @@ -442,7 +444,7 @@ public function testAssignCustomerToNonanonymousCart() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); @@ -463,7 +465,7 @@ public function testAssignCustomerToNonanonymousCart() } /** - * @expectedException \Magento\Framework\Exception\StateException + * @expectedException \Magento\Framework\Exception\NoSuchEntityException */ public function testAssignCustomerNoSuchCustomer() { @@ -472,10 +474,51 @@ public function testAssignCustomerNoSuchCustomer() $storeId = 5; $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, ['getCustomerId', 'setCustomer', 'setCustomerIsGuest'] ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + + $this->quoteRepositoryMock + ->expects($this->once()) + ->method('getActive') + ->with($cartId) + ->willReturn($quoteMock); + + $this->customerRepositoryMock + ->expects($this->once()) + ->method('getById') + ->with($customerId) + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + + $this->expectExceptionMessage( + "No such entity." + ); + + $this->model->assignCustomer($cartId, $customerId, $storeId); + } + + public function testAssignCustomerWithActiveCart() + { + $cartId = 220; + $customerId = 455; + $storeId = 5; + + $this->getPropertyValue($this->model, 'quoteIdMaskFactory') + ->expects($this->once()) + ->method('create') + ->willReturn($this->quoteIdMock); + + $quoteMock = $this->createPartialMock( + Quote::class, + ['getCustomerId', 'setCustomer', 'setCustomerIsGuest', 'setIsActive', 'getIsActive', 'merge'] + ); + + $activeQuoteMock = $this->createPartialMock( + Quote::class, + ['getCustomerId', 'setCustomer', 'setCustomerIsGuest', 'setIsActive', 'getIsActive', 'merge'] + ); + + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) @@ -490,7 +533,7 @@ public function testAssignCustomerNoSuchCustomer() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); @@ -506,17 +549,26 @@ public function testAssignCustomerNoSuchCustomer() ->willReturn([$storeId, 'some store value']); $quoteMock->expects($this->once())->method('getCustomerId')->willReturn(null); - $this->quoteRepositoryMock ->expects($this->once()) ->method('getForCustomer') - ->with($customerId); + ->with($customerId) + ->willReturn($activeQuoteMock); - $this->model->assignCustomer($cartId, $customerId, $storeId); + $quoteMock->expects($this->once())->method('merge')->with($activeQuoteMock)->willReturnSelf(); + $activeQuoteMock->expects($this->once())->method('setIsActive')->with(0); + $this->quoteRepositoryMock->expects($this->atLeastOnce())->method('save')->with($activeQuoteMock); - $this->expectExceptionMessage( - "The customer can't be assigned to the cart because the customer already has an active cart." - ); + $quoteMock->expects($this->once())->method('setCustomer')->with($customerMock); + $quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(0); + $quoteMock->expects($this->once())->method('setIsActive')->with(1); + + $this->quoteIdMock->expects($this->once())->method('load')->with($cartId, 'quote_id')->willReturnSelf(); + $this->quoteIdMock->expects($this->once())->method('getId')->willReturn(10); + $this->quoteIdMock->expects($this->once())->method('delete'); + $this->quoteRepositoryMock->expects($this->atLeastOnce())->method('save')->with($quoteMock); + + $this->model->assignCustomer($cartId, $customerId, $storeId); } public function testAssignCustomer() @@ -529,15 +581,13 @@ public function testAssignCustomer() ->expects($this->once()) ->method('create') ->willReturn($this->quoteIdMock); - $this->quoteIdMock->expects($this->once())->method('load')->with($cartId, 'quote_id')->willReturnSelf(); - $this->quoteIdMock->expects($this->once())->method('getId')->willReturn(10); - $this->quoteIdMock->expects($this->once())->method('delete'); + $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getCustomerId', 'setCustomer', 'setCustomerIsGuest'] + Quote::class, + ['getCustomerId', 'setCustomer', 'setCustomerIsGuest', 'setIsActive', 'getIsActive', 'merge'] ); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $customerMock = $this->createMock(CustomerInterface::class); $this->quoteRepositoryMock ->expects($this->once()) ->method('getActive') @@ -551,10 +601,12 @@ public function testAssignCustomer() ->willReturn($customerMock); $customerModelMock = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['load', 'getSharedStoreIds'] ); + $this->customerFactoryMock->expects($this->once())->method('create')->willReturn($customerModelMock); + $customerModelMock ->expects($this->once()) ->method('load') @@ -572,11 +624,17 @@ public function testAssignCustomer() ->expects($this->once()) ->method('getForCustomer') ->with($customerId) - ->willThrowException(new NoSuchEntityException()); + ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); + + $quoteMock->expects($this->never())->method('merge'); $quoteMock->expects($this->once())->method('setCustomer')->with($customerMock); $quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(0); + $quoteMock->expects($this->once())->method('setIsActive')->with(1); + $this->quoteIdMock->expects($this->once())->method('load')->with($cartId, 'quote_id')->willReturnSelf(); + $this->quoteIdMock->expects($this->once())->method('getId')->willReturn(10); + $this->quoteIdMock->expects($this->once())->method('delete'); $this->quoteRepositoryMock->expects($this->once())->method('save')->with($quoteMock); $this->model->assignCustomer($cartId, $customerId, $storeId); @@ -881,7 +939,7 @@ protected function getQuote( \Magento\Quote\Model\Quote\Address $shippingAddress = null ) { $quote = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, [ 'setIsActive', 'getCustomerEmail', @@ -928,7 +986,7 @@ protected function getQuote( ->willReturn($payment); $customer = $this->createPartialMock( - \Magento\Customer\Model\Customer::class, + Customer::class, ['getDefaultBilling', 'getId'] ); $quote->expects($this->any())->method('getCustomerId')->willReturn($customerId); @@ -1016,12 +1074,12 @@ protected function prepareOrderFactory( } /** - * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testGetCartForCustomer() { $customerId = 100; - $cartMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $cartMock = $this->createMock(Quote::class); $this->quoteRepositoryMock->expects($this->once()) ->method('getActiveForCustomer') ->with($customerId) diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php index 06c6a9eb0652..737ca446bb8e 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/AbstractSidebar.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Sales\Block\Adminhtml\Order\Create\Sidebar; use Magento\Framework\Pricing\PriceCurrencyInterface; diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php index f2200e1c1a10..a927b7177294 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Store\Model\ScopeInterface; /** * Adminhtml sales order create sidebar cart block @@ -146,4 +147,30 @@ private function getCartItemCustomPrice(Product $product): ?float return null; } + + /** + * @inheritdoc + */ + public function getItemCount() + { + $count = $this->getData('item_count'); + if ($count === null) { + $useQty = $this->_scopeConfig->getValue( + 'checkout/cart_link/use_qty', + ScopeInterface::SCOPE_STORE + ); + $allItems = $this->getItems(); + if ($useQty) { + $count = 0; + foreach ($allItems as $item) { + $count += $item->getQty(); + } + } else { + $count = count($allItems); + } + $this->setData('item_count', $count); + } + + return $count; + } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php index 300b7ee37f2e..6e7c2e5ce560 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/AbstractInvoice/View.php @@ -1,17 +1,22 @@ registry = $registry; parent::__construct($context); + $this->registry = $registry; $this->resultForwardFactory = $resultForwardFactory; + $this->invoiceRepository = $invoiceRepository ?: + ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); } /** @@ -70,13 +79,14 @@ public function execute() } /** + * Get invoice using invoice Id from request params + * * @return \Magento\Sales\Model\Order\Invoice|bool */ protected function getInvoice() { try { - $invoice = $this->getInvoiceRepository() - ->get($this->getRequest()->getParam('invoice_id')); + $invoice = $this->invoiceRepository->get($this->getRequest()->getParam('invoice_id')); $this->registry->register('current_invoice', $invoice); } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('Invoice capturing error')); @@ -85,19 +95,4 @@ protected function getInvoice() return $invoice; } - - /** - * @return InvoiceRepository - * - * @deprecated 100.1.0 - */ - private function getInvoiceRepository() - { - if ($this->invoiceRepository === null) { - $this->invoiceRepository = ObjectManager::getInstance() - ->get(InvoiceRepositoryInterface::class); - } - - return $this->invoiceRepository; - } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php index 515c0753542a..23dcae3a858c 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddComment.php @@ -1,23 +1,30 @@ invoiceCommentSender = $invoiceCommentSender; $this->resultJsonFactory = $resultJsonFactory; $this->resultPageFactory = $resultPageFactory; $this->resultRawFactory = $resultRawFactory; - parent::__construct($context, $registry, $resultForwardFactory); + $this->invoiceRepository = $invoiceRepository ?: + ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); + parent::__construct($context, $registry, $resultForwardFactory, $invoiceRepository); } /** @@ -90,7 +106,7 @@ public function execute() ); $this->invoiceCommentSender->send($invoice, !empty($data['is_customer_notified']), $data['comment']); - $invoice->save(); + $this->invoiceRepository->save($invoice); /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml index 76be3a109432..589da3e49dc8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml @@ -46,8 +46,9 @@ - + + @@ -64,6 +65,7 @@ + @@ -75,6 +77,6 @@ - + diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php index 053df5394929..9fe3042fa6bd 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/AddCommentTest.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Invoice; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -186,7 +189,8 @@ protected function setUp() 'invoiceCommentSender' => $this->commentSenderMock, 'resultPageFactory' => $this->resultPageFactoryMock, 'resultRawFactory' => $this->resultRawFactoryMock, - 'resultJsonFactory' => $this->resultJsonFactoryMock + 'resultJsonFactory' => $this->resultJsonFactoryMock, + 'invoiceRepository' => $this->invoiceRepository ] ); @@ -230,8 +234,9 @@ public function testExecute() $invoiceMock->expects($this->once()) ->method('addComment') ->with($data['comment'], false, false); - $invoiceMock->expects($this->once()) - ->method('save'); + $this->invoiceRepository->expects($this->once()) + ->method('save') + ->with($invoiceMock); $this->invoiceRepository->expects($this->once()) ->method('get') @@ -307,11 +312,11 @@ public function testExecuteModelException() public function testExecuteException() { $response = ['error' => true, 'message' => 'Cannot add new comment.']; - $e = new \Exception('test'); + $error = new \Exception('test'); $this->requestMock->expects($this->once()) ->method('getParam') - ->will($this->throwException($e)); + ->will($this->throwException($error)); $this->resultJsonFactoryMock->expects($this->once()) ->method('create') diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index b2afde435a62..c9ecf2e6670c 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -3958,6 +3958,22 @@ .grid tr.headings th > span { white-space: normal; } + + .field { + &.field-subscription { + .admin__field-label { + margin-left: 10px; + float: none; + cursor: pointer; + } + + .admin__field-control { + float: left; + width: auto; + margin: 6px 0px 0px 0px; + } + } + } } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less index 65f3eeef63b0..5f69db5acec4 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less @@ -110,7 +110,7 @@ @_dropdown-list-position-right: 0, @_dropdown-list-pointer-position: right, @_dropdown-list-pointer-position-left-right: 26px, - @_dropdown-list-z-index: 101, + @_dropdown-list-z-index: 101, @_dropdown-toggle-icon-content: @icon-cart, @_dropdown-toggle-active-icon-content: @icon-cart, @_dropdown-list-item-padding: false, @@ -304,7 +304,7 @@ .weee[data-label] { .lib-font-size(11); - + .label { &:extend(.abs-no-display all); } @@ -340,7 +340,6 @@ } .item-qty { - margin-right: @indent__s; text-align: center; width: 45px; } @@ -390,6 +389,16 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { .minicart-wrapper { margin-top: @indent__s; + .lib-clearfix(); + .product { + .actions { + float: left; + margin: 10px 0 0 0; + } + } + .update-cart-item { + float: right; + } } } @@ -400,7 +409,7 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .minicart-wrapper { margin-left: 13px; - + .block-minicart { right: -15px; width: 390px; diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less index af94dd7b97bb..d6cc62c2ddef 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less @@ -120,7 +120,7 @@ @_dropdown-list-position-right: -10px, @_dropdown-list-pointer-position: right, @_dropdown-list-pointer-position-left-right: 12px, - @_dropdown-list-z-index: 101, + @_dropdown-list-z-index: 101, @_dropdown-toggle-icon-content: @icon-cart, @_dropdown-toggle-active-icon-content: @icon-cart, @_dropdown-list-item-padding: false, @@ -136,7 +136,7 @@ .block-minicart { .lib-css(padding, 25px @minicart__padding-horizontal); - + .block-title { display: none; } @@ -233,7 +233,7 @@ .minicart-items { .lib-list-reset-styles(); - + .product-item { padding: @indent__base 0; @@ -316,7 +316,7 @@ &:extend(.abs-toggling-title all); border: 0; padding: 0 @indent__xl @indent__xs 0; - + &:after { .lib-css(color, @color-gray56); margin: 0 0 0 @indent__xs; @@ -349,7 +349,7 @@ @_icon-font-position: after ); } - + > span { &:extend(.abs-visually-hidden-reset all); } @@ -369,7 +369,6 @@ } .item-qty { - margin-right: @indent__s; text-align: center; width: 60px; } @@ -419,6 +418,16 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .minicart-wrapper { margin-top: @indent__s; + .lib-clearfix(); + .product { + .actions { + float: left; + margin: 10px 0 0 0; + } + } + .update-cart-item { + float: right; + } } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php index 6d585561ae3a..08821b08ede5 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartManagementTest.php @@ -310,21 +310,20 @@ public function testAssignCustomerThrowsExceptionIfCartIsAssignedToDifferentStor } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php * @magentoApiDataFixture Magento/Sales/_files/quote.php - * @expectedException \Exception */ - public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart() + public function testAssignCustomerCartMerged() { /** @var $customer \Magento\Customer\Model\Customer */ $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class)->load(1); // Customer has a quote with reserved order ID test_order_1 (see fixture) /** @var $customerQuote \Magento\Quote\Model\Quote */ $customerQuote = $this->objectManager->create(\Magento\Quote\Model\Quote::class) - ->load('test_order_1', 'reserved_order_id'); - $customerQuote->setIsActive(1)->save(); + ->load('test_order_item_with_items', 'reserved_order_id'); /** @var $quote \Magento\Quote\Model\Quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); + $expectedQuoteItemsQty = $customerQuote->getItemsQty() + $quote->getItemsQty(); $cartId = $quote->getId(); $customerId = $customer->getId(); @@ -346,11 +345,13 @@ public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart( 'customerId' => $customerId, 'storeId' => 1, ]; - $this->_webApiCall($serviceInfo, $requestData); + $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); - $this->expectExceptionMessage( - "The customer can't be assigned to the cart because the customer already has an active cart." - ); + $mergedQuote = $this->objectManager + ->create(\Magento\Quote\Model\Quote::class) + ->load('test01', 'reserved_order_id'); + + $this->assertEquals($expectedQuoteItemsQty, $mergedQuote->getItemsQty()); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php index bbd1e59f83f9..120781e674d4 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php @@ -231,21 +231,20 @@ public function testAssignCustomerThrowsExceptionIfTargetCartIsNotAnonymous() } /** - * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php * @magentoApiDataFixture Magento/Sales/_files/quote.php - * @expectedException \Exception */ - public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart() + public function testAssignCustomerCartMerged() { /** @var $customer \Magento\Customer\Model\Customer */ $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class)->load(1); // Customer has a quote with reserved order ID test_order_1 (see fixture) /** @var $customerQuote \Magento\Quote\Model\Quote */ $customerQuote = $this->objectManager->create(\Magento\Quote\Model\Quote::class) - ->load('test_order_1', 'reserved_order_id'); - $customerQuote->setIsActive(1)->save(); + ->load('test_order_item_with_items', 'reserved_order_id'); /** @var $quote \Magento\Quote\Model\Quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); + $expectedQuoteItemsQty = $customerQuote->getItemsQty() + $quote->getItemsQty(); $cartId = $quote->getId(); @@ -284,11 +283,12 @@ public function testAssignCustomerThrowsExceptionIfCustomerAlreadyHasActiveCart( 'customerId' => $customerId, 'storeId' => 1, ]; - $this->_webApiCall($serviceInfo, $requestData); + $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); + $mergedQuote = $this->objectManager + ->create(\Magento\Quote\Model\Quote::class) + ->load('test01', 'reserved_order_id'); - $this->expectExceptionMessage( - "The customer can't be assigned to the cart because the customer already has an active cart." - ); + $this->assertEquals($expectedQuoteItemsQty, $mergedQuote->getItemsQty()); } /** diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index 78fa4733a256..0d2043434d35 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -254,6 +254,33 @@ public function testGetUsedProducts() } } + /** + * Tests the $requiredAttributes parameter; uses meta_description as an example of an attribute that is not + * included in default attribute select. + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php + */ + public function testGetUsedProductsWithRequiredAttributes() + { + $requiredAttributeIds = [86]; + $products = $this->model->getUsedProducts($this->product, $requiredAttributeIds); + foreach ($products as $product) { + self::assertNotNull($product->getData('meta_description')); + } + } + + /** + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php + */ + public function testGetUsedProductsWithoutRequiredAttributes() + { + $products = $this->model->getUsedProducts($this->product); + foreach ($products as $product) { + self::assertNull($product->getData('meta_description')); + } + } + /** * Test getUsedProducts returns array with same indexes regardless collections was cache or not. * diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php new file mode 100644 index 000000000000..d0afeeaf19fe --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php @@ -0,0 +1,145 @@ +reinitialize(); + +require __DIR__ . '/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setMetaDescription('meta_description' . $productId) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +// Remove any previously created product with the same id. +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productToDelete = $productRepository->getById(1); + $productRepository->delete($productToDelete); + + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ + $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class); + $itemResource->getConnection()->delete( + $itemResource->getMainTable(), + 'product_id = ' . $productToDelete->getId() + ); +} catch (\Exception $e) { + // Nothing to remove +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php new file mode 100644 index 000000000000..21953dea6f58 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php @@ -0,0 +1,39 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/lib/internal/Magento/Framework/Setup/Patch/PatchApplier.php b/lib/internal/Magento/Framework/Setup/Patch/PatchApplier.php index bdaca77e5b4e..e1b0e2842628 100644 --- a/lib/internal/Magento/Framework/Setup/Patch/PatchApplier.php +++ b/lib/internal/Magento/Framework/Setup/Patch/PatchApplier.php @@ -161,6 +161,9 @@ public function applyDataPatch($moduleName = null) $this->moduleDataSetup->getConnection()->beginTransaction(); $dataPatch->apply(); $this->patchHistory->fixPatch(get_class($dataPatch)); + foreach ($dataPatch->getAliases() as $patchAlias) { + $this->patchHistory->fixPatch($patchAlias); + } $this->moduleDataSetup->getConnection()->commit(); } catch (\Exception $e) { $this->moduleDataSetup->getConnection()->rollBack(); @@ -237,6 +240,9 @@ public function applySchemaPatch($moduleName = null) $schemaPatch = $this->patchFactory->create($schemaPatch, ['schemaSetup' => $this->schemaSetup]); $schemaPatch->apply(); $this->patchHistory->fixPatch(get_class($schemaPatch)); + foreach ($schemaPatch->getAliases() as $patchAlias) { + $this->patchHistory->fixPatch($patchAlias); + } } catch (\Exception $e) { throw new SetupException( new Phrase( diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php index cb40845bcc48..b649f6f062e9 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php @@ -10,8 +10,11 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Module\ModuleResource; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Setup\Exception; use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchBackwardCompatability; +use Magento\Framework\Setup\Patch\PatchInterface; use Magento\Framework\Setup\SchemaSetupInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Setup\Patch\PatchApplier; @@ -169,8 +172,10 @@ public function testApplyDataPatchForNewlyInstalledModule($moduleName, $dataPatc $patch1 = $this->createMock(\SomeDataPatch::class); $patch1->expects($this->once())->method('apply'); + $patch1->expects($this->once())->method('getAliases')->willReturn([]); $patch2 = $this->createMock(\OtherDataPatch::class); $patch2->expects($this->once())->method('apply'); + $patch2->expects($this->once())->method('getAliases')->willReturn([]); $this->objectManagerMock->expects($this->any())->method('create')->willReturnMap( [ ['\\' . \SomeDataPatch::class, ['moduleDataSetup' => $this->moduleDataSetupMock], $patch1], @@ -188,6 +193,60 @@ public function testApplyDataPatchForNewlyInstalledModule($moduleName, $dataPatc $this->patchApllier->applyDataPatch($moduleName); } + /** + * @param $moduleName + * @param $dataPatches + * @param $moduleVersionInDb + * + * @dataProvider applyDataPatchDataNewModuleProvider() + * + * @expectedException Exception + * @expectedExceptionMessageRegExp "Unable to apply data patch .+ cannot be applied twice" + */ + public function testApplyDataPatchForAlias($moduleName, $dataPatches, $moduleVersionInDb) + { + $this->dataPatchReaderMock->expects($this->once()) + ->method('read') + ->with($moduleName) + ->willReturn($dataPatches); + + $this->moduleResourceMock->expects($this->any())->method('getDataVersion')->willReturnMap( + [ + [$moduleName, $moduleVersionInDb] + ] + ); + + $patch1 = $this->createMock(DataPatchInterface::class); + $patch1->expects($this->once())->method('getAliases')->willReturn(['PatchAlias']); + $patchClass = get_class($patch1); + + $patchRegistryMock = $this->createAggregateIteratorMock(PatchRegistry::class, [$patchClass], ['registerPatch']); + $patchRegistryMock->expects($this->any()) + ->method('registerPatch'); + + $this->patchRegistryFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($patchRegistryMock); + + $this->objectManagerMock->expects($this->any())->method('create')->willReturnMap( + [ + ['\\' . $patchClass, ['moduleDataSetup' => $this->moduleDataSetupMock], $patch1], + ] + ); + $this->connectionMock->expects($this->exactly(1))->method('beginTransaction'); + $this->connectionMock->expects($this->never())->method('commit'); + $this->patchHistoryMock->expects($this->any())->method('fixPatch')->will( + $this->returnCallback( + function ($param1) { + if ($param1 == 'PatchAlias') { + throw new \LogicException(sprintf("Patch %s cannot be applied twice", $param1)); + } + } + ) + ); + $this->patchApllier->applyDataPatch($moduleName); + } + /** * @return array */ @@ -243,8 +302,10 @@ public function testApplyDataPatchForInstalledModule($moduleName, $dataPatches, $patch1 = $this->createMock(\SomeDataPatch::class); $patch1->expects(self::never())->method('apply'); + $patch1->expects(self::any())->method('getAliases')->willReturn([]); $patch2 = $this->createMock(\OtherDataPatch::class); $patch2->expects(self::once())->method('apply'); + $patch2->expects(self::any())->method('getAliases')->willReturn([]); $this->objectManagerMock->expects(self::any())->method('create')->willReturnMap( [ ['\\' . \SomeDataPatch::class, ['moduleDataSetup' => $this->moduleDataSetupMock], $patch1], @@ -279,7 +340,7 @@ public function applyDataPatchDataInstalledModuleProvider() * @param $dataPatches * @param $moduleVersionInDb * - * @expectedException \Magento\Framework\Setup\Exception + * @expectedException Exception * @expectedExceptionMessage Patch Apply Error * * @dataProvider applyDataPatchDataInstalledModuleProvider() @@ -328,7 +389,7 @@ public function testApplyDataPatchRollback($moduleName, $dataPatches, $moduleVer } /** - * @expectedException \Magento\Framework\Setup\Exception + * @expectedException Exception * @expectedExceptionMessageRegExp "Patch [a-zA-Z0-9\_]+ should implement DataPatchInterface" */ public function testNonDataPatchApply() @@ -434,8 +495,10 @@ public function testSchemaPatchAplly($moduleName, $schemaPatches, $moduleVersion $patch1 = $this->createMock(\SomeSchemaPatch::class); $patch1->expects($this->never())->method('apply'); + $patch1->expects($this->any())->method('getAliases')->willReturn([]); $patch2 = $this->createMock(\OtherSchemaPatch::class); $patch2->expects($this->once())->method('apply'); + $patch2->expects($this->any())->method('getAliases')->willReturn([]); $this->patchFactoryMock->expects($this->any())->method('create')->willReturnMap( [ [\SomeSchemaPatch::class, ['schemaSetup' => $this->schemaSetupMock], $patch1], @@ -448,6 +511,55 @@ public function testSchemaPatchAplly($moduleName, $schemaPatches, $moduleVersion $this->patchApllier->applySchemaPatch($moduleName); } + /** + * @param $moduleName + * @param $schemaPatches + * @param $moduleVersionInDb + * + * @dataProvider schemaPatchDataProvider() + * + * @expectedException Exception + * @expectedExceptionMessageRegExp "Unable to apply patch .+ cannot be applied twice" + */ + public function testSchemaPatchApplyForPatchAlias($moduleName, $schemaPatches, $moduleVersionInDb) + { + $this->schemaPatchReaderMock->expects($this->once()) + ->method('read') + ->with($moduleName) + ->willReturn($schemaPatches); + + $this->moduleResourceMock->expects($this->any())->method('getDbVersion')->willReturnMap( + [ + [$moduleName, $moduleVersionInDb] + ] + ); + + $patch1 = $this->createMock(PatchInterface::class); + $patch1->expects($this->once())->method('getAliases')->willReturn(['PatchAlias']); + $patchClass = get_class($patch1); + + $patchRegistryMock = $this->createAggregateIteratorMock(PatchRegistry::class, [$patchClass], ['registerPatch']); + $patchRegistryMock->expects($this->any()) + ->method('registerPatch'); + + $this->patchRegistryFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($patchRegistryMock); + + $this->patchFactoryMock->expects($this->any())->method('create')->willReturn($patch1); + $this->patchHistoryMock->expects($this->any())->method('fixPatch')->will( + $this->returnCallback( + function ($param1) { + if ($param1 == 'PatchAlias') { + throw new \LogicException(sprintf("Patch %s cannot be applied twice", $param1)); + } + } + ) + ); + + $this->patchApllier->applySchemaPatch($moduleName); + } + public function testRevertDataPatches() { $patches = [\RevertableDataPatch::class]; @@ -534,33 +646,43 @@ private function createAggregateIteratorMock($className, array $items = [], arra $someIterator->expects($this->any()) ->method('rewind') - ->willReturnCallback(function () use ($iterator) { - $iterator->rewind(); - }); + ->willReturnCallback( + function () use ($iterator) { + $iterator->rewind(); + } + ); $someIterator->expects($this->any()) ->method('current') - ->willReturnCallback(function () use ($iterator) { - return $iterator->current(); - }); + ->willReturnCallback( + function () use ($iterator) { + return $iterator->current(); + } + ); $someIterator->expects($this->any()) ->method('key') - ->willReturnCallback(function () use ($iterator) { - return $iterator->key(); - }); + ->willReturnCallback( + function () use ($iterator) { + return $iterator->key(); + } + ); $someIterator->expects($this->any()) ->method('next') - ->willReturnCallback(function () use ($iterator) { - $iterator->next(); - }); + ->willReturnCallback( + function () use ($iterator) { + $iterator->next(); + } + ); $someIterator->expects($this->any()) ->method('valid') - ->willReturnCallback(function () use ($iterator) { - return $iterator->valid(); - }); + ->willReturnCallback( + function () use ($iterator) { + return $iterator->valid(); + } + ); return $mockIteratorAggregate; } diff --git a/lib/web/magnifier/magnify.js b/lib/web/magnifier/magnify.js index 9d673092b806..559e7782f247 100644 --- a/lib/web/magnifier/magnify.js +++ b/lib/web/magnifier/magnify.js @@ -680,8 +680,6 @@ define([ $image.removeClass(imageDraggableClass); } } else if (gallery.fullScreen && (!transitionEnabled || !transitionActive)) { - e.preventDefault(); - imagePosY = getTop($image); imagePosX = $image.offset().left;