From c8baed5750f705531e75573ac8a1977dd6fe7d5f Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh Date: Wed, 5 Feb 2020 13:57:49 +0200 Subject: [PATCH 1/5] MC-30963: [Magento Cloud] CMS blocks with identical identifiers --- .../Magento/Cms/Model/ResourceModel/Block.php | 17 +++++------------ .../Test/Mftf/Test/CheckStaticBlocksTest.xml | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block.php b/app/code/Magento/Cms/Model/ResourceModel/Block.php index 30e817713755c..1324b9bd127e9 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block.php @@ -185,13 +185,9 @@ public function getIsUniqueBlockToStores(AbstractModel $object) $entityMetadata = $this->metadataPool->getMetadata(BlockInterface::class); $linkField = $entityMetadata->getLinkField(); - $stores = (array)$object->getData('store_id'); - $isDefaultStore = $this->_storeManager->isSingleStoreMode() - || array_search(Store::DEFAULT_STORE_ID, $stores) !== false; - - if (!$isDefaultStore) { - $stores[] = Store::DEFAULT_STORE_ID; - } + $stores = $this->_storeManager->isSingleStoreMode() + ? [Store::DEFAULT_STORE_ID] + : (array)$object->getData('store_id'); $select = $this->getConnection()->select() ->from(['cb' => $this->getMainTable()]) @@ -200,11 +196,8 @@ public function getIsUniqueBlockToStores(AbstractModel $object) 'cb.' . $linkField . ' = cbs.' . $linkField, [] ) - ->where('cb.identifier = ? ', $object->getData('identifier')); - - if (!$isDefaultStore) { - $select->where('cbs.store_id IN (?)', $stores); - } + ->where('cb.identifier = ? ', $object->getData('identifier')) + ->where('cbs.store_id IN (?)', $stores); if ($object->getId()) { $select->where('cb.' . $entityMetadata->getIdentifierField() . ' <> ?', $object->getId()); diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml index e6ab1c130606b..385616dcca9b9 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -56,12 +56,23 @@ - - + + + + + + + + + + + + + From 9b585ee2963994a56c4ca35fd80c8cb6d5c7fd44 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Fri, 7 Feb 2020 17:13:24 +0200 Subject: [PATCH 2/5] MC-30650: [Magento Cloud] Customer Creation - The store view is not in the associated website --- .../Test/CheckTierPricingOfProductsTest.xml | 2 + .../Controller/Adminhtml/Index/Save.php | 8 +- .../Customer/Model/AccountManagement.php | 24 +- .../Controller/Adminhtml/Index/SaveTest.php | 5 +- .../Test/Unit/Model/AccountManagementTest.php | 161 +++++++------- .../Unit/ViewModel/Customer/StoreTest.php | 210 ++++++++++++++++++ .../Customer/ViewModel/Customer/Store.php | 131 +++++++++++ .../view/base/ui_component/customer_form.xml | 8 +- .../web/js/lib/knockout/bindings/optgroup.js | 2 +- 9 files changed, 460 insertions(+), 91 deletions(-) create mode 100644 app/code/Magento/Customer/Test/Unit/ViewModel/Customer/StoreTest.php create mode 100644 app/code/Magento/Customer/ViewModel/Customer/Store.php diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index 4e0e8d03f59d5..f80cfed54c8f3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -36,6 +36,7 @@ + @@ -328,6 +329,7 @@ + diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index b85b735ea9c4f..a65bfa5d77f9e 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -348,8 +348,14 @@ public function execute() ['customer' => $customer, 'request' => $this->getRequest()] ); - if (isset($customerData['sendemail_store_id'])) { + if (isset($customerData['sendemail_store_id']) && $customerData['sendemail_store_id'] !== false) { $customer->setStoreId($customerData['sendemail_store_id']); + try { + $this->customerAccountManagement->validateCustomerStoreIdByWebsiteId($customer); + } catch (LocalizedException $exception) { + throw new LocalizedException(__("The Store View selected for sending Welcome email from". + " is not related to the customer's associated website.")); + } } // Save customer diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 55da6a62f0625..e2d997ed445b8 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -872,7 +872,6 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash if ($customer->getId()) { $customer = $this->customerRepository->get($customer->getEmail()); $websiteId = $customer->getWebsiteId(); - if ($this->isCustomerInStore($websiteId, $customer->getStoreId())) { throw new InputException(__('This customer already exists in this store.')); } @@ -896,13 +895,10 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash $customer->setWebsiteId($websiteId); } + $this->validateCustomerStoreIdByWebsiteId($customer); + // Update 'created_in' value with actual store name if ($customer->getId() === null) { - $websiteId = $customer->getWebsiteId(); - if ($websiteId && !$this->isCustomerInStore($websiteId, $customer->getStoreId())) { - throw new LocalizedException(__('The store view is not in the associated website.')); - } - $storeName = $this->storeManager->getStore($customer->getStoreId())->getName(); $customer->setCreatedIn($storeName); } @@ -1144,6 +1140,22 @@ public function isCustomerInStore($customerWebsiteId, $storeId) return in_array($storeId, $ids); } + /** + * Validate customer store id by customer website id. + * + * @param CustomerInterface $customer + * @return bool + * @throws LocalizedException + */ + public function validateCustomerStoreIdByWebsiteId(CustomerInterface $customer) + { + if (!$this->isCustomerInStore($customer->getWebsiteId(), $customer->getStoreId())) { + throw new LocalizedException(__('The store view is not in the associated website.')); + } + + return true; + } + /** * Validate the Reset Password Token for a customer. * diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 2e729873961c0..51663861fc8d1 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php @@ -269,7 +269,7 @@ protected function setUp() ->getMock(); $this->managementMock = $this->getMockBuilder(AccountManagement::class) ->disableOriginalConstructor() - ->setMethods(['createAccount']) + ->setMethods(['createAccount', 'validateCustomerStoreIdByWebsiteId']) ->getMock(); $this->addressDataFactoryMock = $this->getMockBuilder(AddressInterfaceFactory::class) ->disableOriginalConstructor() @@ -522,6 +522,9 @@ public function testExecuteWithExistentCustomer() ->with('customer/*/edit', ['id' => $customerId, '_current' => true]) ->willReturn(true); + $this->managementMock->method('validateCustomerStoreIdByWebsiteId') + ->willReturn(true); + $this->assertEquals($redirectMock, $this->model->execute()); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index 3c38cd0f7b4e2..2344e0c8bce02 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -329,7 +329,6 @@ public function testCreateAccountWithPasswordHashWithExistingCustomer() public function testCreateAccountWithPasswordHashWithCustomerWithoutStoreId() { $websiteId = 1; - $storeId = null; $defaultStoreId = 1; $customerId = 1; $customerEmail = 'email@email.com'; @@ -359,9 +358,9 @@ public function testCreateAccountWithPasswordHashWithCustomerWithoutStoreId() $customer->expects($this->atLeastOnce()) ->method('getWebsiteId') ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) + $customer->expects($this->at(10)) ->method('getStoreId') - ->willReturn($storeId); + ->willReturn(1); $customer->expects($this->once()) ->method('setStoreId') ->with($defaultStoreId); @@ -378,9 +377,7 @@ public function testCreateAccountWithPasswordHashWithCustomerWithoutStoreId() ->method('get') ->with($customerEmail) ->willReturn($customer); - $this->share - ->expects($this->atLeastOnce()) - ->method('isWebsiteScope') + $this->share->method('isWebsiteScope') ->willReturn(true); $this->storeManager ->expects($this->atLeastOnce()) @@ -405,7 +402,6 @@ public function testCreateAccountWithPasswordHashWithCustomerWithoutStoreId() public function testCreateAccountWithPasswordHashWithLocalizedException() { $websiteId = 1; - $storeId = null; $defaultStoreId = 1; $customerId = 1; $customerEmail = 'email@email.com'; @@ -419,8 +415,7 @@ public function testCreateAccountWithPasswordHashWithLocalizedException() ->method('getId') ->willReturn($defaultStoreId); $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->once()) - ->method('getStoreIds') + $website->method('getStoreIds') ->willReturn([1, 2, 3]); $website->expects($this->once()) ->method('getDefaultStore') @@ -435,9 +430,9 @@ public function testCreateAccountWithPasswordHashWithLocalizedException() $customer->expects($this->atLeastOnce()) ->method('getWebsiteId') ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) + $customer->expects($this->at(10)) ->method('getStoreId') - ->willReturn($storeId); + ->willReturn(1); $customer->expects($this->once()) ->method('setStoreId') ->with($defaultStoreId); @@ -454,9 +449,7 @@ public function testCreateAccountWithPasswordHashWithLocalizedException() ->method('get') ->with($customerEmail) ->willReturn($customer); - $this->share - ->expects($this->once()) - ->method('isWebsiteScope') + $this->share->method('isWebsiteScope') ->willReturn(true); $this->storeManager ->expects($this->atLeastOnce()) @@ -481,7 +474,6 @@ public function testCreateAccountWithPasswordHashWithLocalizedException() public function testCreateAccountWithPasswordHashWithAddressException() { $websiteId = 1; - $storeId = null; $defaultStoreId = 1; $customerId = 1; $customerEmail = 'email@email.com'; @@ -498,8 +490,7 @@ public function testCreateAccountWithPasswordHashWithAddressException() ->method('getId') ->willReturn($defaultStoreId); $website = $this->getMockBuilder(\Magento\Store\Model\Website::class)->disableOriginalConstructor()->getMock(); - $website->expects($this->once()) - ->method('getStoreIds') + $website->method('getStoreIds') ->willReturn([1, 2, 3]); $website->expects($this->once()) ->method('getDefaultStore') @@ -514,9 +505,9 @@ public function testCreateAccountWithPasswordHashWithAddressException() $customer->expects($this->atLeastOnce()) ->method('getWebsiteId') ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) + $customer->expects($this->at(10)) ->method('getStoreId') - ->willReturn($storeId); + ->willReturn(1); $customer->expects($this->once()) ->method('setStoreId') ->with($defaultStoreId); @@ -533,9 +524,7 @@ public function testCreateAccountWithPasswordHashWithAddressException() ->method('get') ->with($customerEmail) ->willReturn($customer); - $this->share - ->expects($this->once()) - ->method('isWebsiteScope') + $this->share->method('isWebsiteScope') ->willReturn(true); $this->storeManager ->expects($this->atLeastOnce()) @@ -648,7 +637,6 @@ public function testCreateAccountWithPasswordHashWithNewCustomerAndLocalizedExce public function testCreateAccountWithoutPassword() { $websiteId = 1; - $storeId = null; $defaultStoreId = 1; $customerId = 1; $customerEmail = 'email@email.com'; @@ -683,9 +671,8 @@ public function testCreateAccountWithoutPassword() $customer->expects($this->atLeastOnce()) ->method('getWebsiteId') ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) - ->method('getStoreId') - ->willReturn($storeId); + $customer->expects($this->at(10))->method('getStoreId') + ->willReturn(1); $customer->expects($this->once()) ->method('setStoreId') ->with($defaultStoreId); @@ -700,8 +687,7 @@ public function testCreateAccountWithoutPassword() ->method('get') ->with($customerEmail) ->willReturn($customer); - $this->share->expects($this->once()) - ->method('isWebsiteScope') + $this->share->method('isWebsiteScope') ->willReturn(true); $this->storeManager->expects($this->atLeastOnce()) ->method('getWebsite') @@ -861,7 +847,6 @@ public function testCreateAccountInputExceptionExtraLongPassword() public function testCreateAccountWithPassword() { $websiteId = 1; - $storeId = null; $defaultStoreId = 1; $customerId = 1; $customerEmail = 'email@email.com'; @@ -940,9 +925,9 @@ public function testCreateAccountWithPassword() $customer->expects($this->atLeastOnce()) ->method('getWebsiteId') ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) + $customer->expects($this->at(11)) ->method('getStoreId') - ->willReturn($storeId); + ->willReturn(1); $customer->expects($this->once()) ->method('setStoreId') ->with($defaultStoreId); @@ -957,8 +942,7 @@ public function testCreateAccountWithPassword() ->method('get') ->with($customerEmail) ->willReturn($customer); - $this->share->expects($this->once()) - ->method('isWebsiteScope') + $this->share->method('isWebsiteScope') ->willReturn(true); $this->storeManager->expects($this->atLeastOnce()) ->method('getWebsite') @@ -1810,62 +1794,37 @@ public function dataProviderGetConfirmationStatus() /** * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Exception message */ - public function testCreateAccountWithPasswordHashForGuest() + public function testCreateAccountWithPasswordHashForGuestException() { $storeId = 1; - $storeName = 'store_name'; $websiteId = 1; $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() ->getMock(); - $storeMock->expects($this->once()) - ->method('getId') + $storeMock->method('getId') ->willReturn($storeId); - $storeMock->expects($this->once()) - ->method('getWebsiteId') - ->willReturn($websiteId); - $storeMock->expects($this->once()) - ->method('getName') - ->willReturn($storeName); - - $this->storeManager->expects($this->exactly(3)) - ->method('getStore') - ->willReturn($storeMock); + $this->storeManager->method('getStores') + ->willReturn([$storeMock]); $customerMock = $this->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->getMock(); - $customerMock->expects($this->exactly(2)) - ->method('getId') - ->willReturn(null); - $customerMock->expects($this->exactly(3)) + $customerMock->expects($this->at(1)) ->method('getStoreId') - ->willReturn(null); - $customerMock->expects($this->exactly(3)) + ->willReturn($storeId); + $customerMock->expects($this->at(4)) + ->method('getStoreId') + ->willReturn($storeId); + $customerMock->expects($this->at(2)) ->method('getWebsiteId') - ->willReturn(null); - $customerMock->expects($this->once()) - ->method('setStoreId') - ->with($storeId) - ->willReturnSelf(); - $customerMock->expects($this->once()) - ->method('setWebsiteId') - ->with($websiteId) - ->willReturnSelf(); - $customerMock->expects($this->once()) - ->method('setCreatedIn') - ->with($storeName) - ->willReturnSelf(); - $customerMock->expects($this->once()) - ->method('getAddresses') - ->willReturn(null); - $customerMock->expects($this->once()) - ->method('setAddresses') - ->with(null) - ->willReturnSelf(); + ->willReturn($websiteId); + $customerMock->expects($this->at(5)) + ->method('getId') + ->willReturn(1); $this->customerRepository ->expects($this->once()) @@ -2030,7 +1989,6 @@ private function prepareDateTimeFactory() public function testCreateAccountUnexpectedValueException(): void { $websiteId = 1; - $storeId = null; $defaultStoreId = 1; $customerId = 1; $customerEmail = 'email@email.com'; @@ -2048,8 +2006,7 @@ public function testCreateAccountUnexpectedValueException(): void ->method('getId') ->willReturn($defaultStoreId); $website = $this->createMock(\Magento\Store\Model\Website::class); - $website->expects($this->atLeastOnce()) - ->method('getStoreIds') + $website->method('getStoreIds') ->willReturn([1, 2, 3]); $website->expects($this->once()) ->method('getDefaultStore') @@ -2064,9 +2021,9 @@ public function testCreateAccountUnexpectedValueException(): void $customer->expects($this->atLeastOnce()) ->method('getWebsiteId') ->willReturn($websiteId); - $customer->expects($this->atLeastOnce()) + $customer->expects($this->at(10)) ->method('getStoreId') - ->willReturn($storeId); + ->willReturn(1); $customer->expects($this->once()) ->method('setStoreId') ->with($defaultStoreId); @@ -2080,8 +2037,7 @@ public function testCreateAccountUnexpectedValueException(): void ->method('get') ->with($customerEmail) ->willReturn($customer); - $this->share->expects($this->once()) - ->method('isWebsiteScope') + $this->share->method('isWebsiteScope') ->willReturn(true); $this->storeManager->expects($this->atLeastOnce()) ->method('getWebsite') @@ -2162,4 +2118,49 @@ public function testCreateAccountWithStoreNotInWebsite() ->willReturn($website); $this->accountManagement->createAccountWithPasswordHash($customerMock, $hash); } + + /** + * Test for validating customer store id by customer website id. + * + * @return void + */ + public function testValidateCustomerStoreIdByWebsiteId(): void + { + $customerMock = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $customerMock->method('getWebsiteId')->willReturn(1); + $customerMock->method('getStoreId')->willReturn(1); + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + $storeMock->method('getId') + ->willReturn(1); + $this->storeManager->method('getStores') + ->willReturn([$storeMock]); + + $this->assertTrue($this->accountManagement->validateCustomerStoreIdByWebsiteId($customerMock)); + } + + /** + * Test for validating customer store id by customer website id with Exception + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage The store view is not in the associated website. + */ + public function testValidateCustomerStoreIdByWebsiteIdException(): void + { + $customerMock = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + $storeMock->method('getId') + ->willReturn(1); + $this->storeManager->method('getStores') + ->willReturn([$storeMock]); + + $this->assertTrue($this->accountManagement->validateCustomerStoreIdByWebsiteId($customerMock)); + } } diff --git a/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/StoreTest.php b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/StoreTest.php new file mode 100644 index 0000000000000..2e34bcf7ab698 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/StoreTest.php @@ -0,0 +1,210 @@ +systemStore = $this->createMock(SystemStore::class); + $this->store = $this->createMock(Store::class); + $this->configShare = $this->createMock(ConfigShare::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->dataPersistor = $this->createMock(DataPersistorInterface::class); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->customerStore = $this->objectManagerHelper->getObject( + CustomerStore::class, + [ + 'systemStore' => $this->systemStore, + 'configShare' => $this->configShare, + 'storeManager' => $this->storeManager, + 'dataPersistor' => $this->dataPersistor + ] + ); + } + + /** + * Test that method return correct array of options + * + * @param array $options + * @param bool $isWebsiteScope + * @param bool $isCustomerDataInSession + * @dataProvider dataProviderOptionsArray + * @return void + */ + public function testToOptionArray(array $options, bool $isWebsiteScope, bool $isCustomerDataInSession): void + { + $this->configShare->method('isWebsiteScope') + ->willReturn($isWebsiteScope); + $this->store->method('getWebsiteId') + ->willReturn(1); + + if ($isCustomerDataInSession) { + $this->dataPersistor->method('get') + ->with('customer') + ->willReturn([ + 'account' => ['website_id' => '1'] + ]); + } else { + $this->storeManager->method('getDefaultStoreView') + ->willReturn($this->store); + } + + $this->systemStore->method('getStoreData') + ->willReturn($this->store); + $this->systemStore->method('getStoreValuesForForm') + ->willReturn([ + [ + 'label' => 'Main Website', + 'value' => [], + '__disableTmpl' => true, + ], + [ + 'label' => 'Main Website', + 'value' => [ + [ + 'label' => '    Default Store View', + 'value' => '1', + ] + ], + '__disableTmpl' => true, + ] + ]); + + $this->assertEquals($options, $this->customerStore->toOptionArray()); + } + + /** + * Data provider for testToOptionArray test + * + * @return array + */ + public function dataProviderOptionsArray(): array + { + return [ + [ + 'options' => [ + [ + 'label' => 'Main Website', + 'value' => [], + '__disableTmpl' => true, + 'website_id' => '1', + ], + [ + 'label' => 'Main Website', + 'value' => [ + [ + 'label' => '    Default Store View', + 'value' => '1', + 'website_id' => '1', + ] + ], + '__disableTmpl' => true, + 'website_id' => '1', + ] + ], + 'isWebsiteScope' => true, + 'isCustomerDataInSession' => false, + ], + [ + 'options' => [ + [ + 'label' => 'Main Website', + 'value' => [], + '__disableTmpl' => true, + 'website_id' => '1', + ], + [ + 'label' => 'Main Website', + 'value' => [ + [ + 'label' => '    Default Store View', + 'value' => '1', + 'website_id' => '1', + ] + ], + '__disableTmpl' => true, + 'website_id' => '1', + ] + ], + 'isWebsiteScope' => false, + 'isCustomerDataInSession' => false, + ], + [ + 'options' => [ + [ + 'label' => 'Main Website', + 'value' => [], + '__disableTmpl' => true, + 'website_id' => '1', + ], + [ + 'label' => 'Main Website', + 'value' => [ + [ + 'label' => '    Default Store View', + 'value' => '1', + 'website_id' => '1', + ] + ], + '__disableTmpl' => true, + 'website_id' => '1', + ] + ], + 'isWebsiteScope' => false, + 'isCustomerDataInSession' => true, + ] + ]; + } +} diff --git a/app/code/Magento/Customer/ViewModel/Customer/Store.php b/app/code/Magento/Customer/ViewModel/Customer/Store.php new file mode 100644 index 0000000000000..1e6ca69e2d77a --- /dev/null +++ b/app/code/Magento/Customer/ViewModel/Customer/Store.php @@ -0,0 +1,131 @@ +systemStore = $systemStore; + $this->configShare = $configShare; + $this->storeManager = $storeManager; + $this->dataPersistor = $dataPersistor; + } + + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return (bool)$this->configShare->isWebsiteScope() ? $this->getStoreOptions() + : $this->getStoreOptionsWithCurrentWebsiteId(); + } + + /** + * Adding website ID to options list + * + * @return array + */ + private function getStoreOptions(): array + { + $options = $this->systemStore->getStoreValuesForForm(); + + $websiteKey = null; + foreach ($options as $key => $option) { + if ($websiteKey === null) { + $websiteKey = $key; + } + if (is_array($option['value']) && !empty($option['value'])) { + $websiteId = null; + foreach ($option['value'] as $storeViewKey => $storeView) { + $websiteId = $this->systemStore->getStoreData($storeView['value'])->getWebsiteId(); + $options[$key]['value'][$storeViewKey]['website_id'] = $websiteId; + } + if ($websiteId) { + $options[$key]['website_id'] = $websiteId; + if ($websiteKey !== null) { + $options[$websiteKey]['website_id'] = $websiteId; + $websiteKey = null; + } + } + } + } + + return $options; + } + + /** + * Adding current website ID to options list + * + * @return array + */ + private function getStoreOptionsWithCurrentWebsiteId(): array + { + $options = $this->systemStore->getStoreValuesForForm(); + + if (!empty($this->dataPersistor->get('customer')['account'])) { + $currentWebsiteId = (string)$this->dataPersistor->get('customer')['account']['website_id']; + } else { + $currentWebsiteId = $this->storeManager->getDefaultStoreView()->getWebsiteId(); + } + + foreach ($options as $key => $option) { + $options[$key]['website_id'] = $currentWebsiteId; + if (is_array($option['value']) && !empty($option['value'])) { + foreach ($option['value'] as $storeViewKey => $storeView) { + $storeView['website_id'] = $currentWebsiteId; + $options[$key]['value'][$storeViewKey] = $storeView; + } + } + } + + return $options; + } +} diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 7caaeab4f39d6..d5c7154a30f54 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -301,7 +301,7 @@ true - + customer @@ -317,7 +317,11 @@ diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js index 6ff7c1f673213..ab806e89385b6 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/optgroup.js @@ -250,7 +250,7 @@ define([ // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document. // That's why we first added them without selection. Now it's time to set the selection. - if (previousSelectedValues.length) { + if (previousSelectedValues.length && newOptions.value) { isSelected = ko.utils.arrayIndexOf( previousSelectedValues, ko.selectExtensions.readValue(newOptions.value) From f755b429c8022b8a2b03a5c9da0aea5100c90d41 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Mon, 10 Feb 2020 09:43:47 +0200 Subject: [PATCH 3/5] MC-30650: [Magento Cloud] Customer Creation - The store view is not in the associated website --- app/code/Magento/Customer/i18n/en_US.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Customer/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index a70aa08dba735..0bcd5ce27dbb5 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -540,3 +540,5 @@ Addresses,Addresses "Middle Name/Initial","Middle Name/Initial" "Suffix","Suffix" "The Date of Birth should not be greater than today.","The Date of Birth should not be greater than today." +"The store view is not in the associated website.","The store view is not in the associated website." +"The Store View selected for sending Welcome email from is not related to the customer's associated website.","The Store View selected for sending Welcome email from is not related to the customer's associated website." From c5cbcfe14a9b3019ae3f4470e857176ddbaa54f0 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh Date: Mon, 10 Feb 2020 14:57:03 +0200 Subject: [PATCH 4/5] MC-24466: Unable to use API to save quote item when backorder is set to "Allowed and Notify Customer" --- .../Model/Quote/Item/CartItemPersister.php | 16 ++-- .../Quote/Api/GuestCartItemRepositoryTest.php | 73 ++++++++++++++++++- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Quote/Model/Quote/Item/CartItemPersister.php b/app/code/Magento/Quote/Model/Quote/Item/CartItemPersister.php index 9b5f5c9a126df..86dcd0e4bfc07 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/CartItemPersister.php +++ b/app/code/Magento/Quote/Model/Quote/Item/CartItemPersister.php @@ -6,14 +6,17 @@ namespace Magento\Quote\Model\Quote\Item; -use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\LocalizedException; -use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartItemInterface; +/** + * Cart item save handler + */ class CartItemPersister { /** @@ -39,6 +42,8 @@ public function __construct( } /** + * Save cart item into cart + * * @param CartInterface $quote * @param CartItemInterface $item * @return CartItemInterface @@ -73,12 +78,13 @@ public function save(CartInterface $quote, CartItemInterface $item) $item = $quote->updateItem($itemId, $buyRequestData); } else { if ($item->getQty() !== $currentItem->getQty()) { + $currentItem->clearMessage(); $currentItem->setQty($qty); /** * Qty validation errors are stored as items message * @see \Magento\CatalogInventory\Model\Quote\Item\QuantityValidator::validate */ - if (!empty($currentItem->getMessage())) { + if (!empty($currentItem->getMessage()) && $currentItem->getHasError()) { throw new LocalizedException(__($currentItem->getMessage())); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php index 00c8bb85d9be7..e03a54f9463d7 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php @@ -6,6 +6,8 @@ */ namespace Magento\Quote\Api; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Model\Stock; use Magento\TestFramework\TestCase\WebapiAbstract; class GuestCartItemRepositoryTest extends WebapiAbstract @@ -167,9 +169,13 @@ public function testRemoveItem() /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @param array $stockData + * @param string|null $errorMessage + * @dataProvider updateItemDataProvider */ - public function testUpdateItem() + public function testUpdateItem(array $stockData, string $errorMessage = null) { + $this->updateStockData('simple_one', $stockData); /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); @@ -215,6 +221,9 @@ public function testUpdateItem() ], ]; } + if ($errorMessage) { + $this->expectExceptionMessage($errorMessage); + } $this->_webApiCall($serviceInfo, $requestData); $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); @@ -223,4 +232,66 @@ public function testUpdateItem() $this->assertEquals(5, $item->getQty()); $this->assertEquals($itemId, $item->getItemId()); } + + /** + * @return array + */ + public function updateItemDataProvider(): array + { + return [ + [ + [] + ], + [ + [ + 'qty' => 0, + 'is_in_stock' => 1, + 'use_config_manage_stock' => 0, + 'manage_stock' => 1, + 'use_config_backorders' => 0, + 'backorders' => Stock::BACKORDERS_YES_NOTIFY, + ] + ], + [ + [ + 'qty' => 0, + 'is_in_stock' => 1, + 'use_config_manage_stock' => 0, + 'manage_stock' => 1, + 'use_config_backorders' => 0, + 'backorders' => Stock::BACKORDERS_NO, + ], + 'This product is out of stock.' + ], + [ + [ + 'qty' => 2, + 'is_in_stock' => 1, + 'use_config_manage_stock' => 0, + 'manage_stock' => 1, + 'use_config_backorders' => 0, + 'backorders' => Stock::BACKORDERS_NO, + ], + 'The requested qty is not available' + ] + ]; + } + + /** + * Update product stock + * + * @param string $sku + * @param array $stockData + * @return void + */ + private function updateStockData(string $sku, array $stockData): void + { + if ($stockData) { + /** @var $stockRegistry StockRegistryInterface */ + $stockRegistry = $this->objectManager->create(StockRegistryInterface::class); + $stockItem = $stockRegistry->getStockItemBySku($sku); + $stockItem->addData($stockData); + $stockRegistry->updateStockItemBySku($sku, $stockItem); + } + } } From 5f02e44917e639406c531cef6d8b907ac43a83fd Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi Date: Tue, 11 Feb 2020 12:53:57 +0200 Subject: [PATCH 5/5] MC-31157: Error on Place order with Braintree and 3d-secure. Address validation --- .../frontend/web/js/view/payment/3d-secure.js | 21 ++++++++++++++++++- .../view/payment/method-renderer/cc-form.js | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/3d-secure.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/3d-secure.js index 43aec27508ce9..b66725c063414 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/3d-secure.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/3d-secure.js @@ -117,7 +117,7 @@ define([ options.bin = context.paymentPayload.details.bin; } - if (shippingAddress) { + if (shippingAddress && this.isValidShippingAddress(shippingAddress)) { options.additionalInformation = { shippingGivenName: shippingAddress.firstname, shippingSurname: shippingAddress.lastname, @@ -206,6 +206,25 @@ define([ } return false; + }, + + /** + * Validate shipping address + * + * @param {Object} shippingAddress + * @return {Boolean} + */ + isValidShippingAddress: function (shippingAddress) { + var isValid = false; + + // check that required fields are not empty + if (shippingAddress.firstname && shippingAddress.lastname && shippingAddress.telephone && + shippingAddress.street && shippingAddress.city && shippingAddress.regionCode && + shippingAddress.postcode && shippingAddress.countryId) { + isValid = true; + } + + return isValid; } }; }); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js index afe22475981ec..21809f186d252 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/cc-form.js @@ -91,7 +91,7 @@ define( }) .then(function (hostedFieldsInstance) { self.hostedFieldsInstance = hostedFieldsInstance; - self.isPlaceOrderActionAllowed(true); + self.isPlaceOrderActionAllowed(false); self.initFormValidationEvents(hostedFieldsInstance); return self.hostedFieldsInstance;