diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml index b712bc6c9531..7f6f2bbd13fa 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml @@ -170,6 +170,9 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; getSortableUpdateCallback()) : ?> escapeJs($block->getJsObjectName()) ?>.sortableUpdateCallback = getSortableUpdateCallback() ?>; + getFilterKeyPressCallback()) : ?> + escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = getFilterKeyPressCallback() ?>; + escapeJs($block->getJsObjectName()) ?>.bindSortable(); getRowInitCallback()) : ?> escapeJs($block->getJsObjectName()) ?>.initRowCallback = getRowInitCallback() ?>; diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml index 0bb453f25d7c..527ddc436207 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml @@ -272,6 +272,9 @@ $numColumns = count($block->getColumns()); getCheckboxCheckCallback()) : ?> escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = getCheckboxCheckCallback() ?>; + getFilterKeyPressCallback()) : ?> + escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = getFilterKeyPressCallback() ?>; + getRowInitCallback()) : ?> escapeJs($block->getJsObjectName()) ?>.initRowCallback = getRowInitCallback() ?>; escapeJs($block->getJsObjectName()) ?>.initGridRows(); diff --git a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php index 58ce33305da8..2f73dd8f380d 100644 --- a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php +++ b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php @@ -11,6 +11,7 @@ use Braintree\Error\Validation; use Braintree\Result\Error; use Braintree\Result\Successful; +use Braintree\Transaction; /** * Processes errors codes from Braintree response. @@ -38,12 +39,14 @@ public function getErrorCodes($response): array $result[] = $error->code; } - if (isset($response->transaction) && $response->transaction->status === 'gateway_rejected') { - $result[] = $response->transaction->gatewayRejectionReason; - } + if (isset($response->transaction) && $response->transaction) { + if ($response->transaction->status === Transaction::GATEWAY_REJECTED) { + $result[] = $response->transaction->gatewayRejectionReason; + } - if (isset($response->transaction) && $response->transaction->status === 'processor_declined') { - $result[] = $response->transaction->processorResponseCode; + if ($response->transaction->status === Transaction::PROCESSOR_DECLINED) { + $result[] = $response->transaction->processorResponseCode; + } } return $result; diff --git a/app/code/Magento/Catalog/Model/Product/Hydrator.php b/app/code/Magento/Catalog/Model/Product/Hydrator.php new file mode 100644 index 000000000000..dcdce7202b21 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Hydrator.php @@ -0,0 +1,39 @@ +getData(); + } + + /** + * @inheritdoc + */ + public function hydrate($entity, array $data) + { + $lockedAttributes = $entity->getLockedAttributes(); + $entity->unlockAttributes(); + $entity->setData(array_merge($entity->getData(), $data)); + foreach ($lockedAttributes as $attribute) { + $entity->lockAttribute($attribute); + } + + return $entity; + } +} diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 570fa6e7f9ef..ff2fab73e037 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -867,7 +867,7 @@ Magento\Framework\EntityManager\AbstractModelHydrator Magento\Framework\EntityManager\AbstractModelHydrator - Magento\Framework\EntityManager\AbstractModelHydrator + Magento\Catalog\Model\Product\Hydrator diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php new file mode 100644 index 000000000000..2c03550404ae --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -0,0 +1,102 @@ +scopeConfig = $scopeConfig; + } + + /** + * Filter for filtering the requested categories id's based on url_key, ids, name in the result. + * + * @param array $args + * @param Collection $categoryCollection + * @param StoreInterface $store + * @throws InputException + */ + public function applyFilters(array $args, Collection $categoryCollection, StoreInterface $store) + { + $categoryCollection->addAttributeToFilter(CategoryInterface::KEY_IS_ACTIVE, ['eq' => 1]); + foreach ($args['filters'] as $field => $cond) { + foreach ($cond as $condType => $value) { + if ($field === 'ids') { + $categoryCollection->addIdFilter($value); + } else { + $this->addAttributeFilter($categoryCollection, $field, $condType, $value, $store); + } + } + } + } + + /** + * Add filter to category collection + * + * @param Collection $categoryCollection + * @param string $field + * @param string $condType + * @param string|array $value + * @param StoreInterface $store + * @throws InputException + */ + private function addAttributeFilter($categoryCollection, $field, $condType, $value, $store) + { + if ($condType === 'match') { + $this->addMatchFilter($categoryCollection, $field, $value, $store); + return; + } + $categoryCollection->addAttributeToFilter($field, [$condType => $value]); + } + + /** + * Add match filter to collection + * + * @param Collection $categoryCollection + * @param string $field + * @param string $value + * @param StoreInterface $store + * @throws InputException + */ + private function addMatchFilter($categoryCollection, $field, $value, $store) + { + $minQueryLength = $this->scopeConfig->getValue( + Query::XML_PATH_MIN_QUERY_LENGTH, + ScopeInterface::SCOPE_STORE, + $store + ); + $searchValue = str_replace('%', '', $value); + $matchLength = strlen($searchValue); + if ($matchLength < $minQueryLength) { + throw new InputException(__('Invalid match filter')); + } + + $categoryCollection->addAttributeToFilter($field, ['like' => "%{$searchValue}%"]); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php new file mode 100644 index 000000000000..6b8949d61282 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -0,0 +1,114 @@ +categoryTree = $categoryTree; + $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; + $this->categoryFilter = $categoryFilter; + $this->collectionFactory = $collectionFactory; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (isset($value[$field->getName()])) { + return $value[$field->getName()]; + } + $store = $context->getExtensionAttributes()->getStore(); + + $rootCategoryIds = []; + if (!isset($args['filters'])) { + $rootCategoryIds[] = (int)$store->getRootCategoryId(); + } else { + $categoryCollection = $this->collectionFactory->create(); + try { + $this->categoryFilter->applyFilters($args, $categoryCollection, $store); + } catch (InputException $e) { + return []; + } + + foreach ($categoryCollection as $category) { + $rootCategoryIds[] = (int)$category->getId(); + } + } + + $result = $this->fetchCategories($rootCategoryIds, $info); + return $result; + } + + /** + * Fetch category tree data + * + * @param array $categoryIds + * @param ResolveInfo $info + * @return array + * @throws GraphQlNoSuchEntityException + */ + private function fetchCategories(array $categoryIds, ResolveInfo $info) + { + $fetchedCategories = []; + foreach ($categoryIds as $categoryId) { + $categoryTree = $this->categoryTree->getTree($info, $categoryId); + if (empty($categoryTree)) { + continue; + } + $fetchedCategories[] = current($this->extractDataFromCategoryTree->execute($categoryTree)); + } + + return $fetchedCategories; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php index 3912bab05ebb..ae4f2e911a5b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php @@ -60,9 +60,9 @@ private function getProductFields(ResolveInfo $info): array $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames); } } - - $fieldNames = array_merge(...$fieldNames); - + if (!empty($fieldNames)) { + $fieldNames = array_merge(...$fieldNames); + } return $fieldNames; } diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 6f2532635ca2..536992d3fca8 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -11,9 +11,12 @@ type Query { ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( - id: Int @doc(description: "Id of the category.") + id: Int @doc(description: "Id of the category.") ): CategoryTree - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + categoryList( + filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.") + ): [CategoryTree] @doc(description: "Returns an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") } type Price @doc(description: "Price is deprecated, replaced by ProductPrice. The Price object defines the price of a product as well as any tax-related adjustments.") { @@ -294,6 +297,13 @@ input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") } +input CategoryFilterInput @doc(description: "CategoryFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") +{ + ids: FilterEqualTypeInput @doc(description: "Filter by category ID that uniquely identifies the category.") + url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category") + name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.") +} + input ProductFilterInput @doc(description: "ProductFilterInput is deprecated, use @ProductAttributeFilterInput instead. ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.") sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index d319ea501413..6e1b031ab48c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -9,8 +9,9 @@ define([ 'jquery', 'Magento_Checkout/js/model/new-customer-address', 'Magento_Customer/js/customer-data', - 'mage/utils/objects' -], function ($, address, customerData, mageUtils) { + 'mage/utils/objects', + 'underscore' +], function ($, address, customerData, mageUtils, _) { 'use strict'; var countryData = customerData.get('directory-data'); @@ -60,13 +61,15 @@ define([ delete addressData['region_id']; if (addressData['custom_attributes']) { - addressData['custom_attributes'] = Object.entries(addressData['custom_attributes']) - .map(function (customAttribute) { + addressData['custom_attributes'] = _.map( + addressData['custom_attributes'], + function (value, key) { return { - 'attribute_code': customAttribute[0], - 'value': customAttribute[1] + 'attribute_code': key, + 'value': value }; - }); + } + ); } return address(addressData); diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index 6244a616bc6d..74365ef02fe8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -254,6 +254,21 @@ Côtes-d'Armor 12345 + + Xian + Shai + Hunan Fenmian + +86 851 8410 4337 + + Nanyuan Rd, Wudang + Hunan Fenmian + + CN + China + Guiyang + Guizhou Sheng + 550002 + Jany Doe @@ -345,4 +360,19 @@ Yes RegionAE + + John + Doe + Magento + + Chaussee de Wavre + 318 + + Bihain + Hainaut + BE + Belgium + 6690 + 0477-58-77867 + diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml new file mode 100644 index 000000000000..6c0615f701df --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressBelgiumTest.xml @@ -0,0 +1,51 @@ + + + + + + + + + <description value="Update customer address on storefront with Belgium address and verify you can select a region"/> + <testCaseId value="MC-20234"/> + <severity value="AVERAGE"/> + <group value="customer"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address Belgium in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerBelgiumAddress"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.xml new file mode 100644 index 000000000000..285de8d777b4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressChinaTest.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="StorefrontUpdateCustomerAddressChinaTest"> + <annotations> + <stories value="Update Regions list for China country"/> + <title value="Update customer address on storefront with china address"/> + <description value="Update customer address on storefront with china address and verify you can select a region"/> + <testCaseId value="MC-20234"/> + <severity value="AVERAGE"/> + <group value="customer"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerChinaAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerChinaAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerChinaAddress"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php new file mode 100644 index 000000000000..7e53198cb9a4 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForBelgium.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add Regions for Belgium. + */ +class AddDataForBelgium implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Directory\Setup\DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForBelgium() + ); + } + + /** + * Belgium states data. + * + * @return array + */ + private function getDataForBelgium() + { + return [ + ['BE', 'VAN', 'Antwerpen'], + ['BE', 'WBR', 'Brabant wallon'], + ['BE', 'BRU', 'Brussels Hoofdstedelijk Gewest'], + ['BE', 'WHT', 'Hainaut'], + ['BE', 'VLI', 'Limburg'], + ['BE', 'WLG', 'Liege'], + ['BE', 'WLX', 'Luxembourg'], + ['BE', 'WNA', 'Namur'], + ['BE', 'VOV', 'Oost-Vlaanderen'], + ['BE', 'VLG', 'Vlaams Gewest'], + ['BE', 'VBR', 'Vlaams-Brabant'], + ['BE', 'VWV', 'West-Vlaanderen'], + ['BE', 'WAL', 'Wallonne, Region'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php new file mode 100644 index 000000000000..0750f8056c4d --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForChina.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Directory\Setup\Patch\Data; + +use Magento\Directory\Setup\DataInstaller; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add China States + */ +class AddDataForChina implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var \Magento\Directory\Setup\DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForChina() + ); + } + + /** + * China states data. + * + * @return array + */ + private function getDataForChina() + { + return [ + ['CN', 'CN-AH', 'Anhui Sheng'], + ['CN', 'CN-BJ', 'Beijing Shi'], + ['CN', 'CN-CQ', 'Chongqing Shi'], + ['CN', 'CN-FJ', 'Fujian Sheng'], + ['CN', 'CN-GS', 'Gansu Sheng'], + ['CN', 'CN-GD', 'Guangdong Sheng'], + ['CN', 'CN-GX', 'Guangxi Zhuangzu Zizhiqu'], + ['CN', 'CN-GZ', 'Guizhou Sheng'], + ['CN', 'CN-HI', 'Hainan Sheng'], + ['CN', 'CN-HE', 'Hebei Sheng'], + ['CN', 'CN-HL', 'Heilongjiang Sheng'], + ['CN', 'CN-HA', 'Henan Sheng'], + ['CN', 'CN-HK', 'Hong Kong SAR'], + ['CN', 'CN-HB', 'Hubei Sheng'], + ['CN', 'CN-HN', 'Hunan Sheng'], + ['CN', 'CN-JS', 'Jiangsu Sheng'], + ['CN', 'CN-JX', 'Jiangxi Sheng'], + ['CN', 'CN-JL', 'Jilin Sheng'], + ['CN', 'CN-LN', 'Liaoning Sheng'], + ['CN', 'CN-MO', 'Macao SAR'], + ['CN', 'CN-NM', 'Nei Mongol Zizhiqu'], + ['CN', 'CN-NX', 'Ningxia Huizi Zizhiqu'], + ['CN', 'CN-QH', 'Qinghai Sheng'], + ['CN', 'CN-SN', 'Shaanxi Sheng'], + ['CN', 'CN-SD', 'Shandong Sheng'], + ['CN', 'CN-SH', 'Shanghai Shi'], + ['CN', 'CN-SX', 'Shanxi Sheng'], + ['CN', 'CN-SC', 'Sichuan Sheng'], + ['CN', 'CN-TW', 'Taiwan Sheng'], + ['CN', 'CN-TJ', 'Tianjin Shi'], + ['CN', 'CN-XJ', 'Xinjiang Uygur Zizhiqu'], + ['CN', 'CN-XZ', 'Xizang Zizhiqu'], + ['CN', 'CN-YN', 'Yunnan Sheng'], + ['CN', 'CN-ZJ', 'Zhejiang Sheng'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php b/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php deleted file mode 100644 index f88cdfc26fa9..000000000000 --- a/app/code/Magento/Multishipping/Controller/Checkout/Plugin.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Multishipping\Controller\Checkout; - -/** - * Turns Off Multishipping mode for Quote. - */ -class Plugin -{ - /** - * @var \Magento\Checkout\Model\Cart - */ - protected $cart; - - /** - * @param \Magento\Checkout\Model\Cart $cart - */ - public function __construct(\Magento\Checkout\Model\Cart $cart) - { - $this->cart = $cart; - } - - /** - * Disable multishipping - * - * @param \Magento\Framework\App\Action\Action $subject - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeExecute(\Magento\Framework\App\Action\Action $subject) - { - $quote = $this->cart->getQuote(); - if ($quote->getIsMultiShipping()) { - $quote->setIsMultiShipping(0); - $this->cart->saveQuote(); - } - } -} diff --git a/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php new file mode 100644 index 000000000000..fff2346d7624 --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Checkout\Model\Cart; +use Magento\Framework\App\Action\Action; + +/** + * Turns Off Multishipping mode for Quote. + */ +class DisableMultishippingMode +{ + /** + * @var Cart + */ + private $cart; + + /** + * @param Cart $cart + */ + public function __construct( + Cart $cart + ) { + $this->cart = $cart; + } + + /** + * Disable multishipping + * + * @param Action $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(Action $subject) + { + $quote = $this->cart->getQuote(); + if ($quote->getIsMultiShipping()) { + $quote->setIsMultiShipping(0); + $extensionAttributes = $quote->getExtensionAttributes(); + if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $extensionAttributes->setShippingAssignments([]); + } + $this->cart->saveQuote(); + } + } +} diff --git a/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php b/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php new file mode 100644 index 000000000000..af19e4bc91f5 --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/MultishippingQuoteRepository.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Framework\Api\SearchResultsInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingProcessor; +use Magento\Quote\Model\ShippingAssignmentFactory; + +/** + * Plugin for multishipping quote processing. + */ +class MultishippingQuoteRepository +{ + /** + * @var ShippingAssignmentFactory + */ + private $shippingAssignmentFactory; + + /** + * @var ShippingProcessor + */ + private $shippingProcessor; + + /** + * @param ShippingAssignmentFactory $shippingAssignmentFactory + * @param ShippingProcessor $shippingProcessor + */ + public function __construct( + ShippingAssignmentFactory $shippingAssignmentFactory, + ShippingProcessor $shippingProcessor + ) { + $this->shippingAssignmentFactory = $shippingAssignmentFactory; + $this->shippingProcessor = $shippingProcessor; + } + + /** + * Process multishipping quote for get. + * + * @param CartRepositoryInterface $subject + * @param CartInterface $result + * @return CartInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGet( + CartRepositoryInterface $subject, + CartInterface $result + ) { + return $this->processQuote($result); + } + + /** + * Process multishipping quote for get list. + * + * @param CartRepositoryInterface $subject + * @param SearchResultsInterface $result + * + * @return SearchResultsInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetList( + CartRepositoryInterface $subject, + SearchResultsInterface $result + ) { + $items = []; + foreach ($result->getItems() as $item) { + $items[] = $this->processQuote($item); + } + $result->setItems($items); + + return $result; + } + + /** + * Remove shipping assignments for multishipping quote. + * + * @param CartRepositoryInterface $subject + * @param CartInterface $quote + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave(CartRepositoryInterface $subject, CartInterface $quote) + { + $extensionAttributes = $quote->getExtensionAttributes(); + if ($quote->getIsMultiShipping() && $extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $quote->getExtensionAttributes()->setShippingAssignments([]); + } + + return [$quote]; + } + + /** + * Set shipping assignments for multishipping quote according to customer selection. + * + * @param CartInterface $quote + * @return CartInterface + */ + private function processQuote(CartInterface $quote): CartInterface + { + if (!$quote->getIsMultiShipping() || !$quote instanceof Quote) { + return $quote; + } + + if ($quote->getExtensionAttributes() && $quote->getExtensionAttributes()->getShippingAssignments()) { + $shippingAssignments = []; + $addresses = $quote->getAllAddresses(); + + foreach ($addresses as $address) { + $quoteItems = $this->getQuoteItems($quote, $address); + if (!empty($quoteItems)) { + $shippingAssignment = $this->shippingAssignmentFactory->create(); + $shippingAssignment->setItems($quoteItems); + $shippingAssignment->setShipping($this->shippingProcessor->create($address)); + $shippingAssignments[] = $shippingAssignment; + } + } + + if (!empty($shippingAssignments)) { + $quote->getExtensionAttributes()->setShippingAssignments($shippingAssignments); + } + } + + return $quote; + } + + /** + * Returns quote items assigned to address. + * + * @param Quote $quote + * @param Quote\Address $address + * @return Quote\Item[] + */ + private function getQuoteItems(Quote $quote, Quote\Address $address): array + { + $quoteItems = []; + foreach ($address->getItemsCollection() as $addressItem) { + $quoteItem = $quote->getItemById($addressItem->getQuoteItemId()); + if ($quoteItem) { + $multishippingQuoteItem = clone $quoteItem; + $qty = $addressItem->getQty(); + $sku = $multishippingQuoteItem->getSku(); + if (isset($quoteItems[$sku])) { + $qty += $quoteItems[$sku]->getQty(); + } + $multishippingQuoteItem->setQty($qty); + $quoteItems[$sku] = $multishippingQuoteItem; + } + } + + return array_values($quoteItems); + } +} diff --git a/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php b/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php new file mode 100644 index 000000000000..deac19e23a23 --- /dev/null +++ b/app/code/Magento/Multishipping/Plugin/ResetShippingAssigment.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor; + +/** + * Resets quote shipping assignments when item is removed from multishipping quote. + */ +class ResetShippingAssigment +{ + /** + * @var ShippingAssignmentProcessor + */ + private $shippingAssignmentProcessor; + + /** + * @param ShippingAssignmentProcessor $shippingAssignmentProcessor + */ + public function __construct( + ShippingAssignmentProcessor $shippingAssignmentProcessor + ) { + $this->shippingAssignmentProcessor = $shippingAssignmentProcessor; + } + + /** + * Resets quote shipping assignments when item is removed from multishipping quote. + * + * @param Quote $subject + * @param mixed $itemId + * + * @return array + */ + public function beforeRemoveItem(Quote $subject, $itemId): array + { + if ($subject->getIsMultiShipping()) { + $extensionAttributes = $subject->getExtensionAttributes(); + if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $shippingAssignment = $this->shippingAssignmentProcessor->create($subject); + $extensionAttributes->setShippingAssignments([$shippingAssignment]); + } + } + + return [$itemId]; + } +} diff --git a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php b/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php deleted file mode 100644 index a26f2661ebab..000000000000 --- a/app/code/Magento/Multishipping/Test/Unit/Controller/Checkout/PluginTest.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Multishipping\Test\Unit\Controller\Checkout; - -use Magento\Multishipping\Controller\Checkout\Plugin; - -class PluginTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $cartMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteMock; - - /** - * @var Plugin - */ - protected $object; - - protected function setUp() - { - $this->cartMock = $this->createMock(\Magento\Checkout\Model\Cart::class); - $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['__wakeUp', 'setIsMultiShipping', 'getIsMultiShipping'] - ); - $this->cartMock->expects($this->once())->method('getQuote')->will($this->returnValue($this->quoteMock)); - $this->object = new \Magento\Multishipping\Controller\Checkout\Plugin($this->cartMock); - } - - public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void - { - $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); - $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(1); - $this->quoteMock->expects($this->once())->method('setIsMultiShipping')->with(0); - $this->cartMock->expects($this->once())->method('saveQuote'); - $this->object->beforeExecute($subject); - } - - public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void - { - $subject = $this->createMock(\Magento\Checkout\Controller\Index\Index::class); - $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); - $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); - $this->cartMock->expects($this->never())->method('saveQuote'); - $this->object->beforeExecute($subject); - } -} diff --git a/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php new file mode 100644 index 000000000000..02ae1a70ce80 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php @@ -0,0 +1,99 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Test\Unit\Plugin; + +use Magento\Checkout\Controller\Index\Index; +use Magento\Checkout\Model\Cart; +use Magento\Multishipping\Plugin\DisableMultishippingMode; +use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote; + +/** + * Class DisableMultishippingModeTest + */ +class DisableMultishippingModeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $cartMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var DisableMultishippingMode + */ + private $object; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->cartMock = $this->createMock(Cart::class); + $this->quoteMock = $this->createPartialMock( + Quote::class, + ['__wakeUp', 'setIsMultiShipping', 'getIsMultiShipping', 'getExtensionAttributes'] + ); + $this->cartMock->expects($this->once()) + ->method('getQuote') + ->will($this->returnValue($this->quoteMock)); + $this->object = new DisableMultishippingMode($this->cartMock); + } + + /** + * Tests turn off multishipping on multishipping quote. + * + * @return void + */ + public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void + { + $subject = $this->createMock(Index::class); + $extensionAttributes = $this->createPartialMock( + CartExtensionInterface::class, + ['setShippingAssignments', 'getShippingAssignments'] + ); + $extensionAttributes->method('getShippingAssignments') + ->willReturn( + $this->createMock(ShippingAssignmentInterface::class) + ); + $extensionAttributes->expects($this->once()) + ->method('setShippingAssignments') + ->with([]); + $this->quoteMock->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + $this->quoteMock->expects($this->once()) + ->method('getIsMultiShipping')->willReturn(1); + $this->quoteMock->expects($this->once()) + ->method('setIsMultiShipping') + ->with(0); + $this->cartMock->expects($this->once()) + ->method('saveQuote'); + + $this->object->beforeExecute($subject); + } + + /** + * Tests turn off multishipping on non-multishipping quote. + * + * @return void + */ + public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void + { + $subject = $this->createMock(Index::class); + $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); + $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); + $this->cartMock->expects($this->never())->method('saveQuote'); + $this->object->beforeExecute($subject); + } +} diff --git a/app/code/Magento/Multishipping/etc/di.xml b/app/code/Magento/Multishipping/etc/di.xml index 3bccf0b74bcd..ad0d341d6b2e 100644 --- a/app/code/Magento/Multishipping/etc/di.xml +++ b/app/code/Magento/Multishipping/etc/di.xml @@ -9,4 +9,7 @@ <type name="Magento\Quote\Model\Cart\CartTotalRepository"> <plugin name="multishipping_shipping_addresses" type="Magento\Multishipping\Model\Cart\CartTotalRepositoryPlugin" /> </type> + <type name="Magento\Quote\Model\QuoteRepository"> + <plugin name="multishipping_quote_repository" type="Magento\Multishipping\Plugin\MultishippingQuoteRepository" /> + </type> </config> diff --git a/app/code/Magento/Multishipping/etc/frontend/di.xml b/app/code/Magento/Multishipping/etc/frontend/di.xml index 0c2daaf45043..481b95280a4a 100644 --- a/app/code/Magento/Multishipping/etc/frontend/di.xml +++ b/app/code/Magento/Multishipping/etc/frontend/di.xml @@ -31,13 +31,13 @@ </arguments> </type> <type name="Magento\Checkout\Controller\Cart\Add"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Controller\Cart\UpdatePost"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Controller\Index\Index"> - <plugin name="multishipping_disabler" type="Magento\Multishipping\Controller\Checkout\Plugin" sortOrder="50" /> + <plugin name="multishipping_disabler" type="Magento\Multishipping\Plugin\DisableMultishippingMode" sortOrder="50" /> </type> <type name="Magento\Checkout\Model\Cart"> <plugin name="multishipping_session_mapper" type="Magento\Multishipping\Model\Checkout\Type\Multishipping\Plugin" sortOrder="50" /> @@ -45,4 +45,7 @@ <type name="Magento\Checkout\Controller\Cart"> <plugin name="multishipping_clear_addresses" type="Magento\Multishipping\Model\Cart\Controller\CartPlugin" sortOrder="50" /> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="multishipping_reset_shipping_assigment" type="Magento\Multishipping\Plugin\ResetShippingAssigment"/> + </type> </config> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml index 36f9d35327fc..28395f8eeb84 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/checkmo.phtml @@ -7,8 +7,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> <?php if ($block->getInfo()->getAdditionalInformation()) : ?> <?php if ($block->getPayableTo()) : ?> <br /><?= $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml index d8d952526e67..f85a8f8357dd 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/pdf/checkmo.phtml @@ -7,8 +7,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Checkmo */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> {{pdf_row_separator}} <?php if ($block->getInfo()->getAdditionalInformation()) : ?> {{pdf_row_separator}} diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml index 2a6de7f0cc35..ae7f654a1350 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/info/purchaseorder.phtml @@ -6,8 +6,9 @@ /** * @var $block \Magento\OfflinePayments\Block\Info\Purchaseorder */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<div class="order-payment-method-name"><?= $block->escapeHtml($block->getMethod()->getTitle()) ?></div> +<div class="order-payment-method-name"><?= $block->escapeHtml($paymentTitle) ?></div> <table class="data-table admin__table-secondary"> <tr> <th><?= $block->escapeHtml(__('Purchase Order Number')) ?>:</th> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml index 8b9c37f11256..3cd88bddbfb1 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/default.phtml @@ -9,8 +9,9 @@ * @see \Magento\Payment\Block\Info */ $specificInfo = $block->getSpecificInformation(); +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?> +<?= $block->escapeHtml($paymentTitle) ?> <?php if ($specificInfo) : ?> <table class="data-table admin__table-secondary"> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml index a8583ea5549f..54b9e48d07a9 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/info/pdf/default.phtml @@ -8,8 +8,9 @@ * @see \Magento\Payment\Block\Info * @var \Magento\Payment\Block\Info $block */ +$paymentTitle = $block->getMethod()->getConfigData('title', $block->getInfo()->getOrder()->getStoreId()); ?> -<?= $block->escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} +<?= $block->escapeHtml($paymentTitle) ?>{{pdf_row_separator}} <?php if ($specificInfo = $block->getSpecificInformation()) : ?> <?php foreach ($specificInfo as $label => $value) : ?> diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php index 9a271f741edd..001c581dc0da 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php @@ -5,8 +5,7 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search; -use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection - as ProductCollectionDataProvider; +use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection; use Magento\Framework\App\ObjectManager; /** @@ -48,7 +47,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended protected $_productFactory; /** - * @var ProductCollectionDataProvider $productCollectionProvider + * @var ProductCollection $productCollectionProvider */ private $productCollectionProvider; @@ -60,7 +59,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param \Magento\Sales\Model\Config $salesConfig * @param array $data - * @param ProductCollectionDataProvider|null $productCollectionProvider + * @param ProductCollection|null $productCollectionProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -70,14 +69,14 @@ public function __construct( \Magento\Backend\Model\Session\Quote $sessionQuote, \Magento\Sales\Model\Config $salesConfig, array $data = [], - ProductCollectionDataProvider $productCollectionProvider = null + ProductCollection $productCollectionProvider = null ) { $this->_productFactory = $productFactory; $this->_catalogConfig = $catalogConfig; $this->_sessionQuote = $sessionQuote; $this->_salesConfig = $salesConfig; $this->productCollectionProvider = $productCollectionProvider - ?: ObjectManager::getInstance()->get(ProductCollectionDataProvider::class); + ?: ObjectManager::getInstance()->get(ProductCollection::class); parent::__construct($context, $backendHelper, $data); } @@ -94,6 +93,7 @@ protected function _construct() $this->setCheckboxCheckCallback('order.productGridCheckboxCheck.bind(order)'); $this->setRowInitCallback('order.productGridRowInit.bind(order)'); $this->setDefaultSort('entity_id'); + $this->setFilterKeyPressCallback('order.productGridFilterKeyPress'); $this->setUseAjax(true); if ($this->getRequest()->getParam('collapse')) { $this->setIsCollapsed(true); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php index 341ee16ae910..45cd504be201 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php @@ -188,7 +188,7 @@ protected function _processActionData($action = null) && $this->_getOrderCreateModel()->getShippingAddress()->getSameAsBilling() && empty($shippingMethod) ) { $this->_getOrderCreateModel()->setShippingAsBilling(1); - } else { + } elseif ($syncFlag !== null) { $this->_getOrderCreateModel()->setShippingAsBilling((int)$syncFlag); } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 3f178ae02102..90e2aa8e1252 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -92,7 +92,7 @@ <annotations> <description>Clears the Email, First Name, Last Name, Street Line 1, City, Postal Code and Phone fields when adding an Order and then verifies that they are required after attempting to Save.</description> </annotations> - + <seeElement selector="{{AdminOrderFormAccountSection.requiredGroup}}" stepKey="seeCustomerGroupRequired"/> <seeElement selector="{{AdminOrderFormAccountSection.requiredEmail}}" stepKey="seeEmailRequired"/> <clearField selector="{{AdminOrderFormAccountSection.email}}" stepKey="clearEmailField"/> @@ -181,7 +181,7 @@ <annotations> <description>EXTENDS: addConfigurableProductToOrder. Selects the provided Option to the Configurable Product.</description> </annotations> - + <waitForElementVisible selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_frontend_label)}}" stepKey="waitForConfigurablePopover"/> <selectOption selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_frontend_label)}}" userInput="{{option.label}}" stepKey="selectionConfigurableOption"/> </actionGroup> @@ -195,7 +195,7 @@ <argument name="option"/> <argument name="quantity" type="string"/> </arguments> - + <click selector="{{AdminOrderFormItemsSection.configure}}" stepKey="clickConfigure"/> <waitForElementVisible selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_frontend_label)}}" stepKey="waitForConfigurablePopover"/> <wait time="2" stepKey="waitForOptionsToLoad"/> @@ -213,7 +213,7 @@ <argument name="product"/> <argument name="quantity" type="string" defaultValue="1"/> </arguments> - + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterBundle"/> <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> @@ -235,7 +235,7 @@ <arguments> <argument name="price" type="string"/> </arguments> - + <grabTextFrom selector="{{AdminOrderFormItemsSection.rowPrice('1')}}" stepKey="grabProductPriceFromGrid" after="clickOk"/> <assertEquals stepKey="assertProductPriceInGrid" message="Bundle product price in grid should be equal {{price}}" after="grabProductPriceFromGrid"> <expectedResult type="string">{{price}}</expectedResult> @@ -320,6 +320,22 @@ <selectOption selector="{{AdminOrderFormPaymentSection.flatRateOption}}" userInput="flatrate_flatrate" stepKey="checkFlatRate"/> </actionGroup> + <actionGroup name="changeShippingMethod"> + <annotations> + <description>Change Shipping Method on the Admin 'Create New Order for' page.</description> + </annotations> + <arguments> + <argument name="shippingMethod" defaultValue="flatrate_flatrate" type="string"/> + </arguments> + <click selector="{{AdminOrderFormPaymentSection.header}}" stepKey="unfocus"/> + <waitForPageLoad stepKey="waitForJavascriptToFinish"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickShippingMethods1"/> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="waitForChangeShippingMethod"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickShippingMethods2"/> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.shippingMethod}}" stepKey="waitForShippingOptions2"/> + <selectOption selector="{{AdminOrderFormPaymentSection.shippingMethod}}" userInput="{{shippingMethod}}" stepKey="checkFlatRate"/> + </actionGroup> + <!--Select free shipping method--> <actionGroup name="orderSelectFreeShipping"> <annotations> @@ -516,7 +532,7 @@ <argument name="product"/> <argument name="customer"/> </arguments> - + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> <waitForPageLoad stepKey="waitForNewOrderPageOpened"/> <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml index 2d1a4d5a4cba..4fde9db1d21d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBillingAddressSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormBillingAddressSection"> + <element name="selectAddress" type="select" selector="//select[@id='order-billing_address_customer_address_id']"/> <element name="NamePrefix" type="input" selector="#order-billing_address_prefix" timeout="30"/> <element name="FirstName" type="input" selector="#order-billing_address_firstname" timeout="30"/> <element name="MiddleName" type="input" selector="#order-billing_address_middlename" timeout="30"/> @@ -38,4 +39,4 @@ <element name="postalCodeError" type="text" selector="#order-billing_address_postcode-error"/> <element name="phoneError" type="text" selector="#order-billing_address_telephone-error"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index b31582552ccc..a478d79d8553 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormPaymentSection"> + <element name="shippingMethod" type="radio" selector="//input[@name='order[shipping_method]']"/> <element name="header" type="text" selector="#order-methods span.title"/> <element name="getShippingMethods" type="text" selector="#order-shipping_method a.action-default" timeout="30"/> <element name="flatRateOption" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml new file mode 100644 index 000000000000..d6bf0eec301d --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml @@ -0,0 +1,75 @@ +<?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="AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest"> + <annotations> + <title value="Tax should not be displayed for non taxable address"/> + <stories value="MC-21699: Tax does not change when changing the billing address from Admin Panel"/> + <description value="Tax should not be displayed for non taxable address when switching from taxable address"/> + <testCaseId value="MC-21721"/> + <features value="Sales"/> + <severity value="MAJOR"/> + <group value="Sales"/> + </annotations> + <before> + <!--Enable flat rate shipping--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <!--Enable free shipping method --> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> + <!--Create customer--> + <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="simpleCustomer"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="category1"/> + <!--Create product1--> + <createData entity="_defaultProduct" stepKey="product1"> + <requiredEntity createDataKey="category1"/> + </createData> + <!--Create tax rule for US-CA--> + <createData entity="defaultTaxRule" stepKey="createTaxRule"/> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <!--Step 1: Create new order for customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + <!--Step 2: Add product1 to the order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$product1$$"/> + </actionGroup> + <!--Step 2: Select taxable address as billing address--> + <selectOption selector="{{AdminOrderFormBillingAddressSection.selectAddress}}" userInput="{{US_Address_CA.state}}" stepKey="selectTaxableAddress" /> + <!--Step 3: Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShippingMethod"/> + <!--Step 4: Verify that tax is applied to the order--> + <seeElement selector="{{AdminOrderFormTotalSection.total('Tax')}}" stepKey="seeTax" /> + <!--Step 5: Select non taxable address as billing address--> + <selectOption selector="{{AdminOrderFormBillingAddressSection.selectAddress}}" userInput="{{US_Address_TX.state}}" stepKey="selectNonTaxableAddress" /> + <!--Step 6: Change shipping method to Free--> + <actionGroup ref="changeShippingMethod" stepKey="changeShippingMethod"> + <argument name="shippingMethod" value="freeshipping_freeshipping"/> + </actionGroup> + <!--Step 7: Verify that tax is not applied to the order--> + <dontSeeElement selector="{{AdminOrderFormTotalSection.total('Tax')}}" stepKey="dontSeeTax" /> + <after> + <!--Delete product1--> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <!--Delete category--> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + <!--Delete customer--> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <!--Delete tax rule--> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <!--Logout--> + <actionGroup ref="logout" stepKey="logout"/> + <!--Disable free shipping method --> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index 3fe9d0878288..4e0741451074 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -795,6 +795,20 @@ define([ grid.reloadParams = {'products[]':this.gridProducts.keys()}; }, + productGridFilterKeyPress: function (grid, event) { + var returnKey = parseInt(Event.KEY_RETURN || 13, 10); + + if (event.keyCode === returnKey) { + if (typeof event.stopPropagation === 'function') { + event.stopPropagation(); + } + + if (typeof event.preventDefault === 'function') { + event.preventDefault(); + } + } + }, + /** * Submit configured products to quote */ diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 0acece9271f7..e6b03755bea4 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -31,6 +31,11 @@ class EntityUrl implements ResolverInterface */ private $customUrlLocator; + /** + * @var int + */ + private $redirectType; + /** * @param UrlFinderInterface $urlFinder * @param CustomUrlLocatorInterface $customUrlLocator @@ -57,49 +62,83 @@ public function resolve( throw new GraphQlInputException(__('"url" argument should be specified and not empty')); } + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $result = null; $url = $args['url']; if (substr($url, 0, 1) === '/' && $url !== '/') { $url = ltrim($url, '/'); } + $this->redirectType = 0; $customUrl = $this->customUrlLocator->locateUrl($url); $url = $customUrl ?: $url; - $urlRewrite = $this->findCanonicalUrl($url, (int)$context->getExtensionAttributes()->getStore()->getId()); - if ($urlRewrite) { - if (!$urlRewrite->getEntityId()) { + $finalUrlRewrite = $this->findFinalUrl($url, $storeId); + if ($finalUrlRewrite) { + $relativeUrl = $finalUrlRewrite->getRequestPath(); + $resultArray = $this->rewriteCustomUrls($finalUrlRewrite, $storeId) ?? [ + 'id' => $finalUrlRewrite->getEntityId(), + 'canonical_url' => $relativeUrl, + 'relative_url' => $relativeUrl, + 'redirectCode' => $this->redirectType, + 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) + ]; + + if (empty($resultArray['id'])) { throw new GraphQlNoSuchEntityException( __('No such entity found with matching URL key: %url', ['url' => $url]) ); } - $result = [ - 'id' => $urlRewrite->getEntityId(), - 'canonical_url' => $urlRewrite->getTargetPath(), - 'relative_url' => $urlRewrite->getTargetPath(), - 'type' => $this->sanitizeType($urlRewrite->getEntityType()) - ]; + + $result = $resultArray; } return $result; } /** - * Find the canonical url passing through all redirects if any + * Handle custom urls with and without redirects + * + * @param UrlRewrite $finalUrlRewrite + * @param int $storeId + * @return array|null + */ + private function rewriteCustomUrls(UrlRewrite $finalUrlRewrite, int $storeId): ?array + { + if ($finalUrlRewrite->getEntityType() === 'custom' || !($finalUrlRewrite->getEntityId() > 0)) { + $finalCustomUrlRewrite = clone $finalUrlRewrite; + $finalUrlRewrite = $this->findFinalUrl($finalCustomUrlRewrite->getTargetPath(), $storeId, true); + $relativeUrl = + $finalCustomUrlRewrite->getRedirectType() == 0 + ? $finalCustomUrlRewrite->getRequestPath() : $finalUrlRewrite->getRequestPath(); + return [ + 'id' => $finalUrlRewrite->getEntityId(), + 'canonical_url' => $relativeUrl, + 'relative_url' => $relativeUrl, + 'redirectCode' => $finalCustomUrlRewrite->getRedirectType(), + 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) + ]; + } + return null; + } + + /** + * Find the final url passing through all redirects if any * * @param string $requestPath * @param int $storeId + * @param bool $findCustom * @return UrlRewrite|null */ - private function findCanonicalUrl(string $requestPath, int $storeId) : ?UrlRewrite + private function findFinalUrl(string $requestPath, int $storeId, bool $findCustom = false): ?UrlRewrite { $urlRewrite = $this->findUrlFromRequestPath($requestPath, $storeId); - if ($urlRewrite && $urlRewrite->getRedirectType() > 0) { + if ($urlRewrite) { + $this->redirectType = $urlRewrite->getRedirectType(); while ($urlRewrite && $urlRewrite->getRedirectType() > 0) { $urlRewrite = $this->findUrlFromRequestPath($urlRewrite->getTargetPath(), $storeId); } - } - if (!$urlRewrite) { + } else { $urlRewrite = $this->findUrlFromTargetPath($requestPath, $storeId); } - if ($urlRewrite && !$urlRewrite->getEntityId() && !$urlRewrite->getIsAutogenerated()) { + if ($urlRewrite && ($findCustom && !$urlRewrite->getEntityId() && !$urlRewrite->getIsAutogenerated())) { $urlRewrite = $this->findUrlFromTargetPath($urlRewrite->getTargetPath(), $storeId); } @@ -113,7 +152,7 @@ private function findCanonicalUrl(string $requestPath, int $storeId) : ?UrlRewri * @param int $storeId * @return UrlRewrite|null */ - private function findUrlFromRequestPath(string $requestPath, int $storeId) : ?UrlRewrite + private function findUrlFromRequestPath(string $requestPath, int $storeId): ?UrlRewrite { return $this->urlFinder->findOneByData( [ @@ -130,7 +169,7 @@ private function findUrlFromRequestPath(string $requestPath, int $storeId) : ?Ur * @param int $storeId * @return UrlRewrite|null */ - private function findUrlFromTargetPath(string $targetPath, int $storeId) : ?UrlRewrite + private function findUrlFromTargetPath(string $targetPath, int $storeId): ?UrlRewrite { return $this->urlFinder->findOneByData( [ diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index ace3e0eae831..7f7ebb627b4d 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -9,6 +9,7 @@ type EntityUrl @doc(description: "EntityUrl is an output object containing the ` id: Int @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") canonical_url: String @deprecated(reason: "The canonical_url field is deprecated, use relative_url instead.") relative_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") + redirectCode: Int @doc(description: "301 or 302 HTTP code for url permanent or temporary redirect or 0 for the 200 no redirect") type: UrlRewriteEntityTypeEnum @doc(description: "One of PRODUCT, CATEGORY, or CMS_PAGE.") } diff --git a/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php new file mode 100644 index 000000000000..d2ea44fff5bc --- /dev/null +++ b/app/code/Magento/WeeeGraphQl/Model/Resolver/StoreConfig.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WeeeGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Weee\Helper\Data; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Weee\Model\Tax as WeeeDisplayConfig; +use Magento\Framework\Pricing\Render; + +/** + * Resolver for the FPT store config settings + */ +class StoreConfig implements ResolverInterface +{ + /** + * @var string + */ + private static $weeeDisplaySettingsNone = 'FPT_DISABLED'; + + /** + * @var array + */ + private static $weeeDisplaySettings = [ + WeeeDisplayConfig::DISPLAY_INCL => 'INCLUDE_FPT_WITHOUT_DETAILS', + WeeeDisplayConfig::DISPLAY_INCL_DESCR => 'INCLUDE_FPT_WITH_DETAILS', + WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL => 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + WeeeDisplayConfig::DISPLAY_EXCL => 'EXCLUDE_FPT_WITHOUT_DETAILS' + ]; + + /** + * @var Data + */ + private $weeeHelper; + + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var array + */ + private $computedFptSettings = []; + + /** + * @param Data $weeeHelper + * @param TaxHelper $taxHelper + */ + public function __construct(Data $weeeHelper, TaxHelper $taxHelper) + { + $this->weeeHelper = $weeeHelper; + $this->taxHelper = $taxHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($this->computedFptSettings)) { + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $storeId = (int)$store->getId(); + + $this->computedFptSettings = [ + 'product_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + 'category_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + 'sales_fixed_product_tax_display_setting' => self::$weeeDisplaySettingsNone, + ]; + if ($this->weeeHelper->isEnabled($store)) { + $productFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_ITEM_VIEW, $storeId); + $categoryFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_ITEM_LIST, $storeId); + $salesModulesFptDisplay = $this->getWeeDisplaySettingsByZone(Render::ZONE_SALES, $storeId); + + $this->computedFptSettings = [ + 'product_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$productFptDisplay] ?? + self::$weeeDisplaySettingsNone, + 'category_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$categoryFptDisplay] ?? + self::$weeeDisplaySettingsNone, + 'sales_fixed_product_tax_display_setting' => self::$weeeDisplaySettings[$salesModulesFptDisplay] ?? + self::$weeeDisplaySettingsNone, + ]; + } + } + + return $this->computedFptSettings[$info->fieldName] ?? null; + } + + /** + * Get the weee system display setting + * + * @param string $zone + * @param string $storeId + * @return string + */ + private function getWeeDisplaySettingsByZone(string $zone, int $storeId): int + { + return (int) $this->weeeHelper->typeOfDisplay( + null, + $zone, + $storeId + ); + } +} diff --git a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls index 5c6d2f98b43f..18b0e7c1823e 100644 --- a/app/code/Magento/WeeeGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WeeeGraphQl/etc/schema.graphqls @@ -14,3 +14,17 @@ type FixedProductTax @doc(description: "A single FPT that can be applied to a pr amount: Money @doc(description: "Amount of the FPT as a money object.") label: String @doc(description: "The label assigned to the FPT to be displayed on the frontend.") } + +type StoreConfig { + product_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices On Product View Page' field. It indicates how FPT information is displayed on product pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") + category_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices In Product Lists' field. It indicates how FPT information is displayed on category pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") + sales_fixed_product_tax_display_setting : FixedProductTaxDisplaySettings @doc(description: "Corresponds to the 'Display Prices In Sales Modules' field. It indicates how FPT information is displayed on cart, checkout, and order pages") @resolver(class: "Magento\\WeeeGraphQl\\Model\\Resolver\\StoreConfig") +} + +enum FixedProductTaxDisplaySettings @doc(description: "This enumeration display settings for the fixed product tax") { + INCLUDE_FPT_WITHOUT_DETAILS @doc(description: "The displayed price includes the FPT amount without displaying the ProductPrice.fixed_product_taxes values. This value corresponds to 'Including FPT only'") + INCLUDE_FPT_WITH_DETAILS @doc(description: "The displayed price includes the FPT amount while displaying the values of ProductPrice.fixed_product_taxes separately. This value corresponds to 'Including FPT and FPT description'") + EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS @doc(description: "The displayed price does not include the FPT amount. The values of ProductPrice.fixed_product_taxes and the price including the FPT are displayed separately. This value corresponds to 'Excluding FPT, Including FPT description and final price'") + EXCLUDE_FPT_WITHOUT_DETAILS @doc(description: "The displayed price does not include the FPT amount. The values from ProductPrice.fixed_product_taxes are not displayed. This value corresponds to 'Excluding FPT'") + FPT_DISABLED @doc(description: "The FPT feature is not enabled. You can omit ProductPrice.fixed_product_taxes from your query") +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php new file mode 100644 index 000000000000..0e88af2fcb22 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -0,0 +1,476 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test CategoryList GraphQl query + */ +class CategoryListTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @dataProvider filterSingleCategoryDataProvider + * @param $field + * @param $condition + * @param $value + */ + public function testFilterSingleCategoryByField($field, $condition, $value, $expectedResult) + { + $query = <<<QUERY +{ + categoryList(filters: { $field : { $condition : "$value" } }){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertResponseFields($result['categoryList'][0], $expectedResult); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @dataProvider filterMultipleCategoriesDataProvider + * @param $field + * @param $condition + * @param $value + * @param $expectedResult + */ + public function testFilterMultipleCategoriesByField($field, $condition, $value, $expectedResult) + { + $query = <<<QUERY +{ + categoryList(filters: { $field : { $condition : $value } }){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(count($expectedResult), $result['categoryList']); + foreach ($expectedResult as $i => $expected) { + $this->assertResponseFields($result['categoryList'][$i], $expected); + } + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryByMultipleFields() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {in: ["6","7","8","9","10"]}, name: {match: "Movable"}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(3, $result['categoryList']); + + $expectedCategories = [7 => 'Movable', 9 => 'Movable Position 1', 10 => 'Movable Position 2']; + $actualCategories = array_column($result['categoryList'], 'name', 'id'); + $this->assertEquals($expectedCategories, $actualCategories); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterWithInactiveCategory() + { + $query = <<<QUERY +{ + categoryList(filters: {url_key: {in: ["inactive", "category-2"]}}){ + id + name + url_key + url_path + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $actualCategories = array_column($result['categoryList'], 'url_key', 'id'); + $this->assertContains('category-2', $actualCategories); + $this->assertNotContains('inactive', $actualCategories); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testQueryChildCategoriesWithProducts() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {in: ["3"]}}){ + id + name + url_key + url_path + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + url_key + description + products{ + total_count + items{ + name + sku + } + } + children{ + name + } + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $baseCategory = $result['categoryList'][0]; + + $this->assertEquals('Category 1', $baseCategory['name']); + $this->assertArrayHasKey('products', $baseCategory); + //Check base category products + $expectedBaseCategoryProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => '12345', 'name' => 'Simple Product Two'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); + //Check base category children + $expectedBaseCategoryChildren = [ + ['name' => 'Category 1.1', 'description' => 'Category 1.1 description.'], + ['name' => 'Category 1.2', 'description' => 'Its a description of Test Category 1.2'] + ]; + $this->assertCategoryChildren($baseCategory, $expectedBaseCategoryChildren); + + //Check first child category + $firstChildCategory = $baseCategory['children'][0]; + $this->assertEquals('Category 1.1', $firstChildCategory['name']); + $this->assertEquals('Category 1.1 description.', $firstChildCategory['description']); + $firstChildCategoryExpectedProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => '12345', 'name' => 'Simple Product Two'], + ]; + $this->assertCategoryProducts($firstChildCategory, $firstChildCategoryExpectedProducts); + $firstChildCategoryChildren = [['name' =>'Category 1.1.1']]; + $this->assertCategoryChildren($firstChildCategory, $firstChildCategoryChildren); + //Check second child category + $secondChildCategory = $baseCategory['children'][1]; + $this->assertEquals('Category 1.2', $secondChildCategory['name']); + $this->assertEquals('Its a description of Test Category 1.2', $secondChildCategory['description']); + $firstChildCategoryExpectedProducts = [ + ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple-4', 'name' => 'Simple Product Three'] + ]; + $this->assertCategoryProducts($secondChildCategory, $firstChildCategoryExpectedProducts); + $firstChildCategoryChildren = []; + $this->assertCategoryChildren($secondChildCategory, $firstChildCategoryChildren); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testNoResultsFound() + { + $query = <<<QUERY +{ + categoryList(filters: {url_key: {in: ["inactive", "does-not-exist"]}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals([], $result['categoryList']); + } + + /** + * When no filters are supplied, the root category is returned + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testEmptyFiltersReturnRootCategory() + { + $query = <<<QUERY +{ + categoryList{ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + $storeRootCategoryId = $storeManager->getStore()->getRootCategoryId(); + + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals('Default Category', $result['categoryList'][0]['name']); + $this->assertEquals($storeRootCategoryId, $result['categoryList'][0]['id']); + } + + /** + * Filtering with match value less than minimum query should return empty result + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testMinimumMatchQueryLength() + { + $query = <<<QUERY +{ + categoryList(filters: {name: {match: "mo"}}){ + id + name + url_key + url_path + children_count + path + position + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('categoryList', $result); + $this->assertEquals([], $result['categoryList']); + } + + /** + * @return array + */ + public function filterSingleCategoryDataProvider(): array + { + return [ + [ + 'ids', + 'eq', + '4', + [ + 'id' => '4', + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ] + ], + [ + 'name', + 'match', + 'Movable Position 2', + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ], + [ + 'url_key', + 'eq', + 'category-1-1-1', + [ + 'id' => '5', + 'name' => 'Category 1.1.1', + 'url_key' => 'category-1-1-1', + 'url_path' => 'category-1/category-1-1/category-1-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4/5', + 'position' => '1' + ] + ], + ]; + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function filterMultipleCategoriesDataProvider(): array + { + return[ + //Filter by multiple IDs + [ + 'ids', + 'in', + '["4", "9", "10"]', + [ + [ + 'id' => '4', + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ], + [ + 'id' => '9', + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ] + ], + //Filter by multiple url keys + [ + 'url_key', + 'in', + '["category-1-2", "movable"]', + [ + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' + ], + [ + 'id' => '13', + 'name' => 'Category 1.2', + 'url_key' => 'category-1-2', + 'url_path' => 'category-1/category-1-2', + 'children_count' => '0', + 'path' => '1/2/3/13', + 'position' => '2' + ] + ] + ], + //Filter by matching multiple names + [ + 'name', + 'match', + '"Position"', + [ + [ + 'id' => '9', + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ], + [ + 'id' => '11', + 'name' => 'Movable Position 3', + 'url_key' => 'movable-position-3', + 'url_path' => 'movable-position-3', + 'children_count' => '0', + 'path' => '1/2/11', + 'position' => '7' + ] + ] + ] + ]; + } + + /** + * Check category products + * + * @param array $category + * @param array $expectedProducts + */ + private function assertCategoryProducts(array $category, array $expectedProducts) + { + $this->assertEquals(count($expectedProducts), $category['products']['total_count']); + $this->assertCount(count($expectedProducts), $category['products']['items']); + $this->assertResponseFields($category['products']['items'], $expectedProducts); + } + + /** + * Check category child categories + * + * @param array $category + * @param array $expectedChildren + */ + private function assertCategoryChildren(array $category, array $expectedChildren) + { + $this->assertArrayHasKey('children', $category); + $this->assertCount(count($expectedChildren), $category['children']); + foreach ($expectedChildren as $i => $expectedChild) { + $this->assertResponseFields($category['children'][$i], $expectedChild); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php index b15cfe3a5b91..29696e29908f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php @@ -65,33 +65,25 @@ public function testProductUrlResolver() 'store_id' => $storeId ] ); - $targetPath = $actualUrls->getTargetPath(); + $relativePath = $actualUrls->getRequestPath(); $expectedType = $actualUrls->getEntityType(); + $redirectCode = $actualUrls->getRedirectType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $urlPath, + $relativePath, + $expectedType, + $redirectCode + ); } /** - * Tests the use case where relative_url is provided as resolver input in the Query + * Test the use case where non seo friendly is provided as resolver input in the Query * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ - public function testProductUrlWithCanonicalUrlInput() + public function testProductUrlWithNonSeoFriendlyUrlInput() { $productSku = 'p002'; /** @var ProductRepositoryInterface $productRepository */ @@ -122,25 +114,127 @@ public function testProductUrlWithCanonicalUrlInput() 'store_id' => $storeId ] ); - $targetPath = $actualUrls->getTargetPath(); + // even of non seo friendly path requested, the seo friendly path should be prefered + $relativePath = $actualUrls->getRequestPath(); $expectedType = $actualUrls->getEntityType(); - $canonicalPath = $actualUrls->getTargetPath(); + $nonSeoFriendlyPath = $actualUrls->getTargetPath(); + $redirectCode = $actualUrls->getRedirectType(); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $nonSeoFriendlyPath, + $relativePath, + $expectedType, + $redirectCode + ); + } + + /** + * Test the use case where non seo friendly is provided as resolver input in the Query + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testRedirectsAndCustomInput() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + + // generate permanent redirects + $renamedKey = 'p002-ren'; + $product->setUrlKey($renamedKey)->setData('save_rewrites_history', true)->save(); + + $storeId = $product->getStoreId(); + $query = <<<QUERY { - urlResolver(url:"{$canonicalPath}") - { - id - relative_url - type - } + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + url_key + url_suffix + } + } } QUERY; $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + $urlPath = $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + $suffix = $response['products']['items'][0]['url_suffix']; + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + // querying the end redirect gives the same record + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $renamedKey . $suffix, + $actualUrls->getRequestPath(), + $actualUrls->getEntityType(), + 0 + ); + + // querying a url that's a redirect the active redirected final url + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $productSku . $suffix, + $actualUrls->getRequestPath(), + $actualUrls->getEntityType(), + 301 + ); + + // create custom url that doesn't redirect + /** @var UrlRewrite $urlRewriteModel */ + $urlRewriteModel = $this->objectManager->create(UrlRewrite::class); + + $customUrl = 'custom-path'; + $urlRewriteArray = [ + 'entity_type' => 'custom', + 'entity_id' => '0', + 'request_path' => $customUrl, + 'target_path' => 'p002.html', + 'redirect_type' => '0', + 'store_id' => '1', + 'description' => '', + 'is_autogenerated' => '0', + 'metadata' => null, + ]; + foreach ($urlRewriteArray as $key => $value) { + $urlRewriteModel->setData($key, $value); + } + $urlRewriteModel->save(); + + // querying a custom url that should return the target entity but relative should be the custom url + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $customUrl, + $customUrl, + $actualUrls->getEntityType(), + 0 + ); + + // change custom url that does redirect + $urlRewriteModel->setRedirectType('301'); + $urlRewriteModel->setId($urlRewriteModel->getId()); + $urlRewriteModel->save(); + + ObjectManager::getInstance()->get(\Magento\TestFramework\Helper\CacheCleaner::class)->cleanAll(); + + //modifying query by adding spaces to avoid getting cached values. + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $customUrl, + $actualUrls->getRequestPath(), + strtoupper($actualUrls->getEntityType()), + 301 + ); + $urlRewriteModel->delete(); } /** @@ -166,7 +260,7 @@ public function testCategoryUrlResolver() ] ); $categoryId = $actualUrls->getEntityId(); - $targetPath = $actualUrls->getTargetPath(); + $relativePath = $actualUrls->getRequestPath(); $expectedType = $actualUrls->getEntityType(); $query @@ -181,26 +275,17 @@ public function testCategoryUrlResolver() $response = $this->graphQlQuery($query); $urlPath = $response['category']['url_key'] . $response['category']['url_suffix']; - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + $this->queryUrlAndAssertResponse( + (int) $categoryId, + $urlPath, + $relativePath, + $expectedType, + 0 + ); } /** - * Tests the use case where the url_key of the existing product is changed + * Test the use case where the url_key of the existing product is changed * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ @@ -238,24 +323,16 @@ public function testProductUrlRewriteResolver() 'store_id' => $storeId ] ); - $targetPath = $actualUrls->getTargetPath(); + $relativePath = $actualUrls->getRequestPath(); $expectedType = $actualUrls->getEntityType(); - $query - = <<<QUERY -{ - urlResolver(url:"{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($product->getEntityId(), $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + + $this->queryUrlAndAssertResponse( + (int) $product->getEntityId(), + $urlPath, + $relativePath, + $expectedType, + 0 + ); } /** @@ -288,6 +365,7 @@ public function testInvalidUrlResolverInput() id relative_url type + redirectCode } } QUERY; @@ -319,7 +397,7 @@ public function testCategoryUrlWithLeadingSlash() ] ); $categoryId = $actualUrls->getEntityId(); - $targetPath = $actualUrls->getTargetPath(); + $relativePath = $actualUrls->getRequestPath(); $expectedType = $actualUrls->getEntityType(); $query @@ -333,22 +411,14 @@ public function testCategoryUrlWithLeadingSlash() QUERY; $response = $this->graphQlQuery($query); $urlPath = $response['category']['url_key'] . $response['category']['url_suffix']; - - $query = <<<QUERY -{ - urlResolver(url:"/{$urlPath}") - { - id - relative_url - type - } -} -QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($categoryId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); - $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + $urlPathWithLeadingSlash = "/{$urlPath}"; + $this->queryUrlAndAssertResponse( + (int) $categoryId, + $urlPathWithLeadingSlash, + $relativePath, + $expectedType, + 0 + ); } /** @@ -371,7 +441,7 @@ public function testGetNonExistentUrlRewrite() 'store_id' => 1 ] ); - $targetPath = $actualUrls->getTargetPath(); + $relativePath = $actualUrls->getRequestPath(); $query = <<<QUERY { @@ -380,12 +450,50 @@ public function testGetNonExistentUrlRewrite() id relative_url type + redirectCode } } QUERY; $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals('PRODUCT', $response['urlResolver']['type']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } + + /** + * Assert response from GraphQl + * + * @param string $productId + * @param string $urlKey + * @param string $relativePath + * @param string $expectedType + * @param int $redirectCode + */ + private function queryUrlAndAssertResponse( + int $productId, + string $urlKey, + string $relativePath, + string $expectedType, + int $redirectCode + ): void { + $query + = <<<QUERY +{ + urlResolver(url:"{$urlKey}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals($productId, $response['urlResolver']['id']); + $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); + $this->assertEquals($redirectCode, $response['urlResolver']['redirectCode']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php index ece421925a31..072c6bc38de7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php @@ -53,13 +53,34 @@ public function testCMSPageUrlResolver() id relative_url type + redirectCode } } QUERY; $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + + // querying by non seo friendly url path should return seo friendly relative url + $query + = <<<QUERY +{ + urlResolver(url:"{$targetPath}") + { + id + relative_url + type + redirectCode + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals($cmsPageId, $response['urlResolver']['id']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); + $this->assertEquals(strtoupper(str_replace('-', '_', $expectedEntityType)), $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); } /** @@ -77,10 +98,6 @@ public function testResolveSlash() $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); $page->load($homePageIdentifier); $homePageId = $page->getId(); - /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ - $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); - /** @param \Magento\Cms\Api\Data\PageInterface $page */ - $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); $query = <<<QUERY { @@ -89,13 +106,15 @@ public function testResolveSlash() id relative_url type + redirectCode } } QUERY; $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($homePageId, $response['urlResolver']['id']); - $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); + $this->assertEquals($homePageIdentifier, $response['urlResolver']['relative_url']); $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php index 048ccb70af0d..1dc5a813de2b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php @@ -38,6 +38,12 @@ class ProductViewTest extends GraphQlAbstract /** @var \Magento\Tax\Model\Calculation\Rule[] */ private $fixtureTaxRules; + /** @var string */ + private $defaultRegionSystemSetting; + + /** @var string */ + private $defaultPriceDisplayType; + /** * @var StoreManagerInterface */ @@ -52,19 +58,26 @@ protected function setUp() /** @var \Magento\Config\Model\ResourceModel\Config $config */ $config = $this->objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + + $this->defaultRegionSystemSetting = $scopeConfig->getValue( + Config::CONFIG_XML_PATH_DEFAULT_REGION + ); + + $this->defaultPriceDisplayType = $scopeConfig->getValue( + Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE + ); + //default state tax calculation AL $config->saveConfig( Config::CONFIG_XML_PATH_DEFAULT_REGION, - 1, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 1 ); $config->saveConfig( Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, - 3, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + 3 ); $this->getFixtureTaxRates(); $this->getFixtureTaxRules(); @@ -72,6 +85,9 @@ protected function setUp() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $config->reinit(); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); } public function tearDown() @@ -82,16 +98,12 @@ public function tearDown() //default state tax calculation AL $config->saveConfig( Config::CONFIG_XML_PATH_DEFAULT_REGION, - null, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + $this->defaultRegionSystemSetting ); $config->saveConfig( Config::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, - 1, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - 1 + $this->defaultPriceDisplayType ); $taxRules = $this->getFixtureTaxRules(); if (count($taxRules)) { @@ -107,6 +119,10 @@ public function tearDown() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $config->reinit(); + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); } /** @@ -253,6 +269,22 @@ private function getFixtureTaxRules() */ private function assertBaseFields($product, $actualResponse) { + $pricesTypes = [ + 'minimalPrice', + 'regularPrice', + 'maximalPrice', + ]; + foreach ($pricesTypes as $priceType) { + if (isset($actualResponse['price'][$priceType]['amount']['value'])) { + $actualResponse['price'][$priceType]['amount']['value'] = + round($actualResponse['price'][$priceType]['amount']['value'], 4); + } + + if (isset($actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'])) { + $actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'] = + round($actualResponse['price'][$priceType]['adjustments'][0]['amount']['value'], 4); + } + } // product_object_field_name, expected_value $assertionMap = [ ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], @@ -271,7 +303,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.286501, + 'value' => 0.2865, 'currency' => 'USD', ], 'code' => 'TAX', @@ -289,7 +321,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.750001, + 'value' => 0.7500, 'currency' => 'USD', ], 'code' => 'TAX', @@ -307,7 +339,7 @@ private function assertBaseFields($product, $actualResponse) [ 'amount' => [ - 'value' => 0.286501, + 'value' => 0.2865, 'currency' => 'USD', ], 'code' => 'TAX', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php index bd20741fb741..5e6415f82b25 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -39,6 +39,7 @@ public function testNonExistentEntityUrlRewrite() id relative_url type + redirectCode } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceWithFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php similarity index 86% rename from dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceWithFPTTest.php rename to dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php index 3f568c2635d0..385e3419bbf6 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceWithFPTTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/ProductPriceWithFPTTest.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\GraphQl\Catalog; +namespace Magento\GraphQl\Weee; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; @@ -27,12 +27,64 @@ class ProductPriceWithFPTTest extends GraphQlAbstract /** @var ObjectManager $objectManager */ private $objectManager; + /** @var string[] $objectManager */ + private $initialConfig; + + /** @var ScopeConfigInterface */ + private $scopeConfig; + /** * @inheritdoc */ - protected function setUp() :void + protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); + + /** @var ScopeConfigInterface $scopeConfig */ + $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + + $currentSettingsArray = [ + 'tax/display/type', + 'tax/weee/enable', + 'tax/weee/display', + 'tax/defaults/region', + 'tax/weee/apply_vat', + 'tax/calculation/price_includes_tax' + ]; + + foreach ($currentSettingsArray as $configPath) { + $this->initialConfig[$configPath] = $this->scopeConfig->getValue( + $configPath + ); + } + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = $this->objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->reinit(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->writeConfig($this->initialConfig); + } + + /** + * Write configuration for weee + * + * @param array $weeTaxSettings + * @return void + */ + private function writeConfig(array $weeTaxSettings): void + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + $this->scopeConfig->clean(); } /** @@ -49,16 +101,7 @@ protected function setUp() :void */ public function testCatalogPriceExcludeTaxAndIncludeFPTOnly(array $weeTaxSettings) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); - - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); + $this->writeConfig($weeTaxSettings); $skus = ['simple-with-ftp']; $query = $this->getProductQuery($skus); @@ -115,16 +158,7 @@ public function catalogPriceExcludeTaxAndIncludeFPTOnlySettingsProvider() */ public function testCatalogPriceExcludeTaxAndIncludeFPTWithDescription(array $weeTaxSettings) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); - - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); + $this->writeConfig($weeTaxSettings); $skus = ['simple-with-ftp']; $query = $this->getProductQuery($skus); @@ -181,16 +215,7 @@ public function catalogPriceExcludeTaxAndIncludeFPTWithDescriptionSettingsProvid */ public function testCatalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnly(array $weeTaxSettings) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); - - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); + $this->writeConfig($weeTaxSettings); /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); @@ -257,16 +282,7 @@ public function catalogPriceExcludeTaxCatalogDisplayIncludeTaxAndIncludeFPTOnlyS */ public function testCatalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescription(array $weeTaxSettings) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); - - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); + $this->writeConfig($weeTaxSettings); /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); @@ -333,16 +349,7 @@ public function catalogPriceExclTaxCatalogDisplayInclTaxAndInclFPTWithDescriptio */ public function testCatalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescription(array $weeTaxSettings) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); - - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); + $this->writeConfig($weeTaxSettings); $skus = ['simple-with-ftp']; $query = $this->getProductQuery($skus); @@ -400,16 +407,7 @@ public function catalogPriceInclTaxCatalogDisplayExclTaxAndInclFPTWithDescriptio */ public function testCatalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnly(array $weeTaxSettings) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); - - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); + $this->writeConfig($weeTaxSettings); $skus = ['simple-with-ftp']; $query = $this->getProductQuery($skus); @@ -469,16 +467,8 @@ public function catalogPriceInclTaxCatalogDisplayInclTaxAndInclFPTOnlySettingsPr public function testCatalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAppliedOnFPT( array $weeTaxSettings ) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); + $this->writeConfig($weeTaxSettings); - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); $taxClassCollection = $taxClassCollectionFactory->create(); @@ -524,7 +514,7 @@ public function catalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAp return [ [ 'weeTaxSettings' => [ - 'tax/calculation/price_includes_tax' > '1', + 'tax/calculation/price_includes_tax' => '1', 'tax/display/type' => '2', 'tax/weee/enable' => '1', 'tax/weee/display' => '0', @@ -552,16 +542,7 @@ public function catalogPriceIncTaxCatalogDisplayInclTaxInclFPTWithDescrWithTaxAp */ public function testCatalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTs(array $weeTaxSettings) { - /** @var WriterInterface $configWriter */ - $configWriter = $this->objectManager->get(WriterInterface::class); - - foreach ($weeTaxSettings as $path => $value) { - $configWriter->save($path, $value); - } - - /** @var ScopeConfigInterface $scopeConfig */ - $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); - $scopeConfig->clean(); + $this->writeConfig($weeTaxSettings); /** @var TaxClassCollectionFactory $taxClassCollectionFactory */ $taxClassCollectionFactory = $this->objectManager->get(TaxClassCollectionFactory::class); @@ -631,6 +612,65 @@ public function catalogPriceInclTaxCatalogDisplayIncludeTaxAndMuyltipleFPTsSetti ]; } + /** + * Test FPT disabled feature + * + * FPT enabled : FALSE + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider catalogPriceDisabledFPTSettingsProvider + * @magentoApiDataFixture Magento/Weee/_files/product_with_fpt.php + */ + public function testCatalogPriceDisableFPT(array $weeTaxSettings) + { + $this->writeConfig($weeTaxSettings); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var Product $product1 */ + $product1 = $productRepository->get('simple-with-ftp'); + $product1->setFixedProductAttribute( + [['website_id' => 0, 'country' => 'US', 'state' => 0, 'price' => 10, 'delete' => '']] + ); + $productRepository->save($product1); + + $skus = ['simple-with-ftp']; + $query = $this->getProductQuery($skus); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertEquals(100, round($product['price_range']['minimum_price']['regular_price']['value'], 2)); + $this->assertCount( + 0, + $product['price_range']['minimum_price']['fixed_product_taxes'], + 'Fixed product tax count is incorrect' + ); + $this->assertResponseFields( + $product['price_range']['minimum_price']['fixed_product_taxes'], + [] + ); + } + + /** + * CatalogPriceDisableFPT settings data provider + * + * @return array + */ + public function catalogPriceDisabledFPTSettingsProvider() + { + return [ + [ + 'weeTaxSettings' => [ + 'tax/weee/enable' => '0', + 'tax/weee/display' => '1', + ], + ], + ]; + } + /** * Get GraphQl query to fetch products by sku * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php new file mode 100644 index 000000000000..451ea78ee308 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/StoreConfigFPTTest.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Weee; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Weee\Model\Tax as WeeeDisplayConfig; +use Magento\Weee\Model\Config; + +/** + * Test for storeConfig FPT config values + */ +class StoreConfigFPTTest extends GraphQlAbstract +{ + /** @var ObjectManager $objectManager */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() :void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * FPT All Display settings + * + * @param array $weeTaxSettings + * @param string $displayValue + * @return void + * + * @dataProvider sameFPTDisplaySettingsProvider + */ + public function testSameFPTDisplaySettings(array $weeTaxSettings, $displayValue) + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); + + $query = $this->getStoreConfigQuery(); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + + $this->assertNotEmpty($result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['sales_fixed_product_tax_display_setting']); + + $this->assertEquals($displayValue, $result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertEquals($displayValue, $result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertEquals($displayValue, $result['storeConfig']['sales_fixed_product_tax_display_setting']); + } + + /** + * SameFPTDisplaySettings settings data provider + * + * @return array + */ + public function sameFPTDisplaySettingsProvider() + { + return [ + [ + 'weeTaxSettingsDisplayIncludedOnly' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_INCL, + ], + 'displayValue' => 'INCLUDE_FPT_WITHOUT_DETAILS', + ], + [ + 'weeTaxSettingsDisplayIncludedAndDescription' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + ], + 'displayValue' => 'INCLUDE_FPT_WITH_DETAILS', + ], + [ + 'weeTaxSettingsDisplayIncludedAndExcludedAndDescription' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + ], + 'displayValue' => 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + ], + [ + 'weeTaxSettingsDisplayExcluded' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL, + ], + 'displayValue' => 'EXCLUDE_FPT_WITHOUT_DETAILS', + ], + [ + 'weeTaxSettingsDisplayExcluded' => [ + 'tax/weee/enable' => '0', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_EXCL, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL, + ], + 'displayValue' => 'FPT_DISABLED', + ], + ]; + } + + /** + * FPT Display setting shuffled + * + * @param array $weeTaxSettings + * @return void + * + * @dataProvider differentFPTDisplaySettingsProvider + */ + public function testDifferentFPTDisplaySettings(array $weeTaxSettings) + { + /** @var WriterInterface $configWriter */ + $configWriter = $this->objectManager->get(WriterInterface::class); + + foreach ($weeTaxSettings as $path => $value) { + $configWriter->save($path, $value); + } + + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); + + $query = $this->getStoreConfigQuery(); + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + + $this->assertNotEmpty($result['storeConfig']['product_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['category_fixed_product_tax_display_setting']); + $this->assertNotEmpty($result['storeConfig']['sales_fixed_product_tax_display_setting']); + + $this->assertEquals( + 'INCLUDE_FPT_WITHOUT_DETAILS', + $result['storeConfig']['product_fixed_product_tax_display_setting'] + ); + $this->assertEquals( + 'INCLUDE_FPT_WITH_DETAILS', + $result['storeConfig']['category_fixed_product_tax_display_setting'] + ); + $this->assertEquals( + 'EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS', + $result['storeConfig']['sales_fixed_product_tax_display_setting'] + ); + } + + /** + * DifferentFPTDisplaySettings settings data provider + * + * @return array + */ + public function differentFPTDisplaySettingsProvider() + { + return [ + [ + 'weeTaxSettingsDisplay' => [ + 'tax/weee/enable' => '1', + Config::XML_PATH_FPT_DISPLAY_PRODUCT_VIEW => WeeeDisplayConfig::DISPLAY_INCL, + Config::XML_PATH_FPT_DISPLAY_PRODUCT_LIST => WeeeDisplayConfig::DISPLAY_INCL_DESCR, + Config::XML_PATH_FPT_DISPLAY_SALES => WeeeDisplayConfig::DISPLAY_EXCL_DESCR_INCL, + ] + ], + ]; + } + + /** + * Get GraphQl query to fetch storeConfig and FPT serttings + * + * @return string + */ + private function getStoreConfigQuery(): string + { + return <<<QUERY +{ + storeConfig { + product_fixed_product_tax_display_setting + category_fixed_product_tax_display_setting + sales_fixed_product_tax_display_setting + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php new file mode 100644 index 000000000000..46844438fdd9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Multishipping/Api/CartRepositoryTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Api; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SortOrderBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Tests web-api for multishipping quote. + */ +class CartRepositoryTest extends WebapiAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); + $this->sortOrderBuilder = $this->objectManager->create(SortOrderBuilder::class); + $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + try { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $cart = $this->getCart('multishipping_quote_id'); + $quoteRepository->delete($cart); + } catch (\InvalidArgumentException $e) { + // Do nothing if cart fixture was not used + } + parent::tearDown(); + } + + /** + * Tests that multishipping quote contains all addresses in shipping assignments. + * + * @magentoApiDataFixture Magento/Multishipping/Fixtures/quote_with_split_items.php + */ + public function testGetMultishippingCart() + { + $cart = $this->getCart('multishipping_quote_id'); + $cartId = $cart->getId(); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/carts/' . $cartId, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => 'quoteCartRepositoryV1', + 'serviceVersion' => 'V1', + 'operation' => 'quoteCartRepositoryV1Get', + ], + ]; + + $requestData = ['cartId' => $cartId]; + $cartData = $this->_webApiCall($serviceInfo, $requestData); + + $shippingAssignments = $cart->getExtensionAttributes()->getShippingAssignments(); + foreach ($shippingAssignments as $key => $shippingAssignment) { + $address = $shippingAssignment->getShipping()->getAddress(); + $cartItem = $shippingAssignment->getItems()[0]; + $this->assertEquals( + $address->getId(), + $cartData['extension_attributes']['shipping_assignments'][$key]['shipping']['address']['id'] + ); + $this->assertEquals( + $cartItem->getSku(), + $cartData['extension_attributes']['shipping_assignments'][$key]['items'][0]['sku'] + ); + $this->assertEquals( + $cartItem->getQty(), + $cartData['extension_attributes']['shipping_assignments'][$key]['items'][0]['qty'] + ); + } + } + + /** + * Retrieve quote by given reserved order ID + * + * @param string $reservedOrderId + * @return Quote + * @throws \InvalidArgumentException + */ + private function getCart(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + if (empty($items)) { + throw new \InvalidArgumentException('There is no quote with provided reserved order ID.'); + } + + return array_pop($items); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php index 8cb82f5c8f20..5a894758dc9e 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartRepositoryTest.php @@ -42,6 +42,9 @@ class CartRepositoryTest extends WebapiAbstract */ private $filterBuilder; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -59,8 +62,10 @@ protected function setUp() protected function tearDown() { try { + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); $cart = $this->getCart('test01'); - $cart->delete(); + $quoteRepository->delete($cart); } catch (\InvalidArgumentException $e) { // Do nothing if cart fixture was not used } @@ -74,18 +79,27 @@ protected function tearDown() * @return \Magento\Quote\Model\Quote * @throws \InvalidArgumentException */ - protected function getCart($reservedOrderId) + private function getCart($reservedOrderId) { - /** @var $cart \Magento\Quote\Model\Quote */ - $cart = $this->objectManager->get(\Magento\Quote\Model\Quote::class); - $cart->load($reservedOrderId, 'reserved_order_id'); - if (!$cart->getId()) { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + if (empty($items)) { throw new \InvalidArgumentException('There is no quote with provided reserved order ID.'); } - return $cart; + + return array_pop($items); } /** + * Tests successfull get cart web-api call. + * * @magentoApiDataFixture Magento/Sales/_files/quote.php */ public function testGetCart() @@ -130,6 +144,8 @@ public function testGetCart() } /** + * Tests exception when cartId is not provided. + * * @expectedException \Exception * @expectedExceptionMessage No such entity with */ @@ -154,6 +170,8 @@ public function testGetCartThrowsExceptionIfThereIsNoCartWithProvidedId() } /** + * Tests carts search. + * * @magentoApiDataFixture Magento/Sales/_files/quote.php */ public function testGetList() @@ -184,6 +202,7 @@ public function testGetList() $this->searchCriteriaBuilder->addFilters([$grandTotalFilter, $subtotalFilter]); $this->searchCriteriaBuilder->addFilters([$minCreatedAtFilter]); $this->searchCriteriaBuilder->addFilters([$maxCreatedAtFilter]); + $this->searchCriteriaBuilder->addFilter('reserved_order_id', 'test01'); /** @var SortOrder $sortOrder */ $sortOrder = $this->sortOrderBuilder->setField('subtotal')->setDirection(SortOrder::SORT_ASC)->create(); $this->searchCriteriaBuilder->setSortOrders([$sortOrder]); diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php index 9f697a1be633..cd9512c22789 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/Template/TransportBuilderMock.php @@ -6,6 +6,9 @@ namespace Magento\TestFramework\Mail\Template; +/** + * Class TransportBuilderMock + */ class TransportBuilderMock extends \Magento\Framework\Mail\Template\TransportBuilder { /** @@ -38,11 +41,12 @@ public function getSentMessage() * Return transport mock. * * @return \Magento\TestFramework\Mail\TransportInterfaceMock + * @throws \Magento\Framework\Exception\LocalizedException */ public function getTransport() { $this->prepareMessage(); $this->reset(); - return new \Magento\TestFramework\Mail\TransportInterfaceMock(); + return new \Magento\TestFramework\Mail\TransportInterfaceMock($this->message); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php index 8f967b501a59..5bf98b76e7d5 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/TransportInterfaceMock.php @@ -6,8 +6,28 @@ namespace Magento\TestFramework\Mail; +use Magento\Framework\Mail\EmailMessageInterface; + +/** + * Class TransportInterfaceMock + */ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterface { + /** + * @var null|EmailMessageInterface + */ + private $message; + + /** + * TransportInterfaceMock constructor. + * + * @param null|EmailMessageInterface $message + */ + public function __construct($message = null) + { + $this->message = $message; + } + /** * Mock of send a mail using transport * @@ -15,16 +35,17 @@ class TransportInterfaceMock implements \Magento\Framework\Mail\TransportInterfa */ public function sendMessage() { + //phpcs:ignore Squiz.PHP.NonExecutableCode.ReturnNotRequired return; } /** * Get message * - * @return string + * @return null|EmailMessageInterface */ public function getMessage() { - return ''; + return $this->message; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php new file mode 100644 index 000000000000..948170218332 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductHydratorTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\HydratorInterface; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test Product Hydrator + */ +class ProductHydratorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Bootstrap + */ + private $objectManager; + + /** + * @var HydratorPool + */ + private $hydratorPool; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->hydratorPool = $this->objectManager->create(HydratorPool::class); + } + + /** + * Test that Hydrator correctly populates entity with data + */ + public function testProductHydrator() + { + $addAttributes = [ + 'sku' => 'product_updated', + 'name' => 'Product (Updated)', + 'type_id' => 'simple', + 'status' => 1, + ]; + + /** @var Product $product */ + $product = $this->objectManager->create(Product::class); + $product->setId(42) + ->setSku('product') + ->setName('Product') + ->setPrice(10) + ->setQty(123); + $product->lockAttribute('sku'); + $product->lockAttribute('type_id'); + $product->lockAttribute('price'); + + /** @var HydratorInterface $hydrator */ + $hydrator = $this->hydratorPool->getHydrator(ProductInterface::class); + $hydrator->hydrate($product, $addAttributes); + + $expected = [ + 'entity_id' => 42, + 'sku' => 'product_updated', + 'name' => 'Product (Updated)', + 'type_id' => 'simple', + 'status' => 1, + 'price' => 10, + 'qty' => 123, + ]; + $this->assertEquals($expected, $product->getData()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index a5ab96193246..25bb55ffbc32 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -64,6 +64,7 @@ ->setIsActive(true) ->setIsAnchor(true) ->setPosition(1) + ->setDescription('Category 1.1 description.') ->save(); $category = $objectManager->create(\Magento\Catalog\Model\Category::class); @@ -79,6 +80,7 @@ ->setPosition(1) ->setCustomUseParentSettings(0) ->setCustomDesign('Magento/blank') + ->setDescription('This is the description for Category 1.1.1') ->save(); $category = $objectManager->create(\Magento\Catalog\Model\Category::class); diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php index bbbcc3133d4b..6d5f760d7894 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template.php @@ -10,9 +10,9 @@ $template->setOptions(['area' => 'test area', 'store' => 1]); $template->setData( [ - 'template_text' => - file_get_contents(__DIR__ . '/template_fixture.html'), - 'template_code' => \Magento\Theme\Model\Config\ValidatorTest::TEMPLATE_CODE + 'template_text' => file_get_contents(__DIR__ . '/template_fixture.html'), + 'template_code' => \Magento\Theme\Model\Config\ValidatorTest::TEMPLATE_CODE, + 'template_type' => \Magento\Email\Model\Template::TYPE_TEXT ] ); $template->save(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php new file mode 100644 index 000000000000..03bdc9a36552 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mail; + +use Magento\Email\Model\BackendTemplate; +use Magento\Email\Model\Template; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Class EmailMessageTest + */ +class TransportBuilderTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $di; + + /** + * @var TransportBuilder + */ + protected $builder; + + /** + * @var Template + */ + protected $template; + + protected function setUp() + { + $this->di = Bootstrap::getObjectManager(); + $this->builder = $this->di->get(TransportBuilder::class); + $this->template = $this->di->get(Template::class); + } + + /** + * @magentoDataFixture Magento/Email/Model/_files/email_template.php + * @magentoDbIsolation enabled + * + * @param string|array $email + * @dataProvider emailDataProvider + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAddToEmail($email) + { + $templateId = $this->template->load('email_exception_fixture', 'template_code')->getId(); + + $this->builder->setTemplateModel(BackendTemplate::class); + + $vars = ['reason' => 'Reason', 'customer' => 'Customer']; + $options = ['area' => 'frontend', 'store' => 1]; + $this->builder->setTemplateIdentifier($templateId)->setTemplateVars($vars)->setTemplateOptions($options); + + $this->builder->addTo($email); + + /** @var EmailMessage $emailMessage */ + $emailMessage = $this->builder->getTransport(); + + $addresses = $emailMessage->getMessage()->getTo(); + + $emails = []; + /** @var Address $toAddress */ + foreach ($addresses as $address) { + $emails[] = $address->getEmail(); + } + + if (is_string($email)) { + $this->assertCount(1, $emails); + $this->assertEquals($email, $emails[0]); + } else { + $this->assertEquals($email, $emails); + } + } + + /** + * @return array + */ + public function emailDataProvider(): array + { + return [ + [ + 'billy.everything@someserver.com', + ], + [ + [ + 'billy.everything@someserver.com', + 'john.doe@someserver.com', + ] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php new file mode 100644 index 000000000000..a8e9059a84eb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Test caching works for categoryList query + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class CategoryListCacheTest extends AbstractGraphqlCacheTest +{ + /** + * Test cache tags are generated + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testRequestCacheTagsForCategoryList(): void + { + $categoryId ='333'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Test request is served from cache + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testSecondRequestIsServedFromCache() + { + $categoryId ='333'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Test cache tags are generated + * + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + */ + public function testRequestCacheTagsForCategoryListOnMultipleIds(): void + { + $categoryId1 ='400'; + $categoryId2 = '401'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId1", "$categoryId2"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + //added the previous category in expected tags as it is cached + $expectedCacheTags = ['cat_c','cat_c_' .'333', 'cat_c_' . $categoryId1, 'cat_c_' . $categoryId2, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Test request is served from cache + * + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + */ + public function testSecondRequestIsServedFromCacheOnMultipleIds() + { + $categoryId1 ='400'; + $categoryId2 = '401'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId1", "$categoryId2"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + //added the previous category in expected tags as it is cached + $expectedCacheTags = ['cat_c','cat_c_' .'333', 'cat_c_' . $categoryId1, 'cat_c_' . $categoryId2, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php b/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php index 3bd966018b94..ff4f3f8a58bc 100644 --- a/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php +++ b/dev/tests/integration/testsuite/Magento/Payment/Block/InfoTest.php @@ -5,9 +5,24 @@ */ namespace Magento\Payment\Block; +use Magento\Framework\View\Element\Text; +use Magento\Framework\View\LayoutInterface; +use Magento\OfflinePayments\Model\Banktransfer; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\Payment\Block\Info as BlockInfo; +use Magento\Payment\Block\Info\Instructions; +use Magento\Payment\Model\Info; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class InfoTest + */ class InfoTest extends \PHPUnit\Framework\TestCase { /** + * Tests payment info block. + * * @magentoConfigFixture current_store payment/banktransfer/title Bank Method Title * @magentoConfigFixture current_store payment/checkmo/title Checkmo Title Of The Method * @magentoAppArea adminhtml @@ -15,37 +30,32 @@ class InfoTest extends \PHPUnit\Framework\TestCase public function testGetChildPdfAsArray() { /** @var $layout \Magento\Framework\View\Layout */ - $layout = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ); - $block = $layout->createBlock(\Magento\Payment\Block\Info::class, 'block'); + $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $block = $layout->createBlock(BlockInfo::class, 'block'); - /** @var $paymentInfoBank \Magento\Payment\Model\Info */ - $paymentInfoBank = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Payment\Model\Info::class - ); - $paymentInfoBank->setMethodInstance( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\OfflinePayments\Model\Banktransfer::class - ) + /** @var $paymentInfoBank Info */ + $paymentInfoBank = Bootstrap::getObjectManager()->create( + Info::class ); - /** @var $childBank \Magento\Payment\Block\Info\Instructions */ - $childBank = $layout->addBlock(\Magento\Payment\Block\Info\Instructions::class, 'child.one', 'block'); + $order = Bootstrap::getObjectManager()->create(Order::class); + $banktransferPayment = Bootstrap::getObjectManager()->create(Banktransfer::class); + $paymentInfoBank->setMethodInstance($banktransferPayment); + $paymentInfoBank->setOrder($order); + /** @var $childBank Instructions */ + $childBank = $layout->addBlock(Instructions::class, 'child.one', 'block'); $childBank->setInfo($paymentInfoBank); $nonExpectedHtml = 'non-expected html'; - $childHtml = $layout->addBlock(\Magento\Framework\View\Element\Text::class, 'child.html', 'block'); + $childHtml = $layout->addBlock(Text::class, 'child.html', 'block'); $childHtml->setText($nonExpectedHtml); - /** @var $paymentInfoCheckmo \Magento\Payment\Model\Info */ - $paymentInfoCheckmo = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Payment\Model\Info::class - ); - $paymentInfoCheckmo->setMethodInstance( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\OfflinePayments\Model\Checkmo::class - ) + /** @var $paymentInfoCheckmo Info */ + $paymentInfoCheckmo = Bootstrap::getObjectManager()->create( + Info::class ); + $checkmoPayment = Bootstrap::getObjectManager()->create(Checkmo::class); + $paymentInfoCheckmo->setMethodInstance($checkmoPayment); + $paymentInfoCheckmo->setOrder($order); /** @var $childCheckmo \Magento\OfflinePayments\Block\Info\Checkmo */ $childCheckmo = $layout->addBlock( \Magento\OfflinePayments\Block\Info\Checkmo::class, diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index b763130c5a2c..0d36bf02f007 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -432,6 +432,8 @@ private function addAddressByType(string $addressType, $email, ?string $name = n $this->messageData[$addressType], $convertedAddressArray ); + } else { + $this->messageData[$addressType] = $convertedAddressArray; } } } diff --git a/lib/web/mage/adminhtml/grid.js b/lib/web/mage/adminhtml/grid.js index 1c9319f95a64..28bdb96e5cdb 100644 --- a/lib/web/mage/adminhtml/grid.js +++ b/lib/web/mage/adminhtml/grid.js @@ -63,6 +63,7 @@ define([ this.initRowCallback = false; this.doFilterCallback = false; this.sortableUpdateCallback = false; + this.filterKeyPressCallback = false; this.reloadParams = false; @@ -511,6 +512,10 @@ define([ if (event.keyCode == Event.KEY_RETURN) { //eslint-disable-line eqeqeq this.doFilter(); } + + if (this.filterKeyPressCallback) { + this.filterKeyPressCallback(this, event); + } }, /** diff --git a/setup/performance-toolkit/benchmark.jmx b/setup/performance-toolkit/benchmark.jmx index 00ae0bdbb8a4..b6154e136131 100644 --- a/setup/performance-toolkit/benchmark.jmx +++ b/setup/performance-toolkit/benchmark.jmx @@ -39431,6 +39431,64 @@ if (name == null) { Failure = false; } } +</stringProp> + <stringProp name="BeanShellAssertion.filename"/> + <stringProp name="BeanShellAssertion.parameters"/> + <boolProp name="BeanShellAssertion.resetInterpreter">false</boolProp> + </BeanShellAssertion> + <hashTree/> + </hashTree> + + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Query CategoryList" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{"query":"{\n categoryList{\n name\n id\n level\n description\n path\n path_in_store\n url_key\n url_path\n children {\n id\n description\n default_sort_by\n children {\n id\n description\n level\n children {\n level\n id\n children {\n id\n }\n }\n }\n }\n }\n}","variables":null,"operationName":null}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"/> + <stringProp name="HTTPSampler.port"/> + <stringProp name="HTTPSampler.connect_timeout"/> + <stringProp name="HTTPSampler.response_timeout"/> + <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> + <stringProp name="HTTPSampler.contentEncoding"/> + <stringProp name="HTTPSampler.path">${base_path}graphql</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <boolProp name="HTTPSampler.monitor">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"/> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/graphql/query_root_category_list.jmx</stringProp> + </HTTPSamplerProxy> + <hashTree> + <com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.JSONPathExtractor guiclass="com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.gui.JSONPathExtractorGui" testclass="com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.JSONPathExtractor" testname="Extract total_count" enabled="true"> + <stringProp name="VAR">graphql_categoryList_query_name</stringProp> + <stringProp name="JSONPATH">$.data.categoryList[0].name</stringProp> + <stringProp name="DEFAULT"/> + <stringProp name="VARIABLE"/> + <stringProp name="SUBJECT">BODY</stringProp> + <stringProp name="INPUT_FORMAT">JSON</stringProp> + </com.atlantbh.jmeter.plugins.jsonutils.jsonpathextractor.JSONPathExtractor> + <hashTree/> + <BeanShellAssertion guiclass="BeanShellAssertionGui" testclass="BeanShellAssertion" testname="BeanShell Assertion" enabled="true"> + <stringProp name="BeanShellAssertion.query">String name = vars.get("graphql_categoryList_query_name"); +if (name == null) { + Failure = true; + FailureMessage = "Not Expected \"children\" to be null"; +} else { + if (!name.equals("Default Category")) { + Failure = true; + FailureMessage = "Expected \"name\" to equal \"Default Category\", Actual: " + name; + } else { + Failure = false; + } +} </stringProp> <stringProp name="BeanShellAssertion.filename"/> <stringProp name="BeanShellAssertion.parameters"/> @@ -40368,6 +40426,301 @@ function assertCategoryChildren(category, response) { </hashTree> + <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="GraphQL Get Category List by category_url_key" enabled="true"> + <intProp name="ThroughputController.style">1</intProp> + <boolProp name="ThroughputController.perThread">false</boolProp> + <intProp name="ThroughputController.maxThroughput">1</intProp> + <stringProp name="ThroughputController.percentThroughput">${graphqlGetCategoryListByCategoryIdPercentage}</stringProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx</stringProp></ThroughputController> + <hashTree> + <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set Test Label" enabled="true"> + <stringProp name="script"> +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + </stringProp> + <stringProp name="scriptLanguage">javascript</stringProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/setup_label.jmx</stringProp></JSR223PreProcessor> + <hashTree/> + <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Set Label" enabled="true"> + <stringProp name="BeanShellSampler.query"> + vars.put("testLabel", "GraphQL Get Category List by category_url_key"); + </stringProp> + <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> + </BeanShellSampler> + <hashTree/> + + <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true"> + <collectionProp name="HeaderManager.headers"> + <elementProp name="" elementType="Header"> + <stringProp name="Header.name">Content-Type</stringProp> + <stringProp name="Header.value">application/json</stringProp> + </elementProp> + <elementProp name="" elementType="Header"> + <stringProp name="Header.name">Accept</stringProp> + <stringProp name="Header.value">*/*</stringProp> + </elementProp> + </collectionProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/api/header_manager_before_token.jmx</stringProp></HeaderManager> + <hashTree/> + + <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Init Random Generator" enabled="true"> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx</stringProp> + <stringProp name="BeanShellSampler.query"> +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + </stringProp> + <stringProp name="BeanShellSampler.filename"/> + <stringProp name="BeanShellSampler.parameters"/> + <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> + </BeanShellSampler> + <hashTree/> + + <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="SetUp - Prepare Category Data" enabled="true"> + <stringProp name="scriptLanguage">javascript</stringProp> + <stringProp name="parameters"/> + <stringProp name="filename"/> + <stringProp name="cacheKey"/> + <stringProp name="script">random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); +number = random.nextInt(categories.length); + +vars.put("category_url_key", categories[number].url_key); +vars.put("category_name", categories[number].name); +vars.put("category_id", categories[number].id); +vars.putObject("category", categories[number]); + </stringProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/extract_category_setup.jmx</stringProp></JSR223Sampler> + <hashTree/> + + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Category List by category_url_key" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{"query" : "{\n categoryList(filters:{url_key: {in: [\"${category_url_key}\"]}}) {\n id\n children {\n id\n name\n url_key\n url_path\n children_count\n path\n image\n productImagePreview: products(pageSize: 1, sort: {name: ASC}) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}"}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"/> + <stringProp name="HTTPSampler.port">${graphql_port_number}</stringProp> + <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> + <stringProp name="HTTPSampler.response_timeout">200000</stringProp> + <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> + <stringProp name="HTTPSampler.contentEncoding"/> + <stringProp name="HTTPSampler.path">${base_path}graphql</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <boolProp name="HTTPSampler.monitor">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"/> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/graphql/get_category_list_by_category_url_key.jmx</stringProp> + </HTTPSamplerProxy> + <hashTree> + <JSR223Assertion guiclass="TestBeanGUI" testclass="JSR223Assertion" testname="Assert found categories" enabled="true"> + <stringProp name="scriptLanguage">javascript</stringProp> + <stringProp name="parameters"/> + <stringProp name="filename"/> + <stringProp name="cacheKey"/> + <stringProp name="script">var category = vars.getObject("category"); +var response = JSON.parse(prev.getResponseDataAsString()); + +assertCategoryId(category, response); +assertCategoryChildren(category, response); + +function assertCategoryId(category, response) { + if (response.data == undefined || response.data.categoryList == undefined || response.data.categoryList[0].id != category.id) { + AssertionResult.setFailureMessage("Cannot find category with id \"" + category.id + "\""); + AssertionResult.setFailure(true); + } +} + +function assertCategoryChildren(category, response) { + foundCategory = response.data && response.data.categoryList ? response.data.categoryList[0] : null; + if (foundCategory) { + var childrenFound = foundCategory.children.map(function (c) {return parseInt(c.id)}); + var children = category.children.map(function (c) {return parseInt(c)}); + if (JSON.stringify(children.sort()) != JSON.stringify(childrenFound.sort())) { + AssertionResult.setFailureMessage("Cannot math children categories \"" + JSON.stringify(children) + "\" for to found one: \"" + JSON.stringify(childrenFound) + "\""); + AssertionResult.setFailure(true); + } + } + +} + +</stringProp> + </JSR223Assertion> + <hashTree/> + </hashTree> + </hashTree> + + + <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="GraphQL Get Multiple Categories" enabled="true"> + <intProp name="ThroughputController.style">1</intProp> + <boolProp name="ThroughputController.perThread">false</boolProp> + <intProp name="ThroughputController.maxThroughput">1</intProp> + <stringProp name="ThroughputController.percentThroughput">${graphqlGetCategoryListByCategoryIdPercentage}</stringProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/scenario_controller_tmpl.jmx</stringProp></ThroughputController> + <hashTree> + <JSR223PreProcessor guiclass="TestBeanGUI" testclass="JSR223PreProcessor" testname="Set Test Label" enabled="true"> + <stringProp name="script"> +var testLabel = "${testLabel}" ? " (${testLabel})" : ""; +if (testLabel + && sampler.getClass().getName() == 'org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy' +) { + if (sampler.getName().indexOf(testLabel) == -1) { + sampler.setName(sampler.getName() + testLabel); + } +} else if (sampler.getName().indexOf("SetUp - ") == -1) { + sampler.setName("SetUp - " + sampler.getName()); +} + </stringProp> + <stringProp name="scriptLanguage">javascript</stringProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/_system/setup_label.jmx</stringProp></JSR223PreProcessor> + <hashTree/> + <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Set Label" enabled="true"> + <stringProp name="BeanShellSampler.query"> + vars.put("testLabel", "GraphQL Get Multiple Categories"); + </stringProp> + <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> + </BeanShellSampler> + <hashTree/> + + <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true"> + <collectionProp name="HeaderManager.headers"> + <elementProp name="" elementType="Header"> + <stringProp name="Header.name">Content-Type</stringProp> + <stringProp name="Header.value">application/json</stringProp> + </elementProp> + <elementProp name="" elementType="Header"> + <stringProp name="Header.name">Accept</stringProp> + <stringProp name="Header.value">*/*</stringProp> + </elementProp> + </collectionProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/api/header_manager_before_token.jmx</stringProp></HeaderManager> + <hashTree/> + + <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="SetUp - Init Random Generator" enabled="true"> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/init_random_generator_setup.jmx</stringProp> + <stringProp name="BeanShellSampler.query"> +import java.util.Random; + +Random random = new Random(); +if (${seedForRandom} > 0) { + random.setSeed(${seedForRandom} + ${__threadNum}); +} + +vars.putObject("randomIntGenerator", random); + </stringProp> + <stringProp name="BeanShellSampler.filename"/> + <stringProp name="BeanShellSampler.parameters"/> + <boolProp name="BeanShellSampler.resetInterpreter">true</boolProp> + </BeanShellSampler> + <hashTree/> + + <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="SetUp - Prepare Category Data" enabled="true"> + <stringProp name="scriptLanguage">javascript</stringProp> + <stringProp name="parameters"/> + <stringProp name="filename"/> + <stringProp name="cacheKey"/> + <stringProp name="script">random = vars.getObject("randomIntGenerator"); + +var categories = props.get("categories"); + +var numbers = []; + +var sanity = 0; +for(var i = 0; i < 4; i++){ + sanity++; + if(sanity > 100){ + break; + } + var number = random.nextInt(categories.length) + if(numbers.indexOf(number) >= 0){ + i--; + continue; + } + numbers.push(number); +} + +vars.put("category_id_1", categories[numbers[0]].id); +vars.put("category_id_2", categories[numbers[1]].id); +vars.put("category_id_3", categories[numbers[2]].id); +vars.put("category_id_4", categories[numbers[3]].id); +</stringProp> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/common/extract_multiple_categories_setup.jmx</stringProp> + </JSR223Sampler> + <hashTree/> + + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get multiple categories by ID" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{"query" : "{\n categoryList(filters:{ids: {in: [\"${category_id_1}\", \"${category_id_2}\", \"${category_id_3}\", \"${category_id_4}\"]}}) {\n id\n children {\n id\n name\n url_key\n url_path\n children_count\n path\n image\n productImagePreview: products(pageSize: 1, sort: {name: ASC}) {\n items {\n small_image {\n label\n url\n }\n }\n }\n }\n }\n}"}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"/> + <stringProp name="HTTPSampler.port">${graphql_port_number}</stringProp> + <stringProp name="HTTPSampler.connect_timeout">60000</stringProp> + <stringProp name="HTTPSampler.response_timeout">200000</stringProp> + <stringProp name="HTTPSampler.protocol">${request_protocol}</stringProp> + <stringProp name="HTTPSampler.contentEncoding"/> + <stringProp name="HTTPSampler.path">${base_path}graphql</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <boolProp name="HTTPSampler.monitor">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"/> + <stringProp name="TestPlan.comments">mpaf/tool/fragments/ce/graphql/get_multiple_categories_by_id.jmx</stringProp> + </HTTPSamplerProxy> + <hashTree> + <JSR223Assertion guiclass="TestBeanGUI" testclass="JSR223Assertion" testname="Assert category count" enabled="true"> + <stringProp name="scriptLanguage">javascript</stringProp> + <stringProp name="parameters"/> + <stringProp name="filename"/> + <stringProp name="cacheKey"/> + <stringProp name="script">var response = JSON.parse(prev.getResponseDataAsString()); + +if(response.data == undefined || response.data.categoryList == undefined){ + AssertionResult.setFailureMessage("CategoryList results are empty."); + AssertionResult.setFailure(true); +} + +if(response.data.categoryList.length !== 4){ + AssertionResult.setFailureMessage("CategoryList query expected to find 4 categories. " + response.data.categoryList.length + " returned."); + AssertionResult.setFailure(true); +} +</stringProp> + </JSR223Assertion> + <hashTree/> + </hashTree> + </hashTree> + + <ThroughputController guiclass="ThroughputControllerGui" testclass="ThroughputController" testname="GraphQL Get Url Info by url_key" enabled="true"> <intProp name="ThroughputController.style">1</intProp> <boolProp name="ThroughputController.perThread">false</boolProp>