diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminFillSearchTermActionGroup.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminFillSearchTermActionGroup.xml new file mode 100644 index 0000000000000..e402ec0391c0a --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminFillSearchTermActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + Fills the search terms form with sample data. + + + + + + + + + + + + + + diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminOpenNewSearchTermsPageActionGroup.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminOpenNewSearchTermsPageActionGroup.xml new file mode 100644 index 0000000000000..2fbbab8a55721 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminOpenNewSearchTermsPageActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Navigate to search terms form page. + + + + + diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminSaveSearchTermActionGroup.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminSaveSearchTermActionGroup.xml new file mode 100644 index 0000000000000..cac996d97a6bf --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/ActionGroup/AdminSaveSearchTermActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + Save a new search term from Magento admin. + + + + + diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/Data/SearchTermsData.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/Data/SearchTermsData.xml new file mode 100644 index 0000000000000..913784afe66ea --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/Data/SearchTermsData.xml @@ -0,0 +1,17 @@ + + + + + + books + Default Store View + http://sample.com + 1 + + diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/Page/AdminSearchTermsFormPage.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/Page/AdminSearchTermsFormPage.xml new file mode 100644 index 0000000000000..b8997093de840 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/Page/AdminSearchTermsFormPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/Section/AdminSearchTermsPageFormFieldsSection.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/Section/AdminSearchTermsPageFormFieldsSection.xml new file mode 100644 index 0000000000000..ba593b332bf8a --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/Section/AdminSearchTermsPageFormFieldsSection.xml @@ -0,0 +1,17 @@ + + + + +
+ + + + +
+
diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml new file mode 100644 index 0000000000000..88c04263a1bc5 --- /dev/null +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + <description value="Admin should be able to create a new search term using search terms grid"/> + <severity value="CRITICAL"/> + <group value="AdvancedSearch"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminOpenNewSearchTermsPageActionGroup" stepKey="navigateToSearchTermPage"/> + <actionGroup ref="AdminFillSearchTermActionGroup" stepKey="fillNewSearchTermData"> + <argument name="searchQuery" value="{{SearchTerms.searchQuery}}"/> + <argument name="store" value="{{SearchTerms.store}}"/> + <argument name="redirectUrl" value="{{SearchTerms.redirectUrl}}"/> + <argument name="suggestedTerms" value="{{SearchTerms.suggestedTerms}}"/> + </actionGroup> + <actionGroup ref="AdminSaveSearchTermActionGroup" stepKey="saveSearchTerm"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSaveSearchTermSuccessMessage"> + <argument name="message" value="You saved the search term."/> + <argument name="messageType" value="success"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Data/GeneralLocalConfigsData.xml b/app/code/Magento/Backend/Test/Mftf/Data/GeneralLocalConfigsData.xml index 22d595c39407f..0477befccc06e 100644 --- a/app/code/Magento/Backend/Test/Mftf/Data/GeneralLocalConfigsData.xml +++ b/app/code/Magento/Backend/Test/Mftf/Data/GeneralLocalConfigsData.xml @@ -20,4 +20,10 @@ <data key="scope_code">base</data> <data key="value">en_US</data> </entity> + <entity name="GeneralLocalCodeConfigsForMexico"> + <data key="path">general/locale/code</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + <data key="value">es_MX</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Helper/Output.php b/app/code/Magento/Catalog/Helper/Output.php index 93b67965e7234..c61268cd7664e 100644 --- a/app/code/Magento/Catalog/Helper/Output.php +++ b/app/code/Magento/Catalog/Helper/Output.php @@ -182,7 +182,7 @@ public function productAttribute($product, $attributeHtml, $attributeName) if ($attributeHtml !== null && $attribute->getIsHtmlAllowedOnFront() && $attribute->getIsWysiwygEnabled() - && $this->isDirectivesExists($attributeHtml) + && $this->isDirectivesExists((string)$attributeHtml) ) { $attributeHtml = $this->_getTemplateProcessor()->filter($attributeHtml); } @@ -219,7 +219,7 @@ public function categoryAttribute($category, $attributeHtml, $attributeName) if ($attributeHtml !== null && $attribute->getIsHtmlAllowedOnFront() && $attribute->getIsWysiwygEnabled() - && $this->isDirectivesExists($attributeHtml) + && $this->isDirectivesExists((string)$attributeHtml) ) { $attributeHtml = $this->_getTemplateProcessor()->filter($attributeHtml); @@ -238,7 +238,7 @@ public function categoryAttribute($category, $attributeHtml, $attributeName) * @param string $attributeHtml * @return bool */ - public function isDirectivesExists($attributeHtml) + public function isDirectivesExists(string $attributeHtml): bool { $matches = false; foreach ($this->directivePatterns as $pattern) { diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index d89ffa0055d3b..3eaae60d789f5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -39,19 +39,19 @@ <createData entity="CustomerAccountSharingGlobal" stepKey="setConfigCustomerAccountToGlobal"/> </before> - <!--Create website, Sore adn Store View--> + <!--Create website, Store and Store View--> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> - <argument name="newWebsiteName" value="secondWebsite"/> - <argument name="websiteCode" value="second_website"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> - <argument name="website" value="secondWebsite"/> - <argument name="storeGroupName" value="secondStore"/> - <argument name="storeGroupCode" value="second_store"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> </actionGroup> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> - <argument name="StoreGroup" value="customStoreTierPrice"/> - <argument name="customStore" value="customStoreView"/> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> <!--Set Configuration--> <createData entity="CatalogPriceScopeWebsite" stepKey="paymentMethodsSettingConfig"/> @@ -63,10 +63,10 @@ <argument name="product" value="$$product1$$"/> </actionGroup> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricingActionGroup" stepKey="ProductSetAdvancedPricing1"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct2"> @@ -76,10 +76,10 @@ <argument name="product" value="$$product2$$"/> </actionGroup> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite2"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricingActionGroup" stepKey="ProductSetAdvancedPricing2"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct3"> @@ -89,10 +89,10 @@ <argument name="product" value="$$product3$$"/> </actionGroup> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite3"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricingActionGroup" stepKey="ProductSetAdvancedPricing3"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct4"> @@ -102,10 +102,10 @@ <argument name="product" value="$$product4$$"/> </actionGroup> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite4"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="ProductSetAdvancedPricingActionGroup" stepKey="ProductSetAdvancedPricing4"> - <argument name="website" value="secondWebsite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="ClearProductsFilterActionGroup" stepKey="ClearProductsFilterActionGroup"/> @@ -120,7 +120,7 @@ <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="ClickOnAccountInformationSection"/> <waitForPageLoad stepKey="waitForPageOpened1"/> <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="Group"/> - <selectOption selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="secondStoreView" stepKey="clickToSelectStore"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{customStore.name}}" stepKey="clickToSelectStore"/> <click selector="{{AdminCustomerAccountInformationSection.saveCustomer}}" stepKey="save"/> <waitForPageLoad stepKey="waitForCustomersPage"/> <see userInput="You saved the customer." stepKey="CustomerIsSaved"/> @@ -136,7 +136,7 @@ <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> <waitForPageLoad stepKey="waitForPageDiscountPageIsLoaded"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="ship" stepKey="fillRuleName"/> - <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="secondWebsite" stepKey="selectWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{customWebsite.name}}" stepKey="selectWebsites"/> <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="Retailer" stepKey="selectCustomerGroup"/> <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="ship" stepKey="setCode"/> @@ -152,7 +152,7 @@ <!--Create new order--> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> <argument name="customer" value="Simple_US_Customer"/> - <argument name="storeView" value="customStoreView"/> + <argument name="storeView" value="customStore"/> </actionGroup> <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickToAddProduct"/> @@ -327,7 +327,7 @@ <argument name="ruleName" value="ship"/> </actionGroup> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> - <argument name="websiteName" value="secondWebsite"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> <createData entity="CustomerAccountSharingDefault" stepKey="setConfigCustomerAccountDefault"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php index 53f00529b9bcc..31b2ada809823 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php @@ -6,13 +6,21 @@ namespace Magento\CatalogInventory\Model\ResourceModel; +use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; /** * Stock resource model */ -class Stock extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements QtyCounterInterface +class Stock extends AbstractDb implements QtyCounterInterface { /** * @var StockConfigurationInterface @@ -64,12 +72,12 @@ class Stock extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb impleme /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_scopeConfig; /** - * @var \Magento\Framework\Stdlib\DateTime\DateTime + * @var DateTime */ protected $dateTime; @@ -80,17 +88,17 @@ class Stock extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb impleme protected $storeManager; /** - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime + * @param Context $context + * @param ScopeConfigInterface $scopeConfig + * @param DateTime $dateTime * @param StockConfigurationInterface $stockConfiguration * @param StoreManagerInterface $storeManager * @param string $connectionName */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, + Context $context, + ScopeConfigInterface $scopeConfig, + DateTime $dateTime, StockConfigurationInterface $stockConfiguration, StoreManagerInterface $storeManager, $connectionName = null @@ -125,9 +133,18 @@ public function lockProductsStock(array $productIds, $websiteId) return []; } $itemTable = $this->getTable('cataloginventory_stock_item'); - $select = $this->getConnection()->select()->from(['si' => $itemTable]) + + //get indexed entries for row level lock instead of table lock + $itemIds = []; + $preSelect = $this->getConnection()->select()->from($itemTable, 'item_id') ->where('website_id = ?', $websiteId) - ->where('product_id IN(?)', $productIds) + ->where('product_id IN(?)', $productIds); + foreach ($this->getConnection()->query($preSelect)->fetchAll() as $item) { + $itemIds[] = (int)$item['item_id']; + } + + $select = $this->getConnection()->select()->from(['si' => $itemTable]) + ->where('item_id IN (?)', $itemIds) ->forUpdate(true); $productTable = $this->getTable('catalog_product_entity'); @@ -147,12 +164,12 @@ public function lockProductsStock(array $productIds, $websiteId) foreach ($this->getConnection()->fetchAll($selectProducts) as $p) { $items[$p['product_id']]['type_id'] = $p['type_id']; } - + return $items; } /** - * {@inheritdoc} + * @inheritdoc */ public function correctItemsQty(array $items, $websiteId, $operator) { @@ -185,16 +202,16 @@ protected function _initConfig() { if (!$this->_isConfig) { $configMap = [ - '_isConfigManageStock' => \Magento\CatalogInventory\Model\Configuration::XML_PATH_MANAGE_STOCK, - '_isConfigBackorders' => \Magento\CatalogInventory\Model\Configuration::XML_PATH_BACKORDERS, - '_configMinQty' => \Magento\CatalogInventory\Model\Configuration::XML_PATH_MIN_QTY, - '_configNotifyStockQty' => \Magento\CatalogInventory\Model\Configuration::XML_PATH_NOTIFY_STOCK_QTY, + '_isConfigManageStock' => Configuration::XML_PATH_MANAGE_STOCK, + '_isConfigBackorders' => Configuration::XML_PATH_BACKORDERS, + '_configMinQty' => Configuration::XML_PATH_MIN_QTY, + '_configNotifyStockQty' => Configuration::XML_PATH_NOTIFY_STOCK_QTY, ]; foreach ($configMap as $field => $const) { $this->{$field} = (int) $this->_scopeConfig->getValue( $const, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } @@ -317,11 +334,11 @@ public function updateLowStockDate($website) /** * Add low stock filter to product collection * - * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @param Collection $collection * @param array $fields * @return $this */ - public function addLowStockFilter(\Magento\Catalog\Model\ResourceModel\Product\Collection $collection, $fields) + public function addLowStockFilter(Collection $collection, $fields) { $this->_initConfig(); $connection = $collection->getSelect()->getConnection(); @@ -344,14 +361,14 @@ public function addLowStockFilter(\Magento\Catalog\Model\ResourceModel\Product\C $where = []; foreach ($conditions as $k => $part) { - $where[$k] = join(' ' . \Magento\Framework\DB\Select::SQL_AND . ' ', $part); + $where[$k] = join(' ' . Select::SQL_AND . ' ', $part); } $where = $connection->prepareSqlCondition( 'invtr.low_stock_date', ['notnull' => true] - ) . ' ' . \Magento\Framework\DB\Select::SQL_AND . ' ((' . join( - ') ' . \Magento\Framework\DB\Select::SQL_OR . ' (', + ) . ' ' . Select::SQL_AND . ' ((' . join( + ') ' . Select::SQL_OR . ' (', $where ) . '))'; diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php index c9a148f3d8869..d5c1570fea510 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/StockTest.php @@ -7,73 +7,78 @@ namespace Magento\CatalogInventory\Test\Unit\Model\ResourceModel; -use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogInventory\Model\Configuration as StockConfiguration; use Magento\CatalogInventory\Model\ResourceModel\Stock; use Magento\Framework\App\Config; use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\DB\Select; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * Test for \Magento\CatalogInventory\Model\ResourceModel\Stock */ -class StockTest extends \PHPUnit\Framework\TestCase +class StockTest extends TestCase { const PRODUCT_TABLE = 'testProductTable'; const ITEM_TABLE = 'testItemTableName'; /** - * @var Stock|\PHPUnit_Framework_MockObject_MockObject + * @var Stock|MockObject */ private $stock; /** - * @var Mysql|\PHPUnit_Framework_MockObject_MockObject + * @var Mysql|MockObject */ private $connectionMock; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ private $scopeConfigMock; /** - * @var DateTime|\PHPUnit_Framework_MockObject_MockObject + * @var DateTime|MockObject */ private $dateTimeMock; /** - * @var StockConfiguration|\PHPUnit_Framework_MockObject_MockObject + * @var StockConfiguration|MockObject */ private $stockConfigurationMock; /** - * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|MockObject */ private $storeManagerMock; /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|MockObject */ private $contextMock; /** - * @var \Magento\Framework\DB\Select|\PHPUnit_Framework_MockObject_MockObject + * @var Select|MockObject */ private $selectMock; /** - * @var \Zend_Db_Statement_Interface|\PHPUnit_Framework_MockObject_MockObject + * @var \Zend_Db_Statement_Interface|MockObject */ private $statementMock; - + + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); - $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + $this->selectMock = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() ->getMock(); $this->contextMock = $objectManager->getObject(Context::class); @@ -115,23 +120,35 @@ protected function setUp() * @param array $productIds * @param array $products * @param array $result + * @param array $items * * @return void */ - public function testLockProductsStock(int $websiteId, array $productIds, array $products, array $result) - { - $this->selectMock->expects($this->exactly(2)) + public function testLockProductsStock( + int $websiteId, + array $productIds, + array $products, + array $result, + array $items + ) { + $itemIds = []; + foreach ($items as $item) { + $itemIds[] = $item['item_id']; + } + $this->selectMock->expects($this->exactly(3)) ->method('from') ->withConsecutive( + [$this->identicalTo(self::ITEM_TABLE)], [$this->identicalTo(['si' => self::ITEM_TABLE])], [$this->identicalTo(['p' => self::PRODUCT_TABLE]), $this->identicalTo([])] ) ->willReturnSelf(); - $this->selectMock->expects($this->exactly(3)) + $this->selectMock->expects($this->exactly(4)) ->method('where') ->withConsecutive( [$this->identicalTo('website_id = ?'), $this->identicalTo($websiteId)], [$this->identicalTo('product_id IN(?)'), $this->identicalTo($productIds)], + [$this->identicalTo('item_id IN (?)'), $this->identicalTo($itemIds)], [$this->identicalTo('entity_id IN (?)'), $this->identicalTo($productIds)] ) ->willReturnSelf(); @@ -143,14 +160,17 @@ public function testLockProductsStock(int $websiteId, array $productIds, array $ ->method('columns') ->with($this->identicalTo(['product_id' => 'entity_id', 'type_id' => 'type_id'])) ->willReturnSelf(); - $this->connectionMock->expects($this->exactly(2)) + $this->connectionMock->expects($this->exactly(3)) ->method('select') ->willReturn($this->selectMock); - $this->connectionMock->expects($this->once()) + $this->connectionMock->expects($this->exactly(2)) ->method('query') ->with($this->identicalTo($this->selectMock)) ->willReturn($this->statementMock); - $this->statementMock->expects($this->once()) + $this->statementMock->expects($this->at(0)) + ->method('fetchAll') + ->willReturn($items); + $this->statementMock->expects($this->at(1)) ->method('fetchAll') ->willReturn($products); $this->connectionMock->expects($this->once()) @@ -166,7 +186,7 @@ public function testLockProductsStock(int $websiteId, array $productIds, array $ self::ITEM_TABLE, self::PRODUCT_TABLE )); - $this->stock->expects($this->exactly(4)) + $this->stock->expects($this->exactly(6)) ->method('getConnection') ->willReturn($this->connectionMock); @@ -203,6 +223,7 @@ public function productsDataProvider(): array 'type_id' => 'simple', ], ], + [['item_id' => 1], ['item_id' => 2], ['item_id' => 3]] ], ]; } diff --git a/app/code/Magento/CatalogInventory/etc/db_schema.xml b/app/code/Magento/CatalogInventory/etc/db_schema.xml index b5c4a96f24a94..3747f3f89633f 100644 --- a/app/code/Magento/CatalogInventory/etc/db_schema.xml +++ b/app/code/Magento/CatalogInventory/etc/db_schema.xml @@ -87,6 +87,10 @@ <index referenceId="CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> + <index referenceId="CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID_PRODUCT_ID" indexType="btree"> + <column name="website_id"/> + <column name="product_id"/> + </index> <index referenceId="CATALOGINVENTORY_STOCK_ITEM_STOCK_ID" indexType="btree"> <column name="stock_id"/> </index> diff --git a/app/code/Magento/CatalogInventory/etc/db_schema_whitelist.json b/app/code/Magento/CatalogInventory/etc/db_schema_whitelist.json index 2580ec1e336f1..fd881ac6e521c 100644 --- a/app/code/Magento/CatalogInventory/etc/db_schema_whitelist.json +++ b/app/code/Magento/CatalogInventory/etc/db_schema_whitelist.json @@ -43,6 +43,7 @@ }, "index": { "CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID": true, + "CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID_PRODUCT_ID": true, "CATALOGINVENTORY_STOCK_ITEM_STOCK_ID": true }, "constraint": { @@ -123,4 +124,4 @@ "PRIMARY": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml new file mode 100644 index 0000000000000..1170b08b1add9 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCatalogPriceRuleDeleteAllActionGroup"> + <annotations> + <description>Open Catalog Price Rule grid and delete all rules one by one. Need to avoid interference with other tests that test catalog price rules.</description> + </annotations> + + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <helper class="\Magento\CatalogRule\Test\Mftf\Helper\CatalogPriceRuleHelper" method="deleteAllCatalogPriceRules" stepKey="deleteAllCatalogPriceRulesOneByOne"> + <argument name="firstNotEmptyRow">{{AdminDataGridTableSection.firstNotEmptyRow}}</argument> + <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> + <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> + <argument name="successMessageContainer">{{AdminMessagesSection.success}}</argument> + <argument name="successMessage">You deleted the rule.</argument> + </helper> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Helper/CatalogPriceRuleHelper.php b/app/code/Magento/CatalogRule/Test/Mftf/Helper/CatalogPriceRuleHelper.php new file mode 100644 index 0000000000000..2119f5c6ca45d --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Helper/CatalogPriceRuleHelper.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for CatalogRule module. + */ +class CatalogPriceRuleHelper extends Helper +{ + /** + * Delete all Catalog Price Rules obe by one. + * + * @param string $emptyRow + * @param string $modalAceptButton + * @param string $deleteButton + * @param string $successMessageContainer + * @param string $successMessage + * + * @return void + */ + public function deleteAllCatalogPriceRules( + string $firstNotEmptyRow, + string $modalAcceptButton, + string $deleteButton, + string $successMessageContainer, + string $successMessage + ): void { + try { + /** @var MagentoWebDriver $webDriver */ + $magentoWebDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + /** @var FacebookWebDriver $webDriver */ + $webDriver = $magentoWebDriver->webDriver; + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + while (!empty($rows)) { + $rows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForLoadingMaskToDisappear(); + $magentoWebDriver->waitForElementVisible($successMessageContainer, 10); + $magentoWebDriver->see($successMessage, $successMessageContainer); + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/Checkout/CustomerData/DefaultItem.php b/app/code/Magento/Checkout/CustomerData/DefaultItem.php index 21580d1275d0c..6c88e96cd535a 100644 --- a/app/code/Magento/Checkout/CustomerData/DefaultItem.php +++ b/app/code/Magento/Checkout/CustomerData/DefaultItem.php @@ -10,7 +10,7 @@ use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; /** - * Default item + * Default cart item */ class DefaultItem extends AbstractItem { @@ -78,7 +78,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function doGetItemData() { @@ -106,6 +106,7 @@ protected function doGetItemData() ], 'canApplyMsrp' => $this->msrpHelper->isShowBeforeOrderConfirm($this->item->getProduct()) && $this->msrpHelper->isMinimalPriceLessMsrp($this->item->getProduct()), + 'message' => $this->item->getMessage(), ]; } @@ -121,6 +122,8 @@ protected function getOptionList() } /** + * Returns product for thumbnail. + * * @return \Magento\Catalog\Model\Product * @codeCoverageIgnore */ @@ -130,6 +133,8 @@ protected function getProductForThumbnail() } /** + * Returns product. + * * @return \Magento\Catalog\Model\Product * @codeCoverageIgnore */ diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml index 3648ac361ccc9..4320eb8e6160f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyPromoCodeDuringCheckoutTest.xml @@ -29,21 +29,23 @@ <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> <requiredEntity createDataKey="createCartPriceRule"/> </createData> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> <!-- Delete simple product --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <!-- Delete sales rule --> <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> - + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> <!-- Admin log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Go to Storefront as Guest and add simple product to cart --> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> - <argument name="product" value="$$createProduct$$"/> + <argument name="product" value="$createProduct$"/> </actionGroup> <!-- Go to Checkout --> @@ -56,16 +58,18 @@ <!-- Click Apply Discount Code: section is expanded. Input promo code, apply and see success message --> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyCoupon"> - <argument name="discountCode" value="$$createCouponForCartPriceRule.code$$"/> + <argument name="discountCode" value="$createCouponForCartPriceRule.code$"/> </actionGroup> <!-- Apply button is disappeared --> <dontSeeElement selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="dontSeeApplyButton"/> <!-- Cancel coupon button is appeared --> + <waitForElementVisible selector="{{DiscountSection.CancelCouponBtn}}" stepKey="waitCancelButtonAppears"/> <seeElement selector="{{DiscountSection.CancelCouponBtn}}" stepKey="seeCancelCouponButton"/> <!-- Order summary contains information about applied code --> + <waitForElementVisible selector="{{CheckoutPaymentSection.discount}}" stepKey="waitForDiscountCouponInSummaryBlock"/> <seeElement selector="{{CheckoutPaymentSection.discount}}" stepKey="seeDiscountCouponInSummaryBlock"/> <see selector="{{CheckoutPaymentSection.discountPrice}}" userInput="-$5.00" stepKey="seeDiscountPrice"/> @@ -76,16 +80,13 @@ <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> - <!-- Login as admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!-- Verify total on order page --> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderById"> - <argument name="orderId" value="$grabOrderNumber"/> + <argument name="orderId" value="{$grabOrderNumber}"/> </actionGroup> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> - <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$$createProduct.price$$" stepKey="checkTotal"/> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$createProduct.price$" stepKey="checkTotal"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php index 9a408f1ecd1c8..7ac0ee645df63 100644 --- a/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/DefaultItemTest.php @@ -6,6 +6,7 @@ namespace Magento\Checkout\Test\Unit\CustomerData; use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; +use PHPUnit\Framework\MockObject\MockObject; class DefaultItemTest extends \PHPUnit\Framework\TestCase { @@ -25,7 +26,7 @@ class DefaultItemTest extends \PHPUnit\Framework\TestCase private $configurationPool; /** - * @var ItemResolverInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ItemResolverInterface|MockObject */ private $itemResolver; @@ -102,5 +103,6 @@ public function testGetItemData() $this->assertArrayHasKey('product_price_value', $itemData); $this->assertArrayHasKey('product_image', $itemData); $this->assertArrayHasKey('canApplyMsrp', $itemData); + $this->assertArrayHasKey('message', $itemData); } } diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml index 4b5d1033408e4..81ee1a5e6db4c 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml @@ -184,7 +184,7 @@ <block class="Magento\Framework\View\Element\RendererList" name="checkout.cart.item.renderers" as="renderer.list"/> <block class="Magento\Framework\View\Element\Text\ListText" name="checkout.cart.order.actions"/> </block> - <container name="checkout.cart.widget" as="checkout_cart_widget" label="Shopping Cart Items After"/> + <container name="checkout.cart.widget" as="checkout_cart_widget" label="Shopping Cart Items After" after="cart-items"/> </container> <block class="Magento\Checkout\Block\Cart\Crosssell" name="checkout.cart.crosssell" template="Magento_Catalog::product/list/items.phtml" after="-" ifconfig="checkout/cart/crosssell_enabled"> <arguments> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 32bbd66d13e68..5489089452d85 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -112,4 +112,7 @@ </div> </div> </div> + <div class="message notice" if="message"> + <div data-bind="text: message"></div> + </div> </li> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml index ff3d5f24dd5a1..168d1d7ace504 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml @@ -7,11 +7,16 @@ /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Files */ $_width = $block->getImagesWidth(); -$_height = $block->getImagesHeight(); ?> -<?php if ($block->getFilesCount() > 0) : ?> - <?php foreach ($block->getFiles() as $file) : ?> +<?php if ($block->getFilesCount() > 0): ?> + <?php foreach ($block->getFiles() as $file): ?> + <?php + $src = $block->getFileThumbUrl($file); + $width = $block->getFileWidth($file); + $height = $block->getFileHeight($file); + $filename = $block->getFileName($file); + ?> <div data-row="file" class="filecnt" @@ -19,17 +24,18 @@ $_height = $block->getImagesHeight(); data-size="<?= $block->escapeHtmlAttr($file->getSize()) ?>" data-mime-type="<?= $block->escapeHtmlAttr($file->getMimeType()) ?>" > - <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;"> - <?php if ($block->getFileThumbUrl($file)) : ?> - <img src="<?= $block->escapeHtmlAttr($block->getFileThumbUrl($file)) ?>" alt="<?= $block->escapeHtmlAttr($block->getFileName($file)) ?>"/> + <p class="nm"> + <?php if ($block->getFileThumbUrl($file)): ?> + <img src="<?= $block->escapeHtmlAttr($src) ?>" alt="<?= $block->escapeHtmlAttr($filename) ?>"/> <?php endif; ?> </p> - <?php if ($block->getFileWidth($file)) : ?> - <small><?= $block->escapeHtml($block->getFileWidth($file)) ?>x<?= $block->escapeHtml($block->getFileHeight($file)) ?> <?= $block->escapeHtml(__('px.')) ?></small><br/> + <?php if ($block->getFileWidth($file)): ?> + <small><?= $block->escapeHtmlAttr($width) ?>x<?= $block->escapeHtmlAttr($height) ?> + <?= $block->escapeHtml(__('px.')) ?></small><br/> <?php endif; ?> <small><?= $block->escapeHtml($block->getFileShortName($file)) ?></small> </div> <?php endforeach; ?> -<?php else : ?> +<?php else: ?> <div class="empty"><?= $block->escapeHtml(__('No files found')) ?></div> <?php endif; ?> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml index 6194287dd058b..d22430e3c0218 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/CurrencyRatesConfigData.xml @@ -20,6 +20,36 @@ <data key="scope">websites</data> <data key="scope_code">base</data> </entity> + <entity name="SetCurrencyYENBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">JPY</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetCurrencyCADBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">CAD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetCurrencyAUDBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">AUD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetCurrencyHKDBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">HKD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetCurrencyNZDBaseConfig"> + <data key="path">currency/options/base</data> + <data key="value">NZD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> <entity name="SetAllowedCurrenciesConfigForUSD"> <data key="path">currency/options/allow</data> <data key="value">USD</data> @@ -32,6 +62,36 @@ <data key="scope">websites</data> <data key="scope_code">base</data> </entity> + <entity name="SetAllowedCurrenciesConfigForYEN"> + <data key="path">currency/options/allow</data> + <data key="value">JPY</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForCAD"> + <data key="path">currency/options/allow</data> + <data key="value">CAD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForAUD"> + <data key="path">currency/options/allow</data> + <data key="value">AUD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForHKD"> + <data key="path">currency/options/allow</data> + <data key="value">HKD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetAllowedCurrenciesConfigForNZD"> + <data key="path">currency/options/allow</data> + <data key="value">NZD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> <entity name="SetAllowedCurrenciesConfigForRUB"> <data key="path">currency/options/allow</data> <data key="value">RUB</data> @@ -44,6 +104,36 @@ <data key="scope">websites</data> <data key="scope_code">base</data> </entity> + <entity name="SetDefaultCurrencyYENConfig"> + <data key="path">currency/options/default</data> + <data key="value">JPY</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyCADConfig"> + <data key="path">currency/options/default</data> + <data key="value">CAD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyAUDConfig"> + <data key="path">currency/options/default</data> + <data key="value">AUD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyHKDConfig"> + <data key="path">currency/options/default</data> + <data key="value">HKD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> + <entity name="SetDefaultCurrencyNZDConfig"> + <data key="path">currency/options/default</data> + <data key="value">NZD</data> + <data key="scope">websites</data> + <data key="scope_code">base</data> + </entity> <entity name="SetDefaultCurrencyUSDConfig"> <data key="path">currency/options/default</data> <data key="value">USD</data> diff --git a/app/code/Magento/Customer/Block/Address/Renderer/DefaultRenderer.php b/app/code/Magento/Customer/Block/Address/Renderer/DefaultRenderer.php index a8622d98948df..c10ff421b7f92 100644 --- a/app/code/Magento/Customer/Block/Address/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Customer/Block/Address/Renderer/DefaultRenderer.php @@ -8,6 +8,7 @@ use Magento\Customer\Model\Address\AddressModelInterface; use Magento\Customer\Model\Address\Mapper; use Magento\Customer\Model\Metadata\ElementFactory; +use Magento\Directory\Model\Country\Format; use Magento\Framework\View\Element\AbstractBlock; /** @@ -91,6 +92,8 @@ public function setType(\Magento\Framework\DataObject $type) } /** + * Get the format of the address + * * @param AddressModelInterface|null $address * @return string * All new code should use renderArray based on Metadata service @@ -106,8 +109,11 @@ public function getFormat(AddressModelInterface $address = null) } /** - * {@inheritdoc} + * Render address * + * @param AddressModelInterface $address + * @param string|null $format + * @return mixed * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -118,7 +124,7 @@ public function render(AddressModelInterface $address, $format = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFormatArray($addressAttributes = null) { @@ -133,8 +139,11 @@ public function getFormatArray($addressAttributes = null) } /** - * {@inheritdoc} + * Render address by attribute array * + * @param array $addressAttributes + * @param Format|null $format + * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -167,7 +176,7 @@ public function renderArray($addressAttributes, $format = null) $addressAttributes['country_id'] )->getName(); } elseif ($attributeCode == 'region' && isset($addressAttributes['region'])) { - $data['region'] = __($addressAttributes['region']); + $data['region'] = (string)__($addressAttributes['region']); } elseif (isset($addressAttributes[$attributeCode])) { $value = $addressAttributes[$attributeCode]; $dataModel = $this->_elementFactory->create($attributeMetadata, $value, 'customer_address'); diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php index 3aa08cfbf847e..a1ec3164a3c31 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirm.php +++ b/app/code/Magento/Customer/Controller/Account/Confirm.php @@ -24,6 +24,8 @@ /** * Class Confirm * + * Confirm class is responsible for account confirmation flow + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Confirm extends AbstractAccount implements HttpGetActionInterface @@ -168,7 +170,7 @@ public function execute() $metadata->setPath('/'); $this->getCookieManager()->deleteCookie('mage-cache-sessid', $metadata); } - $this->messageManager->addSuccessMessage($this->getSuccessMessage()); + $this->messageManager->addSuccess($this->getSuccessMessage()); $resultRedirect->setUrl($this->getSuccessRedirect()); return $resultRedirect; } catch (StateException $e) { diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php index e2a7c085a0b44..4c1cfa94c5565 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePost.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php @@ -149,6 +149,11 @@ class CreatePost extends AbstractAccount implements CsrfAwareActionInterface, Ht */ private $customerRepository; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param Context $context * @param Session $customerSession @@ -266,9 +271,15 @@ protected function extractAddress() $addressData = []; $regionDataObject = $this->regionDataFactory->create(); + $userDefinedAttr = $this->getRequest()->getParam('address') ?: []; foreach ($allowedAttributes as $attribute) { $attributeCode = $attribute->getAttributeCode(); - $value = $this->getRequest()->getParam($attributeCode); + if ($attribute->isUserDefined()) { + $value = array_key_exists($attributeCode, $userDefinedAttr) ? $userDefinedAttr[$attributeCode] : null; + } else { + $value = $this->getRequest()->getParam($attributeCode); + } + if ($value === null) { continue; } @@ -283,6 +294,9 @@ protected function extractAddress() $addressData[$attributeCode] = $value; } } + $addressData = $addressForm->compactData($addressData); + unset($addressData['region_id'], $addressData['region']); + $addressDataObject = $this->addressDataFactory->create(); $this->dataObjectHelper->populateWithArray( $addressDataObject, diff --git a/app/code/Magento/Customer/Controller/Account/LoginPost.php b/app/code/Magento/Customer/Controller/Account/LoginPost.php index 36d04e949923f..baa08c5f86b45 100644 --- a/app/code/Magento/Customer/Controller/Account/LoginPost.php +++ b/app/code/Magento/Customer/Controller/Account/LoginPost.php @@ -67,6 +67,11 @@ class LoginPost extends AbstractAccount implements CsrfAwareActionInterface, Htt */ private $cookieMetadataManager; + /** + * @var CustomerUrl + */ + private $customerUrl; + /** * @param Context $context * @param Session $customerSession @@ -199,11 +204,11 @@ public function execute() return $resultRedirect; } } catch (EmailNotConfirmedException $e) { - $value = $this->customerUrl->getEmailConfirmationUrl($login['username']); - $message = __( - 'This account is not confirmed. <a href="%1">Click here</a> to resend confirmation email.', - $value + $this->messageManager->addComplexErrorMessage( + 'confirmAccountErrorMessage', + ['url' => $this->customerUrl->getEmailConfirmationUrl($login['username'])] ); + $this->session->setUsername($login['username']); } catch (AuthenticationException $e) { $message = __( 'The account sign-in was incorrect or your account is disabled temporarily. ' diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php index e4daea6f59fdb..462f5d276ef45 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php @@ -161,7 +161,7 @@ public function execute(): Json $resultJson = $this->resultJsonFactory->create(); $resultJson->setData( [ - 'message' => $message, + 'messages' => $message, 'error' => $error, 'data' => [ 'entity_id' => $addressId diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml index 56afa8854ce0d..e4b07e44c0306 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml @@ -24,6 +24,7 @@ <fillField stepKey="fillEmail" userInput="{{Customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> <fillField stepKey="fillPassword" userInput="{{Customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> <fillField stepKey="fillConfirmPassword" userInput="{{Customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> + <waitForPageLoad stepKey="waitForCreateAccountButtonIsActive"/> <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}"/> <see stepKey="seeThankYouMessage" userInput="Thank you for registering with Main Website Store."/> <see stepKey="seeFirstName" userInput="{{Customer.firstname}}" selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}"/> diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php index 5565a807b8135..1cbfb194c50d9 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php @@ -23,77 +23,77 @@ class ConfirmTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\RequestInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $requestMock; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\ResponseInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $responseMock; /** - * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Model\Session|\PHPUnit\Framework\MockObject\MockObject */ protected $customerSessionMock; /** - * @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $redirectMock; /** - * @var \Magento\Framework\Url|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Url|\PHPUnit\Framework\MockObject\MockObject */ protected $urlMock; /** - * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $customerAccountManagementMock; /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $customerRepositoryMock; /** - * @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $customerDataMock; /** - * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $messageManagerMock; /** - * @var \Magento\Customer\Helper\Address|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Helper\Address|\PHPUnit\Framework\MockObject\MockObject */ protected $addressHelperMock; /** - * @var \Magento\Store\Model\StoreManager|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Store\Model\StoreManager|\PHPUnit\Framework\MockObject\MockObject */ protected $storeManagerMock; /** - * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Store\Model\Store|\PHPUnit\Framework\MockObject\MockObject */ protected $storeMock; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $scopeConfigMock; /** - * @var \Magento\Framework\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\Action\Context|\PHPUnit\Framework\MockObject\MockObject */ protected $contextMock; /** - * @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit\Framework\MockObject\MockObject */ protected $redirectResultMock; @@ -282,7 +282,7 @@ public function testSuccessMessage($customerId, $key, $vatValidationEnabled, $ad ->willReturnSelf(); $this->messageManagerMock->expects($this->any()) - ->method('addSuccessMessage') + ->method('addSuccess') ->with($this->stringContains($successMessage)) ->willReturnSelf(); @@ -402,7 +402,7 @@ public function testSuccessRedirect( ->willReturnSelf(); $this->messageManagerMock->expects($this->any()) - ->method('addSuccessMessage') + ->method('addSuccess') ->with($this->stringContains($successMessage)) ->willReturnSelf(); diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php index 05a8b6448af99..c098aba4504c1 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/LoginPostTest.php @@ -28,62 +28,62 @@ class LoginPostTest extends \PHPUnit\Framework\TestCase protected $controller; /** - * @var Context | \PHPUnit_Framework_MockObject_MockObject + * @var Context | \PHPUnit\Framework\MockObject\MockObject */ protected $context; /** - * @var Session | \PHPUnit_Framework_MockObject_MockObject + * @var Session | \PHPUnit\Framework\MockObject\MockObject */ protected $session; /** - * @var AccountManagementInterface | \PHPUnit_Framework_MockObject_MockObject + * @var AccountManagementInterface | \PHPUnit\Framework\MockObject\MockObject */ protected $accountManagement; /** - * @var Url | \PHPUnit_Framework_MockObject_MockObject + * @var Url | \PHPUnit\Framework\MockObject\MockObject */ protected $url; /** - * @var \Magento\Framework\Data\Form\FormKey\Validator | \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Data\Form\FormKey\Validator | \PHPUnit\Framework\MockObject\MockObject */ protected $formkeyValidator; /** - * @var AccountRedirect | \PHPUnit_Framework_MockObject_MockObject + * @var AccountRedirect | \PHPUnit\Framework\MockObject\MockObject */ protected $accountRedirect; /** - * @var Http | \PHPUnit_Framework_MockObject_MockObject + * @var Http | \PHPUnit\Framework\MockObject\MockObject */ protected $request; /** - * @var Redirect | \PHPUnit_Framework_MockObject_MockObject + * @var Redirect | \PHPUnit\Framework\MockObject\MockObject */ protected $resultRedirect; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit\Framework\MockObject\MockObject */ protected $redirectFactory; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit\Framework\MockObject\MockObject */ protected $redirect; /** - * @var \Magento\Framework\Message\ManagerInterface | \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Message\ManagerInterface | \PHPUnit\Framework\MockObject\MockObject */ protected $messageManager; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface | \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\Config\ScopeConfigInterface | \PHPUnit\Framework\MockObject\MockObject */ protected $scopeConfig; @@ -551,14 +551,12 @@ protected function mockExceptions($exception, $username) ->with($username) ->willReturn($url); - $message = __( - 'This account is not confirmed.' . - ' <a href="%1">Click here</a> to resend confirmation email.', - $url - ); $this->messageManager->expects($this->once()) - ->method('addErrorMessage') - ->with($message) + ->method('addComplexErrorMessage') + ->with( + 'confirmAccountErrorMessage', + ['url' => $url] + ) ->willReturnSelf(); $this->session->expects($this->once()) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php index 1bf881ff0a933..1ad404416e552 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php @@ -1,74 +1,86 @@ <?php -declare(strict_types=1); /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Test\Unit\Controller\Address; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Controller\Adminhtml\Address\Save; +use Magento\Customer\Model\Metadata\Form; +use Magento\Customer\Model\Metadata\FormFactory; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class SaveTest extends \PHPUnit\Framework\TestCase +class SaveTest extends TestCase { /** - * @var \Magento\Customer\Controller\Adminhtml\Address\Save + * @var Save */ private $model; /** - * @var \Magento\Customer\Api\AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var AddressRepositoryInterface|MockObject */ private $addressRepositoryMock; /** - * @var \Magento\Customer\Model\Metadata\FormFactory|\PHPUnit_Framework_MockObject_MockObject + * @var FormFactory|MockObject */ private $formFactoryMock; /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CustomerRepositoryInterface|MockObject */ private $customerRepositoryMock; /** - * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject + * @var DataObjectHelper|MockObject */ private $dataObjectHelperMock; /** - * @var \Magento\Customer\Api\Data\AddressInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var AddressInterfaceFactory|MockObject */ private $addressDataFactoryMock; /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var LoggerInterface|MockObject */ private $loggerMock; /** - * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + * @var RequestInterface|MockObject */ private $requestMock; /** - * @var AddressInterface|\PHPUnit_Framework_MockObject_MockObject + * @var AddressInterface|MockObject */ private $address; /** - * @var JsonFactory|\PHPUnit_Framework_MockObject_MockObject + * @var JsonFactory|MockObject */ private $resultJsonFactory; /** - * @var Json|\PHPUnit_Framework_MockObject_MockObject + * @var Json|MockObject */ private $json; @@ -77,13 +89,13 @@ class SaveTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->addressRepositoryMock = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - $this->formFactoryMock = $this->createMock(\Magento\Customer\Model\Metadata\FormFactory::class); - $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->dataObjectHelperMock = $this->createMock(\Magento\Framework\Api\DataObjectHelper ::class); - $this->addressDataFactoryMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterfaceFactory::class); - $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->addressRepositoryMock = $this->createMock(AddressRepositoryInterface::class); + $this->formFactoryMock = $this->createMock(FormFactory::class); + $this->customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class); + $this->dataObjectHelperMock = $this->createMock(DataObjectHelper ::class); + $this->addressDataFactoryMock = $this->createMock(AddressInterfaceFactory::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->requestMock = $this->createMock(RequestInterface::class); $this->address = $this->getMockBuilder(AddressInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -98,7 +110,7 @@ protected function setUp() $objectManager = new ObjectManagerHelper($this); $this->model = $objectManager->getObject( - \Magento\Customer\Controller\Adminhtml\Address\Save::class, + Save::class, [ 'addressRepository' => $this->addressRepositoryMock, 'formFactory' => $this->formFactoryMock, @@ -153,7 +165,7 @@ public function testExecute(): void ->willReturnOnConsecutiveCalls(22, 1); $customerMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class )->disableOriginalConstructor()->getMock(); $this->customerRepositoryMock->expects($this->atLeastOnce()) @@ -161,7 +173,7 @@ public function testExecute(): void ->with($customerId) ->willReturn($customerMock); - $customerAddressFormMock = $this->createMock(\Magento\Customer\Model\Metadata\Form::class); + $customerAddressFormMock = $this->createMock(Form::class); $customerAddressFormMock->expects($this->atLeastOnce()) ->method('extractData') ->with($this->requestMock) @@ -200,7 +212,7 @@ public function testExecute(): void ->method('setData') ->with( [ - 'message' => __('Customer address has been updated.'), + 'messages' => __('Customer address has been updated.'), 'error' => false, 'data' => [ 'entity_id' => $addressId diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml index e07d7d8708a43..9f6d75f8ff64f 100644 --- a/app/code/Magento/Customer/etc/db_schema.xml +++ b/app/code/Magento/Customer/etc/db_schema.xml @@ -457,6 +457,9 @@ <constraint xsi:type="foreign" referenceId="CUSTOMER_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_eav_attribute" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> + <index referenceId="CUSTOMER_EAV_ATTRIBUTE_SORT_ORDER" indexType="btree"> + <column name="sort_order"/> + </index> </table> <table name="customer_form_attribute" resource="default" engine="innodb" comment="Customer Form Attribute"> <column xsi:type="varchar" name="form_code" nullable="false" length="32" comment="Form Code"/> diff --git a/app/code/Magento/Customer/etc/db_schema_whitelist.json b/app/code/Magento/Customer/etc/db_schema_whitelist.json index ec7a53945aba3..1e04a75ab300b 100644 --- a/app/code/Magento/Customer/etc/db_schema_whitelist.json +++ b/app/code/Magento/Customer/etc/db_schema_whitelist.json @@ -284,6 +284,9 @@ "is_filterable_in_grid": true, "is_searchable_in_grid": true }, + "index": { + "CUSTOMER_EAV_ATTRIBUTE_SORT_ORDER": true + }, "constraint": { "PRIMARY": true, "CUSTOMER_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID": true @@ -347,4 +350,4 @@ "CUSTOMER_LOG_CUSTOMER_ID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 8867042cc6dc9..2a6e36a1ea3d7 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -92,6 +92,12 @@ <item name="template" xsi:type="string">Magento_Customer::messages/confirmAccountSuccessMessage.phtml</item> </item> </item> + <item name="confirmAccountErrorMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Customer::messages/confirmAccountErrorMessage.phtml</item> + </item> + </item> <item name="customerVatShippingAddressSuccessMessage" xsi:type="array"> <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> <item name="data" xsi:type="array"> diff --git a/app/code/Magento/Customer/view/frontend/templates/messages/confirmAccountErrorMessage.phtml b/app/code/Magento/Customer/view/frontend/templates/messages/confirmAccountErrorMessage.phtml new file mode 100644 index 0000000000000..134ff3f142cfb --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/templates/messages/confirmAccountErrorMessage.phtml @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Element\Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ + +?> +<?= $escaper->escapeHtml( + __( + 'This account is not confirmed. <a href="%1">Click here</a> to resend confirmation email.', + $block->getData('url') + ), + ['a'] +); diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index 3a5558e6d6f3d..3133e3d4c1957 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -6,11 +6,13 @@ namespace Magento\Directory\Helper; +use Magento\Directory\Model\AllowedCountries; use Magento\Directory\Model\Currency; use Magento\Directory\Model\CurrencyFactory; use Magento\Directory\Model\ResourceModel\Country\Collection; use Magento\Directory\Model\ResourceModel\Region\CollectionFactory; use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Helper\Context; use Magento\Framework\Json\Helper\Data as JsonData; use Magento\Store\Model\ScopeInterface; @@ -21,6 +23,7 @@ * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Data extends \Magento\Framework\App\Helper\AbstractHelper { @@ -156,6 +159,7 @@ public function getRegionCollection() { if (!$this->_regionCollection) { $this->_regionCollection = $this->_regCollectionFactory->create(); + // phpstan:ignore $this->_regionCollection->addCountryFilter($this->getAddress()->getCountryId())->load(); } return $this->_regionCollection; @@ -185,7 +189,9 @@ public function getRegionJson() { \Magento\Framework\Profiler::start('TEST: ' . __METHOD__, ['group' => 'TEST', 'method' => __METHOD__]); if (!$this->_regionJson) { - $cacheKey = 'DIRECTORY_REGIONS_JSON_STORE' . $this->_storeManager->getStore()->getId(); + $scope = $this->getCurrentScope(); + $scopeKey = $scope['value'] ? '_' . implode('_', $scope) : null; + $cacheKey = 'DIRECTORY_REGIONS_JSON_STORE' . $scopeKey; $json = $this->_configCacheType->load($cacheKey); if (empty($json)) { $regions = $this->getRegionData(); @@ -344,10 +350,13 @@ public function getDefaultCountry($store = null) */ public function getRegionData() { - $countryIds = []; - foreach ($this->getCountryCollection() as $country) { - $countryIds[] = $country->getCountryId(); - } + $scope = $this->getCurrentScope(); + $allowedCountries = $this->scopeConfig->getValue( + AllowedCountries::ALLOWED_COUNTRIES_PATH, + $scope['type'], + $scope['value'] + ); + $countryIds = explode(',', $allowedCountries); $collection = $this->_regCollectionFactory->create(); $collection->addCountryFilter($countryIds)->load(); $regions = [ @@ -392,4 +401,31 @@ public function getWeightUnit() { return $this->scopeConfig->getValue(self::XML_PATH_WEIGHT_UNIT, ScopeInterface::SCOPE_STORE); } + + /** + * Get current scope from request + * + * @return array + */ + private function getCurrentScope(): array + { + $scope = [ + 'type' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 'value' => null, + ]; + $request = $this->_getRequest(); + if ($request->getParam(ScopeInterface::SCOPE_WEBSITE)) { + $scope = [ + 'type' => ScopeInterface::SCOPE_WEBSITE, + 'value' => $request->getParam(ScopeInterface::SCOPE_WEBSITE), + ]; + } elseif ($request->getParam(ScopeInterface::SCOPE_STORE)) { + $scope = [ + 'type' => ScopeInterface::SCOPE_STORE, + 'value' => $request->getParam(ScopeInterface::SCOPE_STORE), + ]; + } + + return $scope; + } } diff --git a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php index 6ff0f8ea0f30b..f5cc60b5dfb99 100644 --- a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php @@ -6,59 +6,79 @@ namespace Magento\Directory\Test\Unit\Helper; use Magento\Directory\Helper\Data; +use Magento\Directory\Model\AllowedCountries; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Directory\Model\ResourceModel\Country\Collection as CountryCollection; +use Magento\Directory\Model\ResourceModel\Region\Collection as RegionCollection; +use Magento\Directory\Model\ResourceModel\Region\CollectionFactory; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Json\Helper\Data as JsonDataHelper; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\Constraint\IsIdentical; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class DataTest extends \PHPUnit\Framework\TestCase +class DataTest extends TestCase { /** - * @var \Magento\Directory\Model\ResourceModel\Country\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var CountryCollection|MockObject */ protected $_countryCollection; /** - * @var \Magento\Directory\Model\ResourceModel\Region\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|MockObject */ protected $_regionCollection; /** - * @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var JsonDataHelper|MockObject */ protected $jsonHelperMock; /** - * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject + * @var Store|MockObject */ protected $_store; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|MockObject */ protected $scopeConfigMock; /** - * @var \Magento\Directory\Helper\Data + * @var Data */ protected $_object; protected function setUp() { - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $objectManager = new ObjectManager($this); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(false); - $context = $this->createMock(\Magento\Framework\App\Helper\Context::class); + $requestMock = $this->createMock(RequestInterface::class); + $context = $this->createMock(Context::class); + $context->method('getRequest') + ->willReturn($requestMock); $context->expects($this->any()) ->method('getScopeConfig') ->willReturn($this->scopeConfigMock); + $configCacheType = $this->createMock(Config::class); - $configCacheType = $this->createMock(\Magento\Framework\App\Cache\Type\Config::class); + $this->_countryCollection = $this->createMock(CountryCollection::class); - $this->_countryCollection = $this->createMock(\Magento\Directory\Model\ResourceModel\Country\Collection::class); - - $this->_regionCollection = $this->createMock(\Magento\Directory\Model\ResourceModel\Region\Collection::class); + $this->_regionCollection = $this->createMock(RegionCollection::class); $regCollectionFactory = $this->createPartialMock( - \Magento\Directory\Model\ResourceModel\Region\CollectionFactory::class, + CollectionFactory::class, ['create'] ); $regCollectionFactory->expects( @@ -69,13 +89,13 @@ protected function setUp() $this->returnValue($this->_regionCollection) ); - $this->jsonHelperMock = $this->createMock(\Magento\Framework\Json\Helper\Data::class); + $this->jsonHelperMock = $this->createMock(JsonDataHelper::class); - $this->_store = $this->createMock(\Magento\Store\Model\Store::class); - $storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->_store = $this->createMock(Store::class); + $storeManager = $this->createMock(StoreManagerInterface::class); $storeManager->expects($this->any())->method('getStore')->will($this->returnValue($this->_store)); - $currencyFactory = $this->createMock(\Magento\Directory\Model\CurrencyFactory::class); + $currencyFactory = $this->createMock(CurrencyFactory::class); $arguments = [ 'context' => $context, @@ -86,32 +106,31 @@ protected function setUp() 'storeManager' => $storeManager, 'currencyFactory' => $currencyFactory, ]; - $this->_object = $objectManager->getObject(\Magento\Directory\Helper\Data::class, $arguments); + $this->_object = $objectManager->getObject(Data::class, $arguments); } public function testGetRegionJson() { - $countries = [ - new \Magento\Framework\DataObject(['country_id' => 'Country1']), - new \Magento\Framework\DataObject(['country_id' => 'Country2']) - ]; - $countryIterator = new \ArrayIterator($countries); - $this->_countryCollection->expects( - $this->atLeastOnce() - )->method( - 'getIterator' - )->will( - $this->returnValue($countryIterator) - ); - + $this->scopeConfigMock->method('getValue') + ->willReturnMap( + [ + [ + AllowedCountries::ALLOWED_COUNTRIES_PATH, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + 'Country1,Country2' + ], + [Data::XML_PATH_STATES_REQUIRED, ScopeInterface::SCOPE_STORE, null, ''] + ] + ); $regions = [ - new \Magento\Framework\DataObject( + new DataObject( ['country_id' => 'Country1', 'region_id' => 'r1', 'code' => 'r1-code', 'name' => 'r1-name'] ), - new \Magento\Framework\DataObject( + new DataObject( ['country_id' => 'Country1', 'region_id' => 'r2', 'code' => 'r2-code', 'name' => 'r2-name'] ), - new \Magento\Framework\DataObject( + new DataObject( ['country_id' => 'Country2', 'region_id' => 'r3', 'code' => 'r3-code', 'name' => 'r3-name'] ) ]; @@ -148,7 +167,7 @@ public function testGetRegionJson() )->method( 'jsonEncode' )->with( - new \PHPUnit\Framework\Constraint\IsIdentical($expectedDataToEncode) + new IsIdentical($expectedDataToEncode) )->will( $this->returnValue('encoded_json') ); @@ -220,7 +239,7 @@ public function testGetDefaultCountry() ->method('getValue') ->with( Data::XML_PATH_DEFAULT_COUNTRY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $storeId )->will($this->returnValue($country)); @@ -237,7 +256,7 @@ public function testGetCountryCollection() $this->returnValue(0) ); - $store = $this->createMock(\Magento\Store\Model\Store::class); + $store = $this->createMock(Store::class); $this->_countryCollection->expects( $this->once() )->method( @@ -257,7 +276,7 @@ public function testGetCountryCollection() public function testGetTopCountryCodesReturnsParsedConfigurationValue($topCountriesValue, $expectedResult) { $this->scopeConfigMock->expects($this->once()) - ->method('getValue')->with(\Magento\Directory\Helper\Data::XML_PATH_TOP_COUNTRIES) + ->method('getValue')->with(Data::XML_PATH_TOP_COUNTRIES) ->willReturn($topCountriesValue); $this->assertEquals($expectedResult, $this->_object->getTopCountryCodes()); diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 9e4ad6fb53a33..c8780341271ac 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -6,14 +6,21 @@ namespace Magento\Eav\Model\ResourceModel\Entity; +use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Attribute as EntityAttribute; +use Magento\Eav\Model\Entity\Attribute\FrontendLabel; +use Magento\Eav\Model\Entity\Attribute\Source\Table; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; use Magento\Framework\DB\Select; use Magento\Framework\Exception\CouldNotDeleteException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Store\Model\StoreManagerInterface; /** * EAV attribute resource model @@ -32,7 +39,7 @@ class Attribute extends AbstractDb protected static $_entityAttributes = []; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; @@ -49,15 +56,15 @@ class Attribute extends AbstractDb /** * Class constructor * - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Context $context + * @param StoreManagerInterface $storeManager * @param Type $eavEntityType * @param string $connectionName * @codeCoverageIgnore */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Magento\Store\Model\StoreManagerInterface $storeManager, + Context $context, + StoreManagerInterface $storeManager, Type $eavEntityType, $connectionName = null ) { @@ -94,14 +101,14 @@ protected function _initUniqueFields() /** * Load attribute data by attribute code * - * @param EntityAttribute|\Magento\Framework\Model\AbstractModel $object + * @param EntityAttribute|AbstractModel $object * @param int $entityTypeId * @param string $code * @return bool */ public function loadByCode(AbstractModel $object, $entityTypeId, $code) { - $bind = [':entity_type_id' => $entityTypeId]; + $bind = [':entity_type_id' => (int) $entityTypeId]; $select = $this->_getLoadSelect('attribute_code', $code, $object)->where('entity_type_id = :entity_type_id'); $data = $this->getConnection()->fetchRow($select, $bind); @@ -145,10 +152,10 @@ private function _getMaxSortOrder(AbstractModel $object) /** * Delete entity * - * @param \Magento\Framework\Model\AbstractMode $object + * @param AbstractModel $object * @return $this */ - public function deleteEntity(\Magento\Framework\Model\AbstractModel $object) + public function deleteEntity(AbstractModel $object) { if (!$object->getEntityAttributeId()) { return $this; @@ -167,7 +174,7 @@ public function deleteEntity(\Magento\Framework\Model\AbstractModel $object) * * @param EntityAttribute|AbstractModel $object * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function _beforeSave(AbstractModel $object) { @@ -184,7 +191,7 @@ protected function _beforeSave(AbstractModel $object) */ if (!$object->getId()) { if ($object->getFrontendInput() == 'select') { - $object->setSourceModel(\Magento\Eav\Model\Entity\Attribute\Source\Table::class); + $object->setSourceModel(Table::class); } } @@ -200,7 +207,7 @@ protected function _beforeSave(AbstractModel $object) */ protected function _beforeDelete(AbstractModel $attribute) { - /** @var $attribute \Magento\Eav\Api\Data\AttributeInterface */ + /** @var $attribute AttributeInterface */ if ($attribute->getId() && !$attribute->getIsUserDefined()) { throw new CouldNotDeleteException(__("The system attribute can't be deleted.")); } @@ -232,12 +239,12 @@ protected function _afterSave(AbstractModel $object) /** * Perform actions after object delete * - * @param \Magento\Framework\Model\AbstractModel|\Magento\Framework\DataObject $object + * @param AbstractModel|DataObject $object * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @since 100.0.7 */ - protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object) + protected function _afterDelete(AbstractModel $object) { $this->getConfig()->clear(); return $this; @@ -260,7 +267,7 @@ private function getConfig() /** * Save store labels * - * @param EntityAttribute|\Magento\Framework\Model\AbstractModel $object + * @param EntityAttribute|AbstractModel $object * @return $this */ protected function _saveStoreLabels(AbstractModel $object) @@ -287,7 +294,7 @@ protected function _saveStoreLabels(AbstractModel $object) /** * Save additional data of attribute * - * @param EntityAttribute|\Magento\Framework\Model\AbstractModel $object + * @param EntityAttribute|AbstractModel $object * @return $this */ protected function _saveAdditionalAttributeData(AbstractModel $object) @@ -296,7 +303,7 @@ protected function _saveAdditionalAttributeData(AbstractModel $object) if ($additionalTable) { $connection = $this->getConnection(); $data = $this->_prepareDataForTable($object, $this->getTable($additionalTable)); - $bind = [':attribute_id' => $object->getId()]; + $bind = [':attribute_id' => (int) $object->getId()]; $select = $connection->select()->from( $this->getTable($additionalTable), ['attribute_id'] @@ -410,12 +417,12 @@ protected function _processAttributeOptions($object, $option) * * @param array $values * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function _checkDefaultOptionValue($values) { if (!isset($values[0])) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("The default option isn't defined. Set the option and try again.") ); } @@ -797,8 +804,8 @@ public function __sleep() public function __wakeup() { parent::__wakeup(); - $this->_storeManager = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Store\Model\StoreManagerInterface::class); + $this->_storeManager = ObjectManager::getInstance() + ->get(StoreManagerInterface::class); } /** @@ -807,14 +814,14 @@ public function __wakeup() * @param AbstractModel $object * @param string|null $frontendLabel * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ private function setStoreLabels(AbstractModel $object, $frontendLabel) { $resultLabel = []; $frontendLabels = $object->getFrontendLabels(); if (isset($frontendLabels[0]) - && $frontendLabels[0] instanceof \Magento\Eav\Model\Entity\Attribute\FrontendLabel + && $frontendLabels[0] instanceof FrontendLabel ) { foreach ($frontendLabels as $label) { $resultLabel[$label->getStoreId()] = $label->getLabel(); @@ -830,13 +837,13 @@ private function setStoreLabels(AbstractModel $object, $frontendLabel) * @param array|string|null $frontendLabel * @param array $resultLabels * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ private function checkDefaultFrontendLabelExists($frontendLabel, $resultLabels) { $isAdminStoreLabel = (isset($resultLabels[0]) && !empty($resultLabels[0])); if (empty($frontendLabel) && !$isAdminStoreLabel) { - throw new \Magento\Framework\Exception\LocalizedException(__('The storefront label is not defined.')); + throw new LocalizedException(__('The storefront label is not defined.')); } } } diff --git a/app/code/Magento/Eav/etc/db_schema.xml b/app/code/Magento/Eav/etc/db_schema.xml index 8dc917b22d09f..0e0b599290ca2 100644 --- a/app/code/Magento/Eav/etc/db_schema.xml +++ b/app/code/Magento/Eav/etc/db_schema.xml @@ -305,6 +305,11 @@ <column name="entity_type_id"/> <column name="attribute_code"/> </constraint> + <index referenceId="EAV_ATTRIBUTE_FRONTEND_INPUT_ENTITY_TYPE_ID_IS_USER_DEFINED" indexType="btree"> + <column name="frontend_input"/> + <column name="entity_type_id"/> + <column name="is_user_defined"/> + </index> </table> <table name="eav_entity_store" resource="default" engine="innodb" comment="Eav Entity Store"> <column xsi:type="int" name="entity_store_id" padding="10" unsigned="true" nullable="false" identity="true" diff --git a/app/code/Magento/Eav/etc/db_schema_whitelist.json b/app/code/Magento/Eav/etc/db_schema_whitelist.json index fbcba0741eadf..756c584d0e4d7 100644 --- a/app/code/Magento/Eav/etc/db_schema_whitelist.json +++ b/app/code/Magento/Eav/etc/db_schema_whitelist.json @@ -177,6 +177,9 @@ "is_unique": true, "note": true }, + "index": { + "EAV_ATTRIBUTE_FRONTEND_INPUT_ENTITY_TYPE_ID_IS_USER_DEFINED": true + }, "constraint": { "PRIMARY": true, "EAV_ATTRIBUTE_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID": true, diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index 3d4fc252b57ff..c697734b9df0f 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -546,7 +546,9 @@ protected function applyDesignConfig() protected function cancelDesignConfig() { $this->appEmulation->stopEnvironmentEmulation(); + $this->urlModel->setScope(null); $this->hasDesignBeenApplied = false; + return $this; } diff --git a/app/code/Magento/Email/etc/di.xml b/app/code/Magento/Email/etc/di.xml index 73ba21ed7537b..5f6e30d591b79 100644 --- a/app/code/Magento/Email/etc/di.xml +++ b/app/code/Magento/Email/etc/di.xml @@ -18,7 +18,7 @@ </type> <type name="Magento\Email\Model\Template"> <arguments> - <argument name="urlModel" xsi:type="object" shared="false">Magento\Framework\Url</argument> + <argument name="urlModel" xsi:type="object">Magento\Framework\Url</argument> </arguments> </type> <type name="Magento\Theme\Model\Design\Config\MetadataProvider"> diff --git a/app/code/Magento/Fedex/Model/Carrier.php b/app/code/Magento/Fedex/Model/Carrier.php index 0cbd90150734b..e19b632fc268c 100644 --- a/app/code/Magento/Fedex/Model/Carrier.php +++ b/app/code/Magento/Fedex/Model/Carrier.php @@ -8,6 +8,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Module\Dir; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Webapi\Soap\ClientFactory; @@ -21,6 +22,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\Carrier\CarrierInterface { @@ -149,6 +151,16 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C */ private $soapClientFactory; + /** + * @var array + */ + private $baseCurrencyRate; + + /** + * @var DataObject + */ + private $_rawTrackingRequest; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory @@ -629,12 +641,13 @@ protected function _prepareRateResponse($response) protected function _getRateAmountOriginBased($rate) { $amount = null; + $currencyCode = ''; $rateTypeAmounts = []; - if (is_object($rate)) { // The "RATED..." rates are expressed in the currency of the origin country foreach ($rate->RatedShipmentDetails as $ratedShipmentDetail) { $netAmount = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Amount; + $currencyCode = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Currency; $rateType = (string)$ratedShipmentDetail->ShipmentRateDetail->RateType; $rateTypeAmounts[$rateType] = $netAmount; } @@ -649,11 +662,42 @@ protected function _getRateAmountOriginBased($rate) if ($amount === null) { $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount; } + + $amount = (float)$amount * $this->getBaseCurrencyRate($currencyCode); } return $amount; } + /** + * Returns base currency rate. + * + * @param string $currencyCode + * @return float + * @throws LocalizedException + */ + private function getBaseCurrencyRate(string $currencyCode): float + { + if (!isset($this->baseCurrencyRate[$currencyCode])) { + $baseCurrencyCode = $this->_request->getBaseCurrency()->getCode(); + $rate = $this->_currencyFactory->create() + ->load($currencyCode) + ->getAnyRate($baseCurrencyCode); + if ($rate === false) { + $errorMessage = __( + 'Can\'t convert a shipping cost from "%1-%2" for FedEx carrier.', + $currencyCode, + $baseCurrencyCode + ); + $this->_logger->critical($errorMessage); + throw new LocalizedException($errorMessage); + } + $this->baseCurrencyRate[$currencyCode] = (float)$rate; + } + + return $this->baseCurrencyRate[$currencyCode]; + } + /** * Set free method request * @@ -726,9 +770,8 @@ protected function _getXmlQuotes() $debugData = ['request' => $this->filterDebugData($request)]; try { $url = $this->getConfigData('gateway_url'); - if (!$url) { - $url = $this->_defaultGatewayUrl; - } + + // phpcs:disable Magento2.Functions.DiscouragedFunction $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, $url); @@ -737,6 +780,7 @@ protected function _getXmlQuotes() curl_setopt($ch, CURLOPT_POSTFIELDS, $request); $responseBody = curl_exec($ch); curl_close($ch); + // phpcs:enable $debugData['result'] = $this->filterDebugData($responseBody); $this->_setCachedQuotes($request, $responseBody); diff --git a/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php index 65aae284ea444..a5d72d1e3ad4c 100644 --- a/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php @@ -10,10 +10,12 @@ use Magento\Directory\Helper\Data; use Magento\Directory\Model\Country; use Magento\Directory\Model\CountryFactory; +use Magento\Directory\Model\Currency; use Magento\Directory\Model\CurrencyFactory; use Magento\Directory\Model\RegionFactory; use Magento\Fedex\Model\Carrier; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Module\Dir\Reader; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; @@ -35,7 +37,7 @@ use Magento\Shipping\Model\Tracking\ResultFactory; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\MockObject\MockObject as MockObject; use Psr\Log\LoggerInterface; /** @@ -100,6 +102,11 @@ class CarrierTest extends \PHPUnit\Framework\TestCase */ private $logger; + /** + * @var CurrencyFactory|MockObject + */ + private $currencyFactory; + protected function setUp() { $this->helper = new ObjectManager($this); @@ -141,7 +148,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $currencyFactory = $this->getMockBuilder(CurrencyFactory::class) + $this->currencyFactory = $this->getMockBuilder(CurrencyFactory::class) ->disableOriginalConstructor() ->getMock(); @@ -179,7 +186,7 @@ protected function setUp() 'trackStatusFactory' => $this->statusFactory, 'regionFactory' => $regionFactory, 'countryFactory' => $countryFactory, - 'currencyFactory' => $currencyFactory, + 'currencyFactory' => $this->currencyFactory, 'directoryData' => $data, 'stockRegistry' => $stockRegistry, 'storeManager' => $storeManager, @@ -240,13 +247,21 @@ public function scopeConfigGetValue(string $path) /** * @param float $amount + * @param string $currencyCode + * @param string $baseCurrencyCode * @param string $rateType * @param float $expected * @param int $callNum * @dataProvider collectRatesDataProvider */ - public function testCollectRatesRateAmountOriginBased($amount, $rateType, $expected, $callNum = 1) - { + public function testCollectRatesRateAmountOriginBased( + $amount, + $currencyCode, + $baseCurrencyCode, + $rateType, + $expected, + $callNum = 1 + ) { $this->scope->expects($this->any()) ->method('isSetFlag') ->willReturn(true); @@ -254,6 +269,7 @@ public function testCollectRatesRateAmountOriginBased($amount, $rateType, $expec // @codingStandardsIgnoreStart $netAmount = new \stdClass(); $netAmount->Amount = $amount; + $netAmount->Currency = $currencyCode; $totalNetCharge = new \stdClass(); $totalNetCharge->TotalNetCharge = $netAmount; @@ -274,9 +290,39 @@ public function testCollectRatesRateAmountOriginBased($amount, $rateType, $expec $this->serializer->method('serialize') ->willReturn('CollectRateString' . $amount); + $rateCurrency = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->getMock(); + $rateCurrency->method('load') + ->willReturnSelf(); + $rateCurrency->method('getAnyRate') + ->willReturnMap( + [ + ['USD', 1], + ['EUR', 0.75], + ['UNKNOWN', false] + ] + ); + + if ($baseCurrencyCode === 'UNKNOWN') { + $this->expectException(LocalizedException::class); + } + + $this->currencyFactory->method('create') + ->willReturn($rateCurrency); + + $baseCurrency = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->getMock(); + $baseCurrency->method('getCode') + ->willReturn($baseCurrencyCode); + $request = $this->getMockBuilder(RateRequest::class) + ->setMethods(['getBaseCurrency']) ->disableOriginalConstructor() ->getMock(); + $request->method('getBaseCurrency') + ->willReturn($baseCurrency); $this->soapClient->expects($this->exactly($callNum)) ->method('getRates') @@ -295,22 +341,23 @@ public function testCollectRatesRateAmountOriginBased($amount, $rateType, $expec public function collectRatesDataProvider() { return [ - [10.0, 'RATED_ACCOUNT_PACKAGE', 10], - [10.0, 'RATED_ACCOUNT_PACKAGE', 10, 0], - [11.50, 'PAYOR_ACCOUNT_PACKAGE', 11.5], - [11.50, 'PAYOR_ACCOUNT_PACKAGE', 11.5, 0], - [100.01, 'RATED_ACCOUNT_SHIPMENT', 100.01], - [100.01, 'RATED_ACCOUNT_SHIPMENT', 100.01, 0], - [32.2, 'PAYOR_ACCOUNT_SHIPMENT', 32.2], - [32.2, 'PAYOR_ACCOUNT_SHIPMENT', 32.2, 0], - [15.0, 'RATED_LIST_PACKAGE', 15], - [15.0, 'RATED_LIST_PACKAGE', 15, 0], - [123.25, 'PAYOR_LIST_PACKAGE', 123.25], - [123.25, 'PAYOR_LIST_PACKAGE', 123.25, 0], - [12.12, 'RATED_LIST_SHIPMENT', 12.12], - [12.12, 'RATED_LIST_SHIPMENT', 12.12, 0], - [38.9, 'PAYOR_LIST_SHIPMENT', 38.9], - [38.9, 'PAYOR_LIST_SHIPMENT', 38.9, 0], + [10.0, 'USD', 'EUR', 'RATED_ACCOUNT_PACKAGE', 7.5], + [10.0, 'USD', 'UNKNOWN', 'RATED_ACCOUNT_PACKAGE', null, 0], + [10.0, 'USD', 'USD', 'RATED_ACCOUNT_PACKAGE', 10, 0], + [11.50, 'USD', 'USD', 'PAYOR_ACCOUNT_PACKAGE', 11.5], + [11.50, 'USD', 'USD', 'PAYOR_ACCOUNT_PACKAGE', 11.5, 0], + [100.01, 'USD', 'USD', 'RATED_ACCOUNT_SHIPMENT', 100.01], + [100.01, 'USD', 'USD', 'RATED_ACCOUNT_SHIPMENT', 100.01, 0], + [32.2, 'USD', 'USD', 'PAYOR_ACCOUNT_SHIPMENT', 32.2], + [32.2, 'USD', 'USD', 'PAYOR_ACCOUNT_SHIPMENT', 32.2, 0], + [15.0, 'USD', 'USD', 'RATED_LIST_PACKAGE', 15], + [15.0, 'USD', 'USD', 'RATED_LIST_PACKAGE', 15, 0], + [123.25, 'USD', 'USD', 'PAYOR_LIST_PACKAGE', 123.25], + [123.25, 'USD', 'USD', 'PAYOR_LIST_PACKAGE', 123.25, 0], + [12.12, 'USD', 'USD', 'RATED_LIST_SHIPMENT', 12.12], + [12.12, 'USD', 'USD', 'RATED_LIST_SHIPMENT', 12.12, 0], + [38.9, 'USD', 'USD', 'PAYOR_LIST_SHIPMENT', 38.9], + [38.9, 'USD', 'USD', 'PAYOR_LIST_SHIPMENT', 38.9, 0], ]; } diff --git a/app/code/Magento/Fedex/i18n/en_US.csv b/app/code/Magento/Fedex/i18n/en_US.csv index 778c324c8dfd3..d1509d42730bc 100644 --- a/app/code/Magento/Fedex/i18n/en_US.csv +++ b/app/code/Magento/Fedex/i18n/en_US.csv @@ -77,3 +77,4 @@ Dropoff,Dropoff Debug,Debug "Show Method if Not Applicable","Show Method if Not Applicable" "Sort Order","Sort Order" +"Can't convert a shipping cost from ""%1-%2"" for FedEx carrier.","Can't convert a shipping cost from ""%1-%2"" for FedEx carrier." diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index c7207c853b95e..d760303cbd65f 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -132,11 +132,11 @@ protected function getIndexers(InputInterface $input) $dependentIndexers[] = $this->getDependentIndexerIds($indexer->getId()); } - $relatedIndexers = array_merge(...$relatedIndexers); - $dependentIndexers = array_merge(...$dependentIndexers); + $relatedIndexers = $relatedIndexers ? array_unique(array_merge(...$relatedIndexers)) : []; + $dependentIndexers = $dependentIndexers ? array_merge(...$dependentIndexers) : []; $invalidRelatedIndexers = []; - foreach (array_unique($relatedIndexers) as $relatedIndexer) { + foreach ($relatedIndexers as $relatedIndexer) { if ($allIndexers[$relatedIndexer]->isInvalid()) { $invalidRelatedIndexers[] = $relatedIndexer; } @@ -169,8 +169,9 @@ private function getRelatedIndexerIds(string $indexerId): array $relatedIndexerIds[] = [$relatedIndexerId]; $relatedIndexerIds[] = $this->getRelatedIndexerIds($relatedIndexerId); } + $relatedIndexerIds = $relatedIndexerIds ? array_unique(array_merge(...$relatedIndexerIds)) : []; - return array_unique(array_merge(...$relatedIndexerIds)); + return $relatedIndexerIds; } /** @@ -189,8 +190,9 @@ private function getDependentIndexerIds(string $indexerId): array $dependentIndexerIds[] = $this->getDependentIndexerIds($id); } } + $dependentIndexerIds = $dependentIndexerIds ? array_unique(array_merge(...$dependentIndexerIds)) : []; - return array_unique(array_merge(...$dependentIndexerIds)); + return $dependentIndexerIds; } /** @@ -251,6 +253,8 @@ private function validateSharedIndex($sharedIndex) $indexer = $this->getIndexerRegistry()->get($indexerId); /** @var \Magento\Indexer\Model\Indexer\State $state */ $state = $indexer->getState(); + $state->setStatus(StateInterface::STATUS_WORKING); + $state->save(); $state->setStatus(StateInterface::STATUS_VALID); $state->save(); } diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index ee82c8282f0a9..29a9f3a1f416f 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -10,6 +10,9 @@ use Magento\Framework\Indexer\IndexerInterfaceFactory; use Magento\Framework\Indexer\StateInterface; +/** + * Indexer processor + */ class Processor { /** @@ -70,6 +73,8 @@ public function reindexAllInvalid() } else { /** @var \Magento\Indexer\Model\Indexer\State $state */ $state = $indexer->getState(); + $state->setStatus(StateInterface::STATUS_WORKING); + $state->save(); $state->setStatus(StateInterface::STATUS_VALID); $state->save(); } diff --git a/app/code/Magento/LoginAsCustomer/LICENSE.txt b/app/code/Magento/LoginAsCustomer/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomer/LICENSE_AFL.txt b/app/code/Magento/LoginAsCustomer/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomer.php b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomer.php new file mode 100644 index 0000000000000..df51a1e43f525 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomer.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\LocalizedException; +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class AuthenticateCustomer implements AuthenticateCustomerInterface +{ + /** + * @var Session + */ + private $customerSession; + + /** + * @param Session $customerSession + */ + public function __construct( + Session $customerSession + ) { + $this->customerSession = $customerSession; + } + + /** + * @inheritdoc + */ + public function execute(AuthenticationDataInterface $authenticationData): void + { + if ($this->customerSession->getId()) { + $this->customerSession->logout(); + } + + $result = $this->customerSession->loginById($authenticationData->getCustomerId()); + if (false === $result) { + throw new LocalizedException(__('Login was not successful.')); + } + + $this->customerSession->regenerateId(); + $this->customerSession->setLoggedAsCustomerAdmindId($authenticationData->getAdminId()); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/AuthenticationData.php b/app/code/Magento/LoginAsCustomer/Model/AuthenticationData.php new file mode 100644 index 0000000000000..17d85b970a2ff --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/AuthenticationData.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataExtensionInterface; + +/** + * @inheritdoc + */ +class AuthenticationData implements AuthenticationDataInterface +{ + /** + * @var int + */ + private $customerId; + + /** + * @var int + */ + private $adminId; + + /** + * @var AuthenticationDataExtensionInterface|null + */ + private $extensionAttributes; + + /** + * @param int $customerId + * @param int $adminId + * @param AuthenticationDataExtensionInterface|null $extensionAttributes + */ + public function __construct( + int $customerId, + int $adminId, + AuthenticationDataExtensionInterface $extensionAttributes = null + ) { + $this->customerId = $customerId; + $this->adminId = $adminId; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getCustomerId(): int + { + return $this->customerId; + } + + /** + * @inheritdoc + */ + public function getAdminId(): int + { + return $this->adminId; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?AuthenticationDataExtensionInterface + { + return $this->extensionAttributes; + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/Config.php b/app/code/Magento/LoginAsCustomer/Model/Config.php new file mode 100644 index 0000000000000..2cfafa6ac09a3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/Config.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; + +/** + * @inheritdoc + */ +class Config implements ConfigInterface +{ + /** + * Extension config path + */ + private const XML_PATH_ENABLED + = 'login_as_customer/general/enabled'; + private const XML_PATH_STORE_VIEW_MANUAL_CHOICE_ENABLED + = 'login_as_customer/general/store_view_manual_choice_enabled'; + private const XML_PATH_AUTHENTICATION_EXPIRATION_TIME + = 'login_as_customer/general/authentication_data_expiration_time'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function isEnabled(): bool + { + return (bool)$this->scopeConfig->getValue(self::XML_PATH_ENABLED); + } + + /** + * @inheritdoc + */ + public function isStoreManualChoiceEnabled(): bool + { + return (bool)$this->scopeConfig->getValue(self::XML_PATH_STORE_VIEW_MANUAL_CHOICE_ENABLED); + } + + /** + * @inheritdoc + */ + public function getAuthenticationDataExpirationTime(): int + { + return (int)$this->scopeConfig->getValue(self::XML_PATH_AUTHENTICATION_EXPIRATION_TIME); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/DeleteExpiredAuthenticationData.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/DeleteExpiredAuthenticationData.php new file mode 100644 index 0000000000000..8778c20ab9257 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/DeleteExpiredAuthenticationData.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\DeleteExpiredAuthenticationDataInterface; + +/** + * @inheritdoc + */ +class DeleteExpiredAuthenticationData implements DeleteExpiredAuthenticationDataInterface +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DateTime + */ + private $dateTime; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @param ResourceConnection $resourceConnection + * @param DateTime $dateTime + * @param ConfigInterface $config + */ + public function __construct( + ResourceConnection $resourceConnection, + DateTime $dateTime, + ConfigInterface $config + ) { + $this->resourceConnection = $resourceConnection; + $this->dateTime = $dateTime; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function execute(int $userId): void + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer'); + + $connection->delete( + $tableName, + [ + 'admin_id = ?' => $userId + ] + ); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php new file mode 100644 index 0000000000000..8951ae8f70939 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Exception\LocalizedException; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface; + +/** + * @inheritdoc + */ +class GetAuthenticationDataBySecret implements GetAuthenticationDataBySecretInterface +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DateTime + */ + private $dateTime; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var AuthenticationDataInterfaceFactory + */ + private $authenticationDataFactory; + + /** + * @param ResourceConnection $resourceConnection + * @param DateTime $dateTime + * @param ConfigInterface $config + * @param AuthenticationDataInterfaceFactory $authenticationDataFactory + */ + public function __construct( + ResourceConnection $resourceConnection, + DateTime $dateTime, + ConfigInterface $config, + AuthenticationDataInterfaceFactory $authenticationDataFactory + ) { + $this->resourceConnection = $resourceConnection; + $this->dateTime = $dateTime; + $this->config = $config; + $this->authenticationDataFactory = $authenticationDataFactory; + } + + /** + * @inheritdoc + */ + public function execute(string $secretKey): AuthenticationDataInterface + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer'); + + $timePoint = date( + 'Y-m-d H:i:s', + $this->dateTime->gmtTimestamp() - $this->config->getAuthenticationDataExpirationTime() + ); + + $select = $connection->select() + ->from(['main_table' => $tableName]) + ->where('main_table.secret = ?', $secretKey) + ->where('main_table.created_at > ?', $timePoint); + + $data = $connection->fetchRow($select); + + if (!$data) { + throw new LocalizedException(__('Secret key is not found or was expired.')); + } + + /** @var AuthenticationDataInterface $authenticationData */ + $authenticationData = $this->authenticationDataFactory->create( + [ + 'customerId' => (int)$data['customer_id'], + 'adminId' => (int)$data['admin_id'], + 'extensionAttributes' => null, + ] + ); + return $authenticationData; + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/IsLoginAsCustomerSessionActive.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/IsLoginAsCustomerSessionActive.php new file mode 100644 index 0000000000000..3bbba4b5e6ec4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/IsLoginAsCustomerSessionActive.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerSessionActiveInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerSessionActive implements IsLoginAsCustomerSessionActiveInterface +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId, int $userId): bool + { + $tableName = $this->resourceConnection->getTableName('login_as_customer'); + $connection = $this->resourceConnection->getConnection(); + + $query = $connection->select() + ->from($tableName) + ->where('customer_id = ?', $customerId) + ->where('admin_id = ?', $userId); + + $result = $connection->fetchRow($query); + + return false !== $result; + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php new file mode 100644 index 0000000000000..d120b0eae392e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Math\Random; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; +use Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface; + +/** + * @inheritdoc + */ +class SaveAuthenticationData implements SaveAuthenticationDataInterface +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DateTime + */ + private $dateTime; + + /** + * @var Random + */ + private $random; + + /** + * @param ResourceConnection $resourceConnection + * @param DateTime $dateTime + * @param Random $random + */ + public function __construct( + ResourceConnection $resourceConnection, + DateTime $dateTime, + Random $random + ) { + $this->resourceConnection = $resourceConnection; + $this->dateTime = $dateTime; + $this->random = $random; + } + + /** + * @inheritdoc + */ + public function execute(AuthenticationDataInterface $authenticationData): string + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer'); + + $secret = $this->random->getRandomString(64); + + $connection->insert( + $tableName, + [ + 'customer_id' => $authenticationData->getCustomerId(), + 'admin_id' => $authenticationData->getAdminId(), + 'secret' => $secret, + 'created_at' => $this->dateTime->gmtDate(), + ] + ); + return $secret; + } +} diff --git a/app/code/Magento/LoginAsCustomer/README.md b/app/code/Magento/LoginAsCustomer/README.md new file mode 100644 index 0000000000000..17d37bf553278 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/README.md @@ -0,0 +1,3 @@ +# Magento_LoginAsCustomer module + +The Magento_LoginAsCustomer module is responsible for ability to login into customer account using the admin panel. diff --git a/app/code/Magento/LoginAsCustomer/composer.json b/app/code/Magento/LoginAsCustomer/composer.json new file mode 100755 index 0000000000000..eeef8604feff2 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-login-as-customer", + "description": "Allow for admin to enter a customer account", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-login-as-customer-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomer\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomer/etc/config.xml b/app/code/Magento/LoginAsCustomer/etc/config.xml new file mode 100644 index 0000000000000..936ae1ff2f05d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/etc/config.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <login_as_customer> + <general> + <enabled>0</enabled> + <store_view_manual_choice_enabled>0</store_view_manual_choice_enabled> + <authentication_data_expiration_time>60</authentication_data_expiration_time> + </general> + </login_as_customer> + </default> +</config> diff --git a/app/code/Magento/LoginAsCustomer/etc/db_schema.xml b/app/code/Magento/LoginAsCustomer/etc/db_schema.xml new file mode 100644 index 0000000000000..a693946f0c3ba --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/etc/db_schema.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="login_as_customer" resource="default" engine="innodb" comment="Magento Login As Customer Table"> + <column xsi:type="varchar" name="secret" nullable="false" length="64" comment="Login Secret"/> + <column xsi:type="int" name="customer_id" nullable="false" comment="Customer ID"/> + <column xsi:type="int" name="admin_id" nullable="false" comment="Admin ID"/> + <column xsi:type="timestamp" name="created_at" comment="Creation Time"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="secret"/> + </constraint> + <index referenceId="LOGIN_AS_CUSTOMER_CREATED_AT" indexType="btree"> + <column name="created_at"/> + </index> + </table> +</schema> diff --git a/app/code/Magento/LoginAsCustomer/etc/db_schema_whitelist.json b/app/code/Magento/LoginAsCustomer/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..397ea4525aee2 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/etc/db_schema_whitelist.json @@ -0,0 +1,16 @@ +{ + "login_as_customer": { + "column": { + "secret": true, + "customer_id": true, + "admin_id": true, + "created_at": true + }, + "index": { + "LOGIN_AS_CUSTOMER_CREATED_AT": true + }, + "constraint": { + "PRIMARY": true + } + } +} diff --git a/app/code/Magento/LoginAsCustomer/etc/di.xml b/app/code/Magento/LoginAsCustomer/etc/di.xml new file mode 100755 index 0000000000000..76602534d31e8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/etc/di.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface" type="Magento\LoginAsCustomer\Model\AuthenticationData"/> + <preference for="Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\SaveAuthenticationData"/> + <preference for="Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\GetAuthenticationDataBySecret"/> + <preference for="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface" type="Magento\LoginAsCustomer\Model\AuthenticateCustomer"/> + <preference for="Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataBySecretInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\DeleteAuthenticationDataBySecret"/> + <preference for="Magento\LoginAsCustomerApi\Api\DeleteExpiredAuthenticationDataInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\DeleteExpiredAuthenticationData"/> + <preference for="Magento\LoginAsCustomerApi\Api\ConfigInterface" type="Magento\LoginAsCustomer\Model\Config"/> + <preference for="Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerSessionActiveInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\IsLoginAsCustomerSessionActive"/> +</config> diff --git a/app/code/Magento/LoginAsCustomer/etc/module.xml b/app/code/Magento/LoginAsCustomer/etc/module.xml new file mode 100755 index 0000000000000..d577dc0da5e4f --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomer"/> +</config> diff --git a/app/code/Magento/LoginAsCustomer/i18n/en_US.csv b/app/code/Magento/LoginAsCustomer/i18n/en_US.csv new file mode 100644 index 0000000000000..6ce6cfc532221 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/i18n/en_US.csv @@ -0,0 +1,2 @@ +"Close Session","Close Session" +"You are connected as <strong>%1</strong> on %2","You are connected as <strong>%1</strong> on %2" diff --git a/app/code/Magento/LoginAsCustomer/registration.php b/app/code/Magento/LoginAsCustomer/registration.php new file mode 100755 index 0000000000000..ac43a6d812e9b --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomer', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerApi/Api/AuthenticateCustomerInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/AuthenticateCustomerInterface.php new file mode 100644 index 0000000000000..81cc11bd738fd --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/AuthenticateCustomerInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +use Magento\Framework\Exception\LocalizedException; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; + +/** + * Authenticate a customer + * + * @api + */ +interface AuthenticateCustomerInterface +{ + /** + * Authenticate a customer + * + * @param AuthenticationDataInterface $authenticationData + * @return void + * @throws LocalizedException + */ + public function execute(AuthenticationDataInterface $authenticationData): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php new file mode 100644 index 0000000000000..b3aafa6c8a51b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * LoginAsCustomer config + * + * @api + */ +interface ConfigInterface +{ + /** + * Check if Login As Customer extension is enabled + * + * @return bool + */ + public function isEnabled(): bool; + + /** + * Check if store view manual choice is enabled + * + * @return bool + */ + public function isStoreManualChoiceEnabled(): bool; + + /** + * Get authentication data expiration time (in seconds) + * + * @return int + */ + public function getAuthenticationDataExpirationTime(): int; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php new file mode 100644 index 0000000000000..f74f63c39f7ba --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Authentication data + * + * @api + */ +interface AuthenticationDataInterface extends ExtensibleDataInterface +{ + /** + * Get Customer Id + * + * @return int + */ + public function getCustomerId(): int; + + /** + * Get Admin Id + * + * @return int + */ + public function getAdminId(): int; + + /** + * Get extension attributes + * + * Fully qualified namespaces is needed for proper work of ccode generation + * + * @return \Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataExtensionInterface|null + */ + public function getExtensionAttributes(): ?AuthenticationDataExtensionInterface; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/DeleteAuthenticationDataBySecretInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/DeleteAuthenticationDataBySecretInterface.php new file mode 100644 index 0000000000000..aba63bb9acb20 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/DeleteAuthenticationDataBySecretInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Delete authentication data by secret + * + * @api + */ +interface DeleteAuthenticationDataBySecretInterface +{ + /** + * Delete authentication data by secret + * + * @param string $secret + * @return void + */ + public function execute(string $secret): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/DeleteExpiredAuthenticationDataInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/DeleteExpiredAuthenticationDataInterface.php new file mode 100644 index 0000000000000..3b2e756f40e83 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/DeleteExpiredAuthenticationDataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Delete expired authentication data + * + * @api + */ +interface DeleteExpiredAuthenticationDataInterface +{ + /** + * Delete expired authentication data by user id. + * + * @param int $userId + * @return void + */ + public function execute(int $userId): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php new file mode 100644 index 0000000000000..2ca8d66453739 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +use Magento\Framework\Exception\LocalizedException; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; + +/** + * Get authentication data by secret + * + * @api + */ +interface GetAuthenticationDataBySecretInterface +{ + /** + * Load login details based on secret key + * + * @param string $secretKey + * @return AuthenticationDataInterface + * @throws LocalizedException + */ + public function execute(string $secretKey): AuthenticationDataInterface; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php new file mode 100644 index 0000000000000..30674375ed021 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Check if Login as Customer session is still active. + * + * @api + */ +interface IsLoginAsCustomerSessionActiveInterface +{ + /** + * Check if Login as Customer session is still active. + * + * @param int $customerId + * @param int $userId + * @return bool + */ + public function execute(int $customerId, int $userId): bool; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php new file mode 100644 index 0000000000000..88d4cb8056cf6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +use Magento\Framework\Exception\LocalizedException; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; + +/** + * Save authentication data. Return secret key + * + * @api + */ +interface SaveAuthenticationDataInterface +{ + /** + * Save authentication data. Return secret key + * + * @param Data\AuthenticationDataInterface $authenticationData + * @return string + * @throws LocalizedException + */ + public function execute(AuthenticationDataInterface $authenticationData): string; +} diff --git a/app/code/Magento/LoginAsCustomerApi/LICENSE.txt b/app/code/Magento/LoginAsCustomerApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomerApi/LICENSE_AFL.txt b/app/code/Magento/LoginAsCustomerApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/LoginAsCustomerApi/README.md b/app/code/Magento/LoginAsCustomerApi/README.md new file mode 100644 index 0000000000000..caf2f23b49f43 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/README.md @@ -0,0 +1,3 @@ +# Magento_LoginAsCustomer module + +The Magento_LoginAsCustomerApi module provides API for ability to login into customer account for an admin user. diff --git a/app/code/Magento/LoginAsCustomerApi/composer.json b/app/code/Magento/LoginAsCustomerApi/composer.json new file mode 100644 index 0000000000000..c99c117bda7c3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/composer.json @@ -0,0 +1,19 @@ +{ + "name": "magento/module-login-as-customer-api", + "description": "Allow for admin to enter a customer account", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomerApi\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerApi/etc/module.xml b/app/code/Magento/LoginAsCustomerApi/etc/module.xml new file mode 100644 index 0000000000000..cdfefb80e9b3b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerApi"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerApi/registration.php b/app/code/Magento/LoginAsCustomerApi/registration.php new file mode 100644 index 0000000000000..344d110e46f43 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerApi', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php new file mode 100644 index 0000000000000..10d793d6c4c0b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Data interface for login as customer log. + * + * @api + */ +interface LogInterface extends ExtensibleDataInterface +{ + const LOG_ID = 'log_id'; + const TIME = 'time'; + const CUSTOMER_ID = 'customer_id'; + const CUSTOMER_EMAIL = 'customer_email'; + const USER_ID = 'user_id'; + const USERNAME = 'user_name'; + + /** + * Set login as customer log id. + * + * @param int $logId + * @return void + */ + public function setLogId(int $logId): void; + + /** + * Retrieve login as customer log id. + * + * @return null|int + */ + public function getLogId(): ?int; + + /** + * Set login as customer log time. + * + * @param string $time + * @return void + */ + public function setTime(string $time): void; + + /** + * Retrieve login as customer log time. + * + * @return null|string + */ + public function getTime(): ?string; + + /** + * Set login as customer log user id. + * + * @param int $userId + * @return void + */ + public function setUserId(int $userId): void; + + /** + * Retrieve login as customer log user id. + * + * @return null|int + */ + public function getUserId(): ?int; + + /** + * Set login as customer log user name. + * + * @param string $userName + * @return void + */ + public function setUserName(string $userName): void; + + /** + * Retrieve login as customer log user name. + * + * @return null|string + */ + public function getUserName(): ?string; + + /** + * Set login as customer log customer id. + * + * @param int $customerId + * @return void + */ + public function setCustomerId(int $customerId): void; + + /** + * Retrieve login as customer log customer id. + * + * @return null|int + */ + public function getCustomerId(): ?int; + + /** + * Set login as customer log customer email. + * + * @param string $customerEmail + * @return void + */ + public function setCustomerEmail(string $customerEmail): void; + + /** + * Retrieve login as customer log customer email. + * + * @return null|string + */ + public function getCustomerEmail(): ?string; + + /** + * Set log extension attributes. + * + * @param \Magento\LoginAsCustomerLog\Api\Data\LogExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(LogExtensionInterface $extensionAttributes): void; + + /** + * Retrieve log extension attributes. + * + * @return \Magento\LoginAsCustomerLog\Api\Data\LogExtensionInterface + */ + public function getExtensionAttributes(): LogExtensionInterface; +} diff --git a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php new file mode 100644 index 0000000000000..5b08d28af6335 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Api\Data; + +use \Magento\Framework\Api\SearchResultsInterface; + +/** + * Login as customer log entity search results interface. + * + * @api + */ +interface LogSearchResultsInterface extends SearchResultsInterface +{ + /** + * Get log list. + * + * @return \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] + */ + public function getItems(); + + /** + * Set log list. + * + * @param \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] $items + * @return void + */ + public function setItems(array $items); +} diff --git a/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php new file mode 100644 index 0000000000000..4b5ee382c908a --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Api; + +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\LoginAsCustomerLog\Api\Data\LogSearchResultsInterface; + +/** + * Get login as customer log list considering search criteria. + * + * @api + */ +interface GetLogsListInterface +{ + /** + * Retrieve list of log entities. + * + * @param SearchCriteriaInterface $searchCriteria + * @return LogSearchResultsInterface + */ + public function execute(SearchCriteriaInterface $searchCriteria): LogSearchResultsInterface; +} diff --git a/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php new file mode 100644 index 0000000000000..67e1ece477727 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Api; + +/** + * Save login as custom logs entities. + * + * @api + */ +interface SaveLogsInterface +{ + /** + * Save logs. + * + * @param \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] $logs + * @return void + */ + public function execute(array $logs): void; +} diff --git a/app/code/Magento/LoginAsCustomerLog/Controller/Adminhtml/Log/Index.php b/app/code/Magento/LoginAsCustomerLog/Controller/Adminhtml/Log/Index.php new file mode 100644 index 0000000000000..ce0c50bf347fc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Controller/Adminhtml/Log/Index.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Controller\Adminhtml\Log; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Login As Customer log grid controller. + */ +class Index extends Action implements HttpGetActionInterface +{ + const ADMIN_RESOURCE = 'Magento_LoginAsCustomerLog::login_log'; + + /** + * @inheritdoc + */ + public function execute(): ResultInterface + { + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->setActiveMenu('Magento_LoginAsCustomerLog::login_log') + ->addBreadcrumb(__('Login as Customer Log'), __('List')); + $resultPage->getConfig()->getTitle()->prepend(__('Login as Customer Log')); + + return $resultPage; + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/Model/GetLogList.php b/app/code/Magento/LoginAsCustomerLog/Model/GetLogList.php new file mode 100644 index 0000000000000..f01b9419144a5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Model/GetLogList.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Model; + +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\LoginAsCustomerLog\Api\Data\LogSearchResultsInterface; +use Magento\LoginAsCustomerLog\Api\Data\LogSearchResultsInterfaceFactory; +use Magento\LoginAsCustomerLog\Api\GetLogsListInterface; +use Magento\LoginAsCustomerLog\Model\ResourceModel\Log\CollectionFactory; + +/** + * @inheritDoc + */ +class GetLogList implements GetLogsListInterface +{ + /** + * @var CollectionFactory + */ + private $logCollectionFactory; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var LogSearchResultsInterfaceFactory + */ + private $logSearchResultsFactory; + + /** + * @param CollectionFactory $logCollectionFactory + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param CollectionProcessorInterface $collectionProcessor + * @param LogSearchResultsInterfaceFactory $logSearchResultsFactory + */ + public function __construct( + CollectionFactory $logCollectionFactory, + SearchCriteriaBuilder $searchCriteriaBuilder, + CollectionProcessorInterface $collectionProcessor, + LogSearchResultsInterfaceFactory $logSearchResultsFactory + ) { + $this->logCollectionFactory = $logCollectionFactory; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->collectionProcessor = $collectionProcessor; + $this->logSearchResultsFactory = $logSearchResultsFactory; + } + + /** + * @inheritDoc + */ + public function execute(SearchCriteriaInterface $searchCriteria): LogSearchResultsInterface + { + $collection = $this->logCollectionFactory->create(); + $searchCriteria = $searchCriteria ?: $this->searchCriteriaBuilder->create(); + $this->collectionProcessor->process($searchCriteria, $collection); + + $searchResult = $this->logSearchResultsFactory->create(); + $searchResult->setItems($collection->getItems()); + $searchResult->setTotalCount($collection->getSize()); + $searchResult->setSearchCriteria($searchCriteria); + + return $searchResult; + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/Model/Log.php b/app/code/Magento/LoginAsCustomerLog/Model/Log.php new file mode 100644 index 0000000000000..cdf6789659b3f --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Model/Log.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Model; + +use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\LoginAsCustomerLog\Api\Data\LogExtensionInterface; +use Magento\LoginAsCustomerLog\Api\Data\LogInterface; + +/** + * @inheritDoc + */ +class Log extends AbstractExtensibleModel implements LogInterface +{ + /** + * @inheritDoc + */ + public function setLogId(int $logId): void + { + $this->setData(LogInterface::LOG_ID, $logId); + } + + /** + * @inheritDoc + */ + public function getLogId(): ?int + { + return $this->getData(LogInterface::LOG_ID) ? (int)$this->getData(LogInterface::LOG_ID) : null; + } + + /** + * @inheritDoc + */ + public function setTime(string $time): void + { + $this->setData(LogInterface::TIME, $time); + } + + /** + * @inheritDoc + */ + public function getTime(): ?string + { + return $this->getData(LogInterface::TIME); + } + + /** + * @inheritDoc + */ + public function setUserId(int $userId): void + { + $this->setData(LogInterface::USER_ID, $userId); + } + + /** + * @inheritDoc + */ + public function getUserId(): ?int + { + return $this->getData(LogInterface::USER_ID) ? (int)$this->getData(LogInterface::USER_ID) : null; + } + + /** + * @inheritDoc + */ + public function setUserName(string $userName): void + { + $this->setData(LogInterface::USERNAME, $userName); + } + + /** + * @inheritDoc + */ + public function getUserName(): ?string + { + return $this->getData(LogInterface::USERNAME); + } + + /** + * @inheritDoc + */ + public function setCustomerId(int $customerId): void + { + $this->setData(LogInterface::CUSTOMER_ID, $customerId); + } + + /** + * @inheritDoc + */ + public function getCustomerId(): ?int + { + return $this->getData(LogInterface::CUSTOMER_ID) ? + (int)$this->getData(LogInterface::CUSTOMER_ID) + : null; + } + + /** + * @inheritDoc + */ + public function setCustomerEmail(string $customerEmail): void + { + $this->setData(LogInterface::CUSTOMER_EMAIL, $customerEmail); + } + + /** + * @inheritDoc + */ + public function getCustomerEmail(): ?string + { + return $this->getData(LogInterface::CUSTOMER_EMAIL); + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(LogExtensionInterface $extensionAttributes): void + { + $this->_setExtensionAttributes($extensionAttributes); + } + + /** + * @inheritDoc + */ + public function getExtensionAttributes(): LogExtensionInterface + { + return $this->_getExtensionAttributes(); + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/Model/LogSearchResults.php b/app/code/Magento/LoginAsCustomerLog/Model/LogSearchResults.php new file mode 100644 index 0000000000000..2def7b0e09c6c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Model/LogSearchResults.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Model; + +use Magento\Framework\Api\SearchResults; +use Magento\LoginAsCustomerLog\Api\Data\LogSearchResultsInterface; + +/** + * @inheritDoc + */ +class LogSearchResults extends SearchResults implements LogSearchResultsInterface +{ +} diff --git a/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/Log.php b/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/Log.php new file mode 100644 index 0000000000000..11c142c078322 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/Log.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Model\ResourceModel; + +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\LoginAsCustomerLog\Api\Data\LogInterface; + +/** + * Login as customer log resource model. + */ +class Log extends AbstractDb +{ + const TABLE_NAME_LOG = 'magento_login_as_customer_log'; + + /** + * @inheritdoc + */ + protected function _construct() + { + $this->_init(self::TABLE_NAME_LOG, LogInterface::LOG_ID); + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/Log/Collection.php b/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/Log/Collection.php new file mode 100644 index 0000000000000..a5df9a0067ad1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/Log/Collection.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Model\ResourceModel\Log; + +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Magento\LoginAsCustomerLog\Model\Log; +use Magento\LoginAsCustomerLog\Model\ResourceModel\Log as LogResource; + +/** + * Login as customer log entities collection. + */ +class Collection extends AbstractCollection +{ + /** + * @inheritdoc + */ + protected function _construct() + { + $this->_init(Log::class, LogResource::class); + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/SaveLogs.php b/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/SaveLogs.php new file mode 100644 index 0000000000000..f793f75e678cc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Model/ResourceModel/SaveLogs.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\LoginAsCustomerLog\Api\SaveLogsInterface; + +/** + * @inheritDoc + */ +class SaveLogs implements SaveLogsInterface +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DateTime + */ + private $dateTime; + + /** + * @param ResourceConnection $resourceConnection + * @param DateTime $dateTime + */ + public function __construct(ResourceConnection $resourceConnection, DateTime $dateTime) + { + $this->resourceConnection = $resourceConnection; + $this->dateTime = $dateTime; + } + + /** + * @inheritDoc + */ + public function execute(array $logs): void + { + $logsData = []; + foreach ($logs as $log) { + if (!$log->getTime()) { + $log->setTime($this->dateTime->gmtDate()); + } + $logsData[] = $log->getData(); + } + $logTable = $this->resourceConnection->getTableName(Log::TABLE_NAME_LOG); + $connection = $this->resourceConnection->getConnection(); + $connection->insertMultiple($logTable, $logsData); + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/Plugin/LoginAsCustomerApi/Api/AuthenticateCustomerInterface/LogAuthenticationPlugin.php b/app/code/Magento/LoginAsCustomerLog/Plugin/LoginAsCustomerApi/Api/AuthenticateCustomerInterface/LogAuthenticationPlugin.php new file mode 100644 index 0000000000000..c1d999b552821 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Plugin/LoginAsCustomerApi/Api/AuthenticateCustomerInterface/LogAuthenticationPlugin.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Plugin\LoginAsCustomerApi\Api\AuthenticateCustomerInterface; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; +use Magento\LoginAsCustomerLog\Api\Data\LogInterfaceFactory; +use Magento\LoginAsCustomerLog\Api\SaveLogsInterface; +use Magento\User\Api\Data\UserInterfaceFactory; +use Magento\User\Model\ResourceModel\User; + +/** + * Log user logged in as customer plugin. + */ +class LogAuthenticationPlugin +{ + /** + * @var LogInterfaceFactory + */ + private $logFactory; + + /** + * @var SaveLogsInterface + */ + private $saveLogs; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var UserInterfaceFactory + */ + private $userFactory; + + /** + * @var User + */ + private $userResource; + + /** + * @param LogInterfaceFactory $logFactory + * @param SaveLogsInterface $saveLogs + * @param CustomerRepositoryInterface $customerRepository + * @param UserInterfaceFactory $userFactory + * @param User $userResource + */ + public function __construct( + LogInterfaceFactory $logFactory, + SaveLogsInterface $saveLogs, + CustomerRepositoryInterface $customerRepository, + UserInterfaceFactory $userFactory, + User $userResource + ) { + $this->logFactory = $logFactory; + $this->saveLogs = $saveLogs; + $this->customerRepository = $customerRepository; + $this->userFactory = $userFactory; + $this->userResource = $userResource; + } + + /** + * Log user authentication as customer. + * + * @param AuthenticateCustomerInterface $subject + * @param void $result + * @param AuthenticationDataInterface $data + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute( + AuthenticateCustomerInterface $subject, + $result, + AuthenticationDataInterface $data + ): void { + $customerId = $data->getCustomerId(); + $customerEmail = $this->customerRepository->getById($customerId)->getEmail(); + $userId = $data->getAdminId(); + $user = $this->userFactory->create(); + $this->userResource->load($user, $userId); + $log = $this->logFactory->create( + [ + 'data' => [ + 'customer_id' => $customerId, + 'user_id' => $userId, + 'customer_email' => $customerEmail, + 'user_name' => $user->getUserName(), + ], + ] + ); + $this->saveLogs->execute([$log]); + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/README.md b/app/code/Magento/LoginAsCustomerLog/README.md new file mode 100644 index 0000000000000..a44ae014f2c83 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/README.md @@ -0,0 +1,3 @@ +# Magento_LoginAsCustomerLog module + +The Magento_LoginAsCustomerLog module provides log for Login As Customer functionality diff --git a/app/code/Magento/LoginAsCustomerLog/Ui/DataProvider/LogDataProvider.php b/app/code/Magento/LoginAsCustomerLog/Ui/DataProvider/LogDataProvider.php new file mode 100644 index 0000000000000..53a76b08432d9 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/Ui/DataProvider/LogDataProvider.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerLog\Ui\DataProvider; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider; +use Magento\LoginAsCustomerLog\Api\Data\LogInterface; +use Magento\LoginAsCustomerLog\Api\GetLogsListInterface; +use Magento\Ui\DataProvider\SearchResultFactory; + +/** + * @inheritDoc + */ +class LogDataProvider extends DataProvider +{ + /** + * @var GetLogsListInterface + */ + private $getLogsList; + + /** + * @var SearchResultFactory + */ + private $searchResultFactory; + + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param ReportingInterface $reporting + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RequestInterface $request + * @param FilterBuilder $filterBuilder + * @param GetLogsListInterface $getLogsList + * @param SearchResultFactory $searchResultFactory + * @param SortOrderBuilder $sortOrderBuilder + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + ReportingInterface $reporting, + SearchCriteriaBuilder $searchCriteriaBuilder, + RequestInterface $request, + FilterBuilder $filterBuilder, + GetLogsListInterface $getLogsList, + SearchResultFactory $searchResultFactory, + SortOrderBuilder $sortOrderBuilder, + array $meta = [], + array $data = [] + ) { + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + $this->getLogsList = $getLogsList; + $this->searchResultFactory = $searchResultFactory; + $this->sortOrderBuilder = $sortOrderBuilder; + } + + /** + * @inheritdoc + */ + public function getSearchResult() + { + $searchCriteria = $this->getSearchCriteria(); + $sortOrders = $searchCriteria->getSortOrders(); + $sortOrder = current($sortOrders); + if (!$sortOrder->getField()) { + $sortOrder = $this->sortOrderBuilder->setDescendingDirection()->setField(LogInterface::TIME)->create(); + $searchCriteria->setSortOrders([$sortOrder]); + } + $result = $this->getLogsList->execute($searchCriteria); + + return $this->searchResultFactory->create( + $result->getItems(), + $result->getTotalCount(), + $searchCriteria, + LogInterface::LOG_ID + ); + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/composer.json b/app/code/Magento/LoginAsCustomerLog/composer.json new file mode 100644 index 0000000000000..3ee07ca7565a0 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-login-as-customer-log", + "description": "", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-ui": "*", + "magento/module-user": "*" + }, + "suggest": { + "magento/module-login-as-customer": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomerLog\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/etc/acl.xml b/app/code/Magento/LoginAsCustomerLog/etc/acl.xml new file mode 100644 index 0000000000000..0a46616b4ad7b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/acl.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Customer::customer"> + <resource id="Magento_LoginAsCustomer::login" title="Login as Customer" sortOrder="50"> + <resource id="Magento_LoginAsCustomerLog::login_log" title="View Login as Customer Log" sortOrder="20"/> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/LoginAsCustomerLog/etc/adminhtml/menu.xml b/app/code/Magento/LoginAsCustomerLog/etc/adminhtml/menu.xml new file mode 100644 index 0000000000000..143e0ad4b5a6c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/adminhtml/menu.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> + <menu> + <add id="Magento_LoginAsCustomerLog::login_log" + title="Login As Customer Log" + module="Magento_LoginAsCustomerLog" + parent="Magento_Customer::customer" + sortOrder="40" + resource="Magento_LoginAsCustomerLog::login_log" + action="loginascustomer_log/log/index"/> + </menu> +</config> diff --git a/app/code/Magento/LoginAsCustomerLog/etc/adminhtml/routes.xml b/app/code/Magento/LoginAsCustomerLog/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..9201e5e7ac91f --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="loginascustomer_log" frontName="loginascustomer_log"> + <module name="Magento_LoginAsCustomerLog"/> + </route> + </router> +</config> diff --git a/app/code/Magento/LoginAsCustomerLog/etc/db_schema.xml b/app/code/Magento/LoginAsCustomerLog/etc/db_schema.xml new file mode 100644 index 0000000000000..a1f40b4e5bbf5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/db_schema.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="magento_login_as_customer_log" resource="default" engine="innodb" comment="Login as Customer Logging"> + <column xsi:type="int" name="log_id" padding="11" unsigned="false" nullable="false" identity="true" comment="Log Id"/> + <column xsi:type="timestamp" name="time" on_update="false" nullable="true" comment="Event Date"/> + <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" comment="User Id"/> + <column xsi:type="varchar" name="user_name" nullable="true" length="40" comment="User Name"/> + <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> + <column xsi:type="varchar" name="customer_email" nullable="true" length="40" comment="Customer email"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="log_id"/> + </constraint> + <index referenceId="MAGENTO_LOGIN_AS_CUSTOMER_LOG_USER_ID" indexType="btree"> + <column name="user_id"/> + </index> + </table> +</schema> diff --git a/app/code/Magento/LoginAsCustomerLog/etc/db_schema_whitelist.json b/app/code/Magento/LoginAsCustomerLog/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..6523a283edd0f --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/db_schema_whitelist.json @@ -0,0 +1,17 @@ +{ + "magento_login_as_customer_log": { + "column": { + "log_id": true, + "user_name": true, + "user_id": true, + "customer_id": true, + "customer_email": true + }, + "constraint": { + "PRIMARY": true + }, + "index": { + "MAGENTO_LOGIN_AS_CUSTOMER_LOG_USER_ID": true + } + } +} diff --git a/app/code/Magento/LoginAsCustomerLog/etc/di.xml b/app/code/Magento/LoginAsCustomerLog/etc/di.xml new file mode 100755 index 0000000000000..49d19d85f0d65 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/di.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerLog\Api\Data\LogInterface" type="Magento\LoginAsCustomerLog\Model\Log"/> + <preference for="Magento\LoginAsCustomerLog\Api\Data\LogSearchResultsInterface" type="Magento\LoginAsCustomerLog\Model\LogSearchResults"/> + <preference for="Magento\LoginAsCustomerLog\Api\GetLogsListInterface" type="Magento\LoginAsCustomerLog\Model\GetLogList"/> + <preference for="Magento\LoginAsCustomerLog\Api\SaveLogsInterface" type="Magento\LoginAsCustomerLog\Model\ResourceModel\SaveLogs"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerLog/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerLog/etc/frontend/di.xml new file mode 100755 index 0000000000000..284b2db56e258 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/frontend/di.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface"> + <plugin name="log_authentication_plugin" + type="Magento\LoginAsCustomerLog\Plugin\LoginAsCustomerApi\Api\AuthenticateCustomerInterface\LogAuthenticationPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerLog/etc/module.xml b/app/code/Magento/LoginAsCustomerLog/etc/module.xml new file mode 100644 index 0000000000000..aef4e21780c4e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerLog"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerLog/registration.php b/app/code/Magento/LoginAsCustomerLog/registration.php new file mode 100644 index 0000000000000..2a4594d2218f6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerLog', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerLog/view/adminhtml/layout/loginascustomer_log_log_index.xml b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/layout/loginascustomer_log_log_index.xml new file mode 100644 index 0000000000000..4ec4853877419 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/layout/loginascustomer_log_log_index.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <uiComponent name="login_as_customer_log_listing"/> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml new file mode 100644 index 0000000000000..077fd6e18db7c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml @@ -0,0 +1,94 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">login_as_customer_log_listing.login_as_customer_log_listing_data_source</item> + </item> + </argument> + <settings> + <spinner>login_as_customer_log_listing_columns</spinner> + <deps> + <dep>login_as_customer_log_listing.login_as_customer_log_listing_data_source</dep> + </deps> + </settings> + <dataSource name="login_as_customer_log_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">log_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_LoginAsCustomerLog::login_log</aclResource> + <dataProvider class="Magento\LoginAsCustomerLog\Ui\DataProvider\LogDataProvider" name="login_as_customer_log_listing_data_source"> + <settings> + <requestFieldName>log_id</requestFieldName> + <primaryFieldName>Id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <settings> + <sticky>true</sticky> + </settings> + <bookmark name="bookmarks"/> + <columnsControls name="columns_controls"/> + <filterSearch name="name"/> + <filters name="listing_filters"> + <settings> + <templates> + <filters> + <select> + <param name="template" xsi:type="string">ui/grid/filters/elements/ui-select</param> + <param name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</param> + </select> + </filters> + </templates> + </settings> + </filters> + <paging name="listing_paging"/> + </listingToolbar> + <columns name="login_as_customer_log_listing_columns"> + <column name="log_id" sortOrder="10"> + <settings> + <filter>text</filter> + <label translate="true">ID</label> + </settings> + </column> + <column name="customer_id" sortOrder="20"> + <settings> + <filter>text</filter> + <label translate="true">Customer ID</label> + </settings> + </column> + <column name="customer_email" sortOrder="30"> + <settings> + <filter>text</filter> + <label translate="true">Customer Email</label> + </settings> + </column> + <column name="user_id" sortOrder="40"> + <settings> + <filter>text</filter> + <label translate="true">Admin ID</label> + </settings> + </column> + <column name="user_name" sortOrder="50"> + <settings> + <filter>text</filter> + <label translate="true">Admin Name</label> + </settings> + </column> + <column name="time" sortOrder="60"> + <settings> + <filter>text</filter> + <label translate="true">Logged In</label> + </settings> + </column> + </columns> +</listing> diff --git a/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php b/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php new file mode 100644 index 0000000000000..6b36a0720ecb3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerPageCache\Plugin\PageCache\Model\Config; + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\PageCache\Model\Config; +use Magento\Store\Model\ScopeInterface; + +/** + * Disable PageCache if enabled corresponding option in configuration + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class DisablePageCacheIfNeededPlugin +{ + /** + * Core store config + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var Session + */ + private $customerSession; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param Session $customerSession + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Session $customerSession + ) { + $this->scopeConfig = $scopeConfig; + $this->customerSession = $customerSession; + } + + /** + * Disable page cache if needed when admin is logged as customer + * + * @param Config $subject + * @param bool $isEnabled + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterIsEnabled(Config $subject, $isEnabled): bool + { + if ($isEnabled) { + $disable = $this->scopeConfig->getValue( + 'login_as_customer/general/disable_page_cache', + ScopeInterface::SCOPE_STORE + ); + $adminId = $this->customerSession->getLoggedAsCustomerAdmindId(); + if ($disable && $adminId) { + $isEnabled = false; + } + } + return $isEnabled; + } +} diff --git a/app/code/Magento/LoginAsCustomerPageCache/README.md b/app/code/Magento/LoginAsCustomerPageCache/README.md new file mode 100644 index 0000000000000..91fa43fb4e833 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/README.md @@ -0,0 +1,3 @@ +# LoginAsCustomerPageCache module + +The Magento_LoginAsCustomerPageCache module provides adaptation to PageCache functionality diff --git a/app/code/Magento/LoginAsCustomerPageCache/composer.json b/app/code/Magento/LoginAsCustomerPageCache/composer.json new file mode 100644 index 0000000000000..3412c9fcb611c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-login-as-customer-page-cache", + "description": "", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-store": "*" + }, + "suggest": { + "magento/module-page-cache": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomerPageCache\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerPageCache/etc/adminhtml/system.xml b/app/code/Magento/LoginAsCustomerPageCache/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..a586033af05db --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/etc/adminhtml/system.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="login_as_customer"> + <group id="general"> + <field id="disable_page_cache" translate="label comment" type="select" sortOrder="20" showInDefault="1" canRestore="1"> + <label>Disable Page Cache For Admin User</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment><![CDATA[If set to "Yes", page caching is disabled when the Admin user is logged in as a customer.]]></comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/LoginAsCustomerPageCache/etc/config.xml b/app/code/Magento/LoginAsCustomerPageCache/etc/config.xml new file mode 100644 index 0000000000000..ba3f954f4980b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/etc/config.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <login_as_customer> + <general> + <disable_page_cache>1</disable_page_cache> + </general> + </login_as_customer> + </default> +</config> diff --git a/app/code/Magento/LoginAsCustomerPageCache/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerPageCache/etc/frontend/di.xml new file mode 100644 index 0000000000000..1419c80d918dc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/etc/frontend/di.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\PageCache\Model\Config"> + <plugin name="login-as-customer-disable-page-cache" + type="Magento\LoginAsCustomerPageCache\Plugin\PageCache\Model\Config\DisablePageCacheIfNeededPlugin"/> + </type> +</config> \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomerPageCache/etc/module.xml b/app/code/Magento/LoginAsCustomerPageCache/etc/module.xml new file mode 100644 index 0000000000000..15c845a995794 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerPageCache"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerPageCache/registration.php b/app/code/Magento/LoginAsCustomerPageCache/registration.php new file mode 100644 index 0000000000000..587c3d56050d7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerPageCache/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerPageCache', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerSales/Plugin/AdminAddCommentOnOrderPlacementPlugin.php b/app/code/Magento/LoginAsCustomerSales/Plugin/AdminAddCommentOnOrderPlacementPlugin.php new file mode 100644 index 0000000000000..2ae982e536f49 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/Plugin/AdminAddCommentOnOrderPlacementPlugin.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerSales\Plugin; + +use Magento\Backend\Model\Auth\Session; +use Magento\Sales\Model\Order; + +/** + * Add comment after order placed by admin using admin panel. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class AdminAddCommentOnOrderPlacementPlugin +{ + /** + * @var Session + */ + private $userSession; + + /** + * @param Session $session + */ + public function __construct( + Session $session + ) { + $this->userSession = $session; + } + + /** + * Add comment after order placed by admin using admin panel. + * + * @param Order $subject + * @param Order $result + * @return Order + */ + public function afterPlace(Order $subject, Order $result): Order + { + $adminUser = $this->userSession->getUser(); + if ($adminUser) { + $subject->addCommentToStatusHistory( + 'Order Placed by Store Administrator', + false, + true + )->setIsCustomerNotified(false); + $subject->addCommentToStatusHistory( + "Order Placed by {$adminUser->getFirstName()} {$adminUser->getLastName()} using Admin Panel", + false, + false + )->setIsCustomerNotified(false); + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerSales/Plugin/AuthenticateCustomerPlugin.php b/app/code/Magento/LoginAsCustomerSales/Plugin/AuthenticateCustomerPlugin.php new file mode 100644 index 0000000000000..5d8541d4dd440 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/Plugin/AuthenticateCustomerPlugin.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerSales\Plugin; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; + +/** + * \Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface Plugin + * + * Remove all items from guest shopping cart before execute. Mark customer cart as not-guest after execute + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class AuthenticateCustomerPlugin +{ + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @param CustomerSession $customerSession + * @param CheckoutSession $checkoutSession + * @param CartRepositoryInterface $quoteRepository + */ + public function __construct( + CustomerSession $customerSession, + CheckoutSession $checkoutSession, + CartRepositoryInterface $quoteRepository + ) { + $this->customerSession = $customerSession; + $this->checkoutSession = $checkoutSession; + $this->quoteRepository = $quoteRepository; + } + + /** + * Remove all items from guest shopping cart + * + * @param AuthenticateCustomerInterface $subject + * @param AuthenticationDataInterface $authenticationData + * @return null + * @throws LocalizedException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute( + AuthenticateCustomerInterface $subject, + AuthenticationDataInterface $authenticationData + ) { + if (!$this->customerSession->getId()) { + $quote = $this->checkoutSession->getQuote(); + /* Remove items from guest cart */ + $quote->removeAllItems(); + $this->quoteRepository->save($quote); + } + return null; + } + + /** + * Mark customer cart as not-guest + * + * @param AuthenticateCustomerInterface $subject + * @param void $result + * @param AuthenticationDataInterface $authenticationData + * @return void + * @throws LocalizedException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute( + AuthenticateCustomerInterface $subject, + $result, + AuthenticationDataInterface $authenticationData + ) { + $this->checkoutSession->loadCustomerQuote(); + $quote = $this->checkoutSession->getQuote(); + + $quote->setCustomerIsGuest(0); + $this->quoteRepository->save($quote); + } +} diff --git a/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php b/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php new file mode 100644 index 0000000000000..dc7b295f61c4d --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerSales\Plugin; + +use Magento\Customer\Model\Session; +use Magento\Sales\Model\Order; +use Magento\User\Model\UserFactory; + +/** + * Add comment after order placed by admin using Login as Customer. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class FrontAddCommentOnOrderPlacementPlugin +{ + /** + * @var Session + */ + private $customerSession; + + /** + * @var UserFactory + */ + private $userFactory; + + /** + * @param Session $session + * @param UserFactory $userFactory + */ + public function __construct( + Session $session, + UserFactory $userFactory + ) { + $this->customerSession = $session; + $this->userFactory = $userFactory; + } + + /** + * Add comment after order placed by admin using Login as Customer. + * + * @param Order $subject + * @param Order $result + * @return Order + */ + public function afterPlace(Order $subject, Order $result): Order + { + $adminId = $this->customerSession->getLoggedAsCustomerAdmindId(); + if ($adminId) { + $adminUser = $this->userFactory->create()->load($adminId); + $subject->addCommentToStatusHistory( + 'Order Placed by Store Administrator', + false, + true + )->setIsCustomerNotified(false); + $subject->addCommentToStatusHistory( + "Order Placed by {$adminUser->getFirstName()} {$adminUser->getLastName()} using Login as Customer", + false, + false + )->setIsCustomerNotified(false); + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerSales/README.md b/app/code/Magento/LoginAsCustomerSales/README.md new file mode 100644 index 0000000000000..d5e38d8ec5909 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/README.md @@ -0,0 +1,3 @@ +# Magento_LoginAsCustomerSales module + +The Magento_LoginAsCustomerSales module is responsible for comunication between Magento_LoginAsCustomer and shopping cart state. diff --git a/app/code/Magento/LoginAsCustomerSales/composer.json b/app/code/Magento/LoginAsCustomerSales/composer.json new file mode 100644 index 0000000000000..32c800561784c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-login-as-customer-sales", + "description": "", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-quote": "*", + "magento/module-user": "*" + }, + "suggest": { + "magento/module-sales": "*", + "magento/module-login-as-customer-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\LoginAsCustomerSales\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerSales/etc/adminhtml/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..225688a8c7bab --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/etc/adminhtml/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Sales\Model\Order"> + <plugin name="admin-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\AdminAddCommentOnOrderPlacementPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerSales/etc/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/di.xml new file mode 100644 index 0000000000000..f267fc3850234 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/etc/di.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface"> + <plugin name="login_as_customer_sales_authenticate_customer" + type="Magento\LoginAsCustomerSales\Plugin\AuthenticateCustomerPlugin"/> + </type> +</config> \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomerSales/etc/module.xml b/app/code/Magento/LoginAsCustomerSales/etc/module.xml new file mode 100644 index 0000000000000..91f62000c58f5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerSales"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..1a010fcdead85 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Sales\Model\Order"> + <plugin name="front-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\FrontAddCommentOnOrderPlacementPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerSales/registration.php b/app/code/Magento/LoginAsCustomerSales/registration.php new file mode 100644 index 0000000000000..4ee5647f5e837 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerSales', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerUi/Block/Adminhtml/ConfirmationPopup.php b/app/code/Magento/LoginAsCustomerUi/Block/Adminhtml/ConfirmationPopup.php new file mode 100644 index 0000000000000..6655e0a3a8fc4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Block/Adminhtml/ConfirmationPopup.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Block\Adminhtml; + +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; +use Magento\Backend\Block\Template; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; + +/** + * Admin blog post + * + * @api + */ +class ConfirmationPopup extends Template +{ + /** + * Store Options + * + * @var StoreOptions + */ + private $storeOptions; + + /** + * Config + * + * @var ConfigInterface + */ + private $config; + + /** + * Json Serializer + * + * @var Json + */ + private $json; + + /** + * @param Template\Context $context + * @param StoreOptions $storeOptions + * @param ConfigInterface $config + * @param Json $json + * @param array $data + */ + public function __construct( + Template\Context $context, + StoreOptions $storeOptions, + ConfigInterface $config, + Json $json, + array $data = [] + ) { + parent::__construct($context, $data); + $this->storeOptions = $storeOptions; + $this->config = $config; + $this->json = $json; + } + + /** + * @inheritdoc + */ + public function getJsLayout() + { + $layout = $this->json->unserialize(parent::getJsLayout()); + $showStoreViewOptions = $this->config->isStoreManualChoiceEnabled(); + + $layout['components']['lac-confirmation-popup']['title'] = $showStoreViewOptions + ? __('Login as Customer: Select Store View') + : __('You are about to Login as Customer'); + $layout['components']['lac-confirmation-popup']['content'] = + __('Actions taken while in "Login as Customer" will affect actual customer data.'); + + $layout['components']['lac-confirmation-popup']['showStoreViewOptions'] = $showStoreViewOptions; + $layout['components']['lac-confirmation-popup']['storeViewOptions'] = $showStoreViewOptions + ? $this->storeOptions->toOptionArray() + : []; + + return $this->json->serialize($layout); + } + + /** + * Render block HTML + * + * @return string + */ + protected function _toHtml() + { + if (!$this->config->isEnabled()) { + return ''; + } + return parent::_toHtml(); + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerUi/Controller/Adminhtml/Login/Login.php new file mode 100755 index 0000000000000..c56b3c8e65fb0 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Controller/Adminhtml/Login/Login.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Controller\Adminhtml\Login; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\Auth\Session; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Url; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; +use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\DeleteExpiredAuthenticationDataInterface; +use Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Login as customer action + * Generate secret key and forward to the storefront action + * + * This action can be executed via GET request when "Store View To Login In" is disabled, and POST when it is enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Login extends Action implements HttpGetActionInterface, HttpPostActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_LoginAsCustomer::login'; + + /** + * @var Session + */ + private $authSession; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var AuthenticationDataInterfaceFactory + */ + private $authenticationDataFactory; + + /** + * @var SaveAuthenticationDataInterface + */ + private $saveAuthenticationData; + + /** + * @var DeleteExpiredAuthenticationDataInterface + */ + private $deleteExpiredAuthenticationData; + + /** + * @var Url + */ + private $url; + + /** + * @param Context $context + * @param Session $authSession + * @param StoreManagerInterface $storeManager + * @param CustomerRepositoryInterface $customerRepository + * @param ConfigInterface $config + * @param AuthenticationDataInterfaceFactory $authenticationDataFactory + * @param SaveAuthenticationDataInterface $saveAuthenticationData , + * @param DeleteExpiredAuthenticationDataInterface $deleteExpiredAuthenticationData + * @param Url $url + */ + public function __construct( + Context $context, + Session $authSession, + StoreManagerInterface $storeManager, + CustomerRepositoryInterface $customerRepository, + ConfigInterface $config, + AuthenticationDataInterfaceFactory $authenticationDataFactory, + SaveAuthenticationDataInterface $saveAuthenticationData, + DeleteExpiredAuthenticationDataInterface $deleteExpiredAuthenticationData, + Url $url + ) { + parent::__construct($context); + + $this->authSession = $authSession; + $this->storeManager = $storeManager; + $this->customerRepository = $customerRepository; + $this->config = $config; + $this->authenticationDataFactory = $authenticationDataFactory; + $this->saveAuthenticationData = $saveAuthenticationData; + $this->deleteExpiredAuthenticationData = $deleteExpiredAuthenticationData; + $this->url = $url; + } + + /** + * Login as customer + * + * @return ResultInterface + * @throws NoSuchEntityException + * @throws LocalizedException + */ + public function execute(): ResultInterface + { + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + + if (!$this->config->isEnabled()) { + $this->messageManager->addErrorMessage(__('Login As Customer is disabled.')); + return $resultRedirect->setPath('customer/index/index'); + } + + $customerId = (int)$this->_request->getParam('customer_id'); + if (!$customerId) { + $customerId = (int)$this->_request->getParam('entity_id'); + } + + try { + $this->customerRepository->getById($customerId); + } catch (NoSuchEntityException $e) { + $this->messageManager->addErrorMessage(__('Customer with this ID are no longer exist.')); + return $resultRedirect->setPath('customer/index/index'); + } + + $storeId = (int)$this->_request->getParam('store_id'); + if (empty($storeId) && $this->config->isStoreManualChoiceEnabled()) { + $this->messageManager->addNoticeMessage(__('Please select a Store View to login in.')); + return $resultRedirect->setPath('customer/index/edit', ['id' => $customerId]); + } + + $adminUser = $this->authSession->getUser(); + $userId = (int)$adminUser->getId(); + + /** @var AuthenticationDataInterface $authenticationData */ + $authenticationData = $this->authenticationDataFactory->create( + [ + 'customerId' => $customerId, + 'adminId' => $userId, + 'extensionAttributes' => null, + ] + ); + + $this->deleteExpiredAuthenticationData->execute($userId); + $secret = $this->saveAuthenticationData->execute($authenticationData); + + $redirectUrl = $this->getLoginProceedRedirectUrl($secret, $storeId); + $resultRedirect->setUrl($redirectUrl); + return $resultRedirect; + } + + /** + * Get login proceed redirect url + * + * @param string $secret + * @param int|null $storeId + * @return string + * @throws NoSuchEntityException + */ + private function getLoginProceedRedirectUrl(string $secret, ?int $storeId): string + { + if (null === $storeId) { + $store = $this->storeManager->getDefaultStoreView(); + } else { + $store = $this->storeManager->getStore($storeId); + } + + return $this->url + ->setScope($store) + ->getUrl('loginascustomer/login/index', ['secret' => $secret, '_nosid' => true]); + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/Controller/Login/Index.php b/app/code/Magento/LoginAsCustomerUi/Controller/Login/Index.php new file mode 100755 index 0000000000000..424473c7301ef --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Controller/Login/Index.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Controller\Login; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Message\ManagerInterface; +use Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface; +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerInterface; +use Psr\Log\LoggerInterface; + +/** + * Login As Customer storefront login action + */ +class Index implements HttpGetActionInterface +{ + /** + * @var ResultFactory + */ + private $resultFactory; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var GetAuthenticationDataBySecretInterface + */ + private $getAuthenticationDataBySecret; + + /** + * @var AuthenticateCustomerInterface + */ + private $authenticateCustomer; + + /** + * @var ManagerInterface + */ + private $messageManager; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param ResultFactory $resultFactory + * @param RequestInterface $request + * @param CustomerRepositoryInterface $customerRepository + * @param GetAuthenticationDataBySecretInterface $getAuthenticateDataProcessor + * @param AuthenticateCustomerInterface $authenticateCustomerProcessor + * @param ManagerInterface $messageManager + * @param LoggerInterface $logger + */ + public function __construct( + ResultFactory $resultFactory, + RequestInterface $request, + CustomerRepositoryInterface $customerRepository, + GetAuthenticationDataBySecretInterface $getAuthenticateDataProcessor, + AuthenticateCustomerInterface $authenticateCustomerProcessor, + ManagerInterface $messageManager, + LoggerInterface $logger + ) { + $this->resultFactory = $resultFactory; + $this->request = $request; + $this->customerRepository = $customerRepository; + $this->getAuthenticationDataBySecret = $getAuthenticateDataProcessor; + $this->authenticateCustomer = $authenticateCustomerProcessor; + $this->messageManager = $messageManager; + $this->logger = $logger; + } + + /** + * Login As Customer storefront login + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + + try { + $secret = $this->request->getParam('secret'); + if (empty($secret) || !is_string($secret)) { + throw new LocalizedException(__('Cannot login to account. No secret key provided.')); + } + + $authenticateData = $this->getAuthenticationDataBySecret->execute($secret); + + try { + $customer = $this->customerRepository->getById($authenticateData->getCustomerId()); + } catch (NoSuchEntityException $e) { + throw new LocalizedException(__('Customer are no longer exist.')); + } + + $this->authenticateCustomer->execute($authenticateData); + + $this->messageManager->addSuccessMessage( + __('You are logged in as customer: %1', $customer->getFirstname() . ' ' . $customer->getLastname()) + ); + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->getConfig()->getTitle()->set(__('You are logged in')); + return $this->resultFactory->create(ResultFactory::TYPE_PAGE); + + } catch (LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + $resultRedirect->setPath('/'); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + + $this->messageManager->addErrorMessage(__('Cannot login to account.')); + $resultRedirect->setPath('/'); + } + return $resultRedirect; + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/CustomerData/LoginAsCustomerUi.php b/app/code/Magento/LoginAsCustomerUi/CustomerData/LoginAsCustomerUi.php new file mode 100644 index 0000000000000..be30ee332f621 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/CustomerData/LoginAsCustomerUi.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\CustomerData; + +use Magento\Customer\CustomerData\SectionSourceInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\LocalizedException; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Customer data for the logged_as_customer section + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class LoginAsCustomerUi implements SectionSourceInterface +{ + /** + * @var Session + */ + private $customerSession; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param Session $customerSession + * @param StoreManagerInterface $storeManager + */ + public function __construct( + Session $customerSession, + StoreManagerInterface $storeManager + ) { + $this->customerSession = $customerSession; + $this->storeManager = $storeManager; + } + + /** + * Retrieve private customer data for the logged_as_customer section + * + * @return array + * @throws LocalizedException + */ + public function getSectionData(): array + { + if (!$this->customerSession->getCustomerId()) { + return []; + } + + return [ + 'adminUserId' => $this->customerSession->getLoggedAsCustomerAdmindId(), + 'websiteName' => $this->storeManager->getWebsite()->getName() + ]; + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/Model/Config/Source/StoreViewLogin.php b/app/code/Magento/LoginAsCustomerUi/Model/Config/Source/StoreViewLogin.php new file mode 100644 index 0000000000000..cf77f695287fd --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Model/Config/Source/StoreViewLogin.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Model\Config\Source; + +/** + * @inheritdoc + */ +class StoreViewLogin implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * @const int + */ + private const AUTODETECT = 0; + + /** + * @const int + */ + private const MANUAL = 1; + + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + ['value' => self::AUTODETECT, 'label' => __('Auto-Detection (default)')], + ['value' => self::MANUAL, 'label' => __('Manual Selection')], + ]; + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/Plugin/AdminLogoutPlugin.php b/app/code/Magento/LoginAsCustomerUi/Plugin/AdminLogoutPlugin.php new file mode 100644 index 0000000000000..ff61b9482ac17 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Plugin/AdminLogoutPlugin.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Plugin; + +use Magento\Backend\Model\Auth; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\DeleteExpiredAuthenticationDataInterface; + +/** + * Delete all Login as Customer sessions for logging out admin. + */ +class AdminLogoutPlugin +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var DeleteExpiredAuthenticationDataInterface + */ + private $deleteExpiredAuthenticationData; + + /** + * @param ConfigInterface $config + * @param DeleteExpiredAuthenticationDataInterface $deleteExpiredAuthenticationData + */ + public function __construct( + ConfigInterface $config, + DeleteExpiredAuthenticationDataInterface $deleteExpiredAuthenticationData + ) { + $this->config = $config; + $this->deleteExpiredAuthenticationData = $deleteExpiredAuthenticationData; + } + + /** + * Delete all Login as Customer sessions for logging out admin. + * + * @param Auth $subject + */ + public function beforeLogout(Auth $subject): void + { + if ($this->config->isEnabled()) { + $userId = (int)$subject->getUser()->getId(); + $this->deleteExpiredAuthenticationData->execute($userId); + } + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/Plugin/Button/ToolbarPlugin.php b/app/code/Magento/LoginAsCustomerUi/Plugin/Button/ToolbarPlugin.php new file mode 100644 index 0000000000000..ec2c4dd5dcb65 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Plugin/Button/ToolbarPlugin.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Plugin\Button; + +use Magento\Backend\Block\Widget\Button\ButtonList; +use Magento\Backend\Block\Widget\Button\Toolbar; +use Magento\Framework\View\Element\AbstractBlock; +use Magento\Framework\Escaper; +use Magento\Framework\AuthorizationInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; + +/** + * Plugin for \Magento\Backend\Block\Widget\Button\Toolbar. + */ +class ToolbarPlugin +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var Escaper + */ + private $escaper; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * ToolbarPlugin constructor. + * @param AuthorizationInterface $authorization + * @param ConfigInterface $config + * @param Escaper $escaper + */ + public function __construct( + AuthorizationInterface $authorization, + ConfigInterface $config, + Escaper $escaper + ) { + $this->authorization = $authorization; + $this->config = $config; + $this->escaper = $escaper; + } + + /** + * Add Login As Customer button. + * + * @param \Magento\Backend\Block\Widget\Button\Toolbar $subject + * @param \Magento\Framework\View\Element\AbstractBlock $context + * @param \Magento\Backend\Block\Widget\Button\ButtonList $buttonList + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforePushButtons( + Toolbar $subject, + AbstractBlock $context, + ButtonList $buttonList + ):void { + $order = false; + $nameInLayout = $context->getNameInLayout(); + + if ('sales_order_edit' == $nameInLayout) { + $order = $context->getOrder(); + } elseif ('sales_invoice_view' == $nameInLayout) { + $order = $context->getInvoice()->getOrder(); + } elseif ('sales_shipment_view' == $nameInLayout) { + $order = $context->getShipment()->getOrder(); + } elseif ('sales_creditmemo_view' == $nameInLayout) { + $order = $context->getCreditmemo()->getOrder(); + } + if ($order) { + + $isAllowed = $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); + $isEnabled = $this->config->isEnabled(); + if ($isAllowed && $isEnabled) { + if (!empty($order['customer_id'])) { + $buttonUrl = $context->getUrl('loginascustomer/login/login', [ + 'customer_id' => $order['customer_id'] + ]); + $buttonList->add( + 'guest_to_customer', + [ + 'label' => __('Login As Customer'), + 'onclick' => 'window.lacConfirmationPopup("' + . $this->escaper->escapeHtml($this->escaper->escapeJs($buttonUrl)) + . '")', + 'class' => 'reset' + ], + -1 + ); + } + } + } + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/Plugin/InvalidateExpiredSessionPlugin.php b/app/code/Magento/LoginAsCustomerUi/Plugin/InvalidateExpiredSessionPlugin.php new file mode 100644 index 0000000000000..1f80292ed2738 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Plugin/InvalidateExpiredSessionPlugin.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\LoginAsCustomerUi\Plugin; + +use Magento\Customer\Model\Session; +use Magento\Framework\App\ActionInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerSessionActiveInterface; + +/** + * Invalidate expired and not active Login as Customer sessions. + */ +class InvalidateExpiredSessionPlugin +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var Session + */ + private $session; + + /** + * @var IsLoginAsCustomerSessionActiveInterface + */ + private $isLoginAsCustomerSessionActive; + + /** + * @param ConfigInterface $config + * @param Session $session + * @param IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive + */ + public function __construct( + ConfigInterface $config, + Session $session, + IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive + ) { + $this->session = $session; + $this->isLoginAsCustomerSessionActive = $isLoginAsCustomerSessionActive; + $this->config = $config; + } + + /** + * Invalidate expired and not active Login as Customer sessions. + * + * @param ActionInterface $subject + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(ActionInterface $subject) + { + if ($this->config->isEnabled()) { + $adminId = (int)$this->session->getLoggedAsCustomerAdmindId(); + $customerId = (int)$this->session->getCustomerId(); + if ($adminId && $customerId) { + if (!$this->isLoginAsCustomerSessionActive->execute($customerId, $adminId)) { + $this->session->destroy(); + } + } + } + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/README.md b/app/code/Magento/LoginAsCustomerUi/README.md new file mode 100644 index 0000000000000..11e8da9310920 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/README.md @@ -0,0 +1,3 @@ +# Magento_LoginAsCustomerSales module + +The Magento_LoginAsCustomerUi module provides UI for Magento_LoginAsCustomerUi diff --git a/app/code/Magento/LoginAsCustomerUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php b/app/code/Magento/LoginAsCustomerUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php new file mode 100644 index 0000000000000..88add39eb1efb --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Ui\Customer\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Escaper; +use Magento\Framework\Registry; +use Magento\Backend\Block\Widget\Context; +use Magento\Customer\Block\Adminhtml\Edit\GenericButton; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; + +/** + * Login As Customer button UI component. + */ +class LoginAsCustomerButton extends GenericButton implements ButtonProviderInterface +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * Escaper + * + * @var Escaper + */ + private $escaper; + + /** + * @param Context $context + * @param Registry $registry + * @param ConfigInterface $config + */ + public function __construct( + Context $context, + Registry $registry, + ConfigInterface $config + ) { + parent::__construct($context, $registry); + $this->authorization = $context->getAuthorization(); + $this->config = $config; + $this->escaper = $context->getEscaper(); + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $customerId = $this->getCustomerId(); + $data = []; + $isAllowed = $customerId && $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); + $isEnabled = $this->config->isEnabled(); + if ($isAllowed && $isEnabled) { + $data = [ + 'label' => __('Login As Customer'), + 'class' => 'login login-button', + 'on_click' => 'window.lacConfirmationPopup("' + . $this->escaper->escapeHtml($this->escaper->escapeJs($this->getLoginUrl())) + . '")', + 'sort_order' => 70, + ]; + } + + return $data; + } + + /** + * Get Login As Customer login url. + * + * @return string + */ + public function getLoginUrl(): string + { + return $this->getUrl('loginascustomer/login/login', ['customer_id' => $this->getCustomerId()]); + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/Ui/Store/Component/Control/LoginAsCustomerButton.php b/app/code/Magento/LoginAsCustomerUi/Ui/Store/Component/Control/LoginAsCustomerButton.php new file mode 100644 index 0000000000000..265e1addc48ab --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/Ui/Store/Component/Control/LoginAsCustomerButton.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\Ui\Store\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; + +/** + * Login As Customer button UI component. + */ +class LoginAsCustomerButton implements ButtonProviderInterface +{ + /** + * Get button data + * + * @return array + */ + public function getButtonData(): array + { + return [ + 'label' => __('Login As Customer'), + 'class' => 'save primary', + 'data_attribute' => [ + 'mage-init' => ['button' => ['event' => 'save']], + 'form-role' => 'save', + ], + 'sort_order' => 90, + ]; + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/ViewModel/Configuration.php b/app/code/Magento/LoginAsCustomerUi/ViewModel/Configuration.php new file mode 100644 index 0000000000000..7cbe30b116194 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/ViewModel/Configuration.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerUi\ViewModel; + +use Magento\Customer\Model\Context; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; + +/** + * View model to get extension configuration in the template + */ +class Configuration implements \Magento\Framework\View\Element\Block\ArgumentInterface +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var \Magento\Framework\App\Http\Context + */ + private $httpContext; + + /** + * @param ConfigInterface $config + * @param \Magento\Framework\App\Http\Context $httpContext + */ + public function __construct( + ConfigInterface $config, + \Magento\Framework\App\Http\Context $httpContext + ) { + $this->config = $config; + $this->httpContext = $httpContext; + } + + /** + * Retrieve true if login as a customer is enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->config->isEnabled() && $this->isLoggedIn(); + } + + /** + * Is logged in + * + * @return bool + */ + private function isLoggedIn(): bool + { + return (bool)$this->httpContext->getValue(Context::CONTEXT_AUTH); + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/composer.json b/app/code/Magento/LoginAsCustomerUi/composer.json new file mode 100644 index 0000000000000..ba18098ee13f9 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-login-as-customer-ui", + "description": "", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-store": "*" + }, + "suggest": { + "magento/module-login-as-customer": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomerUi\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/etc/acl.xml b/app/code/Magento/LoginAsCustomerUi/etc/acl.xml new file mode 100755 index 0000000000000..f49526f6bbb04 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/etc/acl.xml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Customer::customer"> + <resource id="Magento_LoginAsCustomer::login" title="Login as Customer" sortOrder="50"> + <resource id="Magento_LoginAsCustomer::login_button" title="Allow Login as Customer Button" sortOrder="10" /> + </resource> + </resource> + <resource id="Magento_Backend::stores"> + <resource id="Magento_Backend::stores_settings"> + <resource id="Magento_Config::config"> + <resource id="Magento_LoginAsCustomer::config_section" title="Login as Customer Section" /> + </resource> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/di.xml b/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..2ee0b83be573e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/di.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Backend\Block\Widget\Button\Toolbar"> + <plugin name="login_as_customer_button_toolbar" type="Magento\LoginAsCustomerUi\Plugin\Button\ToolbarPlugin"/> + </type> + <type name="Magento\Backend\Model\Auth"> + <plugin name="login_as_customer_admin_logout" type="Magento\LoginAsCustomerUi\Plugin\AdminLogoutPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/routes.xml b/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/routes.xml new file mode 100755 index 0000000000000..1122e490db306 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/routes.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="loginascustomer" frontName="loginascustomer"> + <module name="Magento_LoginAsCustomerUi"/> + </route> + </router> +</config> diff --git a/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/system.xml b/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/system.xml new file mode 100755 index 0000000000000..2a5614d44d00c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/etc/adminhtml/system.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="login_as_customer" translate="label" type="text" sortOrder="1" showInDefault="1"> + <class>separator-top</class> + <label>Login as Customer</label> + <tab>customer</tab> + <resource>Magento_LoginAsCustomer::config_section</resource> + <group id="general" translate="label" type="text" sortOrder="10" showInDefault="1" canRestore="1"> + <label>Login as Customer Settings</label> + <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" canRestore="1"> + <label>Enable Extension</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + <field id="store_view_manual_choice_enabled" translate="label comment" type="select" sortOrder="40" showInDefault="1" canRestore="1"> + <label>Store View To Login To</label> + <source_model>Magento\LoginAsCustomerUi\Model\Config\Source\StoreViewLogin</source_model> + <comment><![CDATA[ + Use the "Manual Selection" option on a multi-website setup that has "Share Customer Accounts" enabled globally. + If set to "Manual Selection", the "Login as Customer" admin can select a store view after logging in. + ]]></comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/LoginAsCustomerUi/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerUi/etc/frontend/di.xml new file mode 100755 index 0000000000000..746e908234edc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/etc/frontend/di.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Customer\CustomerData\SectionPoolInterface"> + <arguments> + <argument name="sectionSourceMap" xsi:type="array"> + <item name="loggedAsCustomer" xsi:type="string">Magento\LoginAsCustomerUi\CustomerData\LoginAsCustomerUi</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\ActionInterface"> + <plugin name="invalidate_expired_session_plugin" + type="Magento\LoginAsCustomerUi\Plugin\InvalidateExpiredSessionPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerUi/etc/frontend/routes.xml b/app/code/Magento/LoginAsCustomerUi/etc/frontend/routes.xml new file mode 100755 index 0000000000000..7336b032b21c6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/etc/frontend/routes.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="standard"> + <route id="loginascustomer" frontName="loginascustomer"> + <module name="Magento_LoginAsCustomerUi"/> + </route> + </router> +</config> diff --git a/app/code/Magento/LoginAsCustomerUi/etc/module.xml b/app/code/Magento/LoginAsCustomerUi/etc/module.xml new file mode 100644 index 0000000000000..c937de225c306 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerUi"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerUi/registration.php b/app/code/Magento/LoginAsCustomerUi/registration.php new file mode 100644 index 0000000000000..501dc6a7ca382 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerUi', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/adminhtml_order_shipment_view.xml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/adminhtml_order_shipment_view.xml new file mode 100644 index 0000000000000..4671a2178ded7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/adminhtml_order_shipment_view.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <update handle="loginascustomer_confirmation_popup" /> +</layout> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/customer_index_edit.xml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/customer_index_edit.xml new file mode 100644 index 0000000000000..4671a2178ded7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/customer_index_edit.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <update handle="loginascustomer_confirmation_popup" /> +</layout> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/loginascustomer_confirmation_popup.xml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/loginascustomer_confirmation_popup.xml new file mode 100644 index 0000000000000..542cbbbd39d6e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/loginascustomer_confirmation_popup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <referenceContainer name="content"> + <block class="Magento\LoginAsCustomerUi\Block\Adminhtml\ConfirmationPopup" name="lac.confirmation.popup" template="Magento_LoginAsCustomerUi::confirmation-popup.phtml"> + <arguments> + <argument name="jsLayout" xsi:type="array"> + <item name="components" xsi:type="array"> + <item name="lac-confirmation-popup" xsi:type="array"> + <item name="component" xsi:type="string">Magento_LoginAsCustomerUi/js/confirmation-popup</item> + </item> + </item> + </argument> + </arguments> + </block> + </referenceContainer> +</layout> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_creditmemo_view.xml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_creditmemo_view.xml new file mode 100644 index 0000000000000..4671a2178ded7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_creditmemo_view.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <update handle="loginascustomer_confirmation_popup" /> +</layout> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_invoice_view.xml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_invoice_view.xml new file mode 100644 index 0000000000000..4671a2178ded7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_invoice_view.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <update handle="loginascustomer_confirmation_popup" /> +</layout> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_view.xml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_view.xml new file mode 100644 index 0000000000000..4671a2178ded7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/layout/sales_order_view.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> + <update handle="loginascustomer_confirmation_popup" /> +</layout> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/templates/confirmation-popup.phtml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/templates/confirmation-popup.phtml new file mode 100644 index 0000000000000..0f46bb952f9bb --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/templates/confirmation-popup.phtml @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** @var \Magento\LoginAsCustomerUi\Block\Adminhtml\ConfirmationPopup $block */ +?> + +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout();?> + } + } + </script> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/ui_component/customer_form.xml b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/ui_component/customer_form.xml new file mode 100755 index 0000000000000..96aefae34299e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/ui_component/customer_form.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <settings> + <buttons> + <button name="login_as_customer" + class="\Magento\LoginAsCustomerUi\Ui\Customer\Component\Control\LoginAsCustomerButton"/> + </buttons> + </settings> +</form> diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..2901f95f0e279 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,42 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// +// Variables +// --------------------------------------------- + +@lac-confirm-popup-title-background-color: #ccc; +@lac-confirm-popup-content-color: #514943; + +// +// Common +// --------------------------------------------- + +& when (@media-common = true) { + .modal-popup.confirm.lac-confirm { + .modal-inner-wrap { + max-width: 55rem; + } + .modal-title { + border-bottom: 1px solid @lac-confirm-popup-title-background-color; + padding-bottom: 15px; + width: 100%; + } + + .store-view-ptions { + padding-top: 15px; + } + + .modal-content { + .message-warning { + padding-left: 4.5rem; + &:before { + color: @lac-confirm-popup-content-color; + left: 5px; + } + } + } + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/js/confirmation-popup.js b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/js/confirmation-popup.js new file mode 100644 index 0000000000000..22f379c0009f4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/js/confirmation-popup.js @@ -0,0 +1,92 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'Magento_Ui/js/modal/confirm', + 'jquery', + 'ko', + 'mage/translate', + 'mage/template', + 'text!Magento_LoginAsCustomerUi/template/confirmation-popup/store-view-ptions.html' +], function (Component, confirm, $, ko, $t, template, selectTpl) { + + 'use strict'; + + return Component.extend({ + /** + * Initialize Component + */ + initialize: function () { + var self = this, + content = '<div class="message message-warning">' + self.content + '</div>'; + + this._super(); + + if (self.showStoreViewOptions) { + content = template( + selectTpl, + { + data: { + showStoreViewOptions: self.showStoreViewOptions, + storeViewOptions: self.storeViewOptions, + label: $t('Store View') + } + }) + content; + } + + /** + * Confirmation popup + * + * @param {String} url + * @returns {Boolean} + */ + window.lacConfirmationPopup = function (url) { + confirm({ + title: self.title, + content: content, + modalClass: 'confirm lac-confirm', + actions: { + /** + * Confirm action. + */ + confirm: function () { + var storeId = $('#lac-confirmation-popup-store-id').val(); + + if (storeId) { + url += url.indexOf('?') === -1 ? '?' : '&'; + url += 'store_id=' + storeId; + } + window.open(url); + } + }, + buttons: [{ + text: $t('Cancel'), + class: 'action-secondary action-dismiss', + + /** + * Click handler. + */ + click: function (event) { + this.closeModal(event); + } + }, { + text: $t('Login as Customer'), + class: 'action-primary action-accept', + + /** + * Click handler. + */ + click: function (event) { + this.closeModal(event, true); + } + }] + }); + + return false; + }; + } + }); +}); diff --git a/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html new file mode 100644 index 0000000000000..ed1f991245e70 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html @@ -0,0 +1,32 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<% if(data.showStoreViewOptions){ %> +<div class="store-view-ptions"> + <fieldset class="admin__fieldset" > + <div class="admin__field _required" > + <div class="admin__field-label"> + <label for="lac-confirmation-popup-store-id"> + <span><%= data.label %></span> + </label> + </div> + <div class="admin__field-control"> + <select class="admin__control-select" id="lac-confirmation-popup-store-id"> + <% _.each(data.storeViewOptions, function(website) { %> + <optgroup label="<%= website.label %>"></optgroup> + <% _.each(website.value, function(group) { %> + <optgroup label="<%= group.label %>"></optgroup> + <% _.each(group.value, function(storeview) { %> + <option value="<%= storeview.value %>"><%= storeview.label %></option> + <% }); %> + <% }); %> + <% }); %> + </select> + </div> + </div> + </fieldset> +</div> +<% } %> diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/layout/default.xml b/app/code/Magento/LoginAsCustomerUi/view/frontend/layout/default.xml new file mode 100644 index 0000000000000..c27f3adcf23cc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/layout/default.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="after.body.start"> + <block name="login-as-customer-notice" template="Magento_LoginAsCustomerUi::html/notices.phtml"> + <arguments> + <argument name="config" xsi:type="object">Magento\LoginAsCustomerUi\ViewModel\Configuration</argument> + </arguments> + + <container name="login-as-customer-notice-links"> + <block class="Magento\Customer\Block\Account\AuthorizationLink" name="login-as-customer-logout-link" + template="Magento_LoginAsCustomerUi::html/notices/logout-link.phtml" /> + </container> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/layout/loginascustomer_login_index.xml b/app/code/Magento/LoginAsCustomerUi/view/frontend/layout/loginascustomer_login_index.xml new file mode 100644 index 0000000000000..e7c3010a86364 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/layout/loginascustomer_login_index.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="page.main.title"> + <action method="setPageTitle"> + <argument name="title" translate="true" xsi:type="string">You are logged in.</argument> + </action> + </referenceBlock> + <referenceContainer name="content"> + <block class="Magento\Framework\View\Element\Template" name="loginascustomer_login" template="Magento_LoginAsCustomerUi::login.phtml"/> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/html/notices.phtml b/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/html/notices.phtml new file mode 100644 index 0000000000000..a2358cdb2f18b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/html/notices.phtml @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @var \Magento\Framework\View\Element\Template $block + * @var \Magento\Framework\Escaper $escaper + */ +$viewFileUrl = $block->getViewFileUrl('Magento_LoginAsCustomerUi::images/magento-icon.svg'); +?> +<?php if ($block->getConfig()->isEnabled()): ?> + <div data-bind="scope: 'loginAsCustomer'" > + <div class="lac-notification clearfix" data-bind="visible: isVisible" style="display: none"> + <div class="top-container"> + <div class="lac-notification-icon wrapper"> + <img class="logo-img" src="<?= $escaper->escapeUrl($viewFileUrl) ?>" alt="Magento" /> + </div> + <div class="lac-notification-text wrapper"> + <span data-bind="html: notificationText"></span> + </div> + <div class="lac-notification-links wrapper"> + <?= $block->getChildHtml('login-as-customer-notice-links') ?> + </div> + </div> + </div> + </div> + <script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/core/app": { + "components": { + "loginAsCustomer": { + "component": "Magento_LoginAsCustomerUi/js/view/loginAsCustomer" + } + } + } + } + } + </script> +<?php endif; ?> diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/html/notices/logout-link.phtml b/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/html/notices/logout-link.phtml new file mode 100644 index 0000000000000..ae29324ade0a1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/html/notices/logout-link.phtml @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @var \Magento\Customer\Block\Account\AuthorizationLink $block + * @var \Magento\Framework\Escaper $escaper + */ +$dataPostParam = ''; +if ($block->isLoggedIn()) { + $dataPostParam = sprintf(" data-post='%s'", $block->getPostParams()); +} +?> + +<a class="lac-notification-close-link" <?= /* @noEscape */ $block->getLinkAttributes() +?><?= /* @noEscape */ $dataPostParam ?>> + <?= $escaper->escapeHtml(__('Close Session')) ?> +</a> + diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/login.phtml b/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/login.phtml new file mode 100644 index 0000000000000..2abd8fc204831 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/templates/login.phtml @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** @var \Magento\Framework\View\Element\Template $block */ +?> + +<script type="text/x-magento-init"> +{ + "*": { + "Magento_LoginAsCustomerUi/js/login": { + "redirectUrl": "<?= /* @noEscape */ $block->getUrl('customer/account/index') ?>" + } + } +} +</script> diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/web/css/source/_module.less b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/css/source/_module.less new file mode 100644 index 0000000000000..372405c2635ef --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/css/source/_module.less @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// +// Variables +// --------------------------------------------- + +@lac-notification-background-color: #373330; +@lac-notification-color: #fff; +@lac-notification-links-color: #fff; + +// +// Common +// --------------------------------------------- + +& when (@media-common = true) { + .lac-notification { + background-color: @lac-notification-background-color; + color: @lac-notification-color; + font-size: 16px; + + .lac-notification-icon { + float: left; + margin: 10px 25px 10px 10px; + + .logo-img { + display: block + } + } + + .lac-notification-text { + float: left; + padding: 15px 0; + } + + .lac-notification-links { + float: right; + padding: 15px 0; + + a { + color: @lac-notification-links-color; + font-size: 14px; + } + + .lac-notification-close-link { + &:after { + background: url('../Magento_LoginAsCustomerUi/images/close.svg'); + content: ' '; + display: inline-block; + height: 12px; + margin-left: 5px; + vertical-align: middle; + width: 12px; + } + } + } + } +} + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .lac-notification { + padding: 5px 0; + + .lac-notification-icon { + display: none; + } + + .lac-notification-text, + .lac-notification-links { + float: none; + padding: 5px 0; + text-align: center; + } + } +} diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/web/images/close.svg b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/images/close.svg new file mode 100644 index 0000000000000..0895684d12e63 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/images/close.svg @@ -0,0 +1,2 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 413.348 413.348" height="12px" viewBox="0 0 413.348 413.348" width="12px"><g><path d="m413.348 24.354-24.354-24.354-182.32 182.32-182.32-182.32-24.354 24.354 182.32 182.32-182.32 182.32 24.354 24.354 182.32-182.32 182.32 182.32 24.354-24.354-182.32-182.32z" data-original="#000000" class="active-path" data-old_color="#000000" fill="#FFFFFF"/></g> </svg> diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/web/images/magento-icon.svg b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/images/magento-icon.svg new file mode 100644 index 0000000000000..47e64067795ef --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/images/magento-icon.svg @@ -0,0 +1 @@ +<svg baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" width="28" height="33" viewBox="-0.154 0 54 62"><g fill="#E85D22"><path d="M26.845 8.857"/><path d="M53.692 15.5v31l-7.67 4.43v-31L26.844 8.856 7.67 19.926V50.93L0 46.5v-31L26.845 0zM26.847 62L15.34 55.355V24.357l7.67-4.43V50.93l3.835 2.327 3.837-2.327v-31l7.67 4.427v30.998z"/></g></svg> diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/web/js/login.js b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/js/login.js new file mode 100644 index 0000000000000..ab325bc90cf00 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/js/login.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Customer/js/customer-data', + 'Magento_Customer/js/section-config' +], function ($, customerData, sectionConfig) { + + 'use strict'; + + return function (config) { + $('body').trigger('processStart'); + customerData.reload(sectionConfig.getSectionNames()).done(function () { + window.location.href = config.redirectUrl; + }); + }; +}); diff --git a/app/code/Magento/LoginAsCustomerUi/view/frontend/web/js/view/loginAsCustomer.js b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/js/view/loginAsCustomer.js new file mode 100644 index 0000000000000..7f6cad6ce3f2d --- /dev/null +++ b/app/code/Magento/LoginAsCustomerUi/view/frontend/web/js/view/loginAsCustomer.js @@ -0,0 +1,41 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_Customer/js/customer-data', + 'mage/translate' +], function ($, Component, customerData) { + 'use strict'; + + return Component.extend({ + + defaults: { + isVisible: false + }, + + /** @inheritdoc */ + initialize: function () { + this._super(); + + this.customer = customerData.get('customer'); + this.loginAsCustomer = customerData.get('loggedAsCustomer'); + this.isVisible(this.loginAsCustomer().adminUserId); + + this.notificationText = $.mage.__('You are connected as <strong>%1</strong> on %2') + .replace('%1', this.customer().fullname) + .replace('%2', this.loginAsCustomer().websiteName); + }, + + /** @inheritdoc */ + initObservable: function () { + this._super() + .observe('isVisible'); + + return this; + } + }); +}); diff --git a/app/code/Magento/LoginAsCustomerWebapi/Api/CreateCustomerAccessTokenInterface.php b/app/code/Magento/LoginAsCustomerWebapi/Api/CreateCustomerAccessTokenInterface.php new file mode 100644 index 0000000000000..52acc695af0cc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/Api/CreateCustomerAccessTokenInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerWebapi\Api; + +/** + * Interface providing customer token generation for admin. + * + * @api + */ +interface CreateCustomerAccessTokenInterface +{ + /** + * Create access token for admin by customer id. + * + * Returns created token. + * + * @param int $customerId + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(int $customerId): string; +} diff --git a/app/code/Magento/LoginAsCustomerWebapi/LICENSE.txt b/app/code/Magento/LoginAsCustomerWebapi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomerWebapi/LICENSE_AFL.txt b/app/code/Magento/LoginAsCustomerWebapi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/LoginAsCustomerWebapi/Model/CreateCustomerAccessToken.php b/app/code/Magento/LoginAsCustomerWebapi/Model/CreateCustomerAccessToken.php new file mode 100644 index 0000000000000..0279480f1ba26 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/Model/CreateCustomerAccessToken.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerWebapi\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Integration\Model\Oauth\TokenFactory; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerWebapi\Api\CreateCustomerAccessTokenInterface; + +/** + * @inheritdoc + */ +class CreateCustomerAccessToken implements CreateCustomerAccessTokenInterface +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var ManagerInterface + */ + private $eventManager; + + /** + * @var TokenFactory + */ + private $tokenFactory; + + /** + * @param ConfigInterface $config + * @param CustomerRepositoryInterface $customerRepository + * @param ManagerInterface $eventManager + * @param TokenFactory $tokenFactory + */ + public function __construct( + ConfigInterface $config, + CustomerRepositoryInterface $customerRepository, + ManagerInterface $eventManager, + TokenFactory $tokenFactory + ) { + $this->config = $config; + $this->customerRepository = $customerRepository; + $this->eventManager = $eventManager; + $this->tokenFactory = $tokenFactory; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): string + { + if ($this->config->isEnabled()) { + $customer = $this->customerRepository->getById($customerId); + $this->eventManager->dispatch('customer_login', ['customer' => $customer]); + + return $this->tokenFactory->create()->createCustomerToken($customerId)->getToken(); + } else { + throw new LocalizedException(__('Service is disabled.')); + } + } +} diff --git a/app/code/Magento/LoginAsCustomerWebapi/README.md b/app/code/Magento/LoginAsCustomerWebapi/README.md new file mode 100644 index 0000000000000..ef42fd292cda7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/README.md @@ -0,0 +1,4 @@ +# Magento_LoginAsCustomer module + +The Magento_LoginAsCustomerWebapi module provides API for ability to login into customer account for an admin user. + diff --git a/app/code/Magento/LoginAsCustomerWebapi/composer.json b/app/code/Magento/LoginAsCustomerWebapi/composer.json new file mode 100644 index 0000000000000..3fd430d42f7a1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-login-as-customer-webapi", + "description": "Grants admin token customer account", + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-integration": "*", + "magento/module-login-as-customer-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomerWebapi\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerWebapi/etc/acl.xml b/app/code/Magento/LoginAsCustomerWebapi/etc/acl.xml new file mode 100644 index 0000000000000..33d912843d7d3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/etc/acl.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Customer::customer"> + <resource id="Magento_LoginAsCustomer::login" title="Login as Customer" sortOrder="50"> + <resource id="Magento_LoginAsCustomerWebapi::login_token" title="Create Login as Customer Token" sortOrder="30" /> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/LoginAsCustomerWebapi/etc/di.xml b/app/code/Magento/LoginAsCustomerWebapi/etc/di.xml new file mode 100644 index 0000000000000..f825f533c4961 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/etc/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerWebapi\Api\CreateCustomerAccessTokenInterface" + type="Magento\LoginAsCustomerWebapi\Model\CreateCustomerAccessToken"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerWebapi/etc/module.xml b/app/code/Magento/LoginAsCustomerWebapi/etc/module.xml new file mode 100644 index 0000000000000..f6a2e614cc95b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerWebapi"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerWebapi/etc/webapi.xml b/app/code/Magento/LoginAsCustomerWebapi/etc/webapi.xml new file mode 100644 index 0000000000000..6e80fb1725ae4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/etc/webapi.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd"> + <route url="/V1/login-as-customer/token" method="POST"> + <service class="\Magento\LoginAsCustomerWebapi\Api\CreateCustomerAccessTokenInterface" method="execute"/> + <resources> + <resource ref="Magento_LoginAsCustomerWebapi::login_token"/> + </resources> + </route> +</routes> diff --git a/app/code/Magento/LoginAsCustomerWebapi/registration.php b/app/code/Magento/LoginAsCustomerWebapi/registration.php new file mode 100644 index 0000000000000..f56e57bd7a971 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerWebapi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerWebapi', + __DIR__ +); diff --git a/app/code/Magento/MediaContent/etc/di.xml b/app/code/Magento/MediaContent/etc/di.xml index 35dcf2aa836a0..f2a9459447001 100644 --- a/app/code/Magento/MediaContent/etc/di.xml +++ b/app/code/Magento/MediaContent/etc/di.xml @@ -16,7 +16,6 @@ <preference for="Magento\MediaContentApi\Api\Data\ContentIdentityInterface" type="Magento\MediaContent\Model\ContentIdentity"/> <preference for="Magento\MediaContentApi\Api\Data\ContentAssetLinkInterface" type="Magento\MediaContent\Model\ContentAssetLink"/> <preference for="Magento\MediaContentApi\Model\SearchPatternConfigInterface" type="Magento\MediaContent\Model\Content\SearchPatternConfig"/> - <preference for="Magento\MediaContent\Model\Content\ConfigInterface" type="Magento\MediaContent\Model\Content\Config"/> <type name="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface"> <plugin name="remove_media_content_after_asset_is_removed_by_path" type="Magento\MediaContent\Plugin\MediaGalleryAssetDeleteByPath" /> </type> diff --git a/app/code/Magento/MediaContentApi/Model/Composite/GetEntityContents.php b/app/code/Magento/MediaContentApi/Model/Composite/GetEntityContents.php new file mode 100644 index 0000000000000..a53d61ae61ad3 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/Composite/GetEntityContents.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model\Composite; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; + +/** + * Get concatenated content for all store views + */ +class GetEntityContents implements GetEntityContentsInterface +{ + /** + * @var GetEntityContentsInterface[] + */ + private $items; + + /** + * @param GetEntityContentsInterface[] $items + */ + public function __construct( + $items = [] + ) { + foreach ($items as $item) { + if (!$item instanceof GetEntityContentsInterface) { + throw new \InvalidArgumentException( + __('GetContent items must implement %1.', GetEntityContentsInterface::class) + ); + } + } + + $this->items = $items; + } + + /** + * Get concatenated content for the content identity + * + * @param ContentIdentityInterface $contentIdentity + * @return string[] + */ + public function execute(ContentIdentityInterface $contentIdentity): array + { + if (isset($this->items[$contentIdentity->getEntityType()])) { + return $this->items[$contentIdentity->getEntityType()]->execute($contentIdentity); + } + return []; + } +} diff --git a/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php b/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php new file mode 100644 index 0000000000000..c58d543a597b7 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; + +/** + * Get Entity Contents. + * @api + */ +interface GetEntityContentsInterface +{ + /** + * Get concatenated content by the content identity + * + * @param ContentIdentityInterface $contentIdentity + * @return string[] + */ + public function execute(ContentIdentityInterface $contentIdentity): array; +} diff --git a/app/code/Magento/MediaContentApi/Model/SearchPatternConfigInterface.php b/app/code/Magento/MediaContentApi/Model/SearchPatternConfigInterface.php index c6715615ffe39..3900dc53284c7 100644 --- a/app/code/Magento/MediaContentApi/Model/SearchPatternConfigInterface.php +++ b/app/code/Magento/MediaContentApi/Model/SearchPatternConfigInterface.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\MediaContentApi\Model; diff --git a/app/code/Magento/MediaContentApi/etc/di.xml b/app/code/Magento/MediaContentApi/etc/di.xml new file mode 100644 index 0000000000000..4d4c52e61fb48 --- /dev/null +++ b/app/code/Magento/MediaContentApi/etc/di.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaContentApi\Model\GetEntityContentsInterface" type="Magento\MediaContentApi\Model\Composite\GetEntityContents"/> +</config> diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php new file mode 100644 index 0000000000000..c3766484ce4f1 --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\App\ResourceConnection; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; +use Magento\Eav\Model\Config; + +/** + * Get concatenated content for all store views + */ +class GetEntityContent implements GetEntityContentsInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var Product + */ + private $productResource; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param Config $config + * @param ResourceConnection $resourceConnection + * @param Product $productResource + */ + public function __construct( + Config $config, + ResourceConnection $resourceConnection, + Product $productResource + ) { + $this->config = $config; + $this->productResource = $productResource; + $this->resourceConnection = $resourceConnection; + } + + /** + * Get product content for all store views + * + * @param ContentIdentityInterface $contentIdentity + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(ContentIdentityInterface $contentIdentity): array + { + $attribute = $this->config->getAttribute($contentIdentity->getEntityType(), $contentIdentity->getField()); + $connection = $this->resourceConnection->getConnection(); + + $select = $connection->select()->from( + ['abt' => $attribute->getBackendTable()], + 'abt.value' + )->where( + $connection->quoteIdentifier('abt.attribute_id') . ' = ?', + (int) $attribute->getAttributeId() + )->where( + $connection->quoteIdentifier('abt.' . $attribute->getEntityIdField()) . ' = ?', + $contentIdentity->getEntityId() + )->distinct(true); + + return $connection->fetchCol($select); + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/Category.php b/app/code/Magento/MediaContentCatalog/Observer/Category.php index 21767e0199ae6..5c2deeab258df 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/Category.php +++ b/app/code/Magento/MediaContentCatalog/Observer/Category.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\ObserverInterface; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; /** * Observe the catalog_category_save_after event and run processing relation between category content and media asset. @@ -39,16 +40,26 @@ class Category implements ObserverInterface private $contentIdentityFactory; /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * Create links for category content + * * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param GetEntityContentsInterface $getContent * @param UpdateContentAssetLinksInterface $updateContentAssetLinks * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, + GetEntityContentsInterface $getContent, UpdateContentAssetLinksInterface $updateContentAssetLinks, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; + $this->getContent = $getContent; $this->updateContentAssetLinks = $updateContentAssetLinks; $this->fields = $fields; } @@ -57,6 +68,7 @@ public function __construct( * Retrieve the saved category and pass it to the model processor to save content - asset relations * * @param Observer $observer + * @throws \Exception */ public function execute(Observer $observer): void { @@ -67,16 +79,15 @@ public function execute(Observer $observer): void if (!$model->dataHasChangedFor($field)) { continue; } - $this->updateContentAssetLinks->execute( - $this->contentIdentityFactory->create( - [ - self::TYPE => self::CONTENT_TYPE, - self::FIELD => $field, - self::ENTITY_ID => (string) $model->getId(), - ] - ), - (string) $model->getData($field) + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $model->getEntityId(), + ] ); + $concatenatedContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $this->updateContentAssetLinks->execute($contentIdentity, $concatenatedContent); } } } diff --git a/app/code/Magento/MediaContentCatalog/Observer/Product.php b/app/code/Magento/MediaContentCatalog/Observer/Product.php index e69cc8486f62f..306bcc0b466c2 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/Product.php +++ b/app/code/Magento/MediaContentCatalog/Observer/Product.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\ObserverInterface; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; /** * Observe the catalog_product_save_after event and run processing relation between product content and media asset @@ -26,7 +27,7 @@ class Product implements ObserverInterface /** * @var UpdateContentAssetLinksInterface */ - private $processor; + private $updateContentAssetLinks; /** * @var array @@ -39,17 +40,27 @@ class Product implements ObserverInterface private $contentIdentityFactory; /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * * Create links for product content + * * @param ContentIdentityInterfaceFactory $contentIdentityFactory - * @param UpdateContentAssetLinksInterface $processor + * @param GetEntityContentsInterface $getContent + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, - UpdateContentAssetLinksInterface $processor, + GetEntityContentsInterface $getContent, + UpdateContentAssetLinksInterface $updateContentAssetLinks, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; - $this->processor = $processor; + $this->getContent = $getContent; + $this->updateContentAssetLinks = $updateContentAssetLinks; $this->fields = $fields; } @@ -57,27 +68,25 @@ public function __construct( * Retrieve the saved product and pass it to the model processor to save content - asset relations * * @param Observer $observer + * @throws \Exception */ public function execute(Observer $observer): void { $model = $observer->getEvent()->getData('product'); - if ($model instanceof CatalogProduct) { foreach ($this->fields as $field) { if (!$model->dataHasChangedFor($field)) { continue; } - - $this->processor->execute( - $this->contentIdentityFactory->create( - [ - self::TYPE => self::CONTENT_TYPE, - self::FIELD => $field, - self::ENTITY_ID => (string) $model->getId(), - ] - ), - (string) $model->getData($field) + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $model->getEntityId(), + ] ); + $concatenatedContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $this->updateContentAssetLinks->execute($contentIdentity, $concatenatedContent); } } } diff --git a/app/code/Magento/MediaContentCatalog/composer.json b/app/code/Magento/MediaContentCatalog/composer.json index 9efe7aadba041..c48cb58209f23 100644 --- a/app/code/Magento/MediaContentCatalog/composer.json +++ b/app/code/Magento/MediaContentCatalog/composer.json @@ -5,6 +5,7 @@ "php": "~7.1.3||~7.2.0||~7.3.0", "magento/module-media-content-api": "*", "magento/module-catalog": "*", + "magento/module-eav": "*", "magento/framework": "*" }, "type": "magento2-module", diff --git a/app/code/Magento/MediaContentCatalog/etc/di.xml b/app/code/Magento/MediaContentCatalog/etc/di.xml index b61b9c1df5f1f..a2d300a2bb208 100644 --- a/app/code/Magento/MediaContentCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentCatalog/etc/di.xml @@ -6,6 +6,14 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\MediaContentApi\Model\Composite\GetEntityContents"> + <arguments> + <argument name="items" xsi:type="array"> + <item name="catalog_product" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetEntityContent</item> + <item name="catalog_category" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetEntityContent</item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentCatalog\Observer\Category"> <arguments> <argument name="fields" xsi:type="array"> diff --git a/app/code/Magento/MsrpGroupedProduct/Plugin/Model/Product/Type/Grouped.php b/app/code/Magento/MsrpGroupedProduct/Plugin/Model/Product/Type/Grouped.php new file mode 100644 index 0000000000000..460bd34102e66 --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/Plugin/Model/Product/Type/Grouped.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MsrpGroupedProduct\Plugin\Model\Product\Type; + +use Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection; + +/** + * Minimum advertised price plugin for grouped product + */ +class Grouped +{ + /** + * Add minimum advertised price to the attribute selection for associated products + * + * @param \Magento\GroupedProduct\Model\Product\Type\Grouped $subject + * @param Collection $collection + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetAssociatedProductCollection( + \Magento\GroupedProduct\Model\Product\Type\Grouped $subject, + Collection $collection + ): Collection { + $collection->addAttributeToSelect(['msrp']); + return $collection; + } +} diff --git a/app/code/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculator.php b/app/code/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculator.php index b99f328a8b200..02231caa96526 100644 --- a/app/code/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculator.php +++ b/app/code/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculator.php @@ -31,6 +31,6 @@ public function getMsrpPriceValue(ProductInterface $product): float /** @var Grouped $groupedProduct */ $groupedProduct = $product->getTypeInstance(); - return $groupedProduct->getChildrenMsrp($product); + return (float) $groupedProduct->getChildrenMsrp($product); } } diff --git a/app/code/Magento/MsrpGroupedProduct/etc/di.xml b/app/code/Magento/MsrpGroupedProduct/etc/di.xml index 29b25f15bc2c1..4ad62eb56f49f 100644 --- a/app/code/Magento/MsrpGroupedProduct/etc/di.xml +++ b/app/code/Magento/MsrpGroupedProduct/etc/di.xml @@ -16,4 +16,7 @@ </argument> </arguments> </type> -</config> \ No newline at end of file + <type name="\Magento\GroupedProduct\Model\Product\Type\Grouped"> + <plugin name="grouped_product_minimum_advertised_price" type="\Magento\MsrpGroupedProduct\Plugin\Model\Product\Type\Grouped"/> + </type> +</config> diff --git a/app/code/Magento/Paypal/Controller/Express/GetTokenData.php b/app/code/Magento/Paypal/Controller/Express/GetTokenData.php index 512dac4cdec06..5c41e753e8a95 100644 --- a/app/code/Magento/Paypal/Controller/Express/GetTokenData.php +++ b/app/code/Magento/Paypal/Controller/Express/GetTokenData.php @@ -152,6 +152,10 @@ public function execute(): ResultInterface $responseContent['error_message'] = __('Sorry, but something went wrong'); } + if (!$responseContent['success']) { + $this->messageManager->addErrorMessage($responseContent['error_message']); + } + return $controllerResult->setData($responseContent); } diff --git a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php index 0d7ec3fc6f32d..b496c7f10d937 100644 --- a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php +++ b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php @@ -10,6 +10,7 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Paypal\Model\Config as PayPalConfig; use Magento\Paypal\Model\Express\Checkout as PayPalCheckout; use Magento\Paypal\Model\Api\ProcessableException as ApiProcessableException; @@ -160,7 +161,7 @@ public function execute(): ResultInterface } catch (ApiProcessableException $e) { $responseContent['success'] = false; $responseContent['error_message'] = $e->getUserMessage(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $responseContent['success'] = false; $responseContent['error_message'] = $e->getMessage(); } catch (\Exception $e) { @@ -168,6 +169,10 @@ public function execute(): ResultInterface $responseContent['error_message'] = __('We can\'t process Express Checkout approval.'); } + if (!$responseContent['success']) { + $this->messageManager->addErrorMessage($responseContent['error_message']); + } + return $controllerResult->setData($responseContent); } } diff --git a/app/code/Magento/Paypal/Model/Config.php b/app/code/Magento/Paypal/Model/Config.php index 6f6728ebfa47f..c790a4524f18b 100644 --- a/app/code/Magento/Paypal/Model/Config.php +++ b/app/code/Magento/Paypal/Model/Config.php @@ -1512,7 +1512,10 @@ protected function _mapExpressFieldset($fieldName) case 'allow_ba_signup': case 'in_context': case 'merchant_id': + case 'client_id': + case 'sandbox_client_id': case 'supported_locales': + case 'smart_buttons_supported_locales': return "payment/{$this->_methodCode}/{$fieldName}"; default: return $this->_mapMethodFieldset($fieldName); diff --git a/app/code/Magento/Paypal/Model/Express/LocaleResolver.php b/app/code/Magento/Paypal/Model/Express/LocaleResolver.php index c9136d03036d2..656e33b108fa9 100644 --- a/app/code/Magento/Paypal/Model/Express/LocaleResolver.php +++ b/app/code/Magento/Paypal/Model/Express/LocaleResolver.php @@ -89,7 +89,9 @@ public function setLocale($locale = null) public function getLocale(): string { $locale = $this->localeMap[$this->resolver->getLocale()] ?? $this->resolver->getLocale(); - $allowedLocales = $this->config->getValue('supported_locales'); + $allowedLocales =(bool)(int) $this->config->getValue('in_context') + ? $this->config->getValue('smart_buttons_supported_locales') + : $this->config->getValue('supported_locales'); return strpos($allowedLocales, $locale) !== false ? $locale : 'en_US'; } diff --git a/app/code/Magento/Paypal/Model/SmartButtonConfig.php b/app/code/Magento/Paypal/Model/SmartButtonConfig.php index 921dc4679b089..88d68511ae5fe 100644 --- a/app/code/Magento/Paypal/Model/SmartButtonConfig.php +++ b/app/code/Magento/Paypal/Model/SmartButtonConfig.php @@ -11,9 +11,10 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Locale\ResolverInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Paypal\Model\Config as PayPalConfig; /** - * Smart button configuration. + * Provides configuration values for PayPal in-context checkout */ class SmartButtonConfig { @@ -33,35 +34,50 @@ class SmartButtonConfig private $defaultStyles; /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Maps the old checkout SDK configuration values to the current ones * @var array */ - private $allowedFunding; + private $disallowedFundingMap; /** - * @var ScopeConfigInterface + * These payment methods will be added as parameters to the SDK url to disable them. + * @var array */ - private $scopeConfig; + private $unsupportedPaymentMethods; + + /** + * Base url for Paypal SDK + */ + private const BASE_URL = 'https://www.paypal.com/sdk/js?'; /** * @param ResolverInterface $localeResolver * @param ConfigFactory $configFactory * @param ScopeConfigInterface $scopeConfig * @param array $defaultStyles - * @param array $allowedFunding + * @param array $disallowedFundingMap + * @param array $unsupportedPaymentMethods */ public function __construct( ResolverInterface $localeResolver, ConfigFactory $configFactory, ScopeConfigInterface $scopeConfig, $defaultStyles = [], - $allowedFunding = [] + $disallowedFundingMap = [], + $unsupportedPaymentMethods = [] ) { $this->localeResolver = $localeResolver; $this->config = $configFactory->create(); $this->config->setMethod(Config::METHOD_EXPRESS); $this->scopeConfig = $scopeConfig; $this->defaultStyles = $defaultStyles; - $this->allowedFunding = $allowedFunding; + $this->disallowedFundingMap = $disallowedFundingMap; + $this->unsupportedPaymentMethods = $unsupportedPaymentMethods; } /** @@ -76,20 +92,63 @@ public function getConfig(string $page): array Data::XML_PATH_GUEST_CHECKOUT, ScopeInterface::SCOPE_STORE ); + return [ - 'merchantId' => $this->config->getValue('merchant_id'), - 'environment' => ((int)$this->config->getValue('sandbox_flag') ? 'sandbox' : 'production'), - 'locale' => $this->localeResolver->getLocale(), - 'allowedFunding' => $this->getAllowedFunding($page), - 'disallowedFunding' => $this->getDisallowedFunding(), 'styles' => $this->getButtonStyles($page), 'isVisibleOnProductPage' => (bool)$this->config->getValue('visible_on_product'), - 'isGuestCheckoutAllowed' => $isGuestCheckoutAllowed + 'isGuestCheckoutAllowed' => $isGuestCheckoutAllowed, + 'sdkUrl' => $this->generatePaypalSdkUrl($page) ]; } /** - * Returns disallowed funding from configuration + * Generate the url to download the Paypal SDK + * + * @param string $page + * + * @return string + */ + private function generatePaypalSdkUrl(string $page): string + { + $clientId = (int)$this->config->getValue('sandbox_flag') ? + $this->config->getValue('sandbox_client_id') : $this->config->getValue('client_id'); + $disallowedFunding = implode(',', $this->getDisallowedFunding()); + + $commit = $page === 'checkout' ? 'true' : 'false'; + + $params = + [ + 'client-id' => $clientId, + 'commit' => $commit, + 'merchant-id' => $this->config->getValue('merchant_id'), + 'locale' => $this->localeResolver->getLocale(), + 'intent' => $this->getIntent(), + ]; + if ($disallowedFunding) { + $params['disable-funding'] = $disallowedFunding; + } + + return self::BASE_URL . http_build_query($params); + } + + /** + * Return intent value from the configuration payment_action value + * + * @return string + */ + private function getIntent(): string + { + $paymentAction = $this->config->getValue('paymentAction'); + $mappedIntentValues = [ + Config::PAYMENT_ACTION_AUTH => 'authorize', + Config::PAYMENT_ACTION_SALE => 'capture', + Config::PAYMENT_ACTION_ORDER => 'order' + ]; + return $mappedIntentValues[$paymentAction]; + } + + /** + * Returns disallowed funding from configuration after updating values * * @return array */ @@ -103,18 +162,17 @@ private function getDisallowedFunding(): array array_push($result, 'CARD'); } - return $result; - } + // Map old configuration values to current ones + $result = array_map(function ($oldValue) { + return $this->disallowedFundingMap[$oldValue] ?? $oldValue; + }, + $result); - /** - * Returns allowed funding - * - * @param string $page - * @return array - */ - private function getAllowedFunding(string $page): array - { - return array_values(array_diff($this->allowedFunding[$page], $this->getDisallowedFunding())); + //disable unsupported payment methods + $result = array_combine($result, $result); + $result = array_merge($result, $this->unsupportedPaymentMethods); + + return $result; } /** @@ -165,7 +223,7 @@ private function updateStyles(array $styles, string $page): array // Installment label is only available for specific locales if ($styles['label'] === 'installment') { if (array_key_exists($locale, $installmentPeriodLocale)) { - $styles['installmentperiod'] = (int)$this->config->getValue( + $styles['period'] = (int)$this->config->getValue( $page .'_page_button_' . $installmentPeriodLocale[$locale] . '_installment_period' ); } else { diff --git a/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php b/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php index 8ad55d045ff1a..d55fd216c06b5 100644 --- a/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php +++ b/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php @@ -53,20 +53,6 @@ public function getShape(): array ]; } - /** - * Button size source getter - * - * @return array - */ - public function getSize(): array - { - return [ - 'medium' => __('Medium'), - 'large' => __('Large'), - 'responsive' => __('Responsive') - ]; - } - /** * Button label source getter * @@ -80,7 +66,6 @@ public function getLabel(): array 'buynow' => __('Buy Now'), 'paypal' => __('PayPal'), 'installment' => __('Installment'), - 'credit' => __('Credit') ]; } diff --git a/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php b/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php index 1a9cfe0998fb8..a06949402956d 100644 --- a/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php +++ b/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php @@ -12,24 +12,35 @@ */ class DisableFundingOptions { + + /** + * @var array + */ + private $disallowedFundingOptions; + + /** + * DisableFundingOptions constructor. + * @param array $disallowedFundingOptions + */ + public function __construct($disallowedFundingOptions = []) + { + $this->disallowedFundingOptions = $disallowedFundingOptions; + } + /** * @inheritdoc */ public function toOptionArray(): array { - return [ - [ - 'value' => 'CREDIT', - 'label' => __('PayPal Credit') - ], - [ - 'value' => 'CARD', - 'label' => __('PayPal Guest Checkout Credit Card Icons') - ], - [ - 'value' => 'ELV', - 'label' => __('Elektronisches Lastschriftverfahren - German ELV') - ] - ]; + return array_map( + function ($key, $value) { + return [ + 'value' => $key, + 'label' => __($value) + ]; + }, + array_keys($this->disallowedFundingOptions), + $this->disallowedFundingOptions + ); } } diff --git a/app/code/Magento/Paypal/Setup/Patch/Data/UpdateSmartButtonLabel.php b/app/code/Magento/Paypal/Setup/Patch/Data/UpdateSmartButtonLabel.php new file mode 100644 index 0000000000000..2a8ed1fa07aee --- /dev/null +++ b/app/code/Magento/Paypal/Setup/Patch/Data/UpdateSmartButtonLabel.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Update existing customization for the smart button label to be compatible with the new PayPal SDK + */ +class UpdateSmartButtonLabel implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * PrepareInitialConfig constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + $this->moduleDataSetup->getConnection()->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => 'paypal'], + [ + 'path IN (?)' => [ + 'paypal/style/checkout_page_button_label', + 'paypal/style/cart_page_button_label', + 'paypal/style/mini_cart_page_button_label' + ], + 'value = ? ' => 'credit' + ] + ); + $this->moduleDataSetup->getConnection()->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => 'buynow'], + [ + 'path IN (?)' => ['paypal/style/product_page_button_label'], + 'value = ? ' => 'credit' + ] + ); + return $this->moduleDataSetup->getConnection()->endSetup(); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Paypal/Setup/Patch/Data/UpdateSmartButtonSize.php b/app/code/Magento/Paypal/Setup/Patch/Data/UpdateSmartButtonSize.php new file mode 100644 index 0000000000000..349fa64bfcc71 --- /dev/null +++ b/app/code/Magento/Paypal/Setup/Patch/Data/UpdateSmartButtonSize.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Update existing customization for the smart button size value to be compatible with the new PayPal SDK + */ +class UpdateSmartButtonSize implements DataPatchInterface +{ + /** + * @var array + */ + private $sizeSettingsToUpdate = [ + 'paypal/style/checkout_page_button_size', + 'paypal/style/cart_page_button_size', + 'paypal/style/mini_cart_page_button_size', + 'paypal/style/checkout_page_button_size' + ]; + + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * PrepareInitialConfig constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + $this->moduleDataSetup->getConnection()->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => 'responsive'], + [ + 'path IN (?)' => $this->sizeSettingsToUpdate, + 'value NOT IN (?) ' => ['responsive'] + ] + ); + return $this->moduleDataSetup->getConnection()->endSetup(); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AssertPayPalButtonLayoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AssertPayPalButtonLayoutActionGroup.xml new file mode 100644 index 0000000000000..d8ad106e8c6da --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AssertPayPalButtonLayoutActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertPayPalButtonLayoutActionGroup"> + <annotations> + <description>Assert Paypal button layout when user customize button</description> + </annotations> + <arguments> + <argument name="label" type="string" defaultValue="Buy Now"/> + <argument name="layout" type="string" defaultValue="Horizontal"/> + <argument name="shape" type="string" defaultValue="Pill"/> + <argument name="color" type="string" defaultValue="Gold"/> + </arguments> + + <seeElement selector="{{StorefrontPayPalSmartButtonStylesSection.labelText(label)}}" stepKey="seeButtonLabelText"/> + <seeElement selector="{{CheckoutPaymentSection.PayPalBtn}}{{StorefrontPayPalSmartButtonStylesSection.layout(layout)}}" stepKey="seeButtonLayout"/> + <seeElement selector="{{CheckoutPaymentSection.PayPalBtn}}{{StorefrontPayPalSmartButtonStylesSection.shape(shape)}}" stepKey="seeButtonShape"/> + <seeElement selector="{{CheckoutPaymentSection.PayPalBtn}}{{StorefrontPayPalSmartButtonStylesSection.color(color)}}" stepKey="seeButtonColor"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AssertPayPalButtonLayoutInPaypalLabelActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AssertPayPalButtonLayoutInPaypalLabelActionGroup.xml new file mode 100644 index 0000000000000..28aba7b833f8c --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AssertPayPalButtonLayoutInPaypalLabelActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertPayPalButtonLayoutInPaypalLabelActionGroup" extends="AssertPayPalButtonLayoutActionGroup"> + <annotations> + <description>Assert Paypal button layout when layout is horizontal</description> + </annotations> + <arguments> + </arguments> + <dontSeeElement selector="{{StorefrontPayPalSmartButtonStylesSection.labelText(label)}}" stepKey="seeButtonLabelText"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/ConfigPayPalExpressCheckoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/ConfigPayPalExpressCheckoutActionGroup.xml index 90f0a25a8cd69..01ec295d8bf63 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/ConfigPayPalExpressCheckoutActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/ConfigPayPalExpressCheckoutActionGroup.xml @@ -28,6 +28,7 @@ <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_api_signature}}" stepKey="inputAPISignature"/> <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <waitForElementVisible selector="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" stepKey="waitForMerchantIdField"/> <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.magento/paypal_express_checkout_us_merchant_id}}" stepKey="inputMerchantID"/> <!--Save configuration--> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml index 2b559085a83b5..bf4bce4a8fa09 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml @@ -15,7 +15,7 @@ <arguments> <argument name="countryCode" type="string" defaultValue="us"/> </arguments> - + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> <click selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="openAdvancedSettingTab"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalAssertTransferLineAndShippingMethodActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalAssertTransferLineAndShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..f9cef514c2bf0 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalAssertTransferLineAndShippingMethodActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="PayPalAssertTransferLineAndShippingMethodActionGroup"> + <annotations> + <description>Assert Transfer Cart Line and Shipping Method</description> + </annotations> + <arguments> + <argument name="shippingLabel" defaultValue="no_rate - " type="string"/> + <argument name="productName" defaultValue="Simple Product" type="string"/> + </arguments> + <see selector="{{PayPalPaymentSection.shippingMethod}}" userInput="{{shippingLabel}}" stepKey="transferShippingMethodAssertion"/> + <click selector="{{PayPalPaymentSection.paypalCart}}" stepKey="openPayPalCart"/> + <see selector="{{PayPalPaymentSection.productNamePosition}}" userInput="{{productName}}" stepKey="transferCartLineAssertion"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalAssertTransferLineAndShippingMethodNotExistActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalAssertTransferLineAndShippingMethodNotExistActionGroup.xml new file mode 100644 index 0000000000000..b90c3e70db097 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalAssertTransferLineAndShippingMethodNotExistActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="PayPalAssertTransferLineAndShippingMethodNotExistActionGroup"> + <annotations> + <description>Assert Transfer Cart Line and Shipping Method not exist on paypal side</description> + </annotations> + <dontSeeElement selector="{{PayPalPaymentSection.shippingMethod}}" stepKey="noShippingMethodTransfer"/> + <dontSeeElement selector="{{PayPalPaymentSection.paypalCart}}" stepKey="noCartLineTransfer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountOneStepActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountOneStepActionGroup.xml new file mode 100644 index 0000000000000..c4cad5f3d4cd4 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountOneStepActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" extends="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup"> + <annotations> + <description>EXTENDS: StorefrontLoginToPayPalPaymentAccountOneStepActionGroup. Remove extra Continue step on PayPal site</description> + </annotations> + <remove keyForRemoval="clickNext"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml new file mode 100644 index 0000000000000..5619aa27860ce --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup"> + <arguments> + <argument name="payerName" defaultValue="MPI" type="string"/> + <argument name="credentials" defaultValue="_CREDS"/> + </arguments> + <!--Check in-context--> + <switchToNextTab stepKey="switchToInContentTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeCurrentUrlMatches regex="~\//www.sandbox.paypal.com/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> + <conditionalClick selector="{{PayPalPaymentSection.notYouLink}}" dependentSelector="{{PayPalPaymentSection.notYouLink}}" visible="true" stepKey="selectNotYouSection"/> + <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> + <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> + <click selector="{{PayPalPaymentSection.nextButton}}" stepKey="clickNext"/> + <waitForElementVisible selector="{{PayPalPaymentSection.password}}" stepKey="waitForPasswordField"/> + <click selector="{{PayPalPaymentSection.password}}" stepKey="focusOnPasswordField"/> + <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> + <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> + <waitForPageLoad stepKey="wait"/> + <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml new file mode 100644 index 0000000000000..f627b9158f868 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontLoginToPayPalPaymentFromCartAccountActionGroup" extends="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup"> + <seeElement selector="{{PayPalCheckoutAsGuestSection.CreditDebitBtn}}" stepKey="assertCheckoutAsGuest" before="waitForLoginForm"/> + <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.userName}}" stepKey="seePayerName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml index b7ebf7dab1c8b..8e337abfb2610 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayOrderOnPayPalCheckoutActionGroup.xml @@ -18,7 +18,7 @@ <click selector="{{PayPalPaymentSection.cartIcon}}" stepKey="openCart"/> <waitForPageLoad stepKey="waitForCartLoad"/> <seeElement selector="{{PayPalPaymentSection.itemName(productName)}}" stepKey="seeProductName"/> - <click selector="{{PayPalPaymentSection.PayPalSubmitBtn}}" stepKey="clickPayPalSubmitBtn"/> + <click selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="clickPayPalBtn"/> <switchToPreviousTab stepKey="switchToPreviousTab"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayPalPaymentActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayPalPaymentActionGroup.xml index 331acc1de628a..655a93b2e5044 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayPalPaymentActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPayPalPaymentActionGroup.xml @@ -9,13 +9,12 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="LoginToPayPalPaymentAccount"> <arguments> - <argument name="userName" type="string" defaultValue="{{Payer.buyerEmail}}"/> - <argument name="password" type="string" defaultValue="{{Payer.buyerPassword}"/> + <argument name="credentials" defaultValue="_CREDS"/> </arguments> - <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{userName}}" stepKey="fillEmail"/> + <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> <click selector="{{PayPalPaymentSection.nextButton}}" stepKey="clickNext"/> <waitForElementVisible selector="{{PayPalPaymentSection.password}}" stepKey="waitForPasswordField"/> - <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{password}}" stepKey="fillPassword"/> + <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="clickLogin"/> <waitForPageLoad stepKey="waitForLoginPageLoad"/> <click selector="{{PayPalPaymentSection.continueButton}}" stepKey="clickContinue"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPaypalSwitchBackToMagentoActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPaypalSwitchBackToMagentoActionGroup.xml new file mode 100644 index 0000000000000..5fff967ebdb7d --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPaypalSwitchBackToMagentoActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontPaypalSwitchBackToMagentoActionGroup"> + <annotations> + <description>Click continue button on Paypal site and go back to Magento site</description> + </annotations> + + <!--Click Continue button on PayPal site--> + <scrollTo selector="{{PayPalPaymentSection.continueButton}}" stepKey="scrollToContinueBtn"/> + <click selector="{{PayPalPaymentSection.continueButton}}" stepKey="clickContinue"/> + <!--Go back to Magento site--> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup.xml new file mode 100644 index 0000000000000..1053a08951d40 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" extends="StorefrontPaypalSwitchBackToMagentoActionGroup"> + <annotations> + <description>Click submit button on Paypal site and go back to Magento site</description> + </annotations> + + <!--Click Continue button on PayPal site--> + <scrollTo selector="{{PayPalPaymentSection.paypalSubmitBtn}}" stepKey="scrollToContinueBtn"/> + <click selector="{{PayPalPaymentSection.paypalSubmitBtn}}" stepKey="clickContinue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPlaceOrderOnOrderReviewPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPlaceOrderOnOrderReviewPageActionGroup.xml new file mode 100644 index 0000000000000..07b8302a941b0 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontPlaceOrderOnOrderReviewPageActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontPlaceOrderOnOrderReviewPageActionGroup"> + <annotations> + <description>Place order on Order Review Page after back from PayPal side</description> + </annotations> + <!--SubmitOrder--> + <click selector="{{StorefrontPayPalOrderReviewSection.placeOrderBtn}}" stepKey="clickPlaceOrderBtn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontSelectShippingMethodOnOrderReviewPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontSelectShippingMethodOnOrderReviewPageActionGroup.xml new file mode 100644 index 0000000000000..003ef5645c147 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontSelectShippingMethodOnOrderReviewPageActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectShippingMethodOnOrderReviewPageActionGroup"> + <annotations> + <description>Select Shipping method on Order Review page</description> + </annotations> + <arguments> + <argument name="shippingMethod" defaultValue="Fixed - $5.00" type="string"/> + </arguments> + <waitForElementVisible selector="{{StorefrontPayPalOrderReviewSection.shippingMethod}}" stepKey="waitForShippingMethodDropdown"/> + <selectOption selector="{{StorefrontPayPalOrderReviewSection.shippingMethod}}" userInput="{{shippingMethod}}" stepKey="selectShippingMethod" /> + <waitForPageLoad stepKey="waitForLoadShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontSwitchToPayPalButtonIframeActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontSwitchToPayPalButtonIframeActionGroup.xml new file mode 100644 index 0000000000000..bd7774465bb38 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontSwitchToPayPalButtonIframeActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSwitchToPayPalButtonIframeActionGroup" extends="SwitchToPayPalGroupBtnActionGroup"> + <annotations> + <description>EXTENDS: SwitchToPayPalGroupBtnActionGroup. Switches to PayPal Smart Button iFrame.</description> + </annotations> + + <remove keyForRemoval="clickPayPalBtn"/> + <remove keyForRemoval="switchBackToMainFrame"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SwitchToPayPalGroupBtnActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SwitchToPayPalGroupBtnActionGroup.xml new file mode 100644 index 0000000000000..2e57bdf6265b1 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SwitchToPayPalGroupBtnActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SwitchToPayPalGroupBtnActionGroup"> + <annotations> + <description>Switch to Paypal group button</description> + </annotations> + <arguments> + <argument name="elementNumber" type="string" defaultValue="0"/> + </arguments> + <!--set ID for iframe of PayPal group button--> + <executeJS function="document.getElementsByClassName('component-frame')[{{elementNumber}}].setAttribute('name', 'myFrame');" stepKey="setIDForIframe"/> + <!--switch to iframe of PayPal group button--> + <switchToIFrame userInput="myFrame" stepKey="switchToIframe"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> + <click selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="clickPayPalBtn"/> + <switchToIFrame stepKey="switchBackToMainFrame"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml index 0744207494108..1ad7642f6408a 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalConfigData.xml @@ -49,4 +49,268 @@ <data key="label">No</data> <data key="value">0</data> </entity> + <entity name="StorefrontPaypalEnableTransferCartLineConfigData"> + <data key="path">payment/paypal_express/line_items_enabled</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableTransferCartLineConfigData"> + <data key="path">payment/paypal_express/line_items_enabled</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalEnableTransferShippingOptionConfigData"> + <data key="path">payment/paypal_express/transfer_shipping_options</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalDisableTransferShippingOptionConfigData"> + <data key="path">payment/paypal_express/transfer_shipping_options</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData"> + <data key="path">payment/paypal_express/payment_action</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">Authorization</data> + </entity> + <entity name="StorefrontPaypalExpressSalePaymentActionOptionConfigData"> + <data key="path">payment/paypal_express/payment_action</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">Sale</data> + </entity> + <entity name="StorefrontPaypalExpressOrderPaymentActionOptionConfigData"> + <data key="path">payment/paypal_express/payment_action</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">Order</data> + </entity> + <entity name="StorefrontPaypalExpressEnableCheckoutAsGuestConfigData"> + <data key="path">payment/paypal_express/solution_type</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">Sole</data> + </entity> + <entity name="StorefrontPaypalExpressDisableCheckoutAsGuestConfigData"> + <data key="path">payment/paypal_express/solution_type</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">Mark</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageEnableCustomizeButtonConfigData"> + <data key="path">paypal/style/checkout_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData"> + <data key="path">paypal/style/checkout_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageButtonPayPalLabelConfigData"> + <data key="path">paypal/style/checkout_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">PayPal</data> + <data key="value">paypal</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageButtonPayLabelConfigData"> + <data key="path">paypal/style/checkout_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">Pay</data> + <data key="value">pay</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageButtonHorizontalLayoutConfigData"> + <data key="path">paypal/style/checkout_page_button_layout</data> + <data key="scope_id">1</data> + <data key="label">Horizontal</data> + <data key="value">horizontal</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData"> + <data key="path">paypal/style/checkout_page_button_layout</data> + <data key="scope_id">1</data> + <data key="label">Vertical</data> + <data key="value">vertical</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageButtonPillShapeConfigData"> + <data key="path">paypal/style/checkout_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Pill</data> + <data key="value">pill</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageButtonRectangleShapeConfigData"> + <data key="path">paypal/style/checkout_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Rectangle</data> + <data key="value">rect</data> + </entity> + <entity name="StorefrontPaypalCheckoutPageButtonBlueColorConfigData"> + <data key="path">paypal/style/checkout_page_button_color</data> + <data key="scope_id">1</data> + <data key="label">Blue</data> + <data key="value">blue</data> + </entity> + <entity name="StorefrontPaypalProductPageEnableCustomizeButtonConfigData"> + <data key="path">paypal/style/product_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalProductPageDisableCustomizeButtonConfigData"> + <data key="path">paypal/style/product_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalProductPageButtonPayPalLabelConfigData"> + <data key="path">paypal/style/product_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">PayPal</data> + <data key="value">paypal</data> + </entity> + <entity name="StorefrontPaypalProductPageButtonBuyNowLabelConfigData"> + <data key="path">paypal/style/product_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">Buy Now</data> + <data key="value">buynow</data> + </entity> + <entity name="StorefrontPaypalProductPageButtonHorizontalLayoutConfigData"> + <data key="path">paypal/style/product_page_button_layout</data> + <data key="scope_id">1</data> + <data key="label">Horizontal</data> + <data key="value">horizontal</data> + </entity> + <entity name="StorefrontPaypalProductPageButtonVerticalLayoutConfigData"> + <data key="path">paypal/style/product_page_button_layout</data> + <data key="scope_id">1</data> + <data key="label">Vertical</data> + <data key="value">vertical</data> + </entity> + <entity name="StorefrontPaypalProductPageButtonPillShapeConfigData"> + <data key="path">paypal/style/product_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Pill</data> + <data key="value">pill</data> + </entity> + <entity name="StorefrontPaypalProductPageButtonRectangleShapeConfigData"> + <data key="path">paypal/style/product_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Rectangle</data> + <data key="value">rect</data> + </entity> + <entity name="StorefrontPaypalProductPageButtonSilverColorConfigData"> + <data key="path">paypal/style/product_page_button_color</data> + <data key="scope_id">1</data> + <data key="label">Silver</data> + <data key="value">silver</data> + </entity> + <entity name="StorefrontPaypalCartPageEnableCustomizeButtonConfigData"> + <data key="path">paypal/style/cart_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalCartPageDisableCustomizeButtonConfigData"> + <data key="path">paypal/style/cart_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalCartPageButtonCheckoutLabelConfigData"> + <data key="path">paypal/style/cart_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">Checkout</data> + <data key="value">checkout</data> + </entity> + <entity name="StorefrontPaypalCartPageButtonPayLabelConfigData"> + <data key="path">paypal/style/cart_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">Pay</data> + <data key="value">pay</data> + </entity> + <entity name="StorefrontPaypalCartPageButtonVerticalLayoutConfigData"> + <data key="path">paypal/style/cart_page_button_layout</data> + <data key="scope_id">1</data> + <data key="label">Vertical</data> + <data key="value">vertical</data> + </entity> + <entity name="StorefrontPaypalCartPageButtonResponsiveSizeConfigData"> + <data key="path">paypal/style/cart_page_button_size</data> + <data key="scope_id">1</data> + <data key="label">Responsive</data> + <data key="value">responsive</data> + </entity> + <entity name="StorefrontPaypalCartPageButtonPillShapeConfigData"> + <data key="path">paypal/style/cart_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Pill</data> + <data key="value">pill</data> + </entity> + <entity name="StorefrontPaypalCartPageButtonRectangleShapeConfigData"> + <data key="path">paypal/style/cart_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Rectangle</data> + <data key="value">rect</data> + </entity> + <entity name="StorefrontPaypalCartPageButtonGoldColorConfigData"> + <data key="path">paypal/style/cart_page_button_color</data> + <data key="scope_id">1</data> + <data key="label">Gold</data> + <data key="value">gold</data> + </entity> + <entity name="StorefrontPaypalMiniCartEnableCustomizeButtonConfigData"> + <data key="path">paypal/style/mini_cart_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontPaypalMiniCartDisableCustomizeButtonConfigData"> + <data key="path">paypal/style/mini_cart_page_button_customize</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontPaypalMiniCartButtonBuyNowLabelConfigData"> + <data key="path">paypal/style/mini_cart_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">Buy Now</data> + <data key="value">buynow</data> + </entity> + <entity name="StorefrontPaypalMiniCartButtonInstallmentLabelConfigData"> + <data key="path">paypal/style/mini_cart_page_button_label</data> + <data key="scope_id">1</data> + <data key="label">Installment</data> + <data key="value">installment</data> + </entity> + <entity name="StorefrontPaypalMiniCartButtonVerticalLayoutConfigData"> + <data key="path">paypal/style/mini_cart_page_button_layout</data> + <data key="scope_id">1</data> + <data key="label">Vertical</data> + <data key="value">vertical</data> + </entity> + <entity name="StorefrontPaypalMiniCartButtonPillShapeConfigData"> + <data key="path">paypal/style/mini_cart_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Pill</data> + <data key="value">pill</data> + </entity> + <entity name="StorefrontPaypalMiniCartButtonRectangleShapeConfigData"> + <data key="path">paypal/style/mini_cart_page_button_shape</data> + <data key="scope_id">1</data> + <data key="label">Rectangle</data> + <data key="value">rect</data> + </entity> + <entity name="StorefrontPaypalMiniCartButtonBlackColorConfigData"> + <data key="path">paypal/style/mini_cart_page_button_color</data> + <data key="scope_id">1</data> + <data key="label">Black</data> + <data key="value">black</data> + </entity> </entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index ba56243fdb391..f7d872bd43838 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml @@ -61,17 +61,12 @@ <entity name="DefaultApiSignature" type="api_signature"> <data key="value"/> </entity> - <entity name="Payer" type="paypal_buyer"> - <data key="buyerEmail">buyer.mpi@gmail.com</data> - <data key="buyerPassword">12345678</data> - </entity> <entity name="PayPalLabel" type="paypal"> - <data key="checkout">checkout</data> - <data key="credit">credit</data> - <data key="pay">pay</data> - <data key="buynow">buy now</data> - <data key="paypal">pay pal</data> - <data key="installment">installment</data> + <data key="checkout">Checkout</data> + <data key="pay">Pay</data> + <data key="buynow">Buy Now</data> + <data key="paypal">Paypal</data> + <data key="installment">Pagos en</data> </entity> <entity name="PayPalLayout" type="paypal"> <data key="horizontal">horizontal</data> @@ -84,7 +79,7 @@ </entity> <entity name="PayPalShape" type="paypal"> <data key="pill">pill</data> - <data key="rectangle">rectangle</data> + <data key="rectangle">rect</data> </entity> <entity name="PayPalColor" type="paypal"> <data key="gold">gold</data> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalMerchantCountryData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalMerchantCountryData.xml new file mode 100644 index 0000000000000..fa8f936e89d6f --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalMerchantCountryData.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MerchantUnitedStates"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">United States</data> + <data key="value">us</data> + </entity> + <entity name="MerchantUnitedKingdom"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">United Kingdon</data> + <data key="value">0</data> + </entity> + <entity name="MerchantCanada"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">Canada</data> + <data key="value">CA</data> + </entity> + <entity name="MerchantAustralia"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">Australia</data> + <data key="value">AU</data> + </entity> + <entity name="MerchantJapan"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">Japan</data> + <data key="value">JP</data> + </entity> + <entity name="MerchantFrance"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">France</data> + <data key="value">FR</data> + </entity> + <entity name="MerchantHongKong"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">HongKong</data> + <data key="value">HK</data> + </entity> + <entity name="MerchantNewZealand"> + <data key="path">paypal/general/merchant_country</data> + <data key="scope_id">1</data> + <data key="label">New Zealand</data> + <data key="value">NZ</data> + </entity> +</entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/CheckoutPaymentSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/CheckoutPaymentSection.xml index 9c22dd940890e..e6eb4d875c434 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/CheckoutPaymentSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/CheckoutPaymentSection.xml @@ -11,6 +11,7 @@ <element name="email" type="input" selector="#checkout-customer-email"/> <element name="payPalPaymentBraintree" type="radio" selector="#braintree_paypal"/> <element name="payPalFrame" type="iframe" selector="//iframe[contains(@class, 'zoid-component-frame zoid-visible')]" timeout="5"/> + <element name="smartButtonPayPalFrame" type="iframe" selector=".component-frame" timeout="10"/> <element name="PayPalPaymentRadio" type="radio" selector="input#paypal_express.radio" timeout="30"/> <element name="PayPalBtn" type="radio" selector=".paypal-button.paypal-button-number-0" timeout="30"/> </section> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalCheckoutAsGuestSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalCheckoutAsGuestSection.xml new file mode 100644 index 0000000000000..120ad4025ff09 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalCheckoutAsGuestSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="PayPalCheckoutAsGuestSection"> + <element name="CreditDebitBtn" type="button" selector="#createAccount"/> + </section> +</sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml index a64ff13e8f627..361016c40539c 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml @@ -16,8 +16,13 @@ <element name="reviewUserInfo" type="text" selector="[data-testid=personalized-banner-content]"/> <element name="cartIcon" type="text" selector="[data-testid='header-show-cart-dropdown-btn']"/> <element name="itemName" type="text" selector="//p[contains(@class,'CartDropdown_line') and text()='{{productName}}']" parameterized="true"/> - <element name="PayPalSubmitBtn" type="text" selector="#payment-submit-btn"/> + <element name="paypalSubmitBtn" type="text" selector="#payment-submit-btn"/> <element name="nextButton" type="button" selector="#btnNext"/> <element name="continueButton" type="button" selector=".continueButton"/> + <element name="userName" type="text" selector="#reviewUserInfo"/> + <element name="notYouLink" type="input" selector="#backToInputEmailLink"/> + <element name="shippingMethod" type="text" selector="#shippingMethodCharge > span.selectedShippingMethod"/> + <element name="paypalCart" type="text" selector="#transactionCart"/> + <element name="productNamePosition" type="text" selector=".itemName"/> </section> </sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalButtonOnStorefrontSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/StorefrontPayPalSmartButtonStylesSection.xml similarity index 52% rename from app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalButtonOnStorefrontSection.xml rename to app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/StorefrontPayPalSmartButtonStylesSection.xml index d97c0260adb0d..e9674a321ec0e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalButtonOnStorefrontSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/StorefrontPayPalSmartButtonStylesSection.xml @@ -7,9 +7,10 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="PayPalButtonOnStorefront"> - <element name="label" type="text" selector="[aria-label='{{label}}']" parameterized="true"/> - <element name="layout" type="text" selector="[data-layout='{{layout}}']" parameterized="true"/> + <section name="StorefrontPayPalSmartButtonStylesSection"> + <element name="label" type="text" selector="//div[contains(@class, 'paypal-button-number-0')]//div[@class='paypal-button-label-container']"/> + <element name="labelText" type="text" selector="//div[contains(@class, 'paypal-button-number-0')]//div[@class='paypal-button-label-container']//span[@class='paypal-button-text' and contains(text(), '{{label}}')]" parameterized="true"/> + <element name="layout" type="text" selector=".paypal-button-layout-{{layout}}" parameterized="true"/> <element name="size" type="text" selector="[data-size='{{size}}']" parameterized="true"/> <element name="shape" type="text" selector=".paypal-button-shape-{{shape}}" parameterized="true"/> <element name="color" type="text" selector=".paypal-button-color-{{color}}" parameterized="true"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/StorefrontPayPalOrderReviewSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/StorefrontPayPalOrderReviewSection.xml new file mode 100644 index 0000000000000..d739a5731e052 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/StorefrontPayPalOrderReviewSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontPayPalOrderReviewSection"> + <element name="shippingMethod" type="select" selector="#shipping-method"/> + <element name="placeOrderBtn" type="button" selector="#review-button" timeout="30"/> + </section> +</sections> + diff --git a/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml b/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml new file mode 100644 index 0000000000000..b52fc05ca5a11 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="InContextPaypalSuite"> + <before> + <!-- Login --> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <!--Config PayPal Express Checkout--> + <actionGroup ref="ConfigPayPalExpressCheckoutActionGroup" stepKey="ConfigPayPalExpressCheckout"/> + <!-- Configure PayPal Express Checkout --> + <magentoCLI command="cache:clean" arguments="config full_page" stepKey="cleanFullPageCache"/> + </before> + <after> + <!-- Cleanup Paypal configurations --> + <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{StorefrontPaypalMerchantAccountIdConfigData.value}}" stepKey="deleteMerchantId"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableInContextCheckoutConfigData.path}} {{StorefrontPaypalDisableInContextCheckoutConfigData.value}}" stepKey="disableInContextPayPal"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableConfigData.path}} {{StorefrontPaypalDisableConfigData.value}}" stepKey="disablePaypal"/> + <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> + <magentoCLI command="cache:clean" arguments="config full_page" stepKey="cleanFullPageCache"/> + </after> + <include> + <group name="paypalExpress"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml deleted file mode 100644 index c8089085b7ee5..0000000000000 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckCreditButtonConfigurationTest.xml +++ /dev/null @@ -1,80 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontCheckCreditButtonConfigurationTest"> - <annotations> - <features value="Paypal"/> - <stories value="Button Configuration"/> - <title value="Check Credit Button Configuration"/> - <description value="Admin is able to customize Credit button"/> - <severity value="AVERAGE"/> - <testCaseId value="MC-10900"/> - <group value="paypal"/> - <skip> - <issueId value="DEVOPS-3311"/> - </skip> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - <createData entity="_defaultProduct" stepKey="createPreReqProduct"> - <requiredEntity createDataKey="createPreReqCategory"/> - </createData> - <!-- Create Customer --> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <!--Config PayPal Express Checkout--> - <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> - <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> - </before> - <after> - <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> - <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> - <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </after> - <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <!--Navigate to button configuration setting--> - <comment userInput="Navigate to button configuration setting in Admin site" stepKey="commentNavigateToButtonConfigurationInAdmin"/> - <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> - <waitForElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> - <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> - <!--Verify Credit Button value--> - <comment userInput="Verify Credit Button value" stepKey="commentVerifyDefaultValue2"/> - <selectOption selector="{{ButtonCustomization.label}}" userInput="{{PayPalLabel.credit}}" stepKey="selectCreditAsLabel"/> - <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize"/> - <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape"/> - <dontSeeElement selector="{{ButtonCustomization.layout}}" stepKey="dontSeeLayout"/> - <dontSeeElement selector="{{ButtonCustomization.color}}" stepKey="dontSeeColor"/> - <!--Customize Credit Button--> - <selectOption selector="{{ButtonCustomization.size}}" userInput="{{PayPalSize.medium}}" stepKey="selectSize"/> - <selectOption selector="{{ButtonCustomization.shape}}" userInput="{{PayPalShape.pill}}" stepKey="selectShape"/> - <!--Save configuration--> - <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> - <waitForPageLoad stepKey="waitForConfigSave"/> - <openNewTab stepKey="openNewTab"/> - <amOnPage url="/" stepKey="openStorefront"/> - <!--Login to storefront as previously created customer--> - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> - <argument name="Customer" value="$$createCustomer$$"/> - </actionGroup> - <actionGroup ref="AddProductToCheckoutPageActionGroup" stepKey="addProductToCheckoutPage"> - <argument name="Category" value="$$createPreReqCategory$$"/> - </actionGroup> - <!--set ID for iframe of PayPal group button--> - <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> - <!--switch to iframe of PayPal group button--> - <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> - <switchToIFrame userInput="myIframe" stepKey="clickPrintOrderLink"/> - <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> - <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.size(PayPalSize.medium)}}" stepKey="seeButtonInMediumSize"/> - <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.shape(PayPalShape.pill)}}" stepKey="seeButtonInPillShape"/> - </test> -</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnMiniCartTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnMiniCartTest.xml new file mode 100644 index 0000000000000..19d73ae4d5277 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnMiniCartTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnMiniCartTest" extends="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest"> + <annotations> + <features value="PayPal"/> + <stories value="PayPal Smart Button Configuration"/> + <title value="Check PayPal Smart Button configuration with Buy Now label on Mini Cart"/> + <description value="Admin is able to customize PayPal Smart Button with Buy Now label on Mini Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28711"/> + <group value="paypal"/> + <group value="paypalExpress"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontPaypalMiniCartEnableCustomizeButtonConfigData.path}} {{StorefrontPaypalMiniCartEnableCustomizeButtonConfigData.value}}" stepKey="enableCustomizeButton"/> + <magentoCLI command="config:set {{StorefrontPaypalMiniCartButtonBuyNowLabelConfigData.path}} {{StorefrontPaypalMiniCartButtonBuyNowLabelConfigData.value}}" stepKey="setLabelForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalMiniCartButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalMiniCartButtonVerticalLayoutConfigData.value}}" after="setLabelForPayPalSmartButton" stepKey="setLayoutForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalMiniCartButtonPillShapeConfigData.path}} {{StorefrontPaypalMiniCartButtonPillShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalMiniCartButtonBlackColorConfigData.path}} {{StorefrontPaypalMiniCartButtonBlackColorConfigData.value}}" after="setShapeForPayPalSmartButton" stepKey="setColorForPayPalSmartButton"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalMiniCartDisableCustomizeButtonConfigData.path}} {{StorefrontPaypalMiniCartDisableCustomizeButtonConfigData.value}}" stepKey="disableCustomizeButton"/> + </after> + <!-- Add Product to Cart --> + <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" before="goToPayPalSmartButtonPage" stepKey="addProductToCart"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="goToPayPalSmartButtonPage"/> + <!-- Check PayPal smart button configurations --> + <actionGroup ref="AssertPayPalButtonLayoutActionGroup" stepKey="assertLayoutBtn"> + <argument name="label" value="{{PayPalLabel.buynow}}"/> + <argument name="layout" value="{{PayPalLayout.vertical}}"/> + <argument name="shape" value="{{PayPalShape.pill}}"/> + <argument name="color" value="{{PayPalColor.black}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest.xml new file mode 100644 index 0000000000000..f8e8e4b921779 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest"> + <annotations> + <features value="PayPal"/> + <stories value="PayPal Smart Button Configuration"/> + <title value="Check PayPal Smart Button configuration with Buy Now label on Product page"/> + <description value="Admin is able to customize PayPal Smart Button with Buy Now label on Product page"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28711"/> + <group value="paypal"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!-- Create product --> + <createData entity="simpleProductWithoutCategory" stepKey="createProduct"/> + + <!-- Configure PayPal Smart Button --> + <magentoCLI command="config:set {{StorefrontPaypalProductPageEnableCustomizeButtonConfigData.path}} {{StorefrontPaypalProductPageEnableCustomizeButtonConfigData.value}}" stepKey="enableCustomizeButton"/> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonBuyNowLabelConfigData.path}} {{StorefrontPaypalProductPageButtonBuyNowLabelConfigData.value}}" stepKey="setLabelForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalProductPageButtonVerticalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonRectangleShapeConfigData.path}} {{StorefrontPaypalProductPageButtonRectangleShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonSilverColorConfigData.path}} {{StorefrontPaypalProductPageButtonSilverColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> + </before> + <after> + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!-- Logout from Admin --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- Go to PayPal Smart Button page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToPayPalSmartButtonPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Switch to iframe of PayPal group button --> + <actionGroup ref="StorefrontSwitchToPayPalButtonIframeActionGroup" stepKey="switchToIframe"/> + + <!-- Check PayPal smart button configurations --> + <seeElement selector="{{StorefrontPayPalSmartButtonStylesSection.label}}" stepKey="seeButtonLabel"/> + <actionGroup ref="AssertPayPalButtonLayoutActionGroup" stepKey="assertLayoutBtn"> + <argument name="label" value="{{PayPalLabel.buynow}}"/> + <argument name="layout" value="{{PayPalLayout.vertical}}"/> + <argument name="shape" value="{{PayPalShape.rectangle}}"/> + <argument name="color" value="{{PayPalColor.silver}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithCheckoutLabelOnCartPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithCheckoutLabelOnCartPageTest.xml new file mode 100644 index 0000000000000..f49900337546b --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithCheckoutLabelOnCartPageTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckPayPalSmartButtonWithCheckoutLabelOnCartPageTest" extends="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest"> + <annotations> + <features value="PayPal"/> + <stories value="PayPal Smart Button Configuration"/> + <title value="Check PayPal Smart Button configuration with Checkout label on Cart page"/> + <description value="Admin is able to customize PayPal Smart Button with Checkout label on Cart page"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28711"/> + <group value="paypal"/> + <group value="paypalExpress"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontPaypalCartPageEnableCustomizeButtonConfigData.path}} {{StorefrontPaypalCartPageEnableCustomizeButtonConfigData.value}}" stepKey="enableCustomizeButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonCheckoutLabelConfigData.path}} {{StorefrontPaypalCartPageButtonCheckoutLabelConfigData.value}}" stepKey="setLabelForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalCartPageButtonVerticalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonPillShapeConfigData.path}} {{StorefrontPaypalCartPageButtonPillShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonGoldColorConfigData.path}} {{StorefrontPaypalCartPageButtonGoldColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalCartPageDisableCustomizeButtonConfigData.path}} {{StorefrontPaypalCartPageDisableCustomizeButtonConfigData.value}}" stepKey="disableCustomizeButton"/> + </after> + <!-- Add Product to Cart --> + <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" before="goToPayPalSmartButtonPage" stepKey="addProductToCart"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToPayPalSmartButtonPage"/> + + <actionGroup ref="StorefrontSwitchToPayPalButtonIframeActionGroup" stepKey="switchToIframe"> + <argument name="elementNumber" value="1"/> + </actionGroup> + <!-- Check PayPal smart button configurations --> + <seeElement selector="{{StorefrontPayPalSmartButtonStylesSection.labelText(PayPalLabel.checkout)}}" stepKey="seeButtonLabelText"/> + <actionGroup ref="AssertPayPalButtonLayoutActionGroup" stepKey="assertLayoutBtn"> + <argument name="label" value="{{PayPalLabel.checkout}}"/> + <argument name="layout" value="{{PayPalLayout.vertical}}"/> + <argument name="shape" value="{{PayPalShape.pill}}"/> + <argument name="color" value="{{PayPalColor.gold}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayLabelOnCartPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayLabelOnCartPageTest.xml new file mode 100644 index 0000000000000..5564476596b1e --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayLabelOnCartPageTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckPayPalSmartButtonWithPayLabelOnCartPageTest" extends="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest"> + <annotations> + <features value="PayPal"/> + <stories value="PayPal Smart Button Configuration"/> + <title value="Check PayPal Smart Button configuration with Pay label on Cart page"/> + <description value="Admin is able to customize PayPal Smart Button with Pay label on Cart page"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28711"/> + <group value="paypal"/> + <group value="paypalExpress"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontPaypalCartPageEnableCustomizeButtonConfigData.path}} {{StorefrontPaypalCartPageEnableCustomizeButtonConfigData.value}}" stepKey="enableCustomizeButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonPayLabelConfigData.path}} {{StorefrontPaypalCartPageButtonPayLabelConfigData.value}}" stepKey="setLabelForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalCartPageButtonVerticalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonRectangleShapeConfigData.path}} {{StorefrontPaypalCartPageButtonRectangleShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCartPageButtonGoldColorConfigData.path}} {{StorefrontPaypalCartPageButtonGoldColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalCartPageDisableCustomizeButtonConfigData.path}} {{StorefrontPaypalCartPageDisableCustomizeButtonConfigData.value}}" stepKey="disableCustomizeButton"/> + </after> + <!-- Add Product to Cart --> + <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" before="goToPayPalSmartButtonPage" stepKey="addProductToCart"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToPayPalSmartButtonPage"/> + <actionGroup ref="StorefrontSwitchToPayPalButtonIframeActionGroup" stepKey="switchToIframe"> + <argument name="elementNumber" value="1"/> + </actionGroup> + <!-- Check PayPal smart button configurations --> + <actionGroup ref="AssertPayPalButtonLayoutActionGroup" stepKey="assertLayoutBtn"> + <argument name="label" value="{{PayPalLabel.pay}}"/> + <argument name="layout" value="{{PayPalLayout.vertical}}"/> + <argument name="shape" value="{{PayPalShape.rectangle}}"/> + <argument name="color" value="{{PayPalColor.gold}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayLabelOnCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayLabelOnCheckoutPageTest.xml new file mode 100644 index 0000000000000..7b877d66f27ea --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayLabelOnCheckoutPageTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckPayPalSmartButtonWithPayLabelOnCheckoutPageTest" extends="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest"> + <annotations> + <features value="PayPal"/> + <stories value="PayPal Smart Button Configuration"/> + <title value="Check PayPal Smart Button configuration with Pay label on Checkout page"/> + <description value="Admin is able to customize PayPal Smart Button with Pay label on Checkout page"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28711"/> + <group value="paypal"/> + <group value="paypalExpress"/> + </annotations> + <before> + <createData entity="_defaultCategory" before="createProduct" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Customer --> + <createData entity="Simple_US_Customer" after="createProduct" stepKey="createCustomer"/> + + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageEnableCustomizeButtonConfigData.path}} {{StorefrontPaypalCheckoutPageEnableCustomizeButtonConfigData.value}}" stepKey="enableCustomizeButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonPayLabelConfigData.path}} {{StorefrontPaypalCheckoutPageButtonPayLabelConfigData.value}}" stepKey="setLabelForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonRectangleShapeConfigData.path}} {{StorefrontPaypalCheckoutPageButtonRectangleShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.path}} {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.path}} {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.value}}" stepKey="disableCustomizeButton"/> + <!-- Delete Category --> + <deleteData createDataKey="createCategory" after="deleteProduct" stepKey="deleteCategory"/> + <!--Logout from customer account--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" before="deleteCustomer" stepKey="logoutCustomer"/> + <!--Delete customer --> + <deleteData createDataKey="createCustomer" after="deleteCategory" stepKey="deleteCustomer"/> + </after> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" before="goToPayPalSmartButtonPage" stepKey="loginAsCustomer"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + + <actionGroup ref="AddProductToCheckoutPageActionGroup" stepKey="goToPayPalSmartButtonPage"> + <argument name="Category" value="$createCategory$"/> + </actionGroup> + <!-- Check PayPal smart button configurations --> + <actionGroup ref="AssertPayPalButtonLayoutActionGroup" stepKey="assertLayoutBtn"> + <argument name="label" value="{{PayPalLabel.pay}}"/> + <argument name="shape" value="{{PayPalShape.rectangle}}"/> + <argument name="layout" value="{{PayPalLayout.vertical}}"/> + <argument name="color" value="{{PayPalColor.blue}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml new file mode 100644 index 0000000000000..400e0cfe3cc13 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest" extends="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest"> + <annotations> + <features value="PayPal"/> + <stories value="PayPal Smart Button Configuration"/> + <title value="Check PayPal Smart Button configuration with PayPal label on Checkout page"/> + <description value="Admin is able to customize PayPal Smart Button with PayPal label on Checkout page"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28711"/> + <group value="paypal"/> + <group value="paypalExpress"/> + </annotations> + <before> + <createData entity="_defaultCategory" before="createProduct" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Customer --> + <createData entity="Simple_US_Customer" after="createProduct" stepKey="createCustomer"/> + + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageEnableCustomizeButtonConfigData.path}} {{StorefrontPaypalCheckoutPageEnableCustomizeButtonConfigData.value}}" stepKey="enableCustomizeButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonPayPalLabelConfigData.path}} {{StorefrontPaypalCheckoutPageButtonPayPalLabelConfigData.value}}" stepKey="setLabelForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonPillShapeConfigData.path}} {{StorefrontPaypalCheckoutPageButtonPillShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.path}} {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> + <magentoCLI command="cache:clean" arguments="full_page" stepKey="cleanFullPageCache"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.path}} {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.value}}" stepKey="disableCustomizeButton"/> + + <!-- Delete Category --> + <deleteData createDataKey="createCategory" after="deleteProduct" stepKey="deleteCategory"/> + + <!--Logout from customer account--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" before="deleteCustomer" stepKey="logoutCustomer"/> + + <!--Delete customer --> + <deleteData createDataKey="createCustomer" after="deleteCategory" stepKey="deleteCustomer"/> + </after> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" before="goToPayPalSmartButtonPage" stepKey="loginAsCustomer"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + + <actionGroup ref="AddProductToCheckoutPageActionGroup" stepKey="goToPayPalSmartButtonPage"> + <argument name="Category" value="$createCategory$"/> + </actionGroup> + <!-- Check PayPal smart button configurations --> + <remove keyForRemoval="seeButtonLabelText"/> + <actionGroup ref="AssertPayPalButtonLayoutInPaypalLabelActionGroup" stepKey="assertLayoutBtn"> + <argument name="label" value="{{PayPalLabel.paypal}}"/> + <argument name="layout" value="{{PayPalLayout.vertical}}"/> + <argument name="shape" value="{{PayPalShape.pill}}"/> + <argument name="color" value="{{PayPalColor.blue}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnProductPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnProductPageTest.xml new file mode 100644 index 0000000000000..6e84ffa25871b --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnProductPageTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckPayPalSmartButtonWithPayPalLabelOnProductPageTest" extends="StorefrontCheckPayPalSmartButtonWithBuyNowLabelOnProductPageTest"> + <annotations> + <features value="PayPal"/> + <stories value="PayPal Smart Button Configuration"/> + <title value="Check PayPal Smart Button configuration with PayPal label on Product page"/> + <description value="Admin is able to customize PayPal Smart Button with PayPal label on Product page"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28711"/> + <group value="paypal"/> + <group value="paypalExpress"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonPayPalLabelConfigData.path}} {{StorefrontPaypalProductPageButtonPayPalLabelConfigData.value}}" stepKey="setLabelForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonHorizontalLayoutConfigData.path}} {{StorefrontPaypalProductPageButtonHorizontalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonPillShapeConfigData.path}} {{StorefrontPaypalProductPageButtonPillShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> + <magentoCLI command="config:set {{StorefrontPaypalProductPageButtonSilverColorConfigData.path}} {{StorefrontPaypalProductPageButtonSilverColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> + </before> + <!-- Check PayPal smart button configurations --> + <remove keyForRemoval="seeButtonLabelText"/> + <actionGroup ref="AssertPayPalButtonLayoutInPaypalLabelActionGroup" stepKey="assertLayoutBtn"> + <argument name="label" value="{{PayPalLabel.paypal}}"/> + <argument name="layout" value="{{PayPalLayout.horizontal}}"/> + <argument name="shape" value="{{PayPalShape.pill}}"/> + <argument name="color" value="{{PayPalColor.silver}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index f999566869061..d27ac4c4a92f5 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -11,49 +11,38 @@ <test name="StorefrontPaypalSmartButtonInCheckoutPageTest"> <annotations> <features value="Paypal"/> - <stories value="Generic checkout skeleton flow"/> - <title value="Mainflow of Paypal Smart Button"/> - <description value="Users are able to place order using Paypal Smart Button"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button In-Context on Checkout Page"/> + <description value="Users are able to place order using Paypal Smart Button on Checkout Page, payment action is Sale"/> <severity value="CRITICAL"/> <testCaseId value="MC-13690"/> - <group value="paypal"/> - <skip> - <issueId value="MC-33707"/> - </skip> + <group value="paypalExpress"/> </annotations> <before> - + <!-- Login --> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <!-- Create Product --> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="_defaultProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createCategory"/> </createData> - <!-- Create Customer --> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - - <!-- Set Paypal express config --> - <magentoCLI command="config:set {{StorefrontPaypalEnableConfigData.path}} {{StorefrontPaypalEnableConfigData.value}}" stepKey="enablePaypal"/> - <magentoCLI command="config:set {{StorefrontPaypalEnableInContextCheckoutConfigData.path}} {{StorefrontPaypalEnableInContextCheckoutConfigData.value}}" stepKey="enableInContextPayPal"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableTransferCartLineConfigData.path}} {{StorefrontPaypalDisableTransferCartLineConfigData.value}}" stepKey="disableTransferCartLine"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableTransferShippingOptionConfigData.path}} {{StorefrontPaypalDisableTransferShippingOptionConfigData.value}}" stepKey="disableTransferShippingOption"/> + <magentoCLI command="config:set {{StorefrontPaypalExpressSalePaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressSalePaymentActionOptionConfigData.value}}" stepKey="setPaymentAction"/> <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> - <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{_CREDS.magento/paypal_express_checkout_us_merchant_id}}" stepKey="setMerchantId"/> - <createData entity="PaypalConfig" stepKey="createPaypalExpressConfig"/> - - <!-- Login --> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <!--Enable Free Shipping--> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> </before> <after> - <!-- Cleanup Paypal configurations --> - <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{StorefrontPaypalMerchantAccountIdConfigData.value}}" stepKey="deleteMerchantId"/> - <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> - <magentoCLI command="config:set {{StorefrontPaypalDisableInContextCheckoutConfigData.path}} {{StorefrontPaypalDisableInContextCheckoutConfigData.value}}" stepKey="disableInContextPayPal"/> - <magentoCLI command="config:set {{StorefrontPaypalDisableConfigData.path}} {{StorefrontPaypalDisableConfigData.value}}" stepKey="disablePaypal"/> - <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> - + <magentoCLI command="config:set {{StorefrontPaypalEnableTransferCartLineConfigData.path}} {{StorefrontPaypalEnableTransferCartLineConfigData.value}}" stepKey="enableTransferCartLine"/> + <magentoCLI command="config:set {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.value}}" stepKey="setPaymentAction"/> + <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> <!-- Delete product --> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> - + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> <!--Delete customer --> <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> @@ -65,19 +54,57 @@ <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> <argument name="Customer" value="$$createCustomer$$"/> </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="StorefrontAddProductToCartFromCategoryActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="selectFlatShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> - <!-- Place an order using PayPal payment method --> - <actionGroup ref="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" stepKey="createPayPalOrder"> - <argument name="Category" value="$$createCategory$$"/> - <argument name="payerName" value="{{Payer.firstName}}"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + + <!--Assert grand total--> + <actionGroup ref="VerifyCheckoutPaymentOrderSummaryActionGroup" stepKey="verifyCheckoutPaymentOrderSummary"> + <argument name="orderSummarySubTotal" value="$123.00"/> + <argument name="orderSummaryShippingTotal" value="$0.00"/> + <argument name="orderSummaryTotal" value="$123.00"/> </actionGroup> - <!-- PayPal checkout --> - <actionGroup ref="StorefrontPayOrderOnPayPalCheckoutActionGroup" stepKey="payOrderOnPayPalCheckout"> - <argument name="productName" value="$$createProduct.name$$"/> + <dontSeeElement selector="{{StorefrontOrderReviewSection.taxCost}}" stepKey="taxAssertion"/> + + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="clickPlaceOrder"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"/> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="clickPayPalBtn" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> </actionGroup> + <!--Transfer Cart Line and Shipping Method assertion--> + <actionGroup ref="PayPalAssertTransferLineAndShippingMethodNotExistActionGroup" stepKey="assertPayPalSettings"/> + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + <!-- I see order successful Page instead of Order Review Page --> <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!--Go to Admin and check order information--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGrid"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeAdminOrderStatus"> + <argument name="status" value="Processing"/> + </actionGroup> + <actionGroup ref="AdminAssertNoAuthorizeButtonOnOrderPageActionGroup" stepKey="dontSeeOrderWaitingForAuthorize"/> </test> </tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInMiniCartPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInMiniCartPageTest.xml new file mode 100644 index 0000000000000..f50d2fc9ad9a4 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInMiniCartPageTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonInMiniCartPageTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button In-Context on Mini Cart Page"/> + <description value="Users are able to place order using Paypal Smart Button on Mini Cart Page"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-27604"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!-- Login --> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + + <!-- Create Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableTransferCartLineConfigData.path}} {{StorefrontPaypalEnableTransferCartLineConfigData.value}}" stepKey="enableTransferCartLine"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableTransferShippingOptionConfigData.path}} {{StorefrontPaypalEnableTransferShippingOptionConfigData.value}}" stepKey="enableTransferShippingOption"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableTransferShippingOptionConfigData.path}} {{StorefrontPaypalDisableTransferShippingOptionConfigData.value}}" stepKey="disableTransferShippingOption"/> + + <!-- Delete Product --> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete Customer --> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="StorefrontAddProductToCartFromCategoryActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniShoppingCart"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"/> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentFromCartAccountActionGroup" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Transfer Cart Line and Shipping Method assertion--> + <actionGroup ref="PayPalAssertTransferLineAndShippingMethodActionGroup" stepKey="assertPayPalSettings"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Click PayPal button and go back to Magento site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoActionGroup" stepKey="goBackToMagentoSite"/> + + <actionGroup ref="StorefrontSelectShippingMethodOnOrderReviewPageActionGroup" stepKey="selectShippingMethod"/> + + <!--Assert grand total--> + <actionGroup ref="StorefrontAssertOrderReviewSummaryWithTaxActionGroup" stepKey="verifyCheckoutPaymentOrderSummary"> + <argument name="orderSummarySubTotal" value="$123.00"/> + <argument name="orderSummaryShippingTotal" value="$5.00"/> + <argument name="orderSummaryTax" value="$0.00"/> + <argument name="orderSummaryTotal" value="$128.00"/> + </actionGroup> + + <!--SubmitOrder--> + <actionGroup ref="StorefrontPlaceOrderOnOrderReviewPageActionGroup" stepKey="clickPlaceOrderBtn"/> + + <!-- I see order successful Page instead of Order Review Page --> + <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml new file mode 100644 index 0000000000000..3e4f5c8b4da30 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonInProductPageTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button In-Context on Product Page"/> + <description value="Users are able to place order using Paypal Smart Button on Product Pag, payment action is Order"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-26167"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!-- Login --> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <!-- Create Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <magentoCLI command="config:set {{StorefrontPaypalExpressOrderPaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressOrderPaymentActionOptionConfigData.value}}" stepKey="setPaymentAction"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableTransferCartLineConfigData.path}} {{StorefrontPaypalDisableTransferCartLineConfigData.value}}" stepKey="disableTransferCartLine"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableTransferShippingOptionConfigData.path}} {{StorefrontPaypalDisableTransferShippingOptionConfigData.value}}" stepKey="disableTransferShippingOption"/> + <!-- Enable free shipping method --> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> + + <!-- Create Tax Rule --> + <createData entity="US_CA_Rate_1" stepKey="initialTaxRate"/> + <actionGroup ref="AdminCreateTaxRuleActionGroup" stepKey="createTaxRule"> + <argument name="taxRate" value="$$initialTaxRate$$"/> + <argument name="taxRule" value="SimpleTaxRule"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <magentoCLI command="config:set {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.value}}" stepKey="returnPaymentActionDefaultValue"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableTransferCartLineConfigData.path}} {{StorefrontPaypalEnableTransferCartLineConfigData.value}}" stepKey="enableTransferCartLine"/> + <!-- Disable free shipping method --> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + + <!-- Delete product --> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete customer --> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!--Delete Tax Rule--> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"/> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + <!--Transfer Cart Line and Shipping Method assertion--> + <actionGroup ref="PayPalAssertTransferLineAndShippingMethodNotExistActionGroup" stepKey="assertPayPalSettings"/> + + <!--Click PayPal button and go back to Magento site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" stepKey="goBackToMagentoSite"/> + + <actionGroup ref="StorefrontSelectShippingMethodOnOrderReviewPageActionGroup" stepKey="selectShippingMethod"> + <argument name="shippingMethod" value="Free - $0.00"/> + </actionGroup> + + <!--Assert grand total--> + <actionGroup ref="StorefrontAssertOrderReviewSummaryWithTaxActionGroup" stepKey="verifyCheckoutPaymentOrderSummary"> + <argument name="orderSummarySubTotal" value="$123.00"/> + <argument name="orderSummaryShippingTotal" value="$0.00"/> + <argument name="orderSummaryTotal" value="$133.15"/> + <argument name="orderSummaryTax" value="$10.15"/> + </actionGroup> + + <actionGroup ref="StorefrontPlaceOrderOnOrderReviewPageActionGroup" stepKey="clickPlaceOrderBtn"/> + <!-- I see order successful Page instead of Order Review Page --> + <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!--Go to Admin and check order information--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGrid"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeAdminOrderStatus"> + <argument name="status" value="Processing"/> + </actionGroup> + <actionGroup ref="AdminAssertAuthorizeButtonOnOrderPageActionGroup" stepKey="seeOrderWaitingForAuthorize"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInShoppingCartPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInShoppingCartPageTest.xml new file mode 100644 index 0000000000000..62ebeea2d65be --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInShoppingCartPageTest.xml @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonInShoppingCartPageTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button In-Context on Cart Page"/> + <description value="Users are able to perform PayPal Express Checkout method using PayPal Smart Button on Shopping Cart, payment action is Sale, make sure checkout as guest is not available"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-27605"/> + </annotations> + <before> + <!-- Login --> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + + <!--Config PayPal Express Checkout--> + <actionGroup ref="ConfigPayPalExpressCheckoutActionGroup" stepKey="ConfigPayPalExpressCheckout"/> + <!-- Create Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!--Advanced Settings--> + <magentoCLI command="config:set {{StorefrontPaypalEnableTransferCartLineConfigData.path}} {{StorefrontPaypalEnableTransferCartLineConfigData.value}}" stepKey="enableTransferCartLine"/> + <magentoCLI command="config:set {{StorefrontPaypalEnableTransferShippingOptionConfigData.path}} {{StorefrontPaypalEnableTransferShippingOptionConfigData.value}}" stepKey="enableTransferShippingOption"/> + <magentoCLI command="config:set {{StorefrontPaypalExpressSalePaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressSalePaymentActionOptionConfigData.value}}" stepKey="setPaymentActionSale"/> + <magentoCLI command="config:set {{StorefrontPaypalExpressEnableCheckoutAsGuestConfigData.path}} {{StorefrontPaypalExpressEnableCheckoutAsGuestConfigData.value}}" stepKey="enableCheckoutAsGuest"/> + </before> + <after> + <!-- Cleanup Paypal configurations --> + <magentoCLI command="config:set {{StorefrontPaypalMerchantAccountIdConfigData.path}} {{StorefrontPaypalMerchantAccountIdConfigData.value}}" stepKey="deleteMerchantId"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableInContextCheckoutConfigData.path}} {{StorefrontPaypalDisableInContextCheckoutConfigData.value}}" stepKey="disableInContextPayPal"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableConfigData.path}} {{StorefrontPaypalDisableConfigData.value}}" stepKey="disablePaypal"/> + <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> + + <!--Clean Advanced Settings--> + <magentoCLI command="config:set {{StorefrontPaypalDisableTransferShippingOptionConfigData.path}} {{StorefrontPaypalDisableTransferShippingOptionConfigData.value}}" stepKey="disableTransferShippingOption"/> + <magentoCLI command="config:set {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.value}}" stepKey="setPaymentActionAuthorization"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + + <!-- Delete Product --> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + + <!--Delete Customer --> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="StorefrontAddProductToCartFromCategoryActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="openShoppingCart"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"> + <argument name="elementNumber" value="1"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentFromCartAccountActionGroup" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Transfer Cart Line and Shipping Method assertion--> + <actionGroup ref="PayPalAssertTransferLineAndShippingMethodActionGroup" stepKey="assertPayPalSettings"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Click PayPal button and go back to Magento site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoActionGroup" stepKey="goBackToMagentoSite"/> + + <actionGroup ref="StorefrontSelectShippingMethodOnOrderReviewPageActionGroup" stepKey="selectShippingMethod"/> + + <!--Assert grand total--> + <actionGroup ref="StorefrontAssertOrderReviewSummaryWithTaxActionGroup" stepKey="verifyCheckoutPaymentOrderSummary"> + <argument name="orderSummarySubTotal" value="$123.00"/> + <argument name="orderSummaryShippingTotal" value="$5.00"/> + <argument name="orderSummaryTotal" value="$128.00"/> + <argument name="orderSummaryTax" value="$0.00"/> + </actionGroup> + + <!--SubmitOrder--> + <actionGroup ref="StorefrontPlaceOrderOnOrderReviewPageActionGroup" stepKey="clickPlaceOrderBtn"/> + + <!-- I see order successful Page instead of Order Review Page --> + <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="getOrderNumber"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="addFilterToGridAndOpenOrder"> + <argument name="orderId" value="{$getOrderNumber}"/> + </actionGroup> + <actionGroup ref="AdminAssertNoAuthorizeButtonOnOrderPageActionGroup" stepKey="dontSeeOrderWaitingForAuthorize"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="checkOrderStatus"> + <argument name="status" value="Processing"/> + </actionGroup> + <actionGroup ref="AdminOpenInvoiceFromOrderPageActionGroup" stepKey="openInvoiceFromOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml new file mode 100644 index 0000000000000..4d4f4e8b89957 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonWithAUDCurrencyTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button with Australian currency"/> + <description value="Users are able to place order using Paypal Smart Button using Australian currency"/> + <severity value="MAJOR"/> + <testCaseId value="MC-33274"/> + <group value="paypalExpress"/> + </annotations> + <before> + + <!--Enable Advanced Setting--> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantAustralia.path}} {{MerchantAustralia.value}}" stepKey="setMerchantCountryUK"/> + <!--Set Currency options for Default Config--> + <magentoCLI command="config:set {{SetCurrencyAUDBaseConfig.path}} {{SetCurrencyAUDBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForAUD.value}}" stepKey="setAllowedCurrencyEURandUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyAUDConfig.path}} {{SetDefaultCurrencyAUDConfig.value}}" stepKey="setCurrencyDefaultEUR"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForAUD.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyAUDConfig.scope}} --scope-code={{SetDefaultCurrencyAUDConfig.scope_code}} {{SetDefaultCurrencyAUDConfig.path}} {{SetDefaultCurrencyAUDConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> + </after> + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" after="waitForPlaceOrderButton" stepKey="guestSelectCheckMoneyOrderPayment"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" after="guestSelectCheckMoneyOrderPayment" stepKey="guestPlaceOrder"> + <argument name="elementNumber" value="0"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="guestPlaceOrder" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + + <actionGroup ref="AdminAssertCurrencyInOrderActionGroup" stepKey="seeEURandUSDRate"> + <argument name="rate" value="AUD / USD rate"/> + </actionGroup> + + <assertEquals after ="grabRate" stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[AUD / USD rate:]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithCADCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithCADCurrencyTest.xml new file mode 100644 index 0000000000000..aa35f36268270 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithCADCurrencyTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonWithCADCurrencyTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button with Canadian currency"/> + <description value="Users are able to place order using Paypal Smart Button using Canadian currency"/> + <severity value="MAJOR"/> + <testCaseId value="MC-33274"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!--Enable Advanced Setting--> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantCanada.path}} {{MerchantCanada.value}}" stepKey="setMerchantCountryUK"/> + <!--Set Currency options for Default Config--> + <magentoCLI command="config:set {{SetCurrencyCADBaseConfig.path}} {{SetCurrencyCADBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForCAD.value}}" stepKey="setAllowedCurrencyEURandUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyCADConfig.path}} {{SetDefaultCurrencyCADConfig.value}}" stepKey="setCurrencyDefaultEUR"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForCAD.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyCADConfig.scope}} --scope-code={{SetDefaultCurrencyCADConfig.scope_code}} {{SetDefaultCurrencyCADConfig.path}} {{SetDefaultCurrencyCADConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <!--Set default merchant country--> + <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> + </after> + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" after="waitForPlaceOrderButton" stepKey="guestSelectCheckMoneyOrderPayment"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" after="guestSelectCheckMoneyOrderPayment" stepKey="guestPlaceOrder"> + <argument name="elementNumber" value="0"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="guestPlaceOrder" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + + <!-- I see order successful Page instead of Order Review Page --> + <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + <actionGroup ref="AdminAssertCurrencyInOrderActionGroup" stepKey="seeEURandUSDRate"> + <argument name="rate" value="CAD / USD rate"/> + </actionGroup> + + <assertEquals after ="grabRate" stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[CAD / USD rate:]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml new file mode 100644 index 0000000000000..1adfe3b5d3a70 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonWithEuroCurrencyTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button with Euro currency"/> + <description value="Users are able to place order using Paypal Smart Button using Euro currency"/> + <severity value="MAJOR"/> + <testCaseId value="MC-33274"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantUnitedKingdom.path}} {{MerchantUnitedKingdom.value}}" stepKey="setMerchantCountryUK"/> + <!--Enable Advanced Setting--> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <!--Set default merchant country--> + <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + </after> + + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="guestSelectCheckMoneyOrderPayment"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" after="guestSelectCheckMoneyOrderPayment" stepKey="guestPlaceOrder"> + <argument name="elementNumber" value="0"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="guestPlaceOrder" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + + <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml new file mode 100644 index 0000000000000..0efb3f33739fa --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonWithFranceMerchantCountryTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button with Euro currency and merchant country is France"/> + <description value="Users are able to place order using Paypal Smart Button using Euro currency and merchant country is France"/> + <severity value="MAJOR"/> + <testCaseId value="MC-33274"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantFrance.path}} {{MerchantFrance.value}}" stepKey="setMerchantCountryUK"/> + <!--Enable Advanced Setting--> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <!--Set default merchant country--> + <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + </after> + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="guestSelectCheckMoneyOrderPayment"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" after="guestSelectCheckMoneyOrderPayment" stepKey="guestPlaceOrder"> + <argument name="elementNumber" value="0"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="guestPlaceOrder" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + + <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithHKDCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithHKDCurrencyTest.xml new file mode 100644 index 0000000000000..75a4a5f084b49 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithHKDCurrencyTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonWithHKDCurrencyTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button with HKD currency and merchant country HongKong"/> + <description value="Users are able to place order using Paypal Smart Button using HKD currency and merchant country HongKong"/> + <severity value="MAJOR"/> + <testCaseId value="MC-33274"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!--Enable Advanced Setting--> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantHongKong.path}} {{MerchantHongKong.value}}" stepKey="setMerchantCountryUK"/> + <!--Set Currency options for Default Config--> + <magentoCLI command="config:set {{SetCurrencyHKDBaseConfig.path}} {{SetCurrencyHKDBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForHKD.value}}" stepKey="setAllowedCurrencyEURandUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyHKDConfig.path}} {{SetDefaultCurrencyHKDConfig.value}}" stepKey="setCurrencyDefaultEUR"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForHKD.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyHKDConfig.scope}} --scope-code={{SetDefaultCurrencyHKDConfig.scope_code}} {{SetDefaultCurrencyHKDConfig.path}} {{SetDefaultCurrencyHKDConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + <!--Enable Free Shipping--> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + </before> + <after> + <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <!--Set default merchant country--> + <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + </after> + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" after="waitForPlaceOrderButton" stepKey="guestSelectCheckMoneyOrderPayment"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" after="guestSelectCheckMoneyOrderPayment" stepKey="guestPlaceOrder"> + <argument name="elementNumber" value="0"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="guestPlaceOrder" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + + <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + <actionGroup ref="AdminAssertCurrencyInOrderActionGroup" stepKey="seeEURandUSDRate"> + <argument name="rate" value="HKD / USD rate"/> + </actionGroup> + <assertEquals after ="grabRate" stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[HKD / USD rate:]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithNZDCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithNZDCurrencyTest.xml new file mode 100644 index 0000000000000..4cddf674c9bfe --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithNZDCurrencyTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonWithNZDCurrencyTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button with NZD currency and New Zealand merchant country"/> + <description value="Users are able to place order using Paypal Smart Button using NZD currency and New Zealand merchant country"/> + <severity value="MAJOR"/> + <testCaseId value="MC-33274"/> + <group value="paypalExpress"/> + </annotations> + <before> + <!--Enable Advanced Setting--> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantNewZealand.path}} {{MerchantNewZealand.value}}" stepKey="setMerchantCountryUK"/> + <!--Set Currency options for Default Config--> + <magentoCLI command="config:set {{SetCurrencyNZDBaseConfig.path}} {{SetCurrencyNZDBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForNZD.value}}" stepKey="setAllowedCurrencyEURandUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyNZDConfig.path}} {{SetDefaultCurrencyNZDConfig.value}}" stepKey="setCurrencyDefaultEUR"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForNZD.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyNZDConfig.scope}} --scope-code={{SetDefaultCurrencyNZDConfig.scope_code}} {{SetDefaultCurrencyNZDConfig.path}} {{SetDefaultCurrencyNZDConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + <!--Enable Free Shipping--> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <!--Set default merchant country--> + <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + </after> + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" after="waitForPlaceOrderButton" stepKey="guestSelectCheckMoneyOrderPayment"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" after="guestSelectCheckMoneyOrderPayment" stepKey="guestPlaceOrder"> + <argument name="elementNumber" value="0"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="guestPlaceOrder" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + + <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + <actionGroup ref="AdminAssertCurrencyInOrderActionGroup" stepKey="seeEURandUSDRate"> + <argument name="rate" value="NZD / USD rate"/> + </actionGroup> + <assertEquals after ="grabRate" stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[NZD / USD rate:]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithYENCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithYENCurrencyTest.xml new file mode 100644 index 0000000000000..b5b42b2a6bcbd --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithYENCurrencyTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPaypalSmartButtonWithYENCurrencyTest" extends="AdminOrderRateDisplayedInOneLineTest"> + <annotations> + <features value="Paypal"/> + <stories value="PayPal Express Checkout"/> + <title value="Mainflow of Paypal Smart Button with YEN currency and Japan merchant country"/> + <description value="Users are able to place order using Paypal Smart Button using YEN currency and Japan merchant country"/> + <severity value="MAJOR"/> + <testCaseId value="MC-33274"/> + <group value="paypalExpress"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> + <!--Set merchant country--> + <magentoCLI command="config:set {{MerchantJapan.path}} {{MerchantJapan.value}}" stepKey="setMerchantCountryUK"/> + <!--Set Currency options for Default Config--> + <magentoCLI command="config:set {{SetCurrencyYENBaseConfig.path}} {{SetCurrencyYENBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> + <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForYEN.value}}" stepKey="setAllowedCurrencyEURandUSD"/> + <magentoCLI command="config:set {{SetDefaultCurrencyYENConfig.path}} {{SetDefaultCurrencyYENConfig.value}}" stepKey="setCurrencyDefaultEUR"/> + <!--Set Currency options for Website--> + <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> + <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForYEN.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> + <magentoCLI command="config:set --scope={{SetDefaultCurrencyYENConfig.scope}} --scope-code={{SetDefaultCurrencyYENConfig.scope_code}} {{SetDefaultCurrencyYENConfig.path}} {{SetDefaultCurrencyYENConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> + <!--Enable Free Shipping--> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> + <!--Set default merchant country--> + <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + </after> + <!-- click on PayPal payment radio button --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" after="waitForPlaceOrderButton" stepKey="guestSelectCheckMoneyOrderPayment"/> + + <!--Click Paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" after="guestSelectCheckMoneyOrderPayment" stepKey="guestPlaceOrder"> + <argument name="elementNumber" value="0"/> + </actionGroup> + + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountOneStepActionGroup" after="guestPlaceOrder" stepKey="LoginToPayPal"> + <argument name="payerName" value="{{Payer.firstName}}"/> + </actionGroup> + + <!--Submit payment on PayPal site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> + + <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + <actionGroup ref="AdminAssertCurrencyInOrderActionGroup" stepKey="seeEURandUSDRate"> + <argument name="rate" value="JPY / USD rate"/> + </actionGroup> + <assertEquals after ="grabRate" stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[JPY / USD rate:]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Express/LocaleResolverTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Express/LocaleResolverTest.php index b9e2c959b4f7d..ccd991aa3ff0c 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Express/LocaleResolverTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Express/LocaleResolverTest.php @@ -13,12 +13,12 @@ use Magento\Paypal\Model\Express\LocaleResolver as ExpressLocaleResolver; /** - * Class LocaleResolverTest + * Test for PayPal express checkout resolver */ class LocaleResolverTest extends \PHPUnit\Framework\TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject|ResolverInterface + * @var \PHPUnit\Framework\MockObject\MockObject|ResolverInterface */ private $resolver; @@ -27,18 +27,20 @@ class LocaleResolverTest extends \PHPUnit\Framework\TestCase */ private $model; + /** + * @var Config + */ + private $config; + protected function setUp() { $this->resolver = $this->createMock(ResolverInterface::class); /** @var Config $config */ - $config = $this->createMock(Config::class); - $config->method('getValue') - ->with('supported_locales') - ->willReturn('zh_CN,zh_HK,zh_TW,fr_FR'); + $this->config = $this->createMock(Config::class); /** @var ConfigFactory $configFactory */ $configFactory = $this->createPartialMock(ConfigFactory::class, ['create']); - $configFactory->method('create')->willReturn($config); + $configFactory->method('create')->willReturn($this->config); $this->model = new ExpressLocaleResolver($this->resolver, $configFactory); } @@ -54,7 +56,14 @@ public function testGetLocale(string $locale, string $expectedLocale) { $this->resolver->method('getLocale') ->willReturn($locale); - + $this->config->method('getValue')->will( + $this->returnValueMap( + [ + ['in_context', null, false], + ['supported_locales', null, 'zh_CN,zh_HK,zh_TW,fr_FR'], + ] + ) + ); $this->assertEquals($expectedLocale, $this->model->getLocale()); } @@ -71,4 +80,23 @@ public function getLocaleDataProvider(): array ['locale' => 'unknown', 'expectedLocale' => 'en_US'], ]; } + + /** + * Tests retrieving locales for PayPal Express Smart Buttons. + * + */ + public function testGetLocaleForSmartButtons() + { + $this->resolver->method('getLocale') + ->willReturn('zh_Hans_CN'); + $this->config->method('getValue')->will( + $this->returnValueMap( + [ + ['in_context', null, true], + ['smart_buttons_supported_locales', null, 'zh_CN,zh_HK,zh_TW,fr_FR'], + ] + ) + ); + $this->assertEquals('zh_CN', $this->model->getLocale()); + } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php index 4002c78bb0ecc..063f98bf3d3ea 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php @@ -61,7 +61,8 @@ protected function setUp() $configFactoryMock, $scopeConfigMock, $this->getDefaultStyles(), - $this->getAllowedFundings() + $this->getDisallowedFundingMap(), + $this->getUnsupportedPaymentMethods() ); } @@ -73,7 +74,6 @@ protected function setUp() * @param bool $isCustomize * @param string $disallowedFundings * @param string $layout - * @param string $size * @param string $shape * @param string $label * @param string $color @@ -90,7 +90,6 @@ public function testGetConfig( bool $isCustomize, ?string $disallowedFundings, string $layout, - string $size, string $shape, string $label, string $color, @@ -103,6 +102,7 @@ public function testGetConfig( $this->configMock->method('getValue')->will( $this->returnValueMap( [ + ['sandbox_client_id', null, 'sb'], ['merchant_id', null, 'merchant'], [ 'solution_type', @@ -110,10 +110,10 @@ public function testGetConfig( $isPaypalGuestCheckoutEnabled ? Config::EC_SOLUTION_TYPE_SOLE : Config::EC_SOLUTION_TYPE_MARK ], ['sandbox_flag', null, true], + ['paymentAction', null, 'Authorization'], ['disable_funding_options', null, $disallowedFundings], ["{$page}_page_button_customize", null, $isCustomize], ["{$page}_page_button_layout", null, $layout], - ["{$page}_page_button_size", null, $size], ["{$page}_page_button_color", null, $color], ["{$page}_page_button_shape", null, $shape], ["{$page}_page_button_label", null, $label], @@ -150,12 +150,22 @@ private function getDefaultStyles() } /** - * Get allowed fundings + * Get disallowed funding map * * @return array */ - private function getAllowedFundings() + private function getDisallowedFundingMap() { - return include __DIR__ . '/_files/allowed_fundings.php'; + return include __DIR__ . '/_files/disallowed_funding_map.php'; + } + + /** + * Get unsupported payment methods + * + * @return array + */ + private function getUnsupportedPaymentMethods() + { + return include __DIR__ . '/_files/unsupported_payment_methods.php'; } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php deleted file mode 100644 index 6b6f8ccb87e14..0000000000000 --- a/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -return [ - 'checkout' => [ - 'CREDIT', - 'ELV' - ], - 'cart' => [ - 'CREDIT', - 'ELV' - ], - 'mini_cart' => [ - 'CREDIT', - 'ELV' - ], - 'product' => [ - 'CREDIT' - ] -]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/disallowed_funding_map.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/disallowed_funding_map.php new file mode 100644 index 0000000000000..ea543454975f4 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/disallowed_funding_map.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + "CREDIT" => 'credit', + "CARD" => 'card', + "ELV" => 'sepa' +]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php index f17b9f9fb4979..6089b8b20b1ac 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php @@ -5,6 +5,16 @@ */ declare(strict_types=1); +/** + * Generates expected PayPal SDK url + * @param array $params + * @return String + */ +function generateExpectedPaypalSdkUrl(array $params) : String +{ + return 'https://www.paypal.com/sdk/js?' . http_build_query($params); +} + return [ 'cart' => [ 'cart', @@ -12,29 +22,36 @@ true, 'CREDIT', 'horizontal', - 'small', - 'pillow', + 'pill', 'installment', 'blue', 'my_label', 'mx', true, [ - 'merchantId' => 'merchant', - 'environment' => 'sandbox', - 'locale' => 'es_MX', - 'allowedFunding' => ['ELV'], - 'disallowedFunding' => ['CREDIT'], 'styles' => [ 'layout' => 'horizontal', - 'size' => 'small', + 'size' => null, 'color' => 'blue', - 'shape' => 'pillow', + 'shape' => 'pill', 'label' => 'installment', - 'installmentperiod' => 0 + 'period' => 0 ], 'isVisibleOnProductPage' => false, - 'isGuestCheckoutAllowed' => true + 'isGuestCheckoutAllowed' => true, + 'sdkUrl' => generateExpectedPaypalSdkUrl( + [ + 'client-id' => 'sb', + 'commit' => 'false', + 'merchant-id' => 'merchant', + 'locale' => 'es_MX', + 'intent' => 'authorize', + 'disable-funding' => implode( + ',', + ['credit', 'venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] + ) + ] + ) ] ], 'checkout' => [ @@ -43,50 +60,51 @@ true, null, 'horizontal', - 'small', - 'pillow', + 'pill', 'installment', 'blue', 'my_label', 'br', true, [ - 'merchantId' => 'merchant', - 'environment' => 'sandbox', - 'locale' => 'en_BR', - 'allowedFunding' => ['CREDIT', 'ELV'], - 'disallowedFunding' => [], 'styles' => [ 'layout' => 'horizontal', - 'size' => 'small', + 'size' => null, 'color' => 'blue', - 'shape' => 'pillow', + 'shape' => 'pill', 'label' => 'installment', - 'installmentperiod' => 0 + 'period' => 0 ], 'isVisibleOnProductPage' => false, - 'isGuestCheckoutAllowed' => true + 'isGuestCheckoutAllowed' => true, + 'sdkUrl' => generateExpectedPaypalSdkUrl( + [ + 'client-id' => 'sb', + 'commit' => 'false', + 'merchant-id' => 'merchant', + 'locale' => 'en_BR', + 'intent' => 'authorize', + 'disable-funding' => implode( + ',', + ['venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] + ) + ] + ) ] ], 'mini_cart' => [ 'cart', - 'en', + 'en_US', false, null, 'horizontal', - 'small', - 'pillow', + 'pill', 'installment', 'blue', 'my_label', 'br', true, [ - 'merchantId' => 'merchant', - 'environment' => 'sandbox', - 'locale' => 'en', - 'allowedFunding' => ['CREDIT', 'ELV'], - 'disallowedFunding' => [], 'styles' => [ 'layout' => 'vertical', 'size' => 'responsive', @@ -95,28 +113,35 @@ 'label' => 'paypal' ], 'isVisibleOnProductPage' => false, - 'isGuestCheckoutAllowed' => true + 'isGuestCheckoutAllowed' => true, + 'sdkUrl' => generateExpectedPaypalSdkUrl( + [ + 'client-id' => 'sb', + 'commit' => 'false', + 'merchant-id' => 'merchant', + 'locale' => 'en_US', + 'intent' => 'authorize', + 'disable-funding' => implode( + ',', + ['venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] + ) + ] + ) ] ], 'product' => [ 'cart', - 'en', + 'en_US', false, 'CREDIT', 'horizontal', - 'small', - 'pillow', + 'pill', 'installment', 'blue', 'my_label', 'br', true, [ - 'merchantId' => 'merchant', - 'environment' => 'sandbox', - 'locale' => 'en', - 'allowedFunding' => ['ELV'], - 'disallowedFunding' => ['CREDIT'], 'styles' => [ 'layout' => 'vertical', 'size' => 'responsive', @@ -125,7 +150,20 @@ 'label' => 'paypal', ], 'isVisibleOnProductPage' => false, - 'isGuestCheckoutAllowed' => true + 'isGuestCheckoutAllowed' => true, + 'sdkUrl' => generateExpectedPaypalSdkUrl( + [ + 'client-id' => 'sb', + 'commit' => 'false', + 'merchant-id' => 'merchant', + 'locale' => 'en_US', + 'intent' => 'authorize', + 'disable-funding' => implode( + ',', + ['credit','venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] + ) + ] + ) ] ], 'checkout_with_paypal_guest_checkout_disabled' => [ @@ -134,29 +172,36 @@ true, null, 'horizontal', - 'small', - 'pillow', + 'pill', 'installment', 'blue', 'my_label', 'br', false, [ - 'merchantId' => 'merchant', - 'environment' => 'sandbox', - 'locale' => 'en_BR', - 'allowedFunding' => ['CREDIT', 'ELV'], - 'disallowedFunding' => ['CARD'], 'styles' => [ 'layout' => 'horizontal', - 'size' => 'small', + 'size' => null, 'color' => 'blue', - 'shape' => 'pillow', + 'shape' => 'pill', 'label' => 'installment', - 'installmentperiod' => 0 + 'period' => 0 ], 'isVisibleOnProductPage' => false, - 'isGuestCheckoutAllowed' => true + 'isGuestCheckoutAllowed' => true, + 'sdkUrl' => generateExpectedPaypalSdkUrl( + [ + 'client-id' => 'sb', + 'commit' => 'false', + 'merchant-id' => 'merchant', + 'locale' => 'en_BR', + 'intent' => 'authorize', + 'disable-funding' => implode( + ',', + ['card','venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] + ) + ] + ) ] ], ]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/unsupported_payment_methods.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/unsupported_payment_methods.php new file mode 100644 index 0000000000000..f4b4c72762d93 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/unsupported_payment_methods.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'venmo'=> 'venmo', + 'bancontact' => 'bancontact', + 'eps' => 'eps', + 'giropay' => 'giropay', + 'ideal' => 'ideal', + 'mybank' => 'mybank', + 'p24' => 'p24', + 'sofort' => 'sofort' +]; diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml index 04d5fae435816..fe3ed6ce5e00e 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml @@ -155,10 +155,10 @@ <tooltip>You can look up your merchant ID by logging into https://www.paypal.com/. Click the profile icon on the top right side of the page and then select Profile and settings in the Business Profile menu. (If you do not see the profile icon at the top of the page, click Profile, which appears in the top menu when the My Account tab is selected.) Click My business info on the left, and the Merchant account ID is displayed in the list of profile items on the right.</tooltip> <config_path>payment/paypal_express/merchant_id</config_path> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\MerchantId</frontend_model> + <validate>required-entry</validate> <depends> <field id="enable_in_context_checkout">1</field> </depends> - <validate>required-entry</validate> </field> <field id="enable_express_checkout_bml" translate="label comment" type="select" sortOrder="23" showInDefault="0" showInWebsite="0"> <label>Enable PayPal Credit</label> @@ -703,16 +703,6 @@ </depends> <attribute type="shared">1</attribute> </field> - <field id="checkout_page_button_size" translate="label tooltip" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Size</label> - <config_path>paypal/style/checkout_page_button_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getSize</source_model> - <tooltip>Select Responsive to ensure the PayPal button renders correctly on mobile devices.</tooltip> - <depends> - <field id="checkout_page_button_customize">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> <field id="checkout_page_button_shape" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> <label>Shape</label> <config_path>paypal/style/checkout_page_button_shape</config_path> @@ -765,12 +755,6 @@ <field id="product_page_button_label" negative="1">credit</field> </depends> </field> - <field id="product_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> - <config_path>paypal/style/product_page_button_size</config_path> - <depends> - <field id="product_page_button_customize">1</field> - </depends> - </field> <field id="product_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> <config_path>paypal/style/product_page_button_shape</config_path> <depends> @@ -817,12 +801,6 @@ <field id="cart_page_button_label" negative="1">credit</field> </depends> </field> - <field id="cart_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> - <config_path>paypal/style/cart_page_button_size</config_path> - <depends> - <field id="cart_page_button_customize">1</field> - </depends> - </field> <field id="cart_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> <config_path>paypal/style/cart_page_button_shape</config_path> <depends> @@ -869,12 +847,6 @@ <field id="mini_cart_page_button_label" negative="1">credit</field> </depends> </field> - <field id="mini_cart_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> - <config_path>paypal/style/mini_cart_page_button_size</config_path> - <depends> - <field id="mini_cart_page_button_customize">1</field> - </depends> - </field> <field id="mini_cart_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> <config_path>paypal/style/mini_cart_page_button_shape</config_path> <depends> diff --git a/app/code/Magento/Paypal/etc/config.xml b/app/code/Magento/Paypal/etc/config.xml index 24bb54a86103d..f93572537ffd7 100644 --- a/app/code/Magento/Paypal/etc/config.xml +++ b/app/code/Magento/Paypal/etc/config.xml @@ -69,6 +69,10 @@ <verify_peer>1</verify_peer> <skip_order_review_step>1</skip_order_review_step> <supported_locales>ar_EG,cs_CZ,da_DK,de_DE,el_GR,en_AU,en_GB,en_IN,en_US,es_ES,es_XC,fi_FI,fr_CA,fr_FR,fr_XC,he_IL,hu_HU,id_ID,it_IT,ja_JP,ko_KR,nl_NL,no_NO,pl_PL,pt_BR,pt_PT,ru_RU,sk_SK,sv_SE,th_TH,zh_CN,zh_HK,zh_TW,zh_XC</supported_locales> + <!--For more information see https://developer.paypal.com/docs/checkout/reference/customize-sdk/#locale --> + <smart_buttons_supported_locales>en_AD,fr_AD,es_AD,zh_AD,en_AE,fr_AE,es_AE,zh_AE,ar_AE,en_AG,fr_AG,es_AG,zh_AG,en_AI,fr_AI,es_AI,zh_AI,en_AL,en_AM,fr_AM,es_AM,zh_AM,en_AN,fr_AN,es_AN,zh_AN,en_AO,fr_AO,es_AO,zh_AO,es_AR,en_AR, de_AT,en_AT,en_AU,en_AW,fr_AW,es_AW,zh_AW,en_AZ,fr_AZ,es_AZ,zh_AZ,en_BA,en_BB,fr_BB,es_BB,zh_BB,en_BE,nl_BE,fr_BE,fr_BF,en_BF,es_BF,zh_BF,en_BG,ar_BH,en_BH,fr_BH,es_BH,zh_BH,fr_BI,en_BI,es_BI,zh_BI,fr_BJ,en_BJ,es_BJ,zh_BJ,en_BM,fr_BM,es_BM,zh_BM,en_BN,es_BO,en_BO,fr_BO,zh_BO,pt_BR,en_BR,en_BS,fr_BS,es_BS,zh_BS,en_BT,en_BW,fr_BW,es_BW,zh_BW,en_BY,en_BZ,es_BZ,fr_BZ,zh_BZ,en_CA,fr_CA,fr_CD,en_CD,es_CD,zh_CD,en_CG,fr_CG,es_CG,zh_CG,de_CH,fr_CH,en_CH,fr_CI,en_CI,en_CK,fr_CK,es_CK,zh_CK,es_CL,en_CL,fr_CL,zh_CL,fr_CM,en_CM,zh_CN,es_CO,en_CO,fr_CO,zh_CO,es_CR,en_CR,fr_CR,zh_CR,en_CV,fr_CV,es_CV,zh_CV,en_CY,cs_CZ,en_CZ,fr_CZ,es_CZ,zh_CZ,de_DE,en_DE,fr_DJ,en_DJ,es_DJ,zh_DJ,da_DK,en_DK,en_DM,fr_DM,es_DM,zh_DM,es_DO,en_DO,fr_DO,zh_DO,ar_DZ,en_DZ,fr_DZ,es_DZ,zh_DZ,es_EC,en_EC,fr_EC,zh_EC,en_EE,ru_EE,fr_EE,es_EE,zh_EE,ar_EG,en_EG,fr_EG,es_EG,zh_EG,en_ER,fr_ER,es_ER,zh_ER,es_ES,en_ES,en_ET,fr_ET,es_ET,zh_ET,fi_FI,en_FI,fr_FI,es_FI,zh_FI,en_FJ,fr_FJ,es_FJ,zh_FJ,en_FK,fr_FK,es_FK,zh_FK,en_FM,da_FO,en_FO,fr_FO,es_FO,zh_FO,fr_FR,en_FR,fr_GA,en_GA,es_GA,zh_GA,en_GB,en_GD,fr_GD,es_GD,zh_GD,en_GE,fr_GE,es_GE,zh_GE,en_GF,fr_GF,es_GF,zh_GF,en_GI,fr_GI,es_GI,zh_GI,da_GL,en_GL,fr_GL,es_GL,zh_GL,en_GM,fr_GM,es_GM,zh_GM,fr_GN,en_GN,es_GN,zh_GN,en_GP,fr_GP,es_GP,zh_GP,el_GR,en_GR,fr_GR,es_GR,zh_GR,es_GT,en_GT,fr_GT,zh_GT,en_GW,fr_GW,es_GW,zh_GW,en_GY,fr_GY,es_GY,zh_GY,en_HK,zh_HK,es_HN,en_HN,fr_HN,zh_HN,en_HR,hu_HU,en_HU,fr_HU,es_HU,zh_HU,id_ID,en_ID,en_IE,fr_IE,es_IE,zh_IE,he_IL,en_IL,en_IN,en_IS,it_IT,en_IT,en_JM,es_JM,fr_JM,zh_JM,ar_JO,en_JO,fr_JO,es_JO,zh_JO,ja_JP,en_JP,en_KE,fr_KE,es_KE,zh_KE,en_KG,fr_KG,es_KG,zh_KG,en_KH,en_KI,fr_KI,es_KI,zh_KI,fr_KM,en_KM,es_KM,zh_KM,en_KN,fr_KN,es_KN,zh_KN,ko_KR,en_KR,ar_KW,en_KW,fr_KW,es_KW,zh_KW,en_KY,fr_KY,es_KY,zh_KY,en_KZ,fr_KZ,es_KZ,zh_KZ,en_LA,en_LC,fr_LC,es_LC,zh_LC,en_LI,fr_LI,es_LI,zh_LI,en_LK,en_LS,fr_LS,es_LS,zh_LS,en_LT,ru_LT,fr_LT,es_LT,zh_LT,en_LU,de_LU,fr_LU,es_LU,zh_LU,en_LV,ru_LV,fr_LV,es_LV,zh_LV,ar_MA,en_MA,fr_MA,es_MA,zh_MA,fr_MC,en_MC,en_MD,en_ME,en_MG,fr_MG,es_MG,zh_MG,en_MH,fr_MH,es_MH,zh_MH,en_MK,fr_ML,en_ML,es_ML,zh_ML,en_MN,en_MQ,fr_MQ,es_MQ,zh_MQ,en_MR,fr_MR,es_MR,zh_MR,en_MS,fr_MS,es_MS,zh_MS,en_MT,en_MU,fr_MU,es_MU,zh_MU,en_MV,en_MW,fr_MW,es_MW,zh_MW,es_MX,en_MX,en_MY,en_MZ,fr_MZ,es_MZ,zh_MZ,en_NA,fr_NA,es_NA,zh_NA,en_NC,fr_NC,es_NC,zh_NC,fr_NE,en_NE,es_NE,zh_NE,en_NF,fr_NF,es_NF,zh_NF,en_NG,es_NI,en_NI,fr_NI,zh_NI,nl_NL,en_NL,no_NO,en_NO,en_NP,en_NR,fr_NR,es_NR,zh_NR,en_NU,fr_NU,es_NU,zh_NU,en_NZ,fr_NZ,es_NZ,zh_NZ,ar_OM,en_OM,fr_OM,es_OM,zh_OM,es_PA,en_PA,fr_PA,zh_PA,es_PE,en_PE,fr_PE,zh_PE,en_PF,fr_PF,es_PF,zh_PF,en_PG,fr_PG,es_PG,zh_PG,en_PH,pl_PL,en_PL,en_PM,fr_PM,es_PM,zh_PM,en_PN,fr_PN,es_PN,zh_PN,pt_PT,en_PT,en_PW,fr_PW,es_PW,zh_PW,es_PY,en_PY,en_QA,fr_QA,es_QA,zh_QA,ar_QA,en_RE,fr_RE,es_RE,zh_RE,en_RO,fr_RO,es_RO,zh_RO,en_RS,fr_RS,es_RS,zh_RS,ru_RU,en_RU,fr_RW,en_RW,es_RW,zh_RW,ar_SA,en_SA,fr_SA,es_SA,zh_SA,en_SB,fr_SB,es_SB,zh_SB,fr_SC,en_SC,es_SC,zh_SC,sv_SE,en_SE,en_SG,en_SH,fr_SH,es_SH,zh_SH,en_SI,fr_SI,es_SI,zh_SI,en_SJ,fr_SJ,es_SJ,zh_SJ,sk_SK,en_SK,fr_SK,es_SK,zh_SK,en_SL,fr_SL,es_SL,zh_SL,en_SM,fr_SM,es_SM,zh_SM,fr_SN,en_SN,es_SN,zh_SN,en_SO,fr_SO,es_SO,zh_SO,en_SR,fr_SR,es_SR,zh_SR,en_ST,fr_ST,es_ST,zh_ST,es_SV,en_SV,fr_SV,zh_SV,en_SZ,fr_SZ,es_SZ,zh_SZ,en_TC,fr_TC,es_TC,zh_TC,fr_TD,en_TD,es_TD,zh_TD,fr_TG,en_TG,es_TG,zh_TG,th_TH,en_TH,en_TJ,fr_TJ,es_TJ,zh_TJ,en_TM,fr_TM,es_TM,zh_TM,ar_TN,en_TN,fr_TN,es_TN,zh_TN,en_TO,tr_TR,en_TR,en_TT,fr_TT,es_TT,zh_TT,en_TV,fr_TV,es_TV,zh_TV,zh_TW,en_TW,en_TZ,fr_TZ,es_TZ,zh_TZ,en_UA,ru_UA,fr_UA,es_UA,zh_UA,en_UG,fr_UG,es_UG,zh_UG,en_US,fr_US,es_US,zh_US,es_UY,en_UY,fr_UY,zh_UY,en_VA,fr_VA,es_VA,zh_VA,en_VC,fr_VC,es_VC,zh_VC,es_VE,en_VE,fr_VE,zh_VE,en_VG,fr_VG,es_VG,zh_VG,en_VN,en_VU,fr_VU,es_VU,zh_VU,en_WF,fr_WF,es_WF,zh_WF,en_WS,ar_YE,en_YE,fr_YE,es_YE,zh_YE,en_YT,fr_YT,es_YT,zh_YT,en_ZA,fr_ZA,es_ZA,zh_ZA,en_ZM,fr_ZM,es_ZM,zh_ZM,en_ZW</smart_buttons_supported_locales> + <client_id>ATDZ9_ECFh-fudesZo4kz3fGTSO1pzuWCS4IjZMq4JKdRK7hQR3Rxyafx39H2fP363WtmlQNYXjUiAae</client_id> + <sandbox_client_id>AUZfbDQ_4m8ibp82qV9pi9wxGkGrdGILVYWbWaTWreW9mmTm6LjQorLZxpP7kjymXc7flRnepHBFSQWp</sandbox_client_id> </paypal_express> <paypal_express_bml> <model>Magento\Paypal\Model\Bml</model> diff --git a/app/code/Magento/Paypal/etc/di.xml b/app/code/Magento/Paypal/etc/di.xml index e148320fdaf17..9ea451a213af2 100644 --- a/app/code/Magento/Paypal/etc/di.xml +++ b/app/code/Magento/Paypal/etc/di.xml @@ -258,4 +258,13 @@ <type name="Magento\Sales\Model\Order\Payment"> <plugin name="paypal_transparent" type="Magento\Paypal\Plugin\TransparentOrderPayment"/> </type> + <type name="Magento\Paypal\Model\System\Config\Source\DisableFundingOptions"> + <arguments> + <argument name="disallowedFundingOptions" xsi:type="array"> + <item name="CREDIT" xsi:type="string">PayPal Credit</item> + <item name="CARD" xsi:type="string">PayPal Guest Checkout Credit Card Icons</item> + <item name="ELV" xsi:type="string">Elektronisches Lastschriftverfahren - German ELV</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Paypal/etc/frontend/di.xml b/app/code/Magento/Paypal/etc/frontend/di.xml index a728f5583a8d6..ac3fccff39f75 100644 --- a/app/code/Magento/Paypal/etc/frontend/di.xml +++ b/app/code/Magento/Paypal/etc/frontend/di.xml @@ -158,22 +158,20 @@ <item name="label" xsi:type="string">buynow</item> </item> </argument> - <argument name="allowedFunding" xsi:type="array"> - <item name="checkout" xsi:type="array"> - <item name="0" xsi:type="string">CREDIT</item> - <item name="1" xsi:type="string">ELV</item> - </item> - <item name="cart" xsi:type="array"> - <item name="0" xsi:type="string">CREDIT</item> - <item name="1" xsi:type="string">ELV</item> - </item> - <item name="mini_cart" xsi:type="array"> - <item name="0" xsi:type="string">CREDIT</item> - <item name="1" xsi:type="string">ELV</item> - </item> - <item name="product" xsi:type="array"> - <item name="0" xsi:type="string">CREDIT</item> - </item> + <argument name="disallowedFundingMap" xsi:type="array"> + <item name="CREDIT" xsi:type="string">credit</item> + <item name="CARD" xsi:type="string">card</item> + <item name="ELV" xsi:type="string">sepa</item> + </argument> + <argument name="unsupportedPaymentMethods" xsi:type="array"> + <item name="venmo" xsi:type="string">venmo</item> + <item name="bancontact" xsi:type="string">bancontact</item> + <item name="eps" xsi:type="string">eps</item> + <item name="giropay" xsi:type="string">giropay</item> + <item name="ideal" xsi:type="string">ideal</item> + <item name="mybank" xsi:type="string">mybank</item> + <item name="p24" xsi:type="string">p24</item> + <item name="sofort" xsi:type="string">sofort</item> </argument> <argument name="localeResolver" xsi:type="object">Magento\Paypal\Model\Express\LocaleResolver</argument> </arguments> diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index 912b99e2e6427..852bf39c57966 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -707,7 +707,6 @@ User,User "Label","Label" "The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR.","The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR." "Checkout","Checkout" -"Credit","Credit" "Pay","Pay" "Buy Now","Buy Now" "PayPal","PayPal" @@ -718,8 +717,6 @@ User,User "Vertical","Vertical" "Horizontal","Horizontal" "Size","Size" -"Medium","Medium" -"Large","Large" "Responsive","Responsive" "Shape","Shape" "Pill","Pill" diff --git a/app/code/Magento/Paypal/view/frontend/layout/default.xml b/app/code/Magento/Paypal/view/frontend/layout/default.xml index cb2126cec718f..aa47b43fa153a 100644 --- a/app/code/Magento/Paypal/view/frontend/layout/default.xml +++ b/app/code/Magento/Paypal/view/frontend/layout/default.xml @@ -9,8 +9,7 @@ <body> <referenceContainer name="after.body.start"> <block class="Magento\Paypal\Block\Express\InContext\Component" - name="paypal.express-in-context.component" - template="Magento_Paypal::express/in-context/component.phtml"> + name="paypal.express-in-context.component"> <arguments> <argument name="is_button_context" xsi:type="boolean">true</argument> </arguments> diff --git a/app/code/Magento/Paypal/view/frontend/requirejs-config.js b/app/code/Magento/Paypal/view/frontend/requirejs-config.js index 223ade43d86e5..1f318a717f8aa 100644 --- a/app/code/Magento/Paypal/view/frontend/requirejs-config.js +++ b/app/code/Magento/Paypal/view/frontend/requirejs-config.js @@ -10,13 +10,5 @@ var config = { 'Magento_Paypal/order-review': 'Magento_Paypal/js/order-review', paypalCheckout: 'Magento_Paypal/js/paypal-checkout' } - }, - paths: { - paypalInContextExpressCheckout: 'https://www.paypalobjects.com/api/checkout' - }, - shim: { - paypalInContextExpressCheckout: { - exports: 'paypal' - } } }; diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/component.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/component.phtml deleted file mode 100644 index c102b21830de8..0000000000000 --- a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/component.phtml +++ /dev/null @@ -1,38 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -use Magento\Paypal\Block\Express\InContext\Minicart\SmartButton; - -/** @var \Magento\Paypal\Block\Express\InContext\Component $block */ - -$configuration = [ - 'id' => SmartButton::PAYPAL_BUTTON_ID, - 'path' => $block->getUrl( - 'paypal/express/gettoken', - [ - '_secure' => $block->getRequest()->isSecure() - ] - ), - 'merchantId' => $block->getMerchantId(), - 'button' => $block->isButtonContext(), - 'clientConfig' => [ - 'locale' => $block->getLocale(), - 'environment' => $block->getEnvironment(), - 'button' => [ - SmartButton::PAYPAL_BUTTON_ID, - ] - ] -]; - -?> -<div style="display: none;" id="<?= /* @noEscape */ SmartButton::PAYPAL_BUTTON_ID ?>"></div> -<script type="text/x-magento-init"> - { - "*": { - "Magento_Paypal/js/in-context/express-checkout": - <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($configuration) ?> - } - } -</script> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml index 76d034f462a7a..ce07ba2293bb8 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// phpcs:disable Magento2.Templates.ThisInTemplate /** * @var \Magento\Paypal\Block\Express\InContext\SmartButton $block */ @@ -12,4 +13,5 @@ $widgetConfig = $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonE $widget['Magento_Paypal/js/in-context/product-express-checkout'] ); ?> -<div data-mage-init='{"Magento_Paypal/js/in-context/product-express-checkout":<?= /* @noEscape */ $widgetConfig ?>}'></div> +<div id ="paypal-smart-button" data-mage-init='{"Magento_Paypal/js/in-context/product-express-checkout" +:<?= /* @noEscape */ $widgetConfig ?>}'></div> diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js index 012a1f18f9ae5..719be7b2590a9 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js @@ -45,7 +45,6 @@ define([ /** @inheritdoc */ prepareClientConfig: function () { this._super(); - this.clientConfig.commit = false; return this.clientConfig; } diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js index ad7e86f2e99e0..9100236490848 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js @@ -2,122 +2,126 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +/* eslint-disable max-nested-callbacks */ define([ 'underscore', - 'paypalInContextExpressCheckout' -], function (_, paypal) { + 'jquery', + 'Magento_Paypal/js/in-context/paypal-sdk', + 'domReady!' +], function (_, $, paypalSdk) { 'use strict'; /** - * Returns array of allowed funding + * Triggers beforePayment action on PayPal buttons * - * @param {Object} config - * @return {Array} + * @param {Object} clientConfig + * @returns {Object} jQuery promise */ - function getFunding(config) { - return _.map(config, function (name) { - return paypal.FUNDING[name]; - }); - } - - return function (clientConfig, element) { - paypal.Button.render({ - env: clientConfig.environment, - client: clientConfig.client, - locale: clientConfig.locale, - funding: { - allowed: getFunding(clientConfig.allowedFunding), - disallowed: getFunding(clientConfig.disallowedFunding) - }, - style: clientConfig.styles, - - // Enable Pay Now checkout flow (optional) - commit: clientConfig.commit, + function performCreateOrder(clientConfig) { + var params = { + 'quote_id': clientConfig.quoteId, + 'customer_id': clientConfig.customerId || '', + 'form_key': clientConfig.formKey, + button: clientConfig.button + }; - /** - * Validate payment method - * - * @param {Object} actions - */ - validate: function (actions) { - clientConfig.rendererComponent.validate(actions); - }, - - /** - * Execute logic on Paypal button click - */ - onClick: function () { - clientConfig.rendererComponent.onClick(); - }, + return $.Deferred(function (deferred) { + clientConfig.rendererComponent.beforePayment(deferred.resolve, deferred.reject).then(function () { + $.post(clientConfig.getTokenUrl, params).done(function (res) { + clientConfig.rendererComponent.afterPayment(res, deferred.resolve, deferred.reject); + }).fail(function (jqXHR, textStatus, err) { + clientConfig.rendererComponent.catchPayment(err, deferred.resolve, deferred.reject); + }); + }); + }).promise(); + } - /** - * Set up a payment - * - * @return {*} - */ - payment: function () { - var params = { - 'quote_id': clientConfig.quoteId, - 'customer_id': clientConfig.customerId || '', - 'form_key': clientConfig.formKey, - button: clientConfig.button - }; + /** + * Triggers beforeOnAuthorize action on PayPal buttons + * @param {Object} clientConfig + * @param {Object} data + * @param {Object} actions + * @returns {Object} jQuery promise + */ + function performOnApprove(clientConfig, data, actions) { + var params = { + paymentToken: data.orderID, + payerId: data.payerID, + quoteId: clientConfig.quoteId || '', + customerId: clientConfig.customerId || '', + 'form_key': clientConfig.formKey + }; - return new paypal.Promise(function (resolve, reject) { - clientConfig.rendererComponent.beforePayment(resolve, reject).then(function () { - paypal.request.post(clientConfig.getTokenUrl, params).then(function (res) { - return clientConfig.rendererComponent.afterPayment(res, resolve, reject); - }).catch(function (err) { - return clientConfig.rendererComponent.catchPayment(err, resolve, reject); - }); + return $.Deferred(function (deferred) { + clientConfig.rendererComponent.beforeOnAuthorize(deferred.resolve, deferred.reject, actions) + .then(function () { + $.post(clientConfig.onAuthorizeUrl, params).done(function (res) { + clientConfig.rendererComponent + .afterOnAuthorize(res, deferred.resolve, deferred.reject, actions); + }).fail(function (jqXHR, textStatus, err) { + clientConfig.rendererComponent.catchOnAuthorize(err, deferred.resolve, deferred.reject); }); }); - }, + }).promise(); + } - /** - * Execute the payment - * - * @param {Object} data - * @param {Object} actions - * @return {*} - */ - onAuthorize: function (data, actions) { - var params = { - paymentToken: data.paymentToken, - payerId: data.payerID, - quoteId: clientConfig.quoteId || '', - customerId: clientConfig.customerId || '', - 'form_key': clientConfig.formKey - }; + return function (clientConfig, element) { + paypalSdk(clientConfig.sdkUrl).done(function (paypal) { + paypal.Buttons({ + style: clientConfig.styles, - return new paypal.Promise(function (resolve, reject) { - clientConfig.rendererComponent.beforeOnAuthorize(resolve, reject, actions).then(function () { - paypal.request.post(clientConfig.onAuthorizeUrl, params).then(function (res) { - clientConfig.rendererComponent.afterOnAuthorize(res, resolve, reject, actions); - }).catch(function (err) { - return clientConfig.rendererComponent.catchOnAuthorize(err, resolve, reject); - }); - }); - }); + /** + * onInit is called when the button first renders + * @param {Object} data + * @param {Object} actions + */ + onInit: function (data, actions) { + clientConfig.rendererComponent.validate(actions); + }, + + /** + * Triggers beforePayment action on PayPal buttons + * @returns {Object} jQuery promise + */ + createOrder: function () { + return performCreateOrder(clientConfig); + }, - }, + /** + * Triggers beforeOnAuthorize action on PayPal buttons + * @param {Object} data + * @param {Object} actions + */ + onApprove: function (data, actions) { + performOnApprove(clientConfig, data, actions); + }, - /** - * Process cancel action - * - * @param {Object} data - * @param {Object} actions - */ - onCancel: function (data, actions) { - clientConfig.rendererComponent.onCancel(data, actions); - }, + /** + * Execute logic on Paypal button click + */ + onClick: function () { + clientConfig.rendererComponent.validate(); + clientConfig.rendererComponent.onClick(); + }, - /** - * Process errors - */ - onError: function (err) { - clientConfig.rendererComponent.onError(err); - } - }, element); + /** + * Process cancel action + * @param {Object} data + * @param {Object} actions + */ + onCancel: function (data, actions) { + clientConfig.rendererComponent.onCancel(data, actions); + }, + + /** + * Process errors + * + * @param {Error} err + */ + onError: function (err) { + clientConfig.rendererComponent.onError(err); + } + }).render(element); + }); }; }); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js index 905f860fe2651..c5ec5d28ddd06 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js @@ -7,8 +7,9 @@ define([ 'mage/translate', 'Magento_Customer/js/customer-data', 'Magento_Paypal/js/in-context/express-checkout-smart-buttons', + 'Magento_Ui/js/modal/alert', 'mage/cookies' -], function ($, $t, customerData, checkoutSmartButtons) { +], function ($, $t, customerData, checkoutSmartButtons, alert) { 'use strict'; return { @@ -59,12 +60,11 @@ define([ * @return {*} */ afterPayment: function (res, resolve, reject) { + if (res.success) { return resolve(res.token); } - this.addError(res['error_message']); - return reject(new Error(res['error_message'])); }, @@ -76,7 +76,7 @@ define([ * @param {Function} reject */ catchPayment: function (err, resolve, reject) { - this.addError(this.paymentActionError); + this.addAlert(this.paymentActionError); reject(err); }, @@ -90,6 +90,9 @@ define([ * @return {jQuery.Deferred} */ beforeOnAuthorize: function (resolve, reject, actions) { //eslint-disable-line no-unused-vars + //display loading widget. + $('body').trigger('processStart'); + return $.Deferred().resolve(); }, @@ -104,14 +107,14 @@ define([ * @return {*} */ afterOnAuthorize: function (res, resolve, reject, actions) { + $('body').trigger('processStop'); + if (res.success) { resolve(); - return actions.redirect(window, res.redirectUrl); + return actions.redirect(res.redirectUrl); } - this.addError(res['error_message']); - return reject(new Error(res['error_message'])); }, @@ -123,7 +126,8 @@ define([ * @param {Function} reject */ catchOnAuthorize: function (err, resolve, reject) { - this.addError(this.paymentActionError); + $('body').trigger('processStop'); + this.addAlert(this.paymentActionError); reject(err); }, @@ -134,7 +138,8 @@ define([ * @param {Object} actions */ onCancel: function (data, actions) { - actions.redirect(window, this.clientConfig.onCancelUrl); + $('body').trigger('processStop'); + actions.redirect(this.clientConfig.onCancelUrl); }, /** @@ -163,6 +168,17 @@ define([ }); }, + /** + * Add alert message + * + * @param {String} message + */ + addAlert: function (message) { + alert({ + content: message + }); + }, + /** * @returns {String} */ @@ -176,8 +192,6 @@ define([ * @return {Object} */ prepareClientConfig: function () { - this.clientConfig.client = {}; - this.clientConfig.client[this.clientConfig.environment] = this.clientConfig.merchantId; this.clientConfig.rendererComponent = this; this.clientConfig.formKey = $.mage.cookies.get('form_key'); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout.js deleted file mode 100644 index 80c1dfc44977b..0000000000000 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -define([ - 'underscore', - 'jquery', - 'uiComponent', - 'paypalInContextExpressCheckout', - 'Magento_Customer/js/customer-data', - 'domReady!' -], function (_, $, Component, paypalExpressCheckout, customerData) { - 'use strict'; - - return Component.extend({ - - defaults: { - clientConfig: { - - checkoutInited: false, - - /** - * @param {Object} event - */ - click: function (event) { - $('body').trigger('processStart'); - - event.preventDefault(); - - if (!this.clientConfig.checkoutInited) { - paypalExpressCheckout.checkout.initXO(); - this.clientConfig.checkoutInited = true; - } else { - paypalExpressCheckout.checkout.closeFlow(); - } - - $.getJSON(this.path, { - button: 1 - }).done(function (response) { - var message = response && response.message; - - if (message) { - customerData.set('messages', { - messages: [message] - }); - } - - if (response && response.url) { - paypalExpressCheckout.checkout.startFlow(response.url); - - return; - } - - paypalExpressCheckout.checkout.closeFlow(); - }).fail(function () { - paypalExpressCheckout.checkout.closeFlow(); - }).always(function () { - $('body').trigger('processStop'); - customerData.invalidate(['cart']); - }); - } - } - }, - - /** - * @returns {Object} - */ - initialize: function () { - this._super(); - - return this.initClient(); - }, - - /** - * @returns {Object} - */ - initClient: function () { - _.each(this.clientConfig, function (fn, name) { - if (typeof fn === 'function') { - this.clientConfig[name] = fn.bind(this); - } - }, this); - - paypalExpressCheckout.checkout.setup(this.merchantId, this.clientConfig); - - return this; - } - }); -}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/paypal-sdk.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/paypal-sdk.js new file mode 100644 index 0000000000000..bc2928fb623ba --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/paypal-sdk.js @@ -0,0 +1,37 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery' +], function ($) { + 'use strict'; + + var dfd = $.Deferred(); + + /** + * Loads the PayPal SDK object + * @param {String} paypalUrl - the url of the PayPal SDK + */ + return function loadPaypalScript(paypalUrl) { + //configuration for loaded PayPal script + require.config({ + paths: { + paypalSdk: paypalUrl + }, + shim: { + paypalSdk: { + exports: 'paypal' + } + } + }); + + if (dfd.state() !== 'resolved') { + require(['paypalSdk'], function (paypalObject) { + dfd.resolve(paypalObject); + }); + } + + return dfd.promise(); + }; +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js index b2be5fe2b3d2b..9469e168cdc6b 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js @@ -15,7 +15,8 @@ define([ defaults: { productFormSelector: '#product_addtocart_form', declinePayment: false, - formInvalid: false + formInvalid: false, + productAddedToCart: false }, /** @inheritdoc */ @@ -45,9 +46,10 @@ define([ onClick: function () { var $form = $(this.productFormSelector); - if (!this.declinePayment) { + if (!this.declinePayment && !this.productAddedToCart) { $form.submit(); this.formInvalid = !$form.validation('isValid'); + this.productAddedToCart = true; } }, @@ -74,14 +76,51 @@ define([ return promise; }, + /** + * After payment execute + * + * @param {Object} res + * @param {Function} resolve + * @param {Function} reject + * + * @return {*} + */ + afterPayment: function (res, resolve, reject) { + if (res.success) { + return resolve(res.token); + } + + this.addAlert(res['error_message']); + + return reject(new Error(res['error_message'])); + }, + /** @inheritdoc */ prepareClientConfig: function () { this._super(); this.clientConfig.quoteId = ''; this.clientConfig.customerId = ''; - this.clientConfig.commit = false; return this.clientConfig; + }, + + /** @inheritdoc */ + onError: function (err) { + this.productAddedToCart = false; + this._super(err); + }, + + /** @inheritdoc */ + onCancel: function (data, actions) { + this.productAddedToCart = false; + this._super(data, actions); + }, + + /** @inheritdoc */ + afterOnAuthorize: function (res, resolve, reject, actions) { + this.productAddedToCart = false; + + return this._super(res, resolve, reject, actions); } }); }); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js index 5c509238fe5cc..206355f5a9839 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js @@ -75,9 +75,7 @@ define([ this._super(); this.clientConfig.quoteId = window.checkoutConfig.quoteData['entity_id']; this.clientConfig.customerId = window.customerData.id; - this.clientConfig.merchantId = this.merchantId; this.clientConfig.button = 0; - this.clientConfig.commit = true; return this.clientConfig; }, @@ -99,6 +97,47 @@ define([ messageList.addErrorMessage({ message: message }); + }, + + /** + * After payment execute + * + * @param {Object} res + * @param {Function} resolve + * @param {Function} reject + * + * @return {*} + */ + afterPayment: function (res, resolve, reject) { + if (res.success) { + return resolve(res.token); + } + + this.addError(res['error_message']); + + return reject(new Error(res['error_message'])); + }, + + /** + * After onAuthorize execute + * + * @param {Object} res + * @param {Function} resolve + * @param {Function} reject + * @param {Object} actions + * + * @return {*} + */ + afterOnAuthorize: function (res, resolve, reject, actions) { + if (res.success) { + resolve(); + + return actions.redirect(res.redirectUrl); + } + + this.addError(res['error_message']); + + return reject(new Error(res['error_message'])); } }); }); diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoDownloadableProductTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoDownloadableProductTest.xml index e753716da2469..0c32fc9fbd240 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoDownloadableProductTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoDownloadableProductTest.xml @@ -20,6 +20,9 @@ <group value="catalog"/> <group value="downloadable"/> <group value="productVideo"/> + <skip> + <issueId value="MC-33903"/> + </skip> </annotations> <before> <magentoCLI command="downloadable:domains:add" arguments="static.magento.com" before="setYoutubeApiKeyConfig" stepKey="addDownloadableDomain"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoGroupedProductTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoGroupedProductTest.xml index 4ab8a5be0938c..3f324d8fd3d9f 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoGroupedProductTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoGroupedProductTest.xml @@ -20,6 +20,9 @@ <group value="catalog"/> <group value="groupedProduct"/> <group value="productVideo"/> + <skip> + <issueId value="MC-33903"/> + </skip> </annotations> <before> <createData entity="SimpleProduct2" after="setYoutubeApiKeyConfig" stepKey="createFirstSimpleProduct"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoSimpleProductTest.xml index 025adfd5d06fa..87486178a859b 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddRemoveDefaultVideoSimpleProductTest.xml @@ -19,6 +19,9 @@ <testCaseId value="MC-206"/> <group value="catalog"/> <group value="productVideo"/> + <skip> + <issueId value="MC-33903"/> + </skip> </annotations> <before> <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setYoutubeApiKeyConfig"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml index 4febea66a76f4..3032f5208dd59 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml @@ -18,6 +18,9 @@ <testCaseId value="MAGETWO-95254"/> <useCaseId value="MAGETWO-91707"/> <group value="productVideo"/> + <skip> + <issueId value="MC-33903"/> + </skip> </annotations> <before> diff --git a/app/code/Magento/Review/Model/ResourceModel/Review.php b/app/code/Magento/Review/Model/ResourceModel/Review.php index 2e08838e4c885..b017415418cf3 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review.php @@ -361,12 +361,12 @@ protected function aggregateReviewSummary($object, $ratingSummaryObject) ); $select = $connection->select()->from($this->_aggregateTable) ->where('entity_pk_value = :pk_value') - ->where('entity_type = :entity_type') - ->where('store_id = :store_id'); + ->where('store_id = :store_id') + ->where('entity_type = :entity_type'); $bind = [ ':pk_value' => $object->getEntityPkValue(), - ':entity_type' => $object->getEntityId(), ':store_id' => $ratingSummaryObject->getStoreId(), + ':entity_type' => $object->getEntityId(), ]; $oldData = $connection->fetchRow($select, $bind); $data = new \Magento\Framework\DataObject(); @@ -424,6 +424,7 @@ protected function _loadVotedRatingIds($reviewId) /** * Aggregate this review's ratings. + * * Useful, when changing the review. * * @param int[] $ratingIds @@ -471,6 +472,7 @@ public function getEntityIdByCode($entityCode) /** * Delete reviews by product id. + * * Better to call this method in transaction, because operation performed on two separated tables * * @param int $productId diff --git a/app/code/Magento/Review/Setup/Patch/Schema/AddUniqueConstraintToReviewEntitySummary.php b/app/code/Magento/Review/Setup/Patch/Schema/AddUniqueConstraintToReviewEntitySummary.php new file mode 100644 index 0000000000000..8139e3d3629db --- /dev/null +++ b/app/code/Magento/Review/Setup/Patch/Schema/AddUniqueConstraintToReviewEntitySummary.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Setup\Patch\Schema; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Setup\Patch\SchemaPatchInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * Schema patch to add unique constraint (entity_pk_value, store_id, entity_type) to review_entity_summary. + */ +class AddUniqueConstraintToReviewEntitySummary implements SchemaPatchInterface +{ + /** + * Table name to modify + */ + private const TABLE = 'review_entity_summary'; + /** + * Columns to be unique + */ + private const COLUMNS = [ + 'entity_pk_value', + 'store_id', + 'entity_type', + ]; + + /** + * @var SchemaSetupInterface + */ + private $setup; + + /** + * @param SchemaSetupInterface $setup + */ + public function __construct( + SchemaSetupInterface $setup + ) { + $this->setup = $setup; + } + + /** + * @inheritDoc + */ + public function apply() + { + $this->setup->startSetup(); + $this->addUniqueKey(); + $this->setup->endSetup(); + return $this; + } + + /** + * Add unique constraint (entity_pk_value, store_id, entity_type) to review_entity_summary. + * + * Remove duplicate entries if any and retries until the unique key is successfully added. + */ + private function addUniqueKey(): void + { + $this->setup->getConnection()->addIndex( + $this->setup->getTable(self::TABLE), + $this->setup->getIdxName( + self::TABLE, + self::COLUMNS, + AdapterInterface::INDEX_TYPE_UNIQUE + ), + self::COLUMNS, + AdapterInterface::INDEX_TYPE_UNIQUE + ); + } + + /** + * @inheritDoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php index 6a62ba7fbf0fd..e6a209b541198 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php @@ -6,7 +6,9 @@ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; +use Magento\Customer\Api\GroupManagementInterface; use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Pricing\PriceCurrencyInterface; @@ -50,6 +52,7 @@ class Account extends AbstractForm * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter * @param array $data + * @param GroupManagementInterface|null $groupManagement * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -62,11 +65,13 @@ public function __construct( \Magento\Customer\Model\Metadata\FormFactory $metadataFormFactory, \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, - array $data = [] + array $data = [], + ?GroupManagementInterface $groupManagement = null ) { $this->_metadataFormFactory = $metadataFormFactory; $this->customerRepository = $customerRepository; $this->_extensibleDataObjectConverter = $extensibleDataObjectConverter; + $this->groupManagement = $groupManagement ?: ObjectManager::getInstance()->get(GroupManagementInterface::class); parent::__construct( $context, $sessionQuote, @@ -78,6 +83,13 @@ public function __construct( ); } + /** + * Group Management + * + * @var GroupManagementInterface + */ + private $groupManagement; + /** * Return Header CSS Class * @@ -163,6 +175,7 @@ public function getFormValues() { try { $customer = $this->customerRepository->getById($this->getCustomerId()); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (\Exception $e) { $data = []; } @@ -179,6 +192,10 @@ public function getFormValues() } } + if (array_key_exists('group_id', $data) && empty($data['group_id'])) { + $data['group_id'] = $this->groupManagement->getDefaultGroup($this->getQuote()->getStoreId())->getId(); + } + if ($this->getQuote()->getCustomerEmail()) { $data['email'] = $this->getQuote()->getCustomerEmail(); } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index d1b3e67815098..67a533ea88550 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -1645,6 +1645,7 @@ public function setAccountData($accountData) $data, \Magento\Customer\Api\Data\CustomerInterface::class ); + $customer->setStoreId($this->getQuote()->getStoreId()); $this->getQuote()->updateCustomerData($customer); $data = []; diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertAuthorizeButtonOnOrderPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertAuthorizeButtonOnOrderPageActionGroup.xml new file mode 100644 index 0000000000000..c105a89509ef4 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertAuthorizeButtonOnOrderPageActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertAuthorizeButtonOnOrderPageActionGroup"> + <annotations> + <description>Assert that order waiting for authorization.</description> + </annotations> + <seeElement selector="{{AdminOrderDetailsMainActionsSection.authorize}}" stepKey="seeOrderWaitingForAuthorize"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertCurrencyInOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertCurrencyInOrderActionGroup.xml new file mode 100644 index 0000000000000..385799fb86dac --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertCurrencyInOrderActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertCurrencyInOrderActionGroup"> + <annotations> + <description>Admin assert different currencies</description> + </annotations> + <arguments> + <argument name="rate" type="string" defaultValue="USD / USD rate"/> + </arguments> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="{{rate}}" stepKey="seeRate"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertNoAuthorizeButtonOnOrderPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertNoAuthorizeButtonOnOrderPageActionGroup.xml new file mode 100644 index 0000000000000..e88e0cfac30b2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertNoAuthorizeButtonOnOrderPageActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertNoAuthorizeButtonOnOrderPageActionGroup"> + <annotations> + <description>Assert that order not waiting for authorization.</description> + </annotations> + <dontSeeElement selector="{{AdminOrderDetailsMainActionsSection.authorize}}" stepKey="dontSeeOrderWaitingForAuthorize"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml index ecdf9e34de55a..9ce111663720d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml @@ -25,5 +25,6 @@ <element name="invoiceBtn" type="button" selector="//button[@title='Invoice']"/> <element name="shipBtn" type="button" selector="//button[@title='Ship']"/> <element name="shipmentsTab" type="button" selector="#sales_order_view_tabs_order_shipments"/> + <element name="authorize" type="button" selector="#order_authorize"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml index 0a4445f338960..0ff5080bd8df2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml @@ -24,6 +24,7 @@ <!--Create product--> <createData entity="SimpleProduct2" stepKey="createSimpleProductApi"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> <!--Create the catalog price rule --> <createData entity="CatalogRuleToPercent" stepKey="createCatalogRule"/> <!--Create order via API--> @@ -43,9 +44,7 @@ <after> <deleteData createDataKey="createSimpleProductApi" stepKey="deleteSimpleProductApi"/> - <actionGroup ref="RemoveCatalogPriceRuleActionGroup" stepKey="deleteCatalogPriceRule"> - <argument name="ruleName" value="{{CatalogRuleToPercent.name}}"/> - </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php index c642cdeb2d91e..4cda04f197f4c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php @@ -30,13 +30,14 @@ use Magento\Sales\Model\Order\Item as OrderItem; use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ItemCollection; use Magento\Store\Api\Data\StoreInterface; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class CreateTest extends \PHPUnit\Framework\TestCase +class CreateTest extends TestCase { const CUSTOMER_ID = 1; @@ -46,12 +47,12 @@ class CreateTest extends \PHPUnit\Framework\TestCase private $adminOrderCreate; /** - * @var CartRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CartRepositoryInterface|MockObject */ private $quoteRepository; /** - * @var QuoteFactory|\PHPUnit_Framework_MockObject_MockObject + * @var QuoteFactory|MockObject */ private $quoteFactory; @@ -111,7 +112,7 @@ protected function setUp() ->setMethods(['getForCustomer']) ->getMockForAbstractClass(); - $this->sessionQuote = $this->getMockBuilder(\Magento\Backend\Model\Session\Quote::class) + $this->sessionQuote = $this->getMockBuilder(SessionQuote::class) ->disableOriginalConstructor() ->setMethods( [ @@ -227,6 +228,7 @@ public function testSetAccountData() 'customer_tax_class_id' => $taxClassId ] ); + $quote->method('getStoreId')->willReturn(1); $this->dataObjectHelper->method('populateWithArray') ->with( $customer, @@ -245,6 +247,10 @@ public function testSetAccountData() $this->groupRepository->method('getById') ->willReturn($customerGroup); + $customer->expects($this->once()) + ->method('setStoreId') + ->with(1); + $this->adminOrderCreate->setAccountData(['group_id' => 1]); } diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/action/select-payment-method-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/action/select-payment-method-mixin.js index 0b8eba270b030..1ad0f0b9d70ff 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/js/action/select-payment-method-mixin.js +++ b/app/code/Magento/SalesRule/view/frontend/web/js/action/select-payment-method-mixin.js @@ -19,6 +19,10 @@ define([ originalSelectPaymentMethodAction(paymentMethod); + if (paymentMethod === null) { + return; + } + $.when( setPaymentInformationExtended( messageContainer, diff --git a/app/code/Magento/Tax/Block/Sales/Order/Tax.php b/app/code/Magento/Tax/Block/Sales/Order/Tax.php index 9c992185b4e1d..a08a07260e8c9 100644 --- a/app/code/Magento/Tax/Block/Sales/Order/Tax.php +++ b/app/code/Magento/Tax/Block/Sales/Order/Tax.php @@ -82,7 +82,7 @@ public function initTotals() $this->_order = $parent->getOrder(); $this->_source = $parent->getSource(); - $store = $this->getStore(); + $store = $this->_order->getStore(); $allowTax = $this->_source->getTaxAmount() > 0 || $this->_config->displaySalesZeroTax($store); $grandTotal = (double)$this->_source->getGrandTotal(); if (!$grandTotal || $allowTax && !$this->_config->displaySalesTaxWithGrandTotal($store)) { @@ -131,7 +131,7 @@ public function getStore() */ protected function _initSubtotal() { - $store = $this->getStore(); + $store = $this->_order->getStore(); $parent = $this->getParentBlock(); $subtotal = $parent->getTotal('subtotal'); if (!$subtotal) { @@ -213,7 +213,7 @@ protected function _initSubtotal() */ protected function _initShipping() { - $store = $this->getStore(); + $store = $this->_order->getStore(); /** @var \Magento\Sales\Block\Order\Totals $parent */ $parent = $this->getParentBlock(); $shipping = $parent->getTotal('shipping'); @@ -290,7 +290,7 @@ protected function _initDiscount() */ protected function _initGrandTotal() { - $store = $this->getStore(); + $store = $this->_order->getStore(); $parent = $this->getParentBlock(); $grandototal = $parent->getTotal('grand_total'); if (!$grandototal || !(double)$this->_source->getGrandTotal()) { diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/StorefrontAssertOrderReviewSummaryWithTaxActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/StorefrontAssertOrderReviewSummaryWithTaxActionGroup.xml new file mode 100644 index 0000000000000..a6d16cf49d81f --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/StorefrontAssertOrderReviewSummaryWithTaxActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertOrderReviewSummaryWithTaxActionGroup" extends="VerifyCheckoutPaymentOrderSummaryActionGroup"> + <annotations> + <description>Validates that the provided Subtotal, Shipping Total and Summary Total prices are present and correct on the Storefront Checkout page.</description> + </annotations> + <arguments> + <argument name="orderSummaryTax" type="string"/> + </arguments> + <see selector="{{StorefrontOrderReviewSection.taxCost}}" userInput="{{orderSummaryTax}}" stepKey="seeCorrectTax" after="seeCorrectOrderTotal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/StorefrontOrderReviewSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/StorefrontOrderReviewSection.xml new file mode 100644 index 0000000000000..af9721d784862 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/StorefrontOrderReviewSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontOrderReviewSection"> + <element name="taxCost" type="text" selector="//tr[@class='totals-tax']//span[@class='price']"/> + </section> +</sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index e0f8408a1c30c..fcee31c0bd80c 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -21,5 +21,6 @@ <element name="dataGridEmpty" type="block" selector=".data-grid-tr-no-data td"/> <element name="rowTemplateStrict" type="block" selector="//tbody/tr[td[*[text()[normalize-space()='{{text}}']]]]" parameterized="true" /> <element name="rowTemplate" type="block" selector="//tbody/tr[td[*[contains(.,normalize-space('{{text}}'))]]]" parameterized="true" timeout="30" /> + <element name="firstNotEmptyRow" type="block" selector="table.data-grid tbody tr[data-role=row]:not(.data-grid-tr-no-data):nth-of-type(1)" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml similarity index 99% rename from app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml rename to app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml index b9f726aec668e..4e46ed8e4fc79 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest"> + <test name="AdminCheckUrlRewritesMultipleStoreviewsProductImportTest"> <annotations> <features value="Url Rewrite"/> <stories value="Url Rewrites for Multiple Storeviews"/> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteGeneratedForMultipleStoreviewsDuringProductImportWithConfigTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml similarity index 99% rename from app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteGeneratedForMultipleStoreviewsDuringProductImportWithConfigTurnedOffTest.xml rename to app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml index f9003ce8c0897..1d604ef7648dc 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteGeneratedForMultipleStoreviewsDuringProductImportWithConfigTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminUrlRewriteGeneratedForMultipleStoreviewsDuringProductImportWithConfigTurnedOffTest"> + <test name="AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest"> <annotations> <features value="Url Rewrite"/> <stories value="Url Rewrites for Multiple Storeviews"/> diff --git a/app/code/Magento/Wishlist/Controller/Index/Cart.php b/app/code/Magento/Wishlist/Controller/Index/Cart.php index 870c4231f97c9..023d0756bae6f 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Cart.php @@ -6,93 +6,128 @@ namespace Magento\Wishlist\Controller\Index; +use Magento\Catalog\Helper\Product; use Magento\Catalog\Model\Product\Exception as ProductException; +use Magento\Checkout\Model\Cart as CheckoutCart; +use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Framework\App\Action; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Data\Form\FormKey\Validator; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\Cookie\PublicCookieMetadata; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Wishlist\Controller\AbstractIndex; +use Magento\Wishlist\Controller\WishlistProviderInterface; +use Magento\Wishlist\Helper\Data; +use Magento\Wishlist\Model\Item\OptionFactory; +use Magento\Wishlist\Model\ItemFactory; +use Magento\Wishlist\Model\LocaleQuantityProcessor; +use Magento\Wishlist\Model\ResourceModel\Item\Option\Collection; /** * Add wishlist item to shopping cart and remove from wishlist controller. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Cart extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface +class Cart extends AbstractIndex implements Action\HttpPostActionInterface { /** - * @var \Magento\Wishlist\Controller\WishlistProviderInterface + * @var WishlistProviderInterface */ protected $wishlistProvider; /** - * @var \Magento\Wishlist\Model\LocaleQuantityProcessor + * @var LocaleQuantityProcessor */ protected $quantityProcessor; /** - * @var \Magento\Wishlist\Model\ItemFactory + * @var ItemFactory */ protected $itemFactory; /** - * @var \Magento\Checkout\Model\Cart + * @var CheckoutCart */ protected $cart; /** - * @var \Magento\Checkout\Helper\Cart + * @var CartHelper */ protected $cartHelper; /** - * @var \Magento\Wishlist\Model\Item\OptionFactory + * @var OptionFactory */ private $optionFactory; /** - * @var \Magento\Catalog\Helper\Product + * @var Product */ protected $productHelper; /** - * @var \Magento\Framework\Escaper + * @var Escaper */ protected $escaper; /** - * @var \Magento\Wishlist\Helper\Data + * @var Data */ protected $helper; /** - * @var \Magento\Framework\Data\Form\FormKey\Validator + * @var Validator */ protected $formKeyValidator; + /** + * @var CookieManagerInterface + */ + private $cookieManager; + + /** + * @var CookieMetadataFactory + */ + private $cookieMetadataFactory; + /** * @param Action\Context $context - * @param \Magento\Wishlist\Controller\WishlistProviderInterface $wishlistProvider - * @param \Magento\Wishlist\Model\LocaleQuantityProcessor $quantityProcessor - * @param \Magento\Wishlist\Model\ItemFactory $itemFactory - * @param \Magento\Checkout\Model\Cart $cart - * @param \Magento\Wishlist\Model\Item\OptionFactory $optionFactory - * @param \Magento\Catalog\Helper\Product $productHelper - * @param \Magento\Framework\Escaper $escaper - * @param \Magento\Wishlist\Helper\Data $helper - * @param \Magento\Checkout\Helper\Cart $cartHelper - * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator + * @param WishlistProviderInterface $wishlistProvider + * @param LocaleQuantityProcessor $quantityProcessor + * @param ItemFactory $itemFactory + * @param CheckoutCart $cart + * @param OptionFactory $optionFactory + * @param Product $productHelper + * @param Escaper $escaper + * @param Data $helper + * @param CartHelper $cartHelper + * @param Validator $formKeyValidator + * @param CookieManagerInterface|null $cookieManager + * @param CookieMetadataFactory|null $cookieMetadataFactory + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Action\Context $context, - \Magento\Wishlist\Controller\WishlistProviderInterface $wishlistProvider, - \Magento\Wishlist\Model\LocaleQuantityProcessor $quantityProcessor, - \Magento\Wishlist\Model\ItemFactory $itemFactory, - \Magento\Checkout\Model\Cart $cart, - \Magento\Wishlist\Model\Item\OptionFactory $optionFactory, - \Magento\Catalog\Helper\Product $productHelper, - \Magento\Framework\Escaper $escaper, - \Magento\Wishlist\Helper\Data $helper, - \Magento\Checkout\Helper\Cart $cartHelper, - \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator + WishlistProviderInterface $wishlistProvider, + LocaleQuantityProcessor $quantityProcessor, + ItemFactory $itemFactory, + CheckoutCart $cart, + OptionFactory $optionFactory, + Product $productHelper, + Escaper $escaper, + Data $helper, + CartHelper $cartHelper, + Validator $formKeyValidator, + ?CookieManagerInterface $cookieManager = null, + ?CookieMetadataFactory $cookieMetadataFactory = null ) { $this->wishlistProvider = $wishlistProvider; $this->quantityProcessor = $quantityProcessor; @@ -104,6 +139,9 @@ public function __construct( $this->helper = $helper; $this->cartHelper = $cartHelper; $this->formKeyValidator = $formKeyValidator; + $this->cookieManager = $cookieManager ?: ObjectManager::getInstance()->get(CookieManagerInterface::class); + $this->cookieMetadataFactory = $cookieMetadataFactory ?: + ObjectManager::getInstance()->get(CookieMetadataFactory::class); parent::__construct($context); } @@ -113,14 +151,14 @@ public function __construct( * If Product has required options - item removed from wishlist and redirect * to product view page with message about needed defined required options * - * @return \Magento\Framework\Controller\ResultInterface + * @return ResultInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function execute() { - /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); if (!$this->formKeyValidator->validate($this->getRequest())) { return $resultRedirect->setPath('*/*/'); @@ -167,7 +205,7 @@ public function execute() ); try { - /** @var \Magento\Wishlist\Model\ResourceModel\Item\Option\Collection $options */ + /** @var Collection $options */ $options = $this->optionFactory->create()->getCollection()->addItemFilter([$itemId]); $item->setOptions($options->getOptionsByItem($itemId)); @@ -187,6 +225,27 @@ public function execute() $this->escaper->escapeHtml($item->getProduct()->getName()) ); $this->messageManager->addSuccessMessage($message); + + $productsToAdd = [ + [ + 'sku' => $item->getProduct()->getSku(), + 'name' => $item->getProduct()->getName(), + 'price' => $item->getProduct()->getFinalPrice(), + 'qty' => $item->getQty(), + ] + ]; + + /** @var PublicCookieMetadata $publicCookieMetadata */ + $publicCookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata() + ->setDuration(3600) + ->setPath('/') + ->setHttpOnly(false); + + $this->cookieManager->setPublicCookie( + 'add_to_cart', + \rawurlencode(\json_encode($productsToAdd)), + $publicCookieMetadata + ); } if ($this->cartHelper->getShouldRedirectToCart()) { @@ -199,7 +258,7 @@ public function execute() } } catch (ProductException $e) { $this->messageManager->addErrorMessage(__('This product(s) is out of stock.')); - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $this->messageManager->addNoticeMessage($e->getMessage()); $redirectUrl = $configureUrl; } catch (\Exception $e) { @@ -209,7 +268,7 @@ public function execute() $this->helper->calculate(); if ($this->getRequest()->isAjax()) { - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + /** @var Json $resultJson */ $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); $resultJson->setData(['backUrl' => $redirectUrl]); return $resultJson; diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php index 093f0d8d6b8f8..e4b2e735dde24 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php @@ -5,15 +5,45 @@ */ namespace Magento\Wishlist\Test\Unit\Controller\Index; -use Magento\Wishlist\Controller\Index\Cart; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Exception as ProductException; +use Magento\Checkout\Model\Cart as CheckoutCart; +use Magento\Checkout\Helper\Cart as CartHelper; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Data\Form\FormKey\Validator; +use Magento\Framework\DataObject; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\Quote\Model\Quote; +use Magento\Wishlist\Controller\Index\Cart; +use Magento\Wishlist\Controller\WishlistProviderInterface; +use Magento\Wishlist\Helper\Data; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\Item\Option; +use Magento\Wishlist\Model\Item\OptionFactory; +use Magento\Wishlist\Model\ItemFactory; +use Magento\Wishlist\Model\LocaleQuantityProcessor; +use Magento\Wishlist\Model\ResourceModel\Item\Option\Collection; +use Magento\Wishlist\Model\Wishlist; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CartTest extends \PHPUnit\Framework\TestCase +class CartTest extends TestCase { /** * @var Cart @@ -21,178 +51,188 @@ class CartTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\App\Action\Context|\PHPUnit\Framework\MockObject\MockObject + * @var Context|MockObject */ protected $contextMock; /** - * @var \Magento\Wishlist\Controller\WishlistProviderInterface|\PHPUnit\Framework\MockObject\MockObject + * @var WishlistProviderInterface|MockObject */ protected $wishlistProviderMock; /** - * @var \Magento\Wishlist\Model\LocaleQuantityProcessor|\PHPUnit\Framework\MockObject\MockObject + * @var LocaleQuantityProcessor|MockObject */ protected $quantityProcessorMock; /** - * @var \Magento\Wishlist\Model\ItemFactory|\PHPUnit\Framework\MockObject\MockObject + * @var ItemFactory|MockObject */ protected $itemFactoryMock; /** - * @var \Magento\Checkout\Model\Cart|\PHPUnit\Framework\MockObject\MockObject + * @var CheckoutCart|MockObject */ protected $checkoutCartMock; /** - * @var \Magento\Wishlist\Model\Item\OptionFactory|\PHPUnit\Framework\MockObject\MockObject + * @var OptionFactory|MockObject */ protected $optionFactoryMock; /** - * @var \Magento\Catalog\Helper\Product|\PHPUnit\Framework\MockObject\MockObject + * @var ProductHelper|MockObject */ protected $productHelperMock; /** - * @var \Magento\Framework\Escaper|\PHPUnit\Framework\MockObject\MockObject + * @var Escaper|MockObject */ protected $escaperMock; /** - * @var \Magento\Wishlist\Helper\Data|\PHPUnit\Framework\MockObject\MockObject + * @var Data|MockObject */ protected $helperMock; /** - * @var \Magento\Framework\App\RequestInterface|\PHPUnit\Framework\MockObject\MockObject + * @var RequestInterface|MockObject */ protected $requestMock; /** - * @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit\Framework\MockObject\MockObject + * @var RedirectInterface|MockObject */ protected $redirectMock; /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit\Framework\MockObject\MockObject + * @var ObjectManagerInterface|MockObject */ protected $objectManagerMock; /** - * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit\Framework\MockObject\MockObject + * @var ManagerInterface|MockObject */ protected $messageManagerMock; /** - * @var \Magento\Framework\UrlInterface|\PHPUnit\Framework\MockObject\MockObject + * @var UrlInterface|MockObject */ protected $urlMock; /** - * @var \Magento\Checkout\Helper\Cart|\PHPUnit\Framework\MockObject\MockObject + * @var CartHelper|MockObject */ protected $cartHelperMock; /** - * @var \Magento\Framework\Controller\ResultFactory|\PHPUnit\Framework\MockObject\MockObject + * @var ResultFactory|MockObject */ protected $resultFactoryMock; /** - * @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit\Framework\MockObject\MockObject + * @var Redirect|MockObject */ protected $resultRedirectMock; /** - * @var \Magento\Framework\Controller\Result\Json|\PHPUnit\Framework\MockObject\MockObject + * @var Json|MockObject */ protected $resultJsonMock; /** - * @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit\Framework\MockObject\MockObject + * @var Validator|MockObject */ protected $formKeyValidator; + /** + * @var CookieManagerInterface|MockObject + */ + private $cookieManagerMock; + + /** + * @var CookieMetadataFactory|MockObject + */ + private $cookieMetadataFactoryMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { $this->wishlistProviderMock = $this->getMockBuilder( - \Magento\Wishlist\Controller\WishlistProviderInterface::class + WishlistProviderInterface::class )->disableOriginalConstructor() ->setMethods(['getWishlist']) ->getMockForAbstractClass(); - $this->quantityProcessorMock = $this->getMockBuilder(\Magento\Wishlist\Model\LocaleQuantityProcessor::class) + $this->quantityProcessorMock = $this->getMockBuilder(LocaleQuantityProcessor::class) ->disableOriginalConstructor() ->getMock(); - $this->itemFactoryMock = $this->getMockBuilder(\Magento\Wishlist\Model\ItemFactory::class) + $this->itemFactoryMock = $this->getMockBuilder(ItemFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->checkoutCartMock = $this->getMockBuilder(\Magento\Checkout\Model\Cart::class) + $this->checkoutCartMock = $this->getMockBuilder(CheckoutCart::class) ->disableOriginalConstructor() ->setMethods(['save', 'getQuote', 'getShouldRedirectToCart', 'getCartUrl']) ->getMock(); - $this->optionFactoryMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item\OptionFactory::class) + $this->optionFactoryMock = $this->getMockBuilder(OptionFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->productHelperMock = $this->getMockBuilder(\Magento\Catalog\Helper\Product::class) + $this->productHelperMock = $this->getMockBuilder(ProductHelper::class) ->disableOriginalConstructor() ->getMock(); - $this->escaperMock = $this->getMockBuilder(\Magento\Framework\Escaper::class) + $this->escaperMock = $this->getMockBuilder(Escaper::class) ->disableOriginalConstructor() ->getMock(); - $this->helperMock = $this->getMockBuilder(\Magento\Wishlist\Helper\Data::class) + $this->helperMock = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->getMock(); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) + $this->requestMock = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->setMethods(['getParams', 'getParam', 'isAjax', 'getPostValue']) ->getMockForAbstractClass(); - $this->redirectMock = $this->getMockBuilder(\Magento\Framework\App\Response\RedirectInterface::class) + $this->redirectMock = $this->getMockBuilder(RedirectInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) + $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) ->disableOriginalConstructor() ->setMethods(['addSuccessMessage']) ->getMockForAbstractClass(); - $this->urlMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) + $this->urlMock = $this->getMockBuilder(UrlInterface::class) ->disableOriginalConstructor() ->setMethods(['getUrl']) ->getMockForAbstractClass(); - $this->cartHelperMock = $this->getMockBuilder(\Magento\Checkout\Helper\Cart::class) + $this->cartHelperMock = $this->getMockBuilder(CartHelper::class) ->disableOriginalConstructor() ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) + $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->resultRedirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) + $this->resultRedirectMock = $this->getMockBuilder(Redirect::class) ->disableOriginalConstructor() ->getMock(); - $this->resultJsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $this->resultJsonMock = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); - $this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) + $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); $this->contextMock->expects($this->any()) @@ -222,10 +262,26 @@ protected function setUp() ] ); - $this->formKeyValidator = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) + $this->formKeyValidator = $this->getMockBuilder(Validator::class) ->disableOriginalConstructor() ->getMock(); + $this->cookieManagerMock = $this->createMock(CookieManagerInterface::class); + + $this->cookieMetadataFactoryMock = $this->getMockBuilder(CookieMetadataFactory::class) + ->disableOriginalConstructor() + ->setMethods(['createPublicCookieMetadata', 'setDuration', 'setPath', 'setHttpOnly']) + ->getMock(); + $this->cookieMetadataFactoryMock->expects($this->any()) + ->method('createPublicCookieMetadata') + ->willReturnSelf(); + $this->cookieMetadataFactoryMock->expects($this->any()) + ->method('setDuration') + ->willReturnSelf(); + $this->cookieMetadataFactoryMock->expects($this->any()) + ->method('setPath') + ->willReturnSelf(); + $this->model = new Cart( $this->contextMock, $this->wishlistProviderMock, @@ -237,7 +293,9 @@ protected function setUp() $this->escaperMock, $this->helperMock, $this->cartHelperMock, - $this->formKeyValidator + $this->formKeyValidator, + $this->cookieManagerMock, + $this->cookieMetadataFactoryMock ); } @@ -265,7 +323,7 @@ public function testExecuteWithNoItem() ->with($this->requestMock) ->willReturn(true); - $itemMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $itemMock = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->getMock(); @@ -302,7 +360,7 @@ public function testExecuteWithNoWishlist() ->with($this->requestMock) ->willReturn(true); - $itemMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $itemMock = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->setMethods(['load', 'getId', 'getWishlistId']) ->getMock(); @@ -390,7 +448,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) $params = ['item' => $itemId, 'qty' => $qty]; $refererUrl = 'referer_url'; - $itemMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $itemMock = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->setMethods( [ @@ -427,7 +485,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->method('getWishlistId') ->willReturn($wishlistId); - $wishlistMock = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $wishlistMock = $this->getMockBuilder(Wishlist::class) ->disableOriginalConstructor() ->getMock(); @@ -465,7 +523,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->with('*/*/configure/', ['id' => $itemId, 'product_id' => $productId]) ->willReturn($configureUrl); - $optionMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item\Option::class) + $optionMock = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->getMock(); @@ -473,7 +531,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->method('create') ->willReturn($optionMock); - $optionsMock = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\Option\Collection::class) + $optionsMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $optionMock->expects($this->once()) @@ -501,7 +559,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->method('isAjax') ->willReturn($isAjax); - $buyRequestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) + $buyRequestMock = $this->getMockBuilder(DataObject::class) ->disableOriginalConstructor() ->getMock(); @@ -527,7 +585,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->method('save') ->willReturnSelf(); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['getHasError', 'collectTotals']) ->getMock(); @@ -548,15 +606,15 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->method('getHasError') ->willReturn(false); - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + $productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->getMock(); - $itemMock->expects($this->once()) + $itemMock->expects($this->atLeastOnce()) ->method('getProduct') ->willReturn($productMock); - $productMock->expects($this->once()) + $productMock->expects($this->atLeastOnce()) ->method('getName') ->willReturn($productName); @@ -567,7 +625,7 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) $this->messageManagerMock->expects($this->once()) ->method('addSuccessMessage') - ->with('You added ' . $productName . ' to your shopping cart.', null) + ->with('You added ' . $productName . ' to your shopping cart.', null) ->willReturnSelf(); $this->cartHelperMock->expects($this->once()) @@ -604,7 +662,7 @@ public function testExecuteWithoutQuantityArrayAndOutOfStock() ->with($this->requestMock) ->willReturn(true); - $itemMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $itemMock = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->setMethods( [ @@ -641,7 +699,7 @@ public function testExecuteWithoutQuantityArrayAndOutOfStock() ->method('getWishlistId') ->willReturn($wishlistId); - $wishlistMock = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $wishlistMock = $this->getMockBuilder(Wishlist::class) ->disableOriginalConstructor() ->getMock(); @@ -679,7 +737,7 @@ public function testExecuteWithoutQuantityArrayAndOutOfStock() ->with('*/*/configure/', ['id' => $itemId, 'product_id' => $productId]) ->willReturn($configureUrl); - $optionMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item\Option::class) + $optionMock = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->getMock(); @@ -687,7 +745,7 @@ public function testExecuteWithoutQuantityArrayAndOutOfStock() ->method('create') ->willReturn($optionMock); - $optionsMock = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\Option\Collection::class) + $optionsMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $optionMock->expects($this->once()) @@ -712,7 +770,7 @@ public function testExecuteWithoutQuantityArrayAndOutOfStock() ->method('getParams') ->willReturn($params); - $buyRequestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) + $buyRequestMock = $this->getMockBuilder(DataObject::class) ->disableOriginalConstructor() ->getMock(); @@ -770,7 +828,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() ->with($this->requestMock) ->willReturn(true); - $itemMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $itemMock = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->setMethods( [ @@ -807,7 +865,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() ->method('getWishlistId') ->willReturn($wishlistId); - $wishlistMock = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $wishlistMock = $this->getMockBuilder(Wishlist::class) ->disableOriginalConstructor() ->getMock(); @@ -845,7 +903,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() ->with('*/*/configure/', ['id' => $itemId, 'product_id' => $productId]) ->willReturn($configureUrl); - $optionMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item\Option::class) + $optionMock = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->getMock(); @@ -853,7 +911,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() ->method('create') ->willReturn($optionMock); - $optionsMock = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\Option\Collection::class) + $optionsMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $optionMock->expects($this->once()) @@ -878,7 +936,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() ->method('getParams') ->willReturn($params); - $buyRequestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) + $buyRequestMock = $this->getMockBuilder(DataObject::class) ->disableOriginalConstructor() ->getMock(); @@ -898,7 +956,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() $itemMock->expects($this->once()) ->method('addToCart') ->with($this->checkoutCartMock, true) - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('message'))); + ->willThrowException(new LocalizedException(__('message'))); $this->messageManagerMock->expects($this->once()) ->method('addNoticeMessage') @@ -937,7 +995,7 @@ public function testExecuteWithEditQuantity() ->with($this->requestMock) ->willReturn(true); - $itemMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item::class) + $itemMock = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() ->setMethods( [ @@ -974,7 +1032,7 @@ public function testExecuteWithEditQuantity() ->method('getWishlistId') ->willReturn($wishlistId); - $wishlistMock = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $wishlistMock = $this->getMockBuilder(Wishlist::class) ->disableOriginalConstructor() ->getMock(); @@ -1017,7 +1075,7 @@ public function testExecuteWithEditQuantity() ->with('*/*/configure/', ['id' => $itemId, 'product_id' => $productId]) ->willReturn($configureUrl); - $optionMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item\Option::class) + $optionMock = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->getMock(); @@ -1025,7 +1083,7 @@ public function testExecuteWithEditQuantity() ->method('create') ->willReturn($optionMock); - $optionsMock = $this->getMockBuilder(\Magento\Wishlist\Model\ResourceModel\Item\Option\Collection::class) + $optionsMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); $optionMock->expects($this->once()) @@ -1050,7 +1108,7 @@ public function testExecuteWithEditQuantity() ->method('getParams') ->willReturn($params); - $buyRequestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) + $buyRequestMock = $this->getMockBuilder(DataObject::class) ->disableOriginalConstructor() ->getMock(); @@ -1070,7 +1128,7 @@ public function testExecuteWithEditQuantity() $itemMock->expects($this->once()) ->method('addToCart') ->with($this->checkoutCartMock, true) - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('message'))); + ->willThrowException(new LocalizedException(__('message'))); $this->messageManagerMock->expects($this->once()) ->method('addNoticeMessage') diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less index 84d9cb1530893..2b82330c0049e 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less @@ -52,6 +52,7 @@ } p { + min-height: 72px; text-align: center; } } 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 9ed7e3a1ba839..3ca7e7d161064 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 @@ -246,6 +246,11 @@ margin-bottom: @indent__xs; } + .message { + margin-bottom: 0; + margin-top: 10px; + } + .product { > .product-item-photo, > .product-image-container { 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 a97cc041b1c42..f8311a4fd0afd 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 @@ -264,6 +264,11 @@ margin-bottom: @indent__xs; } + .message { + margin-bottom: 0; + margin-top: 10px; + } + .product-item-name { font-weight: @font-weight__regular; margin: 0 0 @indent__s; diff --git a/composer.json b/composer.json index 2a19802e4fb2a..6d63a5328cec2 100644 --- a/composer.json +++ b/composer.json @@ -193,6 +193,13 @@ "magento/module-instant-purchase": "*", "magento/module-integration": "*", "magento/module-layered-navigation": "*", + "magento/module-login-as-customer": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-login-as-customer-log": "*", + "magento/module-login-as-customer-page-cache": "*", + "magento/module-login-as-customer-sales": "*", + "magento/module-login-as-customer-ui": "*", + "magento/module-login-as-customer-webapi": "*", "magento/module-media-content": "*", "magento/module-media-content-api": "*", "magento/module-media-content-catalog": "*", diff --git a/composer.lock b/composer.lock index f1095dc95b150..8a41c7b609e70 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b7ee4a27d76ea68e295d5025c986854d", + "content-hash": "02c03e264c9040e0e229bc22fbdcec61", "packages": [ { "name": "braintree/braintree_php", @@ -253,16 +253,6 @@ "ssl", "tls" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-04-08T08:27:21+00:00" }, { @@ -4497,20 +4487,6 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-27T16:54:36+00:00" }, { @@ -4581,20 +4557,6 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-27T16:54:36+00:00" }, { @@ -4752,20 +4714,6 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-27T16:54:36+00:00" }, { @@ -9195,20 +9143,6 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-28T10:15:50+00:00" }, { @@ -9407,20 +9341,6 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-29T19:12:22+00:00" }, { @@ -9592,20 +9512,6 @@ "configuration", "options" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-27T16:54:36+00:00" }, { @@ -9715,20 +9621,6 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-27T16:54:36+00:00" }, { diff --git a/dev/tests/api-functional/testsuite/Magento/LoginAsCustomerWebapi/Api/LoginAsCustomerWebapiCreateCustomerAccessTokenTest.php b/dev/tests/api-functional/testsuite/Magento/LoginAsCustomerWebapi/Api/LoginAsCustomerWebapiCreateCustomerAccessTokenTest.php new file mode 100644 index 0000000000000..caf639a678052 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/LoginAsCustomerWebapi/Api/LoginAsCustomerWebapiCreateCustomerAccessTokenTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerWebapi\Api; + +use Magento\Framework\Webapi\Rest\Request; +use Magento\Integration\Model\Oauth\Token as TokenModel; +use Magento\Integration\Model\ResourceModel\Oauth\Token\Collection; +use Magento\Integration\Model\ResourceModel\Oauth\Token\CollectionFactory; +use Magento\TestFramework\Authentication\OauthHelper; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Api-functional test for \Magento\LoginAsCustomerWebapi\Api\LoginAsCustomerWebapiCreateCustomerAccessTokenInterface. + */ +class LoginAsCustomerWebapiCreateCustomerAccessTokenTest extends WebapiAbstract +{ + const RESOURCE_PATH = "/V1/login-as-customer/token"; + + /** + * @var Collection + */ + private $tokenCollection; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->_markTestAsRestOnly(); + $tokenCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + $this->tokenCollection = $tokenCollectionFactory->create(); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture login_as_customer/general/enabled 1 + */ + public function testCreateCustomerAccessToken() + { + // 'Magento_LoginAsCustomerWebapi::login_token' resource required for access. + OauthHelper::clearApiAccessCredentials(); + OauthHelper::getApiAccessCredentials(['Magento_LoginAsCustomerWebapi::login_token']); + try { + $customerId = 1; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + ]; + $requestData = ['customerId' => $customerId]; + $response = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertToken($response, $customerId); + } catch (\Exception $e) { + OauthHelper::clearApiAccessCredentials(); + throw $e; + } + // Restore credentials + OauthHelper::clearApiAccessCredentials(); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture login_as_customer/general/enabled 0 + */ + public function testCreateCustomerAccessTokenLoginModuleDisabled() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Service is disabled.'); + + $customerId = 1; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + ]; + $requestData = ['customerId' => $customerId]; + $this->_webApiCall($serviceInfo, $requestData); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture login_as_customer/general/enabled 1 + */ + public function testCreateCustomerAccessTokenLoginNoAccess() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The consumer isn\'t authorized to access %resources.'); + + // 'Magento_LoginAsCustomerWebapi::login_token' resource required for access. + OauthHelper::clearApiAccessCredentials(); + OauthHelper::getApiAccessCredentials([]); + try { + $customerId = 1; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + ]; + $requestData = ['customerId' => $customerId]; + $this->_webApiCall($serviceInfo, $requestData); + } catch (\Exception $e) { + OauthHelper::clearApiAccessCredentials(); + throw $e; + } + // Restore credentials + OauthHelper::clearApiAccessCredentials(); + } + + /** + * Make sure provided token is valid and belongs to the specified user. + * + * @param string $response + * @param int $customerId + */ + private function assertToken(string $response, int $customerId) + { + $this->tokenCollection->addFilterByCustomerId($customerId); + $isTokenCorrect = false; + foreach ($this->tokenCollection->getItems() as $item) { + /** @var $item TokenModel */ + if ($item->getToken() == $response) { + $isTokenCorrect = true; + } + } + + $this->assertTrue($isTokenCorrect); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/OutputTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/OutputTest.php index 5a4dadedb1c9e..ad646c1384c4a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/OutputTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/OutputTest.php @@ -122,7 +122,7 @@ protected function _testAttribute($method, $entityCode, $expectedResult) try { $this->assertEquals( $expectedResult, - $this->_helper->{$method}(uniqid(), "<p>line1</p>\nline2", $attributeName) + $this->_helper->{$method}(uniqid(), __("<p>line1</p>\nline2"), $attributeName) ); $attribute->setIsHtmlAllowedOnFront($isHtml)->setIsWysiwygEnabled($isWysiwyg); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multiwebsite_different_description.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multiwebsite_different_description.php new file mode 100644 index 0000000000000..b77a53621d0bf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multiwebsite_different_description.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductFactory; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../Store/_files/website.php'; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->create(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->create(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$defaultWebsiteId, $websiteId]) + ->setName('Simple Product on two websites') + ->setSku('simple-on-two-websites-different-description') + ->setPrice(10) + ->setDescription('<p>Product base description</p>'); + +$productRepository->save($product); +$product = $productRepository->get('simple-on-two-websites-different-description'); +$product->setDescription('<p>Product second description</p>'); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multiwebsite_different_description_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multiwebsite_different_description_rollback.php new file mode 100644 index 0000000000000..934d380362fcf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_multiwebsite_different_description_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('simple-on-two-websites-different-description'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/website_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php index 70aa7c07ed536..b16a312488131 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_12345.php @@ -21,7 +21,7 @@ /** @var ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager() - ->create(ProductRepositoryInterface::class); + ->get(ProductRepositoryInterface::class); /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); @@ -105,7 +105,7 @@ $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); try { - $productToDelete = $productRepository->getById(11); + $productToDelete = $productRepository->getById(111); $productRepository->delete($productToDelete); /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php index 10476eb01f306..f55907c17e7bb 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php @@ -28,6 +28,11 @@ class LoginPostTest extends AbstractController /** @var EncoderInterface */ private $urlEncoder; + /** + * @var Url + */ + private $customerUrl; + /** * @inheritdoc */ @@ -37,6 +42,7 @@ protected function setUp() $this->session = $this->_objectManager->get(Session::class); $this->urlEncoder = $this->_objectManager->get(EncoderInterface::class); + $this->customerUrl = $this->_objectManager->get(Url::class); } /** @@ -107,13 +113,16 @@ public function missingParametersDataProvider(): array */ public function testLoginWithUnconfirmedPassword(): void { - $this->markTestSkipped('Blocked by MC-31370.'); $email = 'unconfirmedcustomer@example.com'; $this->prepareRequest($email, 'Qwert12345'); $this->dispatch('customer/account/loginPost'); $this->assertEquals($email, $this->session->getUsername()); + $message = __( + 'This account is not confirmed. <a href="%1">Click here</a> to resend confirmation email.', + $this->customerUrl->getEmailConfirmationUrl($this->session->getUsername()) + ); $this->assertSessionMessages( - $this->equalTo([(string)__('This account is not confirmed. Click here to resend confirmation email.')]), + $this->equalTo([(string)$message]), MessageInterface::TYPE_ERROR ); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index 4ff16189df1ba..3721d189a5544 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -13,6 +13,8 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\ExpiredException; use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Url as UrlBuilder; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; /** @@ -651,6 +653,34 @@ public function testGetDefaultAddressesForNonExistentAddress() $this->assertNull($this->accountManagement->getDefaultShippingAddress($customerId)); } + /** + * Test reset password for customer on second website when shared account is enabled + * + * When customer from second website initiate reset password on first website + * global scope should not be reinited to customer scope + * + * @magentoConfigFixture current_store customer/account_share/scope 0 + * @magentoDataFixture Magento/Customer/_files/customer_for_second_website.php + */ + public function testInitiatePasswordResetForCustomerOnSecondWebsite() + { + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $store = $storeManager->getStore(); + + $this->accountManagement->initiatePasswordReset( + 'customer@example.com', + AccountManagement::EMAIL_RESET, + $storeManager->getWebsite()->getId() + ); + + $this->assertEquals($store->getId(), $storeManager->getStore()->getId()); + $urlBuilder = $this->objectManager->get(UrlBuilder::class); + // to init scope if it has not inited yet + $urlBuilder->setScope($urlBuilder->getData('scope')); + $scope = $urlBuilder->getData('scope'); + $this->assertEquals($store->getId(), $scope->getId()); + } + /** * Set Rp data to Customer in fixture * diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_group_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_group_rollback.php index 20a1f4623a1f7..62750e9ec7de1 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_group_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_group_rollback.php @@ -7,6 +7,7 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Registry; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Exception\NoSuchEntityException; @@ -21,6 +22,11 @@ ->create(); $groups = $groupRepository->getList($searchCriteria) ->getItems(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); foreach ($groups as $group) { try { $groupRepository->delete($group); @@ -28,3 +34,5 @@ //Group already removed } } +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php index ccf867c51c3c8..5b8b9e6d1f3e3 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php @@ -74,6 +74,7 @@ public function testDefaultFiltersAreUsed() public function testParametersAreParsed() { $filter = $this->objectManager->create(Template::class); + $filter->setStrictMode(false); $processor = $this->createWithProcessorsAndFilters( ['mydir' => $this->objectManager->create(MyDirProcessor::class)], diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php index de09b87b04e4a..f99a34a077524 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/TemplateTest.php @@ -155,6 +155,7 @@ public function testIfDirective() public function testNonDataObjectVariableParsing() { + $previous = $this->templateFilter->setStrictMode(false); $this->templateFilter->setVariables( [ 'address' => new class { @@ -168,11 +169,33 @@ public function format($type) $template = '{{var address.format(\'html\')}}'; $expected = '<foo>html</foo>'; - self::assertEquals($expected, $this->templateFilter->filter($template)); + try { + self::assertEquals($expected, $this->templateFilter->filter($template)); + } finally { + $this->templateFilter->setStrictMode($previous); + } + } + + public function testStrictModeByDefault() + { + $this->templateFilter->setVariables( + [ + 'address' => new class { + public function format() + { + throw new \Exception('Should not run'); + } + } + ] + ); + + $template = '{{var address.format(\'html\')}}'; + self::assertEquals('', $this->templateFilter->filter($template)); } public function testComplexVariableArguments() { + $previous = $this->templateFilter->setStrictMode(false); $this->templateFilter->setVariables( [ 'address' => new class { @@ -187,11 +210,16 @@ public function format($a, $b, $c) $template = '{{var address.format($arg1,\'bar\',[param1:baz])}}'; $expected = 'foo bar baz'; - self::assertEquals($expected, $this->templateFilter->filter($template)); + try { + self::assertEquals($expected, $this->templateFilter->filter($template)); + } finally { + $this->templateFilter->setStrictMode($previous); + } } public function testComplexVariableGetterArguments() { + $previous = $this->templateFilter->setStrictMode(false); $this->templateFilter->setVariables( [ 'address' => new class extends DataObject { @@ -206,7 +234,11 @@ public function getFoo($a, $b, $c) $template = '{{var address.getFoo($arg1,\'bar\',[param1:baz])}}'; $expected = 'foo bar baz'; - self::assertEquals($expected, $this->templateFilter->filter($template)); + try { + self::assertEquals($expected, $this->templateFilter->filter($template)); + } finally { + $this->templateFilter->setStrictMode($previous); + } } public function testNonDataObjectRendersBlankInStrictMode() diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContentTest.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContentTest.php new file mode 100644 index 0000000000000..b0343c846ee88 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContentTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetEntityContentsInterface + */ +class GetEntityContentTest extends TestCase +{ + private const CONTENT_TYPE = 'catalog_product'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getContent = $objectManager->get(GetEntityContentsInterface::class); + $this->contentIdentityFactory = $objectManager->get(ContentIdentityInterfaceFactory::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Test for get content from product in different store views + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testProduct(): void + { + $product = $this->productRepository->get('simple'); + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => 'description', + self::ENTITY_ID => (string) $product->getEntityId(), + ] + ); + $this->assertEquals( + ['Description with <b>html tag</b>'], + $this->getContent->execute($contentIdentity) + ); + } + + /** + * Test for get content from product in different store views + * + * @magentoDataFixture Magento/Catalog/_files/product_multiwebsite_different_description.php + */ + public function testProductTwoWebsites(): void + { + $product = $this->productRepository->get('simple-on-two-websites-different-description'); + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => 'description', + self::ENTITY_ID => (string) $product->getEntityId(), + ] + ); + $this->assertEquals( + [ + '<p>Product base description</p>', + '<p>Product second description</p>' + ], + $this->getContent->execute($contentIdentity) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/category_with_asset.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/category_with_asset.php new file mode 100644 index 0000000000000..d890ea52dfad7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/category_with_asset.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Category; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Category $category */ +$category = Bootstrap::getObjectManager()->create(Category::class); +$category->isObjectNew(true); +$category->setId( + 28767 +)->setCreatedAt( + '2014-06-23 09:50:07' +)->setName( + 'Category 1' +)->setDescription( + 'content {{media url="testDirectory/path.jpg"}} content' +)->setParentId( + 2 +)->setPath( + '1/2/333' +)->setLevel( + 2 +)->setAvailableSortBy( + ['position', 'name'] +)->setDefaultSortBy( + 'name' +)->setIsActive( + true +)->setPosition( + 1 +)->save(); diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/category_with_asset_rollback.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/category_with_asset_rollback.php new file mode 100644 index 0000000000000..a5010883e120c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/category_with_asset_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $category \Magento\Catalog\Model\Category */ +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->load(28767); +if ($category->getId()) { + $category->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php new file mode 100644 index 0000000000000..2a1177661572a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +Bootstrap::getInstance()->reinitialize(); + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setId(1567) + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple_with_asset') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription('content {{media url="testDirectory/path.jpg"}} content') + ->setTaxClassId(0) + ->setDescription('content {{media url="testDirectory/path.jpg"}} content') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset_rollback.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset_rollback.php new file mode 100644 index 0000000000000..8f45ecac0d6f1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->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); +try { + $product = $productRepository->get('simple_with_asset', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/block_with_asset.php b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/block_with_asset.php new file mode 100644 index 0000000000000..eb2adf0c0a348 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/block_with_asset.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Cms\Model\Block; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var $block Block */ +$block = Bootstrap::getObjectManager()->create(Block::class); +$block->setTitle( + 'CMS Block Title' +)->setIdentifier( + 'fixture_block_with_asset' +)->setContent( + 'content {{media url="testDirectory/path.jpg"}} content' +)->setIsActive( + 1 +)->setStores( + [ + Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId() + ] +)->save(); diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/block_with_asset_rollback.php b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/block_with_asset_rollback.php new file mode 100644 index 0000000000000..68069953f22f7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/block_with_asset_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var BlockRepositoryInterface $blockRepository */ +$blockRepository = $objectManager->get(BlockRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, 'fixture_block_with_asset') + ->create(); +$result = $blockRepository->getList($searchCriteria); + +/** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + * In that case there is "if" which checks that "fixture_block_with_asset" still exists in database. + */ +foreach ($result->getItems() as $item) { + $blockRepository->delete($item); +} diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/page_with_asset.php b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/page_with_asset.php new file mode 100644 index 0000000000000..8ac3f0aea693a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/page_with_asset.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $page \Magento\Cms\Model\Page */ +$page = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Cms\Model\Page::class); +$page->setTitle('Cms Page 100') + ->setIdentifier('fixture_page_with_asset') + ->setStores([0]) + ->setIsActive(1) + ->setContent('content {{media url="testDirectory/path.jpg"}} content') + ->setContentHeading('<h2>Cms Page 100 Title</h2>') + ->setMetaTitle('Cms Meta title for page100') + ->setMetaKeywords('Cms Meta Keywords for page100') + ->setMetaDescription('Cms Meta Description for page100') + ->setPageLayout('1column') + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/page_with_asset_rollback.php b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/page_with_asset_rollback.php new file mode 100644 index 0000000000000..ebe24d4f05983 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCms/_files/page_with_asset_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var PageRepositoryInterface $pageRepository */ +$pageRepository = $objectManager->get(PageRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, 'fixture_page_with_asset') + ->create(); +$result = $pageRepository->getList($searchCriteria); + +/** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + * In that case there is "if" which checks that "fixture_page_with_asset" still exists in database. + */ +foreach ($result->getItems() as $item) { + $pageRepository->delete($item); +} diff --git a/dev/tests/integration/testsuite/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculatorTest.php new file mode 100644 index 0000000000000..4a35f6fe552f4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MsrpGroupedProduct/Pricing/MsrpPriceCalculatorTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MsrpGroupedProduct\Pricing; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test group product minimum advertised price model + */ +class MsrpPriceCalculatorTest extends TestCase +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** + * @var MsrpPriceCalculator + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->model = $objectManager->get(MsrpPriceCalculator::class); + } + + /** + * Test grouped product minimum advertised price + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/GroupedProduct/_files/product_grouped.php + * @dataProvider getMsrpPriceValueDataProvider + * @param float|null $simpleProductPriceMsrp + * @param float|null $virtualProductMsrp + * @param float|null $expectedMsrp + */ + public function testGetMsrpPriceValue( + ?float $simpleProductPriceMsrp, + ?float $virtualProductMsrp, + ?float $expectedMsrp + ): void { + $this->setProductMinimumAdvertisedPrice('simple', $simpleProductPriceMsrp); + $this->setProductMinimumAdvertisedPrice('virtual-product', $virtualProductMsrp); + $groupedProduct = $this->getProduct('grouped-product'); + $this->assertEquals($expectedMsrp, $this->model->getMsrpPriceValue($groupedProduct)); + } + + /** + * Set product minimum advertised price by sku + * + * @param string $sku + * @param float|null $msrp + */ + private function setProductMinimumAdvertisedPrice(string $sku, ?float $msrp): void + { + $product = $this->getProduct($sku); + $product->setMsrp($msrp); + $this->productRepository->save($product); + } + + /** + * Get product by sku + * + * @param string $sku + * @return ProductInterface + */ + private function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get($sku, false, null, true); + } + + /** + * @return array + */ + public function getMsrpPriceValueDataProvider(): array + { + return [ + [ + 12.0, + 8.0, + 8.0 + ], + [ + 12.0, + null, + 12.0 + ], + [ + null, + null, + 0.0 + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php index 6b2806f147dea..e2f86dd17bfdd 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Quote\Model; use Magento\Customer\Api\AddressRepositoryInterface; @@ -12,11 +11,19 @@ use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Customer\Model\Vat; +use Magento\Customer\Observer\AfterAddressSaveObserver; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\EstimateAddressInterface; +use Magento\Quote\Api\GuestShippingMethodManagementInterface; use Magento\Quote\Api\ShippingMethodManagementInterface; +use Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver; +use Magento\Quote\Observer\Frontend\Quote\Address\VatValidator; use Magento\Store\Model\ScopeInterface; use Magento\Tax\Api\Data\TaxClassInterface; use Magento\Tax\Api\TaxClassRepositoryInterface; @@ -24,6 +31,7 @@ use Magento\Tax\Model\Config as TaxConfig; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; /** * Test for shipping methods management @@ -31,7 +39,7 @@ * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ShippingMethodManagementTest extends \PHPUnit\Framework\TestCase +class ShippingMethodManagementTest extends TestCase { /** @var ObjectManagerInterface $objectManager */ private $objectManager; @@ -56,14 +64,14 @@ protected function setUp() * @magentoDataFixture Magento/SalesRule/_files/cart_rule_100_percent_off.php * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php * @return void - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function testRateAppliedToShipping(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ - $quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $objectManager->create(CartRepositoryInterface::class); $customerQuote = $quoteRepository->getForCustomer(1); $this->assertEquals(0, $customerQuote->getBaseGrandTotal()); } @@ -80,17 +88,17 @@ public function testRateAppliedToShipping(): void */ public function testTableRateFreeShipping() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $objectManager->get(\Magento\Quote\Model\Quote::class); + $objectManager = Bootstrap::getObjectManager(); + /** @var Quote $quote */ + $quote = $objectManager->get(Quote::class); $quote->load('test01', 'reserved_order_id'); $cartId = $quote->getId(); if (!$cartId) { $this->fail('quote fixture failed'); } - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id @@ -103,10 +111,10 @@ public function testTableRateFreeShipping() 'region_id' => null ] ]; - /** @var \Magento\Quote\Api\Data\EstimateAddressInterface $address */ - $address = $objectManager->create(\Magento\Quote\Api\Data\EstimateAddressInterface::class, $data); - /** @var \Magento\Quote\Api\GuestShippingMethodManagementInterface $shippingEstimation */ - $shippingEstimation = $objectManager->get(\Magento\Quote\Api\GuestShippingMethodManagementInterface::class); + /** @var EstimateAddressInterface $address */ + $address = $objectManager->create(EstimateAddressInterface::class, $data); + /** @var GuestShippingMethodManagementInterface $shippingEstimation */ + $shippingEstimation = $objectManager->get(GuestShippingMethodManagementInterface::class); $result = $shippingEstimation->estimateByAddress($cartId, $address); $this->assertNotEmpty($result); $expectedResult = [ @@ -134,25 +142,25 @@ public function testTableRateFreeShipping() */ public function testTableRateWithCartRuleForFreeShipping() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); $quote = $this->getQuote('tableRate'); $cartId = $quote->getId(); if (!$cartId) { $this->fail('quote fixture failed'); } - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id $cartId = $quoteIdMask->getMaskedId(); - $addressFactory = $this->objectManager->get(\Magento\Quote\Api\Data\AddressInterfaceFactory::class); + $addressFactory = $this->objectManager->get(AddressInterfaceFactory::class); /** @var \Magento\Quote\Api\Data\AddressInterface $address */ $address = $addressFactory->create(); $address->setCountryId('US'); - /** @var \Magento\Quote\Api\GuestShippingMethodManagementInterface $shippingEstimation */ - $shippingEstimation = $objectManager->get(\Magento\Quote\Api\GuestShippingMethodManagementInterface::class); + /** @var GuestShippingMethodManagementInterface $shippingEstimation */ + $shippingEstimation = $objectManager->get(GuestShippingMethodManagementInterface::class); $result = $shippingEstimation->estimateByExtendedAddress($cartId, $address); $this->assertCount(1, $result); $rate = reset($result); @@ -234,17 +242,17 @@ public function testEstimateByAddress() */ private function executeTestFlow($flatRateAmount, $tableRateAmount) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $objectManager->get(\Magento\Quote\Model\Quote::class); + $objectManager = Bootstrap::getObjectManager(); + /** @var Quote $quote */ + $quote = $objectManager->get(Quote::class); $quote->load('test01', 'reserved_order_id'); $cartId = $quote->getId(); if (!$cartId) { $this->fail('quote fixture failed'); } - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id @@ -257,17 +265,17 @@ private function executeTestFlow($flatRateAmount, $tableRateAmount) 'region_id' => null ] ]; - /** @var \Magento\Quote\Api\Data\EstimateAddressInterface $address */ - $address = $objectManager->create(\Magento\Quote\Api\Data\EstimateAddressInterface::class, $data); - /** @var \Magento\Quote\Api\GuestShippingMethodManagementInterface $shippingEstimation */ - $shippingEstimation = $objectManager->get(\Magento\Quote\Api\GuestShippingMethodManagementInterface::class); + /** @var EstimateAddressInterface $address */ + $address = $objectManager->create(EstimateAddressInterface::class, $data); + /** @var GuestShippingMethodManagementInterface $shippingEstimation */ + $shippingEstimation = $objectManager->get(GuestShippingMethodManagementInterface::class); $result = $shippingEstimation->estimateByAddress($cartId, $address); $this->assertNotEmpty($result); $expectedResult = [ 'tablerate' => [ - 'method_code' => 'bestway', - 'amount' => $tableRateAmount - ], + 'method_code' => 'bestway', + 'amount' => $tableRateAmount + ], 'flatrate' => [ 'method_code' => 'flatrate', 'amount' => $flatRateAmount @@ -295,17 +303,15 @@ private function executeTestFlow($flatRateAmount, $tableRateAmount) */ public function testEstimateByAddressWithInclExclTaxAndVATGroup() { - $this->markTestSkipped('MC-33463'); - - /** @var CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); - $customer = $customerRepository->get('customer@example.com'); - /** @var GroupInterface $customerGroup */ $customerGroup = $this->findCustomerGroupByCode('custom_group'); + $this->mockCustomerVat((int)$customerGroup->getId()); + $customerGroup->setTaxClassId($this->getTaxClass('CustomerTaxClass')->getClassId()); $this->groupRepository->save($customerGroup); - + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $customer = $customerRepository->get('customer@example.com'); $customer->setGroupId($customerGroup->getId()); $customer->setTaxvat('12'); $customerRepository->save($customer); @@ -322,11 +328,46 @@ public function testEstimateByAddressWithInclExclTaxAndVATGroup() $this->assertEquals(5.0, $result[0]->getPriceExclTax()); } + /** + * Create a test double fot customer vat class + * + * @param int $customerGroupId + */ + private function mockCustomerVat(int $customerGroupId): void + { + $gatewayResponse = new DataObject([ + 'is_valid' => false, + 'request_date' => '', + 'request_identifier' => '123123123', + 'request_success' => false, + 'request_message' => __('Error during VAT Number verification.'), + ]); + $customerVat = $this->createPartialMock( + Vat::class, + [ + 'checkVatNumber', + 'isCountryInEU', + 'getCustomerGroupIdBasedOnVatNumber', + 'getMerchantCountryCode', + 'getMerchantVatNumber' + ] + ); + $customerVat->method('checkVatNumber')->willReturn($gatewayResponse); + $customerVat->method('isCountryInEU')->willReturn(true); + $customerVat->method('getMerchantCountryCode')->willReturn('GB'); + $customerVat->method('getMerchantVatNumber')->willReturn('11111'); + $customerVat->method('getCustomerGroupIdBasedOnVatNumber')->willReturn($customerGroupId); + $this->objectManager->removeSharedInstance(Vat::class); + $this->objectManager->addSharedInstance($customerVat, Vat::class); + + // Remove instances where the customer vat object is cached + $this->objectManager->removeSharedInstance(CollectTotalsObserver::class); + } + /** * Find the group with a given code. * * @param string $code - * * @return GroupInterface */ protected function findCustomerGroupByCode(string $code): ?GroupInterface diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index b75501911be6b..207899ceca3e5 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php @@ -10,16 +10,20 @@ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Backend\Model\Session\Quote as SessionQuote; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterfaceFactory; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Data\Option; use Magento\Customer\Model\Metadata\Form; use Magento\Customer\Model\Metadata\FormFactory; use Magento\Framework\View\LayoutInterface; use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * Class for test Account @@ -27,7 +31,7 @@ * @magentoAppArea adminhtml * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AccountTest extends \PHPUnit\Framework\TestCase +class AccountTest extends TestCase { /** * @var Account @@ -81,9 +85,9 @@ public function testGetFormWithCustomer() ); $fixtureCustomerId = 1; - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); - /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + /** @var CustomerInterface $customer */ $customer = $customerRepository->getById($fixtureCustomerId); $customer->setGroupId($customerGroup); $customerRepository->save($customer); @@ -125,8 +129,8 @@ public function testGetFormWithCustomer() */ public function testGetFormWithUserDefinedAttribute() { - /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ - $storeManager = Bootstrap::getObjectManager()->get(\Magento\Store\Model\StoreManagerInterface::class); + /** @var StoreManagerInterface $storeManager */ + $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); $secondStore = $storeManager->getStore('secondstore'); $quoteSession = $this->objectManager->get(SessionQuote::class); @@ -159,6 +163,46 @@ public function testGetFormWithUserDefinedAttribute() ); } + /** + * Test for get form with default customer group + * + */ + public function testGetFormWithDefaultCustomerGroup() + { + $customerGroup = 0; + $quote = $this->objectManager->create(Quote::class); + $quote->setCustomerGroupId($customerGroup); + + $this->session = $this->getMockBuilder(SessionQuote::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId', 'getQuote']) + ->getMock(); + $this->session->method('getQuote') + ->willReturn($quote); + $this->session->method('getCustomerId') + ->willReturn(1); + + $formFactory = $this->getFormFactoryMock(); + $this->objectManager->addSharedInstance($formFactory, FormFactory::class); + + /** @var LayoutInterface $layout */ + $layout = $this->objectManager->get(LayoutInterface::class); + $accountBlock = $layout->createBlock( + Account::class, + 'address_block' . rand(), + ['sessionQuote' => $this->session] + ); + + $expectedGroupId = 1; + $form = $accountBlock->getForm(); + + self::assertEquals( + $expectedGroupId, + $form->getElement('group_id')->getValue(), + 'The Customer Group specified for the chosen customer should be selected.' + ); + } + /** * Creates a mock for Form object. * diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/TotalsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/TotalsTest.php new file mode 100644 index 0000000000000..e16ed1ef98ad5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/TotalsTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Phrase; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Block\Adminhtml\Order\Totals\Tax; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea adminhtml + */ +class TotalsTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $om; + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var OrderInterfaceFactory + */ + private $orderFactory; + + /** + * @inheritDoc + */ + public function setUp() + { + $this->om = Bootstrap::getObjectManager(); + $this->layout = $this->om->get(LayoutInterface::class); + $this->orderFactory = $this->om->get(OrderInterfaceFactory::class); + } + + /** + * Test block totals including tax. + * + * @magentoConfigFixture default_store tax/sales_display/subtotal 2 + * @magentoConfigFixture default_store tax/sales_display/shipping 2 + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testTotalsInclTax(): void + { + $order = $this->prepareOrderInclTax('100000001'); + + $blockTotals = $this->getBlockTotals()->setOrder($order); + $this->assertSubtotal($blockTotals->toHtml(), (float) $order->getSubtotal()); + $this->assertShipping($blockTotals->toHtml(), (float) $order->getShippingAmount()); + + $blockTax = $this->getBlockTax(); + $blockTotals->setChild('child_tax_block', $blockTax); + $blockTax->initTotals(); + + $this->assertSubtotal($blockTotals->toHtml(), (float) $order->getSubtotalInclTax()); + $this->assertShipping($blockTotals->toHtml(), (float) $order->getShippingInclTax()); + } + + /** + * Check if subtotal amount present in block. + * + * @param string $blockTotalsHtml + * @param float $amount + * @return void + */ + private function assertSubtotal(string $blockTotalsHtml, float $amount): void + { + $this->assertTrue( + $this->isBlockContainsTotalAmount($blockTotalsHtml, __('Subtotal'), $amount), + 'Subtotal amount is missing or incorrect.' + ); + } + + /** + * Check if shipping amount present in block. + * + * @param string $blockTotalsHtml + * @param float $amount + * @return void + */ + private function assertShipping(string $blockTotalsHtml, float $amount): void + { + $this->assertTrue( + $this->isBlockContainsTotalAmount($blockTotalsHtml, __('Shipping & Handling'), $amount), + 'Shipping & Handling amount is missing or incorrect.' + ); + } + + /** + * Prepare order for test. + * + * @param string $incrementId + * @return Order + */ + private function prepareOrderInclTax(string $incrementId): Order + { + /** @var Order $order */ + $order = $this->orderFactory->create()->loadByIncrementId($incrementId); + + $order->setSubtotalInclTax(110); + $order->setBaseSubtotalInclTax(110); + + $order->setShippingAmount(10); + $order->setBaseShippingAmount(10); + $order->setShippingInclTax(11); + $order->setBaseShippingInclTax(11); + + return $order; + } + + /** + * Create block totals. + * + * @return Totals + */ + private function getBlockTotals(): Totals + { + /** @var Totals $block */ + $block = $this->layout->createBlock(Totals::class, 'block_totals'); + $block->setTemplate('Magento_Sales::order/totals.phtml'); + + return $block; + } + + /** + * Create block tax. + * + * @return Tax + */ + private function getBlockTax(): Tax + { + /** @var Tax $block */ + $block = $this->layout->createBlock(Tax::class, 'block_tax'); + $block->setTemplate('Magento_Sales::order/totals/tax.phtml'); + + return $block; + } + + /** + * Check if amount present in appropriate block node. + * + * @param string $blockTotalsHtml + * @param Phrase $totalLabel + * @param float $totalAmount + * @return bool + */ + private function isBlockContainsTotalAmount( + string $blockTotalsHtml, + Phrase $totalLabel, + float $totalAmount + ): bool { + $dom = new \DOMDocument(); + $dom->loadHTML($blockTotalsHtml); + $query = sprintf( + "//tr[contains(., '%s')]//span[contains(text(), '%01.2f')]", + $totalLabel, + $totalAmount + ); + + return (bool) (new \DOMXPath($dom))->query($query)->count(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/_files/track.php b/dev/tests/integration/testsuite/Magento/Shipping/_files/track.php new file mode 100644 index 0000000000000..bf6b5a102ff48 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/_files/track.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\ShipmentTrackRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Track; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../../Magento/Sales/_files/order.php'; + +/** @var Order $order */ +$items = []; +foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); +} + +/** @var Shipment $shipment */ +$shipment = Bootstrap::getObjectManager()->get(ShipmentFactory::class)->create($order, $items); +$shipment->setPackages([['1'], ['2']]); +$shipment->setShipmentStatus(Shipment::STATUS_NEW); +$shipment->save(); + +/** @var Track $track */ +$track = Bootstrap::getObjectManager()->create(Track::class); +$track->setOrderId($order->getId()); +$track->setParentId($shipment->getId()); +$track->setTitle('Shipment Title'); +$track->setCarrierCode(Track::CUSTOM_CARRIER_CODE); +$track->setTrackNumber('track_number'); +$track->setDescription('Description of shipment'); + +/** @var ShipmentTrackRepositoryInterface $shipmentTrackRepository */ +$shipmentTrackRepository = Bootstrap::getObjectManager()->get(ShipmentTrackRepositoryInterface::class); +$shipmentTrackRepository->save($track); diff --git a/dev/tests/integration/testsuite/Magento/Shipping/_files/track_rollback.php b/dev/tests/integration/testsuite/Magento/Shipping/_files/track_rollback.php new file mode 100644 index 0000000000000..c401f4052b190 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/_files/track_rollback.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Shipping\Model\Order\Track; +use Magento\Shipping\Model\ResourceModel\Order\Track\Collection; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../../Magento/Sales/_files/order_rollback.php'; + +/** @var Registry $registry */ +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$trackCollection = Bootstrap::getObjectManager()->create(Collection::class); +/** @var $track Track */ +foreach ($trackCollection as $track) { + $track->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes_de_rollback.php b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes_de_rollback.php index a87a031ee78a6..0004002dc6d26 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes_de_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_classes_de_rollback.php @@ -6,6 +6,8 @@ declare(strict_types=1); use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Registry; +use Magento\Tax\Api\TaxClassManagementInterface; use Magento\Tax\Model\ClassModel; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\ObjectManagerInterface; @@ -18,10 +20,6 @@ /** @var ObjectManagerInterface $objectManager */ $objectManager = Bootstrap::getObjectManager(); -$taxClasses = [ - 'CustomerTaxClass', - 'ProductTaxClass', -]; $taxRuleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); /** @var SearchCriteriaBuilder $searchBuilder */ $searchBuilder = $objectManager->get(SearchCriteriaBuilder::class); @@ -29,6 +27,11 @@ ->create(); $taxRules = $taxRuleRepository->getList($searchCriteria) ->getItems(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); foreach ($taxRules as $taxRule) { try { $taxRuleRepository->delete($taxRule); @@ -36,12 +39,17 @@ //Rule already removed } } -$searchCriteria = $searchBuilder->addFilter(ClassModel::KEY_NAME, $taxClasses, 'in') - ->create(); -/** @var TaxClassRepositoryInterface $groupRepository */ + +/** @var TaxClassRepositoryInterface $taxClassRepository */ $taxClassRepository = $objectManager->get(TaxClassRepositoryInterface::class); -$taxClasses = $taxClassRepository->getList($searchCriteria) - ->getItems(); +$searchCriteria = $searchBuilder->addFilter(ClassModel::KEY_NAME, 'CustomerTaxClass') + ->addFilter(ClassModel::KEY_TYPE, TaxClassManagementInterface::TYPE_CUSTOMER) + ->create(); +$taxClasses = $taxClassRepository->getList($searchCriteria)->getItems(); +$searchCriteria = $searchBuilder->addFilter(ClassModel::KEY_NAME, 'ProductTaxClass') + ->addFilter(ClassModel::KEY_TYPE, TaxClassManagementInterface::TYPE_PRODUCT) + ->create(); +$taxClasses = array_merge($taxClasses, $taxClassRepository->getList($searchCriteria)->getItems()); foreach ($taxClasses as $taxClass) { try { $taxClassRepository->delete($taxClass); @@ -49,6 +57,7 @@ //TaxClass already removed } } + $searchCriteria = $searchBuilder->addFilter(Rate::KEY_CODE, 'Denmark') ->create(); /** @var TaxRateRepositoryInterface $groupRepository */ @@ -62,3 +71,5 @@ //TaxRate already removed } } +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/in-context/express-checkout.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/in-context/express-checkout.test.js deleted file mode 100644 index 0420256e20ef5..0000000000000 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/in-context/express-checkout.test.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/* eslint-disable max-nested-callbacks */ -define([ - 'squire', - 'jquery' -], function (Squire, $) { - 'use strict'; - - describe('Magento_Paypal/js/in-context/express-checkout', function () { - - var model, - event, - paypalExpressCheckout, - injector = new Squire(), - mocks = { - 'paypalInContextExpressCheckout': { - checkout: jasmine.createSpyObj('checkout', - ['setup', 'initXO', 'startFlow', 'closeFlow'] - ) - }, - 'Magento_Customer/js/customer-data': { - set: jasmine.createSpy(), - invalidate: jasmine.createSpy() - } - }; - - /** - * Run before each test method - * - * @return void - */ - beforeEach(function (done) { - event = { - /** Stub */ - preventDefault: jasmine.createSpy('preventDefault') - }; - - injector.mock(mocks); - - injector.require([ - 'paypalInContextExpressCheckout', - 'Magento_Paypal/js/in-context/express-checkout'], function (PayPal, Constr) { - paypalExpressCheckout = PayPal; - model = new Constr(); - - done(); - }); - }); - - afterEach(function () { - try { - injector.clean(); - injector.remove(); - } catch (e) {} - }); - - describe('clientConfig.click method', function () { - - it('Check for properties defined ', function () { - expect(model.hasOwnProperty('clientConfig')).toBeDefined(); - expect(model.clientConfig.hasOwnProperty('click')).toBeDefined(); - expect(model.clientConfig.hasOwnProperty('checkoutInited')).toBeDefined(); - }); - - it('Check properties type', function () { - expect(typeof model.clientConfig.checkoutInited).toEqual('boolean'); - expect(typeof model.clientConfig.click).toEqual('function'); - }); - - it('Check properties value', function () { - expect(model.clientConfig.checkoutInited).toEqual(false); - }); - - it('Check call "click" method', function () { - - spyOn(jQuery.fn, 'trigger'); - spyOn(jQuery, 'get').and.callFake(function () { - var d = $.Deferred(); - - d.resolve({ - 'url': true - }); - - return d.promise(); - }); - - model.clientConfig.click(event); - - expect(event.preventDefault).toHaveBeenCalled(); - expect(paypalExpressCheckout.checkout.initXO).toHaveBeenCalled(); - expect(model.clientConfig.checkoutInited).toEqual(true); - expect(jQuery.get).toHaveBeenCalled(); - expect(jQuery('body').trigger).toHaveBeenCalledWith( - jasmine.arrayContaining(['processStart'], ['processStop']) - ); - }); - - it('Check call "click" method', function () { - var message = { - text: 'text', - type: 'error' - }; - - spyOn(jQuery.fn, 'trigger'); - spyOn(jQuery, 'get').and.callFake(function () { - var d = $.Deferred(); - - d.resolve({ - message: message - }); - - return d.promise(); - }); - - model.clientConfig.click(event); - expect(mocks['Magento_Customer/js/customer-data'].set).toHaveBeenCalledWith('messages', { - messages: [message] - }); - expect(event.preventDefault).toHaveBeenCalled(); - expect(paypalExpressCheckout.checkout.initXO).toHaveBeenCalled(); - expect(model.clientConfig.checkoutInited).toEqual(true); - expect(jQuery.get).toHaveBeenCalled(); - expect(jQuery('body').trigger).toHaveBeenCalledWith( - jasmine.arrayContaining(['processStart'], ['processStop']) - ); - }); - }); - }); -}); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Api/_files/ExtensibleInterfacesTest/blacklist_ce.txt b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Api/_files/ExtensibleInterfacesTest/blacklist_ce.txt index e9b1e6e428e03..c544cf71dfce1 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Api/_files/ExtensibleInterfacesTest/blacklist_ce.txt +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Api/_files/ExtensibleInterfacesTest/blacklist_ce.txt @@ -1,4 +1,5 @@ module Magento_Payment Model/Info.php module Magento_Customer Model/Address/AbstractAddress.php library magento/framework MessageQueue/Rpc/Publisher.php -module Magento_GraphQl Model/Query/ContextInterface.php \ No newline at end of file +module Magento_GraphQl Model/Query/ContextInterface.php +module Magento_LoginAsCustomerApi Api/Data/AuthenticationDataInterface.php \ No newline at end of file diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt index d153593a5ed4f..53bba8d71d65d 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt @@ -7,6 +7,7 @@ lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php lib/internal/Magento/Framework/Cache/Backend/Eaccelerator.php lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php dev/tests/integration/framework/deployTestModules.php +dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php dev/tests/integration/testsuite/Magento/Framework/Session/ConfigTest.php dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index 1ac024d505fc3..ca597a7056566 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -91,7 +91,7 @@ class Template implements \Zend_Filter_Interface /** * @var bool */ - private $strictMode = false; + private $strictMode = true; /** * @var VariableResolverInterface|null diff --git a/lib/internal/Magento/Framework/Url.php b/lib/internal/Magento/Framework/Url.php index a2318f1169715..567fd2a96c18a 100644 --- a/lib/internal/Magento/Framework/Url.php +++ b/lib/internal/Magento/Framework/Url.php @@ -424,8 +424,10 @@ protected function _isSecure() */ public function setScope($params) { - $this->setData('scope', $this->_scopeResolver->getScope($params)); - $this->getRouteParamsResolver()->setScope($this->_scopeResolver->getScope($params)); + $scope = $this->_scopeResolver->getScope($params); + $this->setData('scope', $scope); + $this->getRouteParamsResolver()->setScope($scope); + return $this; }